Resolves the parallel Phase B/C work that landed on the sister branch
while this branch was in review. Both branches independently implemented
real OTel + Wazero runtimes; this merge keeps the best of each.
Conflict resolution
- Server/plugin/sandbox_wazero.go: rewritten as a hybrid. Keeps the
HEAD lifecycle (eager `platformInit` with WASI preview-1, explicit
`platformDeactivate` per-instance, runtime closed via Registry.Close)
AND adopts the sister branch's richer artefacts:
* `WithMemoryLimitPages` actually enforces `cfg.MaxMemoryMB`,
* the JSON-over-linear-memory ABI
(`allocate` / `command_dispatch(ptr,len) → (ptr,len)` /
`deallocate`),
* `listExportedCommands` auto-binds commands the plugin exports
via `list_commands` at activation time (capability-gated).
- Server/plugin/registry.go: kept the HEAD `activate()` snapshot
pattern (read `runtimePlatform` under RLock, pass into
`activateWithRuntime` as a parameter) so a concurrent Close can't
race the wazero call. Sister branch's LoadAll stale-staging cleanup
and UninstallPlugin on-disk dir removal came in via auto-merge.
- Server/telemetry/telemetry_otel.go: kept the HEAD implementation
(race-fixed AppMetrics rebind, uint64 overflow guard, idempotent
shutdown, trace-provider cleanup on prom failure) and wired in the
sister branch's `OTLPInsecure` config field for plaintext gRPC opt-in.
- Server/go.mod: accepted sister branch's `BurntSushi/toml v1.6.0`
for the new TOML manifest support.
- Client/tauri-client/vitest.config.ts: union of both globs
(`tests/**/*.test.ts`, `src/**/*.test.ts`, `src/**/*.test.tsx`).
- PHASE_BC_LOCAL_TODO.md: combined the two checkbox histories;
TOML manifest, hello.wasm fixture, and OTLPInsecure are all marked
done now.
Sister branch additions accepted via auto-merge
- Server/plugin/examples/hello/{hello.wasm,main.go}: precompiled 925
KiB TinyGo plugin with the full ABI (allocate, deallocate,
list_commands, command_dispatch, on_event).
- Server/plugin/manifest_{toml,nottoml}.go: TOML manifest parser
behind the wazero build tag, JSON fallback elsewhere.
- Server/plugin/loader.go: prefers `plugin.toml`, falls back to
`plugin.json`.
- Server/api/plugins_handler.go: structured error responses + slog.
- Server/main.go, Server/config/config.go: OTLPInsecure plumbing,
defaults polish.
- docs/{contributing.md,server-configuration.md}: documentation
updates.
Test status
- `go build` passes on default, -tags otel, -tags wazero, and
-tags otel,wazero.
- `go vet` passes on every tag combination.
- `go test ./...` passes on default and on -tags otel,wazero.
- Client: `npx tsc --noEmit` clean; vitest 3188/3188 across 112 files.
https://claude.ai/code/session_01AZni6CDSQeu67WSWY1YCDX
14 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 ./...— passes on the dev machine with Go 1.24.x.cd Server && go test ./store/... ./ws/... ./plugin/... ./telemetry/...— all pass; full suitego test ./...green.cd Server && go vet ./...— clean.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. All 111 test files / 3186 tests pass.
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 Vitest config preset: vite-plugin-solid added to vitest.config.ts, include expanded to pick up src/components/solid/**/*.test.tsx. Badge.test.tsx now runs automatically (112 files, 3188 tests pass).
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-generate— done;db/pgdbgen/events.sql.goanddb/pgdbgen/plugins.sql.gogenerated;//go:build postgrestag prepended to all 19 pgdbgen files to gate pgx/v5 import. - Replace the postgres EventStore stubs in
Server/store/postgres.gowith real implementations using PostgreSQL SQL syntax ($1/$2params,RETURNING id, nativebool/time.Time). - 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. Landed in
Server/ws/reconnect_db_test.go(TestReconnect_BufferMiss_FallsBackToDBTier). - Add a
replay_sourcefield to the auth_ok payload — landed in Pass 4.buildAuthOKtakes the tier as a parameter, "none" on fresh connect, "buffer" or "db" on resume. - 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: otel v1.43.0, sdk v1.43.0, exporters/prometheus v0.65.0, exporters/otlp/otlptrace/otlptracegrpc v1.43.0, andgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttpv0.67.0 (otelhttp replaces the unmaintained otelchi wrapper referenced by the original plan; otelhttp is upstream-supported and wraps anyhttp.Handlerincluding a Chi router). - Replace the placeholder body of
telemetry/telemetry_otel.go'sInitwith the real tracer + meter provider construction. The tagged build wires an OTel Prometheus exporter (pull), an OTLP/gRPC trace exporter whenexporter=otlp(withOTLPInsecureopt-in for plaintext gRPC),otelhttp.NewHandleras the HTTP middleware, and a real provider that re-bindsAppMetricsinstruments viaresetAppMetricsForInit. Tests inServer/telemetry/telemetry_otel_test.go(TestOtelInitPrometheusExporter,TestOtelTracerRecordsSpan,TestOtelHistogramRecordsSeconds,TestOtelShutdownIdempotent,TestOtelConvertAttrsHandlesUnsignedInts,TestOtelConvertAttrsUint64OverflowFallsBackToString,TestOtelAppMetricsRebindsAfterInit) run undergo test -tags otel ./telemetry/.... - Build with
-tags otel— passes. Full 4-tag matrix green (default, otel, wazero, otel+wazero) against Go 1.25.1. - Add a CI job that exercises
go build -tags otel ./...andgo test -tags otel ./telemetry/.... - 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. Landed inServer/Makefile(otel-up/otel-down); overlay file atServer/docker-compose.otel.yml; Prometheus config atServer/prometheus.dev.yml.
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
github.com/tetratelabs/wazero v1.11.0togo.mod. - Replace the placeholder body in
Server/plugin/sandbox_wazero.gowith real wazero runtime construction. The tagged build owns a sharedwazero.Runtimecreated inplatformInit(with the configuredMaxMemoryMBtranslated toWithMemoryLimitPagesand WASI preview-1 imports pre-instantiated), compiles + instantiates each plugin's.wasmentrypoint inactivateWithRuntime, and tears modules + runtime down inplatformDeactivate/Close. The host-guest command ABI is JSON-over-linear-memory:allocate(size)/command_dispatch(ptr,len) → (ptr,len)/deallocate(ptr,len), with optionallist_commandsfor command auto-registration. Tests inServer/plugin/sandbox_wazero_test.go(TestWazeroRegistryCreatesRuntime,TestWazeroActivateCompilesModule,TestWazeroDispatchCommandMissingExport,TestWazeroCloseTearsDownRuntime,TestWazeroInvalidWASMFailsActivation,TestWazeroDisablePluginFreesModule) run undergo test -tags wazero ./plugin/...using a 41-byte embedded WASM fixture for the smoke tests. - Add a precompiled
Server/plugin/examples/hello/hello.wasm(925 KiB) built with TinyGo 0.40.1 + Go 1.25.3 + Binaryen wasm-opt 129. Source inexamples/hello/main.go; exports:allocate,deallocate,list_commands,command_dispatch,on_event. - Replace JSON-only manifest parsing with TOML support behind the
wazerobuild tag. Addedgithub.com/BurntSushi/tomlv1.6.0,manifest_toml.go(wazero) +manifest_nottoml.go(!wazero);loader.goprefersplugin.tomland falls back toplugin.json. - Wire
Server/plugin/host_events.gointo the WS pub/sub hub. Landed:EventSink.SetBroadcaster/Emitadded; hub gainsSetPluginEventSink;deliverBroadcastcallssink.Dispatchon each sequenced broadcast; wired inapi/router.go. - Wire
Server/plugin/host_commands.gointo the WS slash-command dispatcher. Landed:chat_commandV1 handler inServer/ws/handlers_command.go; hub gainsSetPluginRegistry; wired inapi/router.go. Tests inhandlers_command_test.go. - 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 precompiled
Server/plugin/examples/hello/hello.wasm(925 KiB). Built with TinyGo 0.40.1 + Go 1.25.3 + Binaryen wasm-opt 129. Source in main.go; exports: allocate, deallocate, list_commands, command_dispatch, on_event. - Implement plugin marketplace install path
(
POST /api/v1/admin/plugins/installwith multipart zip) — landed in Pass 4.Registry.InstallFromZipdoes zip-slip validation, no symlinks, 16 MiB compressed cap, 64 MiB uncompressed cap, then atomic rename into the plugin directory. - Replace plugin postgres stubs in
Server/store/postgres.gowith real SQL implementations (same session as EventStore stubs). - 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).