Closes #694 ## Status This PR contains the stable 2.0.4 client migration, authenticated V2 discovery fallback and shared cross-runtime compatibility implementation. The validation results below are local execution evidence; the newly added CI matrix must also run on the updated PR head. ## Published changes - Pin UI/server to official `@opencode/client@2.0.4` and the bundled pruning plugin to `@opencode/plugin@2.0.4`, with the corresponding lockfile migration. - Probe authenticated `/api/status` first. Fall back to earlier V2 `/api/health` **only on HTTP 404**, retaining the endpoint, credentials, 64 KiB response bound and absolute deadline. Authentication, transport, server and malformed-status failures do not trigger fallback. - Migrate the canonical UI/server contract to stable Forms, permission decisions, command names, interrupts, renaming, forks, message lookup and inbox delivery. - Update the guarded proxy for stable methods/paths, native active-session envelopes, native `cursor.next`, global Form directory headers and current resource/plugin events. - Follow the renamed upstream platform binary `bin/opencode(.exe)` while retaining public CLI aliases. Discovery success alone does not establish compatibility with an earlier runtime's conversation APIs. ## Shared cross-runtime compatibility ### Shared cross-runtime integration - Introduce one connection-scoped adapter under `packages/server/src/opencode/compatibility/`, used by both the guarded UI proxy and direct server callers, including Yolo. - Select the contract using authenticated daemon identity rather than the CLI executable's version. Recognize unknown publications through authenticated, bounded OpenAPI negotiation instead of an exact runtime-version gate. - Bind authorization, client, transport and profile to one connection generation. Fence actual mutation dispatch after asynchronous body preparation; prevent late old streams from publishing or invalidating their replacement. - Translate earlier V2 routes, methods and payloads before sending. Never retry a mutation speculatively after 400, 404, authentication or transport failures. - Normalize legacy HTTP inbox timestamps, including compaction admission, before the pinned Solid reducer. Preserve the distinct native SSE inbox schema and durable event metadata. - Restore legacy catalog/content invalidation and permission-event convergence. Preserve meaningful method/path/status/profile diagnostics and native 401 error envelopes. ### Location authority and pruning - Preserve legacy workspace identity internally through request serialization, catalog/cache keys, Forms, imports, cursors, events and exact move rollback. Modern public APIs continue to reject obsolete workspace selectors. - Authorize complete locations before translating directories or synthesizing upstream Form/location headers. Keep native cursor tokens unchanged. - Preserve session/path ownership, worktree mutation fences and the restricted proxy allowlist. Generic RPC, plugin management and destructive revert commit remain blocked. - Carry verified legacy identity through pruning membership checks, preview and the existing transactional storage/claim fence; do not relax deletion checks. - Treat native idle records as control records, retaining identity/outcome without fabricated assistant text. - Add a common multi-runtime CI matrix and the audit/evidence register in `dev-docs/OPENCODE_V2_COMPATIBILITY.md`, including preserved historical architecture findings. ## Local validation All native tests use isolated synthetic daemons, configuration and databases, never the shared daemon or user storage. | Check | Result | | --- | --- | | Same native suite on beta-19271, 2.0.3, 2.0.4 and 2.0.5 with pinned client/plugin 2.0.4 | Passed | | Full native pruning suite with non-null legacy workspace identity on beta-19271 and 2.0.3 | Passed | | Rendered UI on modern 2.0.5 and beta-19271 with non-null identity | Passed | | Native 2.0.5 suite inside Ubuntu/WSL, using a temporary Linux filesystem root | Passed | | Server tests (after CI follow-up) | 500 passed, 2 skipped | | UI store tests | 441 passed | | Electron native tests | 194 passed, 4 skipped | | UI/server/Electron typechecks | Passed | | UI/server and Electron production builds | Passed | | Windows Tauri release executable (`--no-bundle`) | Passed | | Electron/Tauri packaged-resource smoke checks | Passed | | Desktop packaging invariants | 4 passed | | `git diff --check` | Passed | The native matrix exercises the actual shared connection, generated client, UI fetch adapter and guarded proxy: voice-off/on instructions, prompts, commands, session Shell, background Shell output cursors/removal, UI/Yolo permission acceptance and rejection, busy inbox delivery/cancellation, interrupt/wait, staged revert clearing, session/global/worktree Forms, native pagination, export/import, same-directory legacy identities, scoped events, foreign authority refusal and exact rollback. Pruning coverage includes actual storage identity and execution claims, busy refusal, idempotency, competing admission, next-model payload, fork isolation, pre-compaction history, restart, backend presence disposal/reopening and crash expiry. Rendered tests cover individual/group/response/session cleanup, confirmation counts, persistence and reload. ## Autonomous gatekeeper review Review/fix/retest loops concluded with no remaining actionable findings in the reviewed transport/lifecycle/proxy, identity/UI/events/pruning/rollback, scripts/CI and rendered-fixture scopes. Corrections included dispatch-generation races, compaction admission normalization, independent negotiation cancellation, first-request profile resolution, preservation of streamed 401 envelopes and direct Shell/PTY identity propagation. A final rendered failure was traced to the fixture awaiting only the first RPC of a cross-message deletion; the fixture and regression now await every affected message. Production deletion guards were not relaxed. This scoped approval does not close the separately preserved historical architecture findings. ## Validation boundaries ### CI follow-up The first run on `a0c1c943` ([35142559235](https://github.com/NeuralNomadsAI/CodeNomad/actions/runs/35142559235)) failed in five native jobs. The local bundle tests had missed the standalone npm archive's missing sibling import, and Windows legacy jobs exposed a short-path versus canonical-path project comparison. The dependent artifact-comment job reported that same build failure. The follow-up makes the plugin's identity primitives self-contained, adds an isolated npm-file-list regression for all exported entrypoints, and corrects project identity selection with alias and nested-repository regressions. The packed-plugin failure was reproduced before correction; the corrected installed package passes the native suite on Windows 2.0.4 and Linux/WSL 2.0.4/2.0.5. The corrected 2.0.4 shipped bundle also passes rendered/native pruning. Fresh full beta-19271 and 2.0.3 `--legacy-pruning` suites pass under a reproducing Windows short-path TEMP root after the final exact-directory fix. Independent final reviews approve packaging/documentation and worktree identity with zero actionable findings. Remote checks on the new head remain the CI authority; these are local follow-up results. - Full interaction with the installed desktop application through Developer Mode was not verified because Developer Mode is inactive. The release executable and packaged-resource checks above are build/smoke evidence, not a fresh-install interactive certification. - Remote run [35146390688](https://github.com/NeuralNomadsAI/CodeNomad/actions/runs/35146390688) at `486e856d` passes all runtime-contract jobs, Windows/Linux native pruning, service compatibility, general tests and both Tauri checks. macOS passes packed-plugin loading/pruning, then times out on late bundle discovery. The fixture now canonicalizes its root before daemon startup to align `/var` configuration paths with `/private/var` FSEvents paths. Late installation remains mandatory and asserts the plugin was initially absent; failures now upload isolated diagnostic logs/inventory/captures. Actual macOS confirmation remains pending on the new head. - Interactive native TUI validation was not performed. - The passing WSL result is Linux-native execution inside WSL, not a complete Windows-host/WSL desktop traversal. With the fixture's configuration directory under `/mnt/c`, conversation/proxy checks passed but late plugin discovery timed out; the otherwise equivalent Linux-filesystem run passed. Mounted-configuration late discovery remains a documented environment limitation. - Other audited publications are declaration-reviewed, not individually native-certified. The four-runtime matrix does not imply universal V2 compatibility. - Earlier PR artifacts do not contain the completed cross-runtime implementation; use artifacts built from the updated head. Local passing results do not imply that the updated remote CI has completed.
9.4 KiB
Session content pruning through a V2 plugin
Status and safety boundary
This replaces CodeNomad's two removed session.messageUpdate call sites without
forking OpenCode. A native local plugin entry registers preview and prune.
No extra write-enable option or runtime version allowlist is required. Each pruning
request checks storage/execution/ownership. Unsupported or absent plugins
produce a localized failure, never a legacy API fallback or optimistic deletion.
The write path has no runtime-version allowlist. Compatible storage, ownership,
the fresh database challenge and native execution claims determine whether a request
can proceed. The initial validation used 0.0.0-beta-19419, not a required version.
It was exercised locally with the official Windows x64 executable, generated sessions,
two HTTP clients and a local mock provider. The independently installed package also
passed native CI on Windows, Linux and macOS in run 34424614579. This is not a
compatibility promise for future betas, every provider, WSL, arbitrary DB schemas
or third-party writers.
The native desktop and interactive TUI release checks listed below remain separate.
The original implementation was validated with isolated synthetic data only. The PR's Tauri build was subsequently launched manually for interactive use.
Why this exists
- OpenCode issue 44984 originally tracked V1 part deletion parity.
- PR 48043 removed the replacement
API, first published in
0.0.0-beta-19365. - Maintainer guidance
proposes plugin-owned SQLite access and RPC events;
ctx.dbis only a possibility. - Follow-up: 48090.
An offline snapshot measured 23.12 GB initially, 18.67 GB after VACUUM alone, and 4.92 GB after pruning V2 content, legacy V1 parts and legacy technical-part events, then vacuuming. Those figures include overlapping V1/V2 histories. That experiment included incomplete messages and is not the production algorithm or a replay compatibility proof. This prototype rejects incomplete messages.
Modules and request flow
packages/server/src/opencode/session-pruning/:
contract.ts: bounded, strict RPC schemas, no SQL/path/replacement-content inputs.revision.ts: browser-safe canonical content hashing.planner.ts: completed-assistant checks and technical-part-only selection.preview-store.ts: explicit read-only database access, including history before compaction.database-path.ts: daemon-side OpenCode data-directory/environment conventions, with an optional path override.transaction.ts: synchronous content-only CAS and atomic idempotency receipt.claim-fence.ts: native execution-claim and database-identity checks.service.ts: database connection and a freshctx.storagechallenge.isolated-store.ts: in-memory-only test adapter; never a live write bypass.plugin.ts: native RPC registration and post-commit notifications.desktop-plugin.ts/presence.ts: register RPC while a CodeNomad backend is present; dispose on final close or lease expiry.tui.ts/tui-reload.ts: companion cache invalidation via the public TUI API.
pruning-lifecycle.ts and pruning-installation.ts beside that directory own
automatic provisioning and backend leases. The server build bundles the payload
and its dependencies under dist/plugins/session-pruning/, copied by both desktop hosts.
The UI keeps individual, per-message, group and session cleanup entry points. It fetches the native message, resolves selected tools by ID and reasoning by a unique content/time match, then sends indexes plus a canonical SHA-256 content revision. Ambiguous reasoning or stale content fails closed. It never submits replacement text. Selections allow up to 100,001 unique indices (0–100,000), within a 1 MiB broker body and 16 MiB stored-message budget. Larger messages fail closed. A 5,000-part regression test covers the previous 4,096 limit. Bulk cleanup remains a sequence of per-message transactions, not an all-session transaction; existing UI failure counts report partial completion. There is no bulk progress/cancel UI in this PR.
POST /api/workspaces/:id/session-pruning/{preview,prune} validates workspace
ownership from the native session location and participates in the worktree deletion
fence. It pins codenomad.session-pruning, the method and runtime location; arbitrary
RPC remains blocked by the instance proxy. The removed PATCH route is also blocked.
The existing server authentication middleware protects these new routes.
After commit the caller re-reads native history, never a synthesized remainder.
rpc.codenomad.session-pruning.pruned contains session/message IDs and the content
revision. The UI invalidates both message-load authority and its native SDK transcript,
including pending rotations, then coalesces active-session reloads. Late reads cannot
overwrite a newer pruning invalidation. Reconnect uses authoritative reconciliation.
The optional TUI companion invalidates/synchronizes its public message cache and
reconciles loaded sessions on reconnect. Unmodified third-party clients do not know
this event and may display cached history until they reload.
The receipt and content change commit together; an identical retry returns the same receipt and re-emits the event. Receipts contain no deleted content. Event delivery is best-effort: failure cannot roll back a committed prune. A timed-out caller must re-read or retry the same request, not assume no mutation occurred.
Version and loading notes
- UI/server pin
@opencode/client@2.0.4. The published low-levelrpc.calltransport is used by the broker. - Plugin definition pins
@opencode/plugin@2.0.4, a development-only dependency bundled into the shipped plugin payload. No npm installation is performed at app startup. The native integration test uses the stable client against an explicitly supplied isolated official runtime, including RPC, custom events and the guarded UI proxy. CI uses CLI 2.0.4; production discovery has no exact runtime-version gate. - In that plugin contract,
ctx.session.messageandctx.dbdo not exist. - SQLite unit tests use Node's
node:sqlite; the native test also exercises that driver inside the official compiled runtime, not an embedded SDK substitute. The 30 plugin/SQLite/TUI-companion unit tests also pass on Node 22. - The module has a packable manifest with
index.tsand./tuientrypoints. Local configuration must target the directory, notplugin.ts. - An automatically provisioned entry in the global OpenCode
pluginsdirectory is discovered for all projects. Its small lifecycle module remains loaded; pruning RPCs exist only while CodeNomad is present. See deployment. No daemon upgrade or restart is performed by pruning commands.
Coordination and remaining validation
See the audited boundary. BEGIN IMMEDIATE alone
is not sufficient: the native runner commits time_suspended before reading
history, and clears it only at terminal settlement. We check that marker under
the acquired write lock; we never set or release it ourselves. A busy connection
returns immediately rather than deadlocking native async transactions on the same
event loop. Moved ownership, staged revert/compaction, retained aggregate events,
remote event ownership, attachments and triggers fail closed.
Verified: real preview/prune RPC, active-generation refusal, both prompt/transaction orderings, retry receipts, two event subscribers, next primary request payload, fork isolation, pre-compaction deletion without summary changes, and server restart. Unit tests cover rollback and client projection/read races. No actual provider token savings are claimed: the mock's usage numbers are synthetic.
Still required before general availability: native WSL runs, interactive TUI and two desktop-window/scroll verification, provider-specific continuation-state and budget coverage, dedicated bulk progress/cancel UI and installation/capability UI. Do not describe these as passed on the strength of HTTP or mocked cache tests. Physical VACUUM, V1 cleanup and repair are separate maintenance work.
Checks
node --import tsx --test packages/server/src/opencode/session-pruning/*.test.ts packages/server/src/server/routes/session-pruning.test.ts packages/server/src/server/__tests__/instance-proxy.test.ts
node --import tsx --conditions=browser --test --test-force-exit packages/ui/src/stores/session-actions.test.ts packages/ui/src/stores/session-pruning-events.test.ts packages/ui/src/stores/session-pruning-projection.test.ts packages/ui/src/stores/opencode-data.test.ts
node scripts/test-session-pruning-native.mjs C:/isolated-install/opencode2.exe
npm run typecheck --workspace @neuralnomads/codenomad
npm run typecheck --workspace @codenomad/ui
npm run build --workspace @codenomad/ui
UI store tests retain application timers; --test-force-exit terminates those after
the assertions finish. The separate native test creates its own DB/config/home,
starts serve --port 0 directly (never service discovery), supplies a local provider,
and terminates only its own child. It retains its synthetic fixture/logs for inspection.
An optional third argument tests an independently installed plugin package directory.