Files
OwnCord/PHASE_BC_LOCAL_TODO.md
T
Claude 1c476ccb58 merge: reconcile sister branch claude/plan-phases-b-c-bGpoS
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
2026-04-07 06:23:09 +00:00

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's go 1.25.0 directive,
  • 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 suite go test ./... green.
  • cd Server && go vet ./... — clean.
  • cd Client/tauri-client && npm install && npm run lint && npm run build — Pulls in solid-js, vite-plugin-solid, and @solidjs/testing-library (added to package.json); confirms the Solid pipeline compiles inside the existing Vite + TS setup.
  • cd Client/tauri-client && npm run test — runs the new Badge.test.tsx smoke 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-library in package.json
  • src/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 leaf
  • src/components/solid/ChannelListItem.tsx — store-subscribed leaf
  • src/components/solid/Badge.test.tsx — pipeline smoke test
  • src/components/solid/README.md — migration recipe

Still TODO locally:

  • Run npm install and verify the build passes (sandbox had no network).
  • Migrate the remaining leaf components in src/components/ one PR at a time, following the recipe in src/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 mountSolid calls in containers with native Solid components and delete the old vanilla DOM utilities (createComponent, factory shells) referenced from src/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)
  • events table appended to Server/migrations/postgres/001_initial_schema.sql
  • Server/db/queries/sqlite/events.sql, Server/db/queries/postgres/events.sql
  • Server/db/persisted_event.go — domain type
  • EventStore sub-interface added to Server/store/store.go
  • SQLite implementation in Server/store/sqlite_events.go (raw SQL via *sql.DB, no dbgen dependency)
  • MemStore implementation in Server/store/memstore_events.go
  • Postgres stubs returning ErrPostgresNotImplemented
  • Server/ws/event_persister.go — batched async writer
  • Server/ws/event_pruner.go — retention pruner goroutine
  • Three replayBuf.Push call sites in Server/ws/hub.go now also call h.persistEvent(...)
  • Tiered reconnect replay in Server/ws/serve.go (buffer → DB → full)
  • Reconnect-tier metrics in the hub + telemetry counter
  • EventPersistenceConfig added to Server/config/config.go with defaults {enabled: true, retention_hours: 24, batch_size: 50, batch_flush_ms: 100, pruner_interval_minutes: 60}
  • Server/main.go wires the persister + pruner
  • Server/ws/event_persister_test.go — batching, drop, drain tests

Still TODO locally:

  • Run make sqlc-generate — done; db/pgdbgen/events.sql.go and db/pgdbgen/plugins.sql.go generated; //go:build postgres tag prepended to all 19 pgdbgen files to gate pgx/v5 import.
  • Replace the postgres EventStore stubs in Server/store/postgres.go with real implementations using PostgreSQL SQL syntax ($1/$2 params, RETURNING id, native bool/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_source field to the auth_ok payload — landed in Pass 4. buildAuthOK takes the tier as a parameter, "none" on fresh connect, "buffer" or "db" on resume.
  • Document the new event_persistence block in defaultYAML inside Server/config/config.go — landed in Pass 3.

Phase B Step 8 — OpenTelemetry

The session landed:

  • Server/telemetry/telemetry.go — public API + no-op provider
  • Server/telemetry/telemetry_default.go — default-build Init
  • Server/telemetry/telemetry_otel.go — wazero/postgres-style build-tag skeleton (build with -tags otel); compiles only when the OTel modules are in go.mod and is currently a structural placeholder
  • Server/telemetry/metrics.goAppMetrics bundle
  • Server/telemetry/middleware.goHTTPMiddleware + PrometheusHandler
  • Server/telemetry/telemetry_test.go
  • Server/api/router.go mounts telemetry.HTTPMiddleware() unconditionally and the Prometheus exporter when non-nil
  • Server/main.go calls telemetry.Init early and defers Shutdown
  • TelemetryConfig added to Server/config/config.go
  • Spans added to MessageService.SendMessage, PermissionService.HasChannelPerm, ChannelService.ListVisibleChannels
  • Reconnect-tier counter wired into WSReconnectTierTotal from Server/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, and go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 (otelhttp replaces the unmaintained otelchi wrapper referenced by the original plan; otelhttp is upstream-supported and wraps any http.Handler including a Chi router).
  • Replace the placeholder body of telemetry/telemetry_otel.go's Init with the real tracer + meter provider construction. The tagged build wires an OTel Prometheus exporter (pull), an OTLP/gRPC trace exporter when exporter=otlp (with OTLPInsecure opt-in for plaintext gRPC), otelhttp.NewHandler as the HTTP middleware, and a real provider that re-binds AppMetrics instruments via resetAppMetricsForInit. Tests in Server/telemetry/telemetry_otel_test.go (TestOtelInitPrometheusExporter, TestOtelTracerRecordsSpan, TestOtelHistogramRecordsSeconds, TestOtelShutdownIdempotent, TestOtelConvertAttrsHandlesUnsignedInts, TestOtelConvertAttrsUint64OverflowFallsBackToString, TestOtelAppMetricsRebindsAfterInit) run under go 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 ./... and go 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 telemetry block in defaultYAML inside Server/config/config.go — landed in Pass 3.
  • Add a make otel-up target that spins up Jaeger via docker-compose for local tracing development. Landed in Server/Makefile (otel-up / otel-down); overlay file at Server/docker-compose.otel.yml; Prometheus config at Server/prometheus.dev.yml.

Phase C Step 9 — Wazero plugin runtime

The session landed:

  • Server/plugin/manifest.go — JSON manifest parser + capability checks
  • Server/plugin/loader.go — directory scan + entrypoint validation
  • Server/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 surfaces
  • Server/plugin/sandbox_default.go — no-op runtime (default build)
  • Server/plugin/sandbox_wazero.go-tags wazero skeleton
  • Server/plugin/errors.go
  • Server/plugin/plugin_test.go
  • Server/plugin/examples/hello/plugin.json + README.md
  • Server/migrations/015_plugins.sql (SQLite)
  • plugins + plugin_kv tables appended to the postgres schema
  • Server/db/queries/sqlite/plugins.sql, Server/db/queries/postgres/plugins.sql
  • PluginStore sub-interface in Server/store/store.go with SQLite, MemStore, and postgres-stub implementations
  • Server/api/plugins_handler.go — admin REST surface
  • Server/api/router.go mounts the admin plugin handler
  • Server/main.go constructs and starts the registry when cfg.Plugins.Enabled
  • PluginsConfig added to Server/config/config.go
  • Client/tauri-client/src/lib/pluginBridge.ts — iframe + postMessage host
  • Client/tauri-client/src/components/solid/PluginContainer.tsx — Solid host component for plugin tabs

Still TODO locally:

  • Add github.com/tetratelabs/wazero v1.11.0 to go.mod.
  • Replace the placeholder body in Server/plugin/sandbox_wazero.go with real wazero runtime construction. The tagged build owns a shared wazero.Runtime created in platformInit (with the configured MaxMemoryMB translated to WithMemoryLimitPages and WASI preview-1 imports pre-instantiated), compiles + instantiates each plugin's .wasm entrypoint in activateWithRuntime, and tears modules + runtime down in platformDeactivate / Close. The host-guest command ABI is JSON-over-linear-memory: allocate(size) / command_dispatch(ptr,len) → (ptr,len) / deallocate(ptr,len), with optional list_commands for command auto-registration. Tests in Server/plugin/sandbox_wazero_test.go (TestWazeroRegistryCreatesRuntime, TestWazeroActivateCompilesModule, TestWazeroDispatchCommandMissingExport, TestWazeroCloseTearsDownRuntime, TestWazeroInvalidWASMFailsActivation, TestWazeroDisablePluginFreesModule) run under go 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 in examples/hello/main.go; exports: allocate, deallocate, list_commands, command_dispatch, on_event.
  • Replace JSON-only manifest parsing with TOML support behind the wazero build tag. Added github.com/BurntSushi/toml v1.6.0, manifest_toml.go (wazero) + manifest_nottoml.go (!wazero); loader.go prefers plugin.toml and falls back to plugin.json.
  • Wire Server/plugin/host_events.go into the WS pub/sub hub. Landed: EventSink.SetBroadcaster/Emit added; hub gains SetPluginEventSink; deliverBroadcast calls sink.Dispatch on each sequenced broadcast; wired in api/router.go.
  • Wire Server/plugin/host_commands.go into the WS slash-command dispatcher. Landed: chat_command V1 handler in Server/ws/handlers_command.go; hub gains SetPluginRegistry; wired in api/router.go. Tests in handlers_command_test.go.
  • Pass the live *plugin.Registry from Server/main.go into NewPluginAdminHandler — landed in Pass 2. The router now accepts a *plugin.Registry parameter and the handler is also wrapped in admin.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/install with multipart zip) — landed in Pass 4. Registry.InstallFromZip does 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.go with real SQL implementations (same session as EventStore stubs).
  • Build the first real plugin: game detection. Pulls Steam API, tracks playtime, exposes /playtime slash command. This is the acceptance criterion in phase-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 .wasm execution (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).