## Summary Restore selective deletion of completed assistant tool/reasoning content after OpenCode removed `session.messageUpdate`, without forking OpenCode or opening a generic RPC proxy. The original draft's unconditional `maintenance_required` stub is replaced by a working **experimental, explicit opt-in** plugin path for the audited `0.0.0-beta-19419` runtime. The default remains read-only. Unknown runtimes/storage and active native execution fail closed. Nothing is installed or enabled on app startup. ## Implementation - Keep individual/message/group/session cleanup entry points. Send selected technical indices and a canonical SHA-256 revision, never arbitrary replacement content, SQL, database filenames or caller-selected locations. - Use an ownership-checked CodeNomad broker with a fixed RPC ID/method/location and worktree-deletion fence. Keep the removed PATCH and generic RPC routes blocked. - Bind the plugin's explicitly configured database connection to the actual daemon DB with a fresh `ctx.storage` challenge. Inside a synchronous SQLite write transaction, recheck directory/project/workspace ownership, native durable execution claim, staged revert/compaction state, event ownership and retained payloads. - Rely on the audited native **write-ahead execution claim before runner history reads**, not an `idle` check, plugin mutex or `BEGIN IMMEDIATE` alone. Never set or release the native claim ourselves. - CAS only the completed assistant's content array. Preserve IDs, ordering, text, snapshots, usage and metadata. Commit a content-free idempotency receipt atomically with the change; identical retries cannot delete a second shifted block. - Emit the custom invalidation after commit. Re-read via the native API, invalidate the SDK transcript and pending rotations, and fence late reads against newer invalidations. Add a public-API TUI cache companion and reconnect handling. - Correct native Windows path encoding; support more than 4,096 selected parts within explicit request/message budgets. - Provide an independently packable plugin with main and `./tui` entrypoints, deployment/safety docs, and a Windows/Linux/macOS native package CI matrix. Plugin installation remains manual and separately approved. ## Validation performed locally | Check | Result | |---|---| | Server/plugin/SQLite/broker/proxy tests | 72 passed | | UI actions, events, SDK projection and stale-read tests | 60 passed | | Plugin/SQLite/TUI-companion tests under Node 22 | 30 passed | | Server and UI TypeScript checks | Passed | | Production UI build | Passed; existing large-chunk warnings | | Official Windows x64 beta-19419 executable | Passed on a private daemon and generated DB | | Independently packed and installed plugin | Same native test passed, outside the checkout | The native integration test uses the real beta-19271 network client against the official beta-19419 runtime, with a local synthetic provider and no credentials. It verifies actual preview/prune RPC, refusal while a primary generation holds the native claim, both prompt/transaction orderings, idempotent retry, two event subscribers, native re-reading, the subsequent primary model payload, fork isolation, pre-compaction history without summary changes, and persistence after server restart. The new three-OS native CI matrix must pass on this head before merge. No CI result is inferred from the local Windows run. ## Boundaries and remaining validation - This is a reviewable opt-in implementation, **not general availability across OpenCode versions/providers/platforms**. Exact runtime gating is intentional; don't widen it without re-auditing/testing. - Interactive official TUI, two native CodeNomad windows/scroll behavior, WSL, provider-specific continuation state and budget/token estimates are not claimed as validated. TUI companion tests use its cache contract, not an interactive terminal. Two HTTP subscribers are not two GUI windows. - Existing bulk cleanup is per-message and reports failure counts; dedicated installation/capability and bulk progress/cancel UI are not added here. - Existing summaries/native checkpoints, fork copies and already assembled/sent requests are not retroactively scrubbed. Historical usage remains historical usage; mock token values are not a token-savings measurement. - This changes persisted V2 content, not only visibility. It frees reusable SQLite pages but does **not** promise a smaller physical file. No VACUUM, V1 cleanup, durable-event rewriting, automatic backup restoration or universal repair is included. - No active user DB was changed, no shared-daemon plugin installed, no shared OpenCode service stopped/upgraded, and no native desktop executable/profile rebuilt or replaced. Tests use isolated synthetic data only. ## Documentation - [Request flow and validation](dev-docs/SESSION_PRUNING_RPC.md) - [Audited safety boundary](dev-docs/SESSION_PRUNING_SAFETY.md) - [Explicit deployment and recovery](dev-docs/SESSION_PRUNING_DEPLOYMENT.md) References: anomalyco/opencode#44984, anomalyco/opencode#48043, anomalyco/opencode#48090 and the maintainer's plugin/RPC direction. No upstream API or distribution fork is introduced.
5.3 KiB
Pruning safety boundary
This is an internal storage contract, not a public OpenCode lock API. The
source audit used upstream f91c6d8b25; the executable tests independently target
the published 0.0.0-beta-19419. Version strings alone do not certify a modified
binary. Runtime version labels do not enable or disable pruning: actual storage,
identity and claim checks decide each request. We do not support custom Core
patches or arbitrary direct SQL writers.
Native ownership
session/execution.ts:Execution.Startedcommitsstore.claimbefore the runner. Success/failure/deliberate interruption releases on terminal commit. Shutdown keeps the claim for recovery. The plugin never impersonates these lifecycle events.session/run-coordinator.ts: the started hook completes before the first drain; queued/steered successor drains stay within the same busy period. The coordinator is process-local, so the plugin does not rely on its in-memoryisActivevalue.session/store.ts: the durable claim issession_v2.time_suspended. Under our SQLite write lock either the native claim committed first (refuse) or its write waits until we finish (the runner subsequently reads the pruned history).session/history.ts/runner/index.ts: runner history is loaded from SQL, not a permanent cached copy of completed assistant messages. The native smoke test proves this for a subsequent primary request and after restart.session/projector.ts/bus.ts: native projections run transactionally. The plugin changes only an already-completed assistant row's content, with exact-data CAS; it does not manufacture sequence values or use a removed replacement event.
Move/revert controls do not constitute a plugin lock. Directory/project/workspace identity and staged-revert/compaction state are rechecked inside the transaction. Other native writes serialize on SQLite; the plugin preserves snapshots, metadata, IDs and ordering. A concurrently staged revert may apply after the prune, just as it can apply after another committed operation. It must not restore deleted payloads.
session/generate.ts is deliberately different: it previews history and issues a
read-only auxiliary request without claiming or mutating the session. Readers that
captured a pre-commit snapshot, including HTTP readers and auxiliary generations,
may finish with that old snapshot. Pruning is not retroactive cancellation or a
confidentiality barrier for requests already assembled/sent. Future fresh reads
observe the committed history. Titles and already-written summaries are not scrubbed.
Identity, transaction and replay
- Require an explicit pruning request and compatible storage, an absolute existing local
DB path, native session ownership matching the plugin location, and a fresh random
ctx.storagechallenge. A same-session backup is not enough: the challenge must be visible through the candidate connection in the same DB'skvtable. - Use synchronous
node:sqlite,busy_timeout=0, thenBEGIN IMMEDIATE. Do not await anything while holding the write lock. This avoids blocking a native async transaction that needs the same JS event loop to complete. - Recheck identity and claim. Reject unknown schema/query failures, attachments, triggers, remote event ownership or retained event rows for the aggregate. There is no event rewrite/replay repair and no V1 table mutation.
- Bound/read the row, check assistant completion, completed/error tool state,
known content kinds, selected technical indices and canonical SHA-256 revision.
Update only
$.content, with CAS against the exact stored row data. - Insert a content-free receipt in the same transaction; commit or roll back both. Identical requests cannot accidentally delete the next block after indices shift. Receipt retention is intentional; automatic GC/expiry is not part of this patch.
- Emit the custom event after commit. A failed notification cannot undo the write. The caller re-reads via the native API; reconnect reconciles authoritative data.
This does not globally coordinate arbitrary tools that directly modify opencode.db,
other unsafe plugins, or multiple independent Core servers sharing the same DB.
Use the supported single shared daemon; other clients may connect to it normally.
Meaning of deletion
- UI: re-read stored history; not a hide flag. The SDK projection is invalidated too, so a later native delta cannot resurrect deleted parts from its old cache.
- Model context: future primary reads omit deleted technical blocks when that history is in the context window. A copied summary/native checkpoint or fork is a separate representation and is not recursively purged. Provider-specific state continuation and budget estimates need their own validation.
- Persistence: only selected V2 content arrays change. Usage/cost record past execution, not an estimate of the next prompt; they are intentionally preserved.
- Physical storage: freed SQLite pages become reusable. The DB file need not shrink until separately consented maintenance such as VACUUM. No such maintenance or automatic backup restore is performed by this plugin.
SQLite integrity, application replay, UI cache state and provider payload correctness
are distinct checks. Do not swap the earlier pruned.db experiment into a real daemon.