CodeNomad/dev-docs/SESSION_PRUNING_RPC.md
Pascal André aa51cbb9ff
fix(opencode): support earlier and modern V2 runtime contracts (#695)
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.
2026-09-16 23:48:08 +02:00

9.4 KiB
Raw Blame History

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.db is 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 fresh ctx.storage challenge.
  • 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-level rpc.call transport 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.message and ctx.db do 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.ts and ./tui entrypoints. Local configuration must target the directory, not plugin.ts.
  • An automatically provisioned entry in the global OpenCode plugins directory 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.