Eight focused follow-ups from the medium-severity review bucket. All
in-sandbox tractable; no module changes, no new dependencies.
Performance
- Drop the defensive memcpy in EventPersister.Enqueue. The hub already
passes a fresh slice from wrapWithSeq and the copy was happening under
seqMu, serializing broadcast throughput. Documented the no-mutate
contract on the call site.
Observability
- AppMetrics gains WSEventsPersistErrors counter; the persister run loop
bumps both it and the existing WSEventsPersisted counter via cached
metrics handle.
- Hub.persistEvent now extracts the real event type ("chat_message",
"voice_join", ...) from the wrapped JSON envelope via a small
no-allocation byte scan instead of recording the generic
"broadcast"/"channel_broadcast" label.
- Added OTel spans + ServiceCallDurationMs histogram entries on one
public method per remaining service: DMService.CreateDM,
VoiceService.JoinChannel, InviteService.CreateInvite,
ModerationService.BanUser, BlockService.BlockUser,
UserService.UpdateProfile. Mirrors the existing pattern from
MessageService.SendMessage.
Hardening
- plugin/loader now Lstat-walks each plugin directory and rejects any
symlink, plus refuses an entrypoint that is itself a symlink. The
asset handler's prefix check stays as defense in depth.
- ipAllowed (plugin HTTP capability) now rejects RFC6598 carrier-grade
NAT (100.64.0.0/10), closing a gap in net.IP.IsPrivate which only
covers RFC1918 + RFC4193.
- Registry.activateAll syncs Instance.Enabled := true after a successful
activate so callers reading the in-memory flag see the live state.
Documentation
- defaultYAML now documents the new event_persistence, telemetry, and
plugins config blocks with their defaults and one-line descriptions.
- PHASE_BC_LOCAL_TODO.md ticks off five items (defaultYAML docs ×2,
remaining service spans, registry wiring already-fixed in Pass 2).
https://claude.ai/code/session_01UsBsQW2YiA2usk9pnJjAWk
13 KiB
Phase B + C — Local Follow-up TODO
This file enumerates everything from phase-b-acceleration.md and
phase-c-differentiation.md that could not be completed inside the
sandboxed Claude session because the work requires:
- network access to fetch new modules / npm packages,
- a Go toolchain matching
go.mod'sgo 1.25.0directive, - a WASM toolchain (TinyGo / Rust / AssemblyScript),
- a real machine that can run
npm install,cargo,tauri, etc.
The session branch is claude/plan-phases-b-c-bGpoS. Everything below
must be run on a developer machine (or CI) before the branch is mergeable.
The session-resident plan that was actually executed lives in
/root/.claude/plans/woolly-wiggling-wolf.md (not in this repo).
Verification (do first — confirms the in-session work compiles)
cd Server && go build ./...— The repo'sgo.modrequires Go 1.25.0; the sandbox only had 1.24.7, sogo buildandgo vetcould not be run. Manual file-by-file audit found no errors, but a compile is the source of truth.cd Server && go test ./store/... ./ws/... ./plugin/... ./telemetry/...— Exercises the new EventStore, EventPersister, telemetry no-op provider, and plugin manifest/loader tests.cd Server && go vet ./...cd Client/tauri-client && npm install && npm run lint && npm run build— Pulls insolid-js,vite-plugin-solid, and@solidjs/testing-library(added topackage.json); confirms the Solid pipeline compiles inside the existing Vite + TS setup.cd Client/tauri-client && npm run test— runs the newBadge.test.tsxsmoke test.
Phase B Step 6 — Solid.js migration (rest of the components)
The session landed:
- Vite + TS toolchain wiring (
vite.config.ts,tsconfig.json) solid-js+vite-plugin-solid+@solidjs/testing-libraryinpackage.jsonsrc/lib/solidAdapter.ts(wraps custom stores as Solid signals)src/lib/solidMount.ts({mount, destroy}adapter for Solid roots)src/components/solid/Badge.tsx— first leafsrc/components/solid/ChannelListItem.tsx— store-subscribed leafsrc/components/solid/Badge.test.tsx— pipeline smoke testsrc/components/solid/README.md— migration recipe
Still TODO locally:
- Run
npm installand verify the build passes (sandbox had no network). - Migrate the remaining leaf components in
src/components/one PR at a time, following the recipe insrc/components/solid/README.md. Suggested order: presence pills, typing indicators, message attachments, voice volume meters, then containers (channel list, member list, message list). - Once every leaf is migrated, replace the manual
mountSolidcalls in containers with native Solid components and delete the old vanilla DOM utilities (createComponent, factory shells) referenced fromsrc/components/. - Add a Vitest config preset under
vitest.config.tsthat pulls in@solidjs/testing-libraryautomatically (currently the test imports it directly).
Phase B Step 7 — Event persistence
The session landed:
Server/migrations/014_events_table.sql(SQLite)eventstable appended toServer/migrations/postgres/001_initial_schema.sqlServer/db/queries/sqlite/events.sql,Server/db/queries/postgres/events.sqlServer/db/persisted_event.go— domain typeEventStoresub-interface added toServer/store/store.go- SQLite implementation in
Server/store/sqlite_events.go(raw SQL via*sql.DB, nodbgendependency) - MemStore implementation in
Server/store/memstore_events.go - Postgres stubs returning
ErrPostgresNotImplemented Server/ws/event_persister.go— batched async writerServer/ws/event_pruner.go— retention pruner goroutine- Three
replayBuf.Pushcall sites inServer/ws/hub.gonow also callh.persistEvent(...) - Tiered reconnect replay in
Server/ws/serve.go(buffer → DB → full) - Reconnect-tier metrics in the hub + telemetry counter
EventPersistenceConfigadded toServer/config/config.gowith defaults{enabled: true, retention_hours: 24, batch_size: 50, batch_flush_ms: 100, pruner_interval_minutes: 60}Server/main.gowires the persister + prunerServer/ws/event_persister_test.go— batching, drop, drain tests
Still TODO locally:
- Run
make sqlc-generatesodb/dbgenanddb/pgdbgenlearn aboutevents.sql. The session used raw SQL through*sql.DB(matching the existingpgdbgenworkaround), so this is optional for SQLite but required for the postgres backend. - Replace the postgres EventStore stubs in
Server/store/postgres.gowith real wrappers around the generatedpgdbgencode (the same mechanical work tracked indocs/phase-a-status.mdfor the other stub methods). - Add an integration test that pushes more than 1000 events through a
real hub with a 1000-slot buffer, disconnects at seq=500, and asserts
the DB tier returns the missing events. The session test
(
event_persister_test.go) covers the persister in isolation but not the buffer→DB handoff insidehandleReconnect. - Add a
replay_sourcefield to the auth_ok payload so the client can log the tier. The hub already records the tier in metrics; the client surface change is a separate UX call. - Document the new
event_persistenceblock indefaultYAMLinsideServer/config/config.go— landed in Pass 3.
Phase B Step 8 — OpenTelemetry
The session landed:
Server/telemetry/telemetry.go— public API + no-op providerServer/telemetry/telemetry_default.go— default-buildInitServer/telemetry/telemetry_otel.go— wazero/postgres-style build-tag skeleton (build with-tags otel); compiles only when the OTel modules are ingo.modand is currently a structural placeholderServer/telemetry/metrics.go—AppMetricsbundleServer/telemetry/middleware.go—HTTPMiddleware+PrometheusHandlerServer/telemetry/telemetry_test.goServer/api/router.gomountstelemetry.HTTPMiddleware()unconditionally and the Prometheus exporter when non-nilServer/main.gocallstelemetry.Initearly and defersShutdownTelemetryConfigadded toServer/config/config.go- Spans added to
MessageService.SendMessage,PermissionService.HasChannelPerm,ChannelService.ListVisibleChannels - Reconnect-tier counter wired into
WSReconnectTierTotalfromServer/ws/serve.go
Still TODO locally:
- Add the OTel modules to
go.mod:cd Server go get go.opentelemetry.io/otel@latest \ go.opentelemetry.io/otel/sdk@latest \ go.opentelemetry.io/otel/exporters/prometheus@latest \ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc@latest \ go.opentelemetry.io/contrib/instrumentation/github.com/go-chi/chi/v5/otelchi@latest go mod tidy - Replace the placeholder body of
telemetry/telemetry_otel.go'sInitwith the real tracer + meter provider construction and theotelchi.Middlewarewiring (see the inline TODO comment with the call graph). - Build with
-tags otelonce the SDK is ingo.modand add a CI job that exercises the tagged build. - Add spans to the remaining service-layer entry points
(
DMService,VoiceService,InviteService,ModerationService,BlockService,UserService) — landed in Pass 3, one entrypoint per service. Add additional spans on demand. - Document the new
telemetryblock indefaultYAMLinsideServer/config/config.go— landed in Pass 3. - Add a
make otel-uptarget that spins up Jaeger via docker-compose for local tracing development.
Phase C Step 9 — Wazero plugin runtime
The session landed:
Server/plugin/manifest.go— JSON manifest parser + capability checksServer/plugin/loader.go— directory scan + entrypoint validationServer/plugin/registry.go— registry + lifecycle (install/enable/uninstall)Server/plugin/host_commands.go,host_storage.go,host_events.go,host_http.go,host_ui.go— capability surfacesServer/plugin/sandbox_default.go— no-op runtime (default build)Server/plugin/sandbox_wazero.go—-tags wazeroskeletonServer/plugin/errors.goServer/plugin/plugin_test.goServer/plugin/examples/hello/plugin.json+README.mdServer/migrations/015_plugins.sql(SQLite)plugins+plugin_kvtables appended to the postgres schemaServer/db/queries/sqlite/plugins.sql,Server/db/queries/postgres/plugins.sqlPluginStoresub-interface inServer/store/store.gowith SQLite, MemStore, and postgres-stub implementationsServer/api/plugins_handler.go— admin REST surfaceServer/api/router.gomounts the admin plugin handlerServer/main.goconstructs and starts the registry whencfg.Plugins.EnabledPluginsConfigadded toServer/config/config.goClient/tauri-client/src/lib/pluginBridge.ts— iframe + postMessage hostClient/tauri-client/src/components/solid/PluginContainer.tsx— Solid host component for plugin tabs
Still TODO locally:
- Add wazero to
go.mod:cd Server go get github.com/tetratelabs/wazero@latest go mod tidy - Replace the placeholder body in
Server/plugin/sandbox_wazero.gowith real wazero runtime construction. The file contains an inline TODO with the exact API call graph. - Replace JSON-only manifest parsing with TOML support behind the
wazerobuild tag (the design doc namesplugin.toml). Addgithub.com/BurntSushi/tomland aparseTOMLshim that falls back to the existingParseManifestif noplugin.tomlis found. - Wire
Server/plugin/host_events.gointo the WS pub/sub hub (Server/ws/pubsub.go). The session left this as a stub because the registration surface needs to be designed alongside the actual plugin event format — the hub-side code path is straightforward once the format is fixed. - Wire
Server/plugin/host_commands.gointo the WS slash-command dispatcher. There is currently no slash-command dispatcher in the WS layer. Either add one (small surface) or fold plugin commands into the REST layer first. The plugin Registry already exposesDispatchCommandso the hookup is one call site. - Pass the live
*plugin.RegistryfromServer/main.gointoNewPluginAdminHandler— landed in Pass 2. The router now accepts a*plugin.Registryparameter and the handler is also wrapped inadmin.RequireAdminAuth(Pass 2 closed the auth bypass too). - Add a precompiled trivial
.wasmblob underServer/plugin/examples/hello/hello.wasmso the example plugin can actually be loaded by an integration test once wazero is wired. Build it locally with TinyGo:cd Server/plugin/examples/hello tinygo build -o hello.wasm -target wasi ./main.go - Implement plugin marketplace install path
(
POST /api/v1/admin/plugins/installwith multipart zip). The handler is scaffolded but the install endpoint is currently absent. - Replace plugin postgres stubs in
Server/store/postgres.gowith realpgdbgen-backed implementations oncemake sqlc-generateruns (same blocker as Phase B Step 7). - Build the first real plugin: game detection. Pulls Steam API,
tracks playtime, exposes
/playtimeslash command. This is the acceptance criterion inphase-c-differentiation.md.
Build-tag matrix the user should set up in CI
| Tag set | What it builds | Why |
|---|---|---|
| (none) | Default sqlite-only server, no OTel SDK, no wazero | Existing path |
otel |
Above + OpenTelemetry SDK + Prometheus exporter | Phase B Step 8 |
wazero |
Above + plugin runtime executes WASM modules | Phase C Step 9 |
postgres |
Replaces sqlite with postgres backend | Phase A pending |
otel,wazero,postgres |
Full community-hub build | Production target |
Each tag is independently selectable; CI should test every combination at least minimally so the build-tag boundaries don't drift.
Things explicitly out of scope for this branch
(Documenting so reviewers don't expect them.)
- Migration of the entire vanilla TypeScript component tree to Solid. Two proof-of-concept components landed; the rest is mechanical PRs.
- Full OTel SDK wiring (only the public API + no-op default + structural build-tag skeleton landed).
- Real Wazero
.wasmexecution (only the registry, host APIs, and a build-tag skeleton landed). - Real game-detection plugin (the manifest fields and host APIs needed to build it are in place).
- A reverse postgres → sqlite migration (Phase A documented this is deliberately unavailable; nothing changed here).