@becos-ai/becos-opencode-v2
v0.28.9
Published
becos for OpenCode V2: default-on live capture and automatic context, append-only lifecycle facts, opt-in tools, and explicit non-fork backfill.
Readme
becos for OpenCode V2
TODO — entity history (file history) is NOT enabled. ⚠ Enabling it causes the prompt-cache-miss bug: it still rides the system prompt, so each newly touched file rewrites the cached conversation. It must move to session instruction entries before release (
PENDING_V2.md).VONIC_CODECOLLAB_ENTITY_HISTORYstays off by default and must not be turned on, by default or in an install, until thorough testing and code evaluation are done. See the checklist at the top ofPENDING_V2.md.TODO — server-side recall dedup is required. Recall repeats facts already sent in the session; the backend should exclude them and use the budget for new items. See
PENDING_V2.md.
0.28.8: fixes a prompt-cache miss: recall and static context are now session instruction entries, so later prompts read the conversation from cache instead of rewriting it (one measured Claude session had rewritten ~340k tokens per prompt). See CHANGELOG.md.
Local file-persistence correction: OpenCode's tool executed marker means
provider-executed, not “the host ran this tool.” Completed native local calls
use false; only those contribute tool-derived local file operations. Errors,
unfinished/uncalled operations, provider-hosted calls and missing markers do not.
This is verified with an isolated OpenCode 2.2.5 fake-model probe; the SDK target
remains 2.0.32. No filesystem scan, backfill, upload-owner or core screening change.
becos_status.collector.persisted now counts files in validated core sealing
summaries, not zero-exit subprocesses. persistInvocations, persistUnchanged,
persistSkipped (files), persistPolicySkips (invocations), persistPartial and
persistFailures distinguish outcomes. Partial results count their saved files
and one partial failure; empty/malformed output is a failure, not proof of storage.
Mirror refresh remains separate. shared_handoff.py persist-turn is the existing
screened path for an explicitly authorized, path-scoped manual repair; do not
duplicate its uploads with conversational save tools.
Run the no-backend native regression with:
OPENCODE_V2_BIN=/absolute/path/to/opencode bun run test:persistence:v20.27.0 rebrand: commands are /becos-*, tools and permission actions are becos_*,
and client RPCs are becos.ops / becos.notices. The npm package is
@becos-ai/becos-opencode-v2, with CLI becos-oc-v2 (bin/becos-oc.mjs).
Re-vendored the shared core at 77acf91, matching OpenCode V1 c5d6be5.
Existing directory-based installs need no configuration change. Reload the server plugin and
reopen the terminal UI to replace its already-registered commands; do not restart the shared service.
Environment variables, oc-codecollab.env, ~/.cache/codecollab/, backend vonic_* tools,
and evidence/context tags keep their existing names for compatibility. No legacy aliases are
registered. If you configured explicit permission rules, rename their codecollab_* keys to
becos_*; mutations still require confirmation and configured denials stay final.
Generated-file ownership (0.26.4): native context (on by default; options.context=false opts out) carries a fresh local
upload-owner declaration on each request; project tools also carry it. Only enabled nocode pipelines
declare client_hooks; options.capture=false, file opt-outs, Git scope and Coding Mode declare
none. Do not duplicate hook uploads using conversational save tools or claim success without a receipt.
Capture defaults ON when this plugin loads. Set
options.capture=falseto opt out, orVONIC_CODECOLLAB_DISABLED=1to stand down. Capture covers only fully observed new live turns and append-only fork/revert facts. Fork history is never backfilled. SeePLAN.mdfor remaining acceptance work.
Independent port of @becos-ai/[email protected], preserving its Git history and vendored
Python core. Targets OpenCode 2.0.32. Package: @becos-ai/[email protected].
Not a production replacement for V1. Includes a native status tool, opt-in tool/command preview, verified V2 normalization and moved-session segmentation, authenticated paginated backfill, and offline/native-host contract tests.
Automatic capture is on by default, with options.capture=false as the explicit opt-out.
Automatic context injection is also on by default, with options.context=false as the explicit opt-out. Shared-handoff mutations
and project switching are implemented behind the host's own confirmation prompt (options.tools=true).
Per-turn file persistence for projects without a git repository runs with capture.
A ./tui export provides the terminal surface: status, sign-in, the project picker and the shared inbox.
The inherited V1 implementation remains as migration input and regression tests;
it is not the V2 entrypoint.
Published to public npm as @becos-ai/becos-opencode-v2 by CI on a version tag (npm Trusted Publishing,
same as V1); see docs/RELEASING.md. Install with
{ "plugins": [{ "package": "@becos-ai/becos-opencode-v2" }] }, then bunx @becos-ai/becos-opencode-v2 login.
Development
Requires Bun, Node 22+, and Python 3.
bun install --frozen-lockfile
bun run typecheck
bun run build
env -u OPENCODE -u CLAUDE_AGENT_SDK_VERSION -u CURSOR_VERSION -u VONIC_CODECOLLAB_CLIENT_TAG bun run test
bun run check-vendorTo load the plugin in a separate V2 profile, point plugins at this repository's directory,
not dist/plugin.js. Do not replace your working V1 config:
{
"$schema": "https://opencode.ai/config.json",
"plugins": [{ "package": "/home/amitoj/oc-v2-codecollab-plugin", "options": { "capture": false } }]
}The native-host probe creates and removes its own disposable XDG profile, imports artificial history, and calls no model or production CodeCollab service:
OPENCODE_V2_BIN=/absolute/path/to/v2/opencode bun run test:smoke:v2
OPENCODE_V2_BIN=/absolute/path/to/v2/opencode bun run test:package:v2
OPENCODE_V2_BIN=/absolute/path/to/v2/opencode bun run test:history:v2The history probe additionally runs real executions, mid-turn compaction, interruption, restart and moves against a local fake model. CodeCollab tool execution in that probe uses a mocked Python transport. It contacts neither real model providers nor production CodeCollab services.
Opt-in tool/command preview
In a separate V2 profile, configure the directory entry with "options": { "tools": true }:
{
"$schema": "https://opencode.ai/config.json",
"plugins": [{ "package": "/home/amitoj/oc-v2-codecollab-plugin", "options": { "tools": true } }]
}This registers tools without making startup network calls. Invoking them uses your real CodeCollab login/backend unless a test injects a mock; don't invoke them as production smoke tests.
| Tool | Preview actions |
|---|---|
| becos_recall | Standalone query, or omit it to search this turn's own prompt (V1 parity) |
| becos_team | Team-activity query |
| becos_rulebook | list, or decide only for the user's explicit choices |
| becos_project | list, or use only for the user's explicit choice |
| becos_handoff | Reports this session's automatic handoff, or fetches on demand with budget_chars (unavailable with context: false) |
| becos_handles | me, search, resolve; claim behind a confirmation prompt |
| becos_share | draft inspects selected files and writes a local review manifest; confirm sends it behind a confirmation prompt |
| becos_shared | list, get, seen; accept, decline, download behind a confirmation prompt |
| becos_backfill | Dry run by default; deliver: true ingests past sessions behind a confirmation prompt |
Native commands: /becos-recall, /becos-team, /becos-rulebook,
/becos-project, /becos-share, /becos-shared, /becos-backfill. No global command files are written.
Rulebook/project changes retain the explicit-request convention. Shared/project opt-outs are checked
per session on each invocation; commands/tools remain visible but report disabled.
Confirmed mutations
Five shared actions and the project switch change something outside this session, so each one calls
permission.assert before any subprocess runs and performs nothing if the user declines:
| Action | Permission action | Resource shown |
|---|---|---|
| becos_handles action=claim | becos_shared_handoff | handles claim @ada |
| becos_share action=confirm | becos_shared_handoff | share confirm @ada (addressee, else the draft id) |
| becos_shared action=accept | becos_shared_handoff | shared accept <handoff_id> |
| becos_shared action=decline | becos_shared_handoff | shared decline <handoff_id> |
| becos_shared action=download | becos_shared_handoff | shared download <handoff_id> <artifact_id> |
| becos_project action=use | becos_project_switch | project use <name> |
| becos_backfill deliver=true | becos_backfill_deliver | backfill deliver <scope> |
A permission.hook("evaluate") registered with the tools escalates both actions to ask, so a
configured blanket allow still prompts; a configured deny stays final and never reaches the hook.
Nothing is saved for next time (save: []), and a host rejection propagates as the host's own error
rather than becoming tool output the model could ignore. seen stays ungated: it is an idempotent
receipt that discloses no content and transfers nothing. The native acceptance probe proves the
prompt under a blanket allow, and that a decline leaves the shared core uncalled.
Python calls have bounded input/output, timeout/cancellation and unload cleanup. Scope/environment is
resolved from the invoking session, never the plugin instance's directory. The global env file
respects XDG_CONFIG_HOME; repository env files cannot override the interpreter, process PATH/HOME/XDG
roots, capture opt-ins or body caps. Core auth, screening and delivery logic stays vendored unchanged.
Context channels (options.context)
The whole request-context surface is on by default; options.context: false turns all of it off. Each
channel can also be narrowed independently, under V1's flag names and V1's reading of them, so "instructions but not
recall injection" is expressible here as it is there:
| Channel | Flag | Default |
|---|---|---|
| Recalled memory | VONIC_CODECOLLAB_RECALL_INJECT | on; 0 skips the backend query entirely, not just the block |
| Accepted rules | VONIC_CODECOLLAB_RULEBOOK | off; 1 enables |
| Project line | VONIC_CODECOLLAB_PROJECTS | on; 0 disables |
| Startup handoff | VONIC_CODECOLLAB_HANDOFF_AUTO, VONIC_HANDOFF_SESSION_START | on; either set to 0 disables |
| File history | VONIC_CODECOLLAB_ENTITY_HISTORY | off; not enabled pending evaluation (PENDING_V2.md) |
| Subagent recall | VONIC_CODECOLLAB_RECALL_SUBAGENTS | on; 0 excludes dispatched subagents from both the tool and injection |
One deliberate divergence: V1 defaults RECALL_INJECT off, because there the silent injection is
itself the opt-in. V2 defaults it on, like the Claude Code plugin. Instructions have no adapter-side switch — the unchanged core composes them from
VONIC_CODECOLLAB_PROVENANCE, ..._RECALL_FEEDBACK, ..._RESOLVE_TOOL, ..._ENTITY_HISTORY and
..._TEAM_ACTIVITY, and turning all of those off already yields an empty section; a second answer to
the same question would only drift. becos_status reports the resolved channels.
Terminal surface (./tui)
The package exports ./tui beside the server plugin, so OpenCode loads it automatically with the
plugin; a CLI-only install can list the package in cli.json to keep it active against a remote
server. A directory entry in opencode.json is resolved by physical path (<dir>/server,
<dir>/tui) rather than through exports, which is why server.js and tui.js shims sit at the
repository root. CLI plugins load when the terminal starts: unlike the server plugin, which the
running server hot-reloads, a new or changed terminal surface needs the TUI restarted.
It adds /becos-status, /becos-login, /becos-project and
/becos-inbox (also in the command palette, under "becos"). The terminal's inbox read has
its own name because /becos-shared is the server plugin's model-facing route; one slash name
never resolves to two different surfaces.
The surface owns no logic: every action is one becos.ops RPC call into the server plugin,
which owns the Python core, the environment and the screening. It spawns no process, reads no
credential and contacts no backend itself. If no CodeCollab server plugin answers, the commands
report that and the terminal keeps working.
- Sign-in runs the unchanged
connect.pybrowser flow through the server: the core's own message (link plus code) is shown verbatim, the code is pasted back into a prompt, and the token is written by the core into this profile's cache. The adapter never asks for a raw token, and an invalid code is refused locally before any process starts. - Project picker lists the user's projects for this folder and records only the chosen one. A folder whose project comes from its git origin is explained instead of offered.
- Shared inbox is read-only here. Accepting, declining or downloading a package stays with the gated tools so it always passes the host's confirmation prompt.
- Notices are pushed by the server, not polled here. The server plugin emits an RPC event on
becos.noticesat the moment it knows something: how much recall it injected on a turn, how much prior-session evidence the startup handoff added (once per session), and the session's project line (again only if the session is re-filed). The terminal subscribes and draws them; it decides nothing. That restores V1's arrangement, where the server plugin calledshowToastitself — V2's server context has nouidomain and a subscribe-onlyeventdomain, so an RPC event is the equivalent. A missing sign-in is still read fromstatus(no request produces it), and waiting shared handoffs are still polled once per idle turn, because the server has no arrival event to push from. - A client with no terminal — headless, desktop, a remote session — renders no notices. The pull
surfaces are
becos_statusandbecos.opsstatus. Notices are never routed into the model's request context as a substitute.
becos.ops methods (status, login, project, inbox) are user-initiated from a client of
this server and are not reachable by the model, whose path is the gated tools above. Identifiers are
passed as arguments the core parses, never as option fragments, and a failing child is reported as
becos unavailable (<status>) so stderr, paths and tokens never cross the boundary.
Explicit CLI
node bin/becos-oc.mjs --help
node bin/becos-oc.mjs backfill # dry run; reads session history
node bin/becos-oc.mjs backfill --project myrepo # directory substring filter
node bin/becos-oc.mjs backfill --session ses_example # exact id or prefixBackfill discovers an already-running, compatible V2 service in the current XDG profile and
uses its authentication. It never starts an arbitrary opencode from PATH, stops your service,
or reads the private database. Select the intended XDG profile explicitly if V1 and V2 coexist.
Discovery fails before any upload for unreadable/changing history, malformed pagination, unsettled
messages, mid-turn/inconsistent moves, and staged reverts/forks (the selected policy is not wired yet).
Native failed/interrupted terminal markers settle partial replies as error turns; running/streaming
tool inputs are excluded. A failed/unexecuted edit is an attempted tool event, not an applied change.
Shell working directories are attributed independently. Settled moves are split into stable segments:
the first retains the original session ID, later segments use a suffix derived from the move message ID.
Each segment uses its own cwd/environment and upload-manifest key. Remote-server selection is pending.
--deliver is an explicit opt-in to the inherited Python upload pipeline; production delivery
has not been acceptance-tested for this V2 alpha. --watch additionally waits for positive delivery
and distillation receipts. Do not run production delivery as a smoke test.
becos-oc-v2 login [authorization-code-UUID] delegates PKCE directly to the shared Python
core. It never installs command files or rewrites V1's environment/mint scripts. Login uses the
shared CodeCollab credential cache; it is not isolated automatically and must be requested explicitly.
Incremental capture (default on, explicit opt-out)
Optional automatic context
Automatic recall is on by default, independently of capture and tools, so each new prompt
queries memory once. Set "options": { "context": false } on the plugin entry to opt out.
VONIC_CODECOLLAB_DISABLED=1, VONIC_RECALL_ENABLED=0 or VONIC_RECALL_SCOPE=off stand down.
Carrier (0.28.8): a session instruction entry. The prompt hook queries the unchanged
recall.py core once per user prompt, before admission, and stores the result as OpenCode's
session.instructions.entry under key becos-recall. The host shows the first value at the start of
the session's instructions and each later value as a chronological "changed and supersedes" update,
so the request prefix stays the same between prompts and the prompt cache holds. Recall previously
changed the system prompt on every prompt, which invalidated the whole cached conversation (about
340k tokens rewritten per prompt in one measured Claude session; see docs/PLAN_cache_miss_fix.md).
The static sections (instructions, upload ownership, project line, accepted rules and the startup
handoff) are entries too (becos-instructions, becos-persistence, becos-project,
becos-rulebook, becos-handoff). They are put at admission, so the first request has them, and
written again only when their bytes change; a change becomes one update message. A failed section
lookup or scope check leaves the last good values in place. Opt-in file history stays a system part.
A prompt repeated verbatim in the same scope and configuration keeps the recall already in place
instead of repeating an identical lookup (recallReused). A continuation prompt (exactly
"continue", "go on", "yes", "ok" or "proceed", ignoring case and trailing punctuation) keeps the
previous prompt's recall without a lookup (recallContinued); anything longer is looked up.
A recalled item already delivered in this session is replaced by an inline pointer in its ranked
place: - (still current; full text sent earlier this session: decision_…), about 77 characters
against a median 396 for the item (recallPointers, recallPointerSavedChars). The wording matters:
the host labels earlier recall blocks superseded, and in live tests (3 Haiku + 3 Sonnet runs) the model
answered a pointed-to fact correctly every time, but said UNKNOWN every time when the repeat was simply
dropped. Items are sent in full again after compaction, a revert, or a removed entry. Text in an
unrecognised format passes through unchanged.
A lookup may hold a prompt for at most 20 seconds. Past that the prompt goes through and the
result is applied while its turn is still running, reaching the next model step; a result whose turn
has ended, or that a newer prompt replaced, is dropped. A lookup failure never rejects the prompt.
An empty result, a slash command, an oversized prompt or VONIC_CODECOLLAB_RECALL_INJECT=0 removes
the entry, so the previous prompt's recall never reads as current. A host without instruction
entries keeps the previous per-step system-part carrier.
Retention. Recalled memory is now stored in OpenCode's local session database, appears in
session.context and session exports, and (host behaviour) survives session deletion in its
instruction_blob table. Treat exports as containing team memory. Live capture and backfill still
exclude it.
The lookup uses only the newly submitted user prompt and the session's current cwd. It never extracts a query from model history,
files/attachments, compaction summaries, synthetic messages or pre-admission drafts. No old prompts
are queried after attaching/reloading mid-turn. A dispatched subagent gets recall on its own task
(caller_mode: subagent, at most four at once per parent, VONIC_CODECOLLAB_RECALL_SUBAGENTS=0 turns
it off); a primary fork child is treated as primary.
Returned context retains core provenance/citations and receives an explicit retrieved-evidence warning. User input is unchanged, and context is not copied into capture. Actual assistant answers still capture normally. Explicit recall-tool calls remain independent.
On the system-part fallback (hosts without instruction entries): every model step checks local scope, and recall is cached for the current delivered prompt + core repository/project identity (branch and author, not the commit) + VONIC configuration; prompt/scope/config changes requery.
Bounds: 32 observed sessions, 128 queued items, 20 KiB query text, 20,000 characters of injected core output, and a 25-second overall lookup deadline. Adapter queries/results remain in memory only; the core's explicitly enabled debug logging keeps its inherited behavior. Interruption, move, revert, shutdown or stream loss cancels obsolete work; scope/env is checked again before injection. Unavailable/malformed/oversized output fails open without raw diagnostic output. Unknown deliveries, oversized prompts and slash commands do not reuse the preceding prompt's recall.
The same context surface supplies these additional request-only channels:
- Instructions: compact/legacy provenance, feedback and resolve guidance come from the unchanged core text selectors. Static parts are reattached on each outgoing primary request, including after compaction. Explicit instruction flags remain authoritative; unavailable team/entity tools are not advertised. Instruction copies are suppressed in the recall subprocess to avoid legacy duplication.
- Rulebook:
VONIC_CODECOLLAB_RULEBOOK=1injects only accepted cached rules, never candidates. A plugin-owned, cancellable five-second refresh updates the shared cache (at most one per session at a time, normally at least 30 seconds apart); the next request sees fresh rules. No detached SessionStart refresh process, automatic acceptance, or project selection is launched. - Startup handoff: the fetch starts at the session's
session.createdevent, before any prompt, for primary sessions only, and is never awaited. It then follows V1's lifecycle exactly (pending → ready → bound/delivered,empty,failed), defers past a turn already in flight, and is re-applied to each later request without duplicating. One logical fetch per session with at most one retry; the active session is excluded by the shared core.becos_handoffreports that state or fetches on demand with its ownbudget_chars.VONIC_CODECOLLAB_HANDOFF_AUTO=0orVONIC_HANDOFF_SESSION_START=0disables it. A resumed or attached session gets no catch-up fetch, and fork ancestry is never hydrated. The backend scopes a handoff to the authenticated tenant and the repository, so a teammate's captured work on it is included and every turn names its author; the adapter adds that attribution to the framed block because the vendored upstream preamble still describes the older caller-only scope — see docs/HANDOFF.md for the full contract and the V1/V2 parity table. - Project: the session's project is resolved once per repository identity (refreshed on a
60-second TTL) through the unchanged core, which pins a dotfile result so a mid-session edit cannot
re-file later turns. The model is told where the conversation is saved; a line resolved for another
identity is dropped rather than shown.
VONIC_CODECOLLAB_PROJECTS=0disables it. No picker, backend write or detached list refresh happens here — switching is the confirmedbecos_projectaction or the terminal picker. - File history: with
tools: trueandVONIC_CODECOLLAB_ENTITY_HISTORY=1, native reads and edit/write/patch attempts use the unchanged entity-history core's path screening and once-per-file claims. History is queued into the next primary request, not appended to tool output. An unseen file's edit is refused once with a generic retry message; it does not put the retrieved text into a durable tool error. Current scope/cancellation checks remain fail-open. Unknown/MCP names are never inferred to be native file operations.
Carrier. Context is added as a tagged SystemPart by default. @openchamber/opencode-claude
replaces the system prompt with a Claude Code preset and never reads the request's system array, so
for claude-code models the same content is prefixed onto the request's own copy of the latest user
text part instead (<codecollab-context>), copy-on-write and idempotent. The stored conversation is
untouched either way, and capture never reads model messages. Without this guard the whole context
channel is silently dropped for that provider — V1 made the same distinction with
receivesSystemTransform.
Native Claude Agent (claude-agent, OpenCode 2.2.5). This is not the external claude-code
bridge. Its built-in hook snapshots system instructions before external plugins run. Becos keeps
its own recall and appends all its request context to the native claudeCodeInstructions option,
preserving the host snapshot. Only Becos's tagged suffix is replaced on reused requests; user
messages and stored history are untouched. Claude hooks remain disabled by the native provider.
Delivery diagnostics. becos_status.context.attached counts outgoing request attachments, with
systemAttached, claudeInstructionsAttached, and promptAttached identifying the route; cacheHits
counts reused lookups. injected is a compatibility alias for attached, not proof of model receipt.
modelReceipt: "unverified" is explicit: this adapter has no provider acknowledgement or model-side
receipt. Recall notices say “attached to request.” Run OPENCODE_V2_BIN=/path/to/opencode bun run
test:context:v2 for the isolated real-host snapshot-order/cache regression (fake model/core, no live
Claude or backend). A real-Claude end-to-end receipt check remains outstanding.
The host may start a turn's first model request before the plugin's event stream reports that turn's delivery. The context hook therefore waits up to 1.5 seconds for that live boundary (never reading model messages instead); if it still has not arrived, that request proceeds without CodeCollab context.
Additional channels are capped at 48,000 characters per request (instructions 12k, accepted rules 6k,
handoff 8k, then newest file history within the remainder), separate from the 20k recall cap. At most
16 file-history sections of 15k characters are retained per observed session; concurrent file lookups
are capped at eight, with a ten-second deadline. The core owns existing rulebook/entity claim caches;
the adapter writes no raw context journal. File-history text is not reconstructed after a plugin
reload: core once-per-file claims retain their existing lifecycle behavior. Local repository identity is rechecked before injection:
branch/commit/project or environment changes cannot reuse stale sections. Legacy Claude SessionStart
hooks are not loaded; the adapter-owned oc_context.py calls core functions directly.
The native acceptance probe (bun run test:acceptance:v2, with OPENCODE_V2_BIN set) uses only a
loopback fake model/MCP server. It verifies direct MCP/Code Mode settlement, opaque argument exclusion,
shared-core result screening, request-only recall, actual plugin hot reload and a fault-injected
subscription disconnect. The pending provider response is explicitly gated during reload/disconnect;
partial turns are skipped and fresh turns resume without replay.
Native Claude Agent (claude-agent, OpenCode 2.2.5). This is not the external claude-code
bridge. Its built-in hook snapshots system instructions before external plugins run, including
instruction entries, so all Becos context reaches it through the host. Only opt-in file history, and
the fallback carrier on hosts without entries, still append to the native claudeCodeInstructions
option, preserving the host snapshot. Only Becos's tagged suffix is replaced on reused requests; user
messages and stored history are untouched. Claude hooks remain disabled by the native provider.
Delivery diagnostics. becos_status.context.attached counts outgoing request attachments, with
systemAttached, claudeInstructionsAttached, and promptAttached identifying the route; cacheHits
counts reused lookups on the fallback carrier. recallCarrier names the recall carrier;
entryPuts, entryRemoves, budgetExpired and lateDropped count instruction-entry activity.
Prompt-cache diagnostics. becos_status.context.cache reads the host's own per-step token counts
(session.step.ended). A step whose cache read falls below half of the previous request's prompt size
counts as missSuspected, unless more than 5 minutes passed (coldAfterIdle) or the session was just
compacted (afterCompaction); last holds the latest step's read/write/input. missSuspected should
stay 0. bun run test:context:v2 asserts that every request starts with the previous request's
messages byte for byte (and fails on the old system-prompt carrier). bun run test:cache:live
[model] is a manual probe with real Claude and real recall (uses quota): it requires every request to
read at least 90% of the previous one from cache, across a tool step, an interrupt and a restart. injected is a compatibility alias for attached, not proof of model receipt.
modelReceipt: "unverified" is explicit: this adapter has no provider acknowledgement or model-side
receipt. Recall notices say “attached to request.” Run OPENCODE_V2_BIN=/path/to/opencode bun run
test:context:v2 for the isolated real-host snapshot-order/cache regression (fake model/core, no live
Claude or backend). A real-Claude end-to-end receipt check remains outstanding.
Observed capture and routing
V2 2.0.32 does have incremental APIs: the prompt hook, accepted/enqueued/delivered inbox events,
finalized assistant text, tool calls/results and execution-terminal events. A validated adapter can
collect these as they happen and submit assembled turns to the existing shared delivery flow without
re-reading the full transcript. It must not rely on session.context(), which compaction can truncate.
Loading the plugin without a capture option enables capture independently of preview tools.
The example above explicitly disables it. capture: true also enables it; capture: false disables
both live observation and startup retry. tools: true is still required for preview tools/commands.
src/v2/collector.ts correlates enqueued/delivered IDs, execution boundaries, finalized text,
tool settlement and steps. Draft prompt hooks, deltas, attachments and provider state are not journaled.
Execution boundaries may omit location; only a previously observed session supplies that scope.
Compaction does not clear collected evidence. Unknown deliveries, disconnects and bounds violations
discard incomplete executions; only new fully observed executions resume.
Per-turn file persistence
With capture on and shared handoffs enabled, each completed turn's created/modified paths are handed
to the unchanged shared_handoff.py persist-turn. Projects without a git repository only — but
the core decides that, not this adapter: it resolves the project, owns VONIC_CODECOLLAB_PERSIST_FILES
and the host stand-downs, screens the bytes, applies the 10-file / 25 MiB caps and always exits 0.
The adapter checks only the shared-handoff flag and "there is nothing to persist", selects paths from
the same normalized turn it just delivered (no second transcript parse), and never awaits the child:
hashing, screening and uploads cannot delay observing the next turn. At most eight run concurrently;
a crashed or failing child only increments a local counter. Persisted files stay private to the user
until a shared handoff attaches them by reference.
Exactly one runtime owns a conversation. Under OpenCode's Claude bridge (OPENCODE=1 plus
CLAUDE_AGENT_SDK_VERSION), the cc runtime stands down — capture.hosted_by_opencode() — while
this adapter keeps capturing; a plain claude started from an OpenCode terminal has no SDK marker
and keeps its own capture. A regression test pins both directions.
Raw evidence stays in bounded memory (32 sessions, 512 KiB/session, 8 MiB total; delivery queue
16 jobs/8 MiB). Overflow skips work and increments local status counters, never triggers backfill.
The adapter-owned deliver/oc_incremental.py bridge serializes deltas into the unchanged shared
core's cumulative snapshot boundary. Body persistence occurs only after core screening. Stable source
IDs deduplicate appends, including a retry after recording but before the adapter receipt was written.
On attachment, only this adapter's already-screened pending buffers for the exact location are retried.
VONIC_CODECOLLAB_DISABLED=1 stands down. Unscreened handoff failures receive three bounded in-memory
attempts, then are counted as rejected; they are not recovered from history after a crash.
This is not production acceptance. Moves discard any in-flight partial execution, never relabel
prior evidence. Observed committed moves now produce structural checkpoints shared by location
instances through the existing local cache. Their canonical message ID is derived from the moved
event (evt_ → msg_, the pinned host reducer's SessionMessage.ID.fromEvent rule), not the
move inbox ID. Each finalized live turn carries its terminal durable sequence; the Python bridge
selects the corresponding observed boundary and exact cwd. The first segment retains the source
session ID; later segments use :oc-location:<16-hex SHA-256(move-message-ID)>, identical to explicit
history normalization. Moving back creates another segment, not a merge into the original buffer.
deliver/oc_locations.py stores only validated IDs, sequence numbers and local directory references
(10,000 source-session checkpoints, 1,024 moves/session). It reads no transcript and ships no paths
in a new wire field. Location instances use their own environment for delivery. A separate bounded
structural lane prevents move checkpoints waiting behind backend calls; an observed move hint must
match the durable checkpoint before body recording. Duplicate checkpoints are idempotent; stale or
conflicting controls and cwd mismatches fail closed. Conflicting backfill buffers remain refused.
Reload can reuse already observed structural checkpoints to route new complete turns; it does
not reconstruct missed moves. A later newly observed move can establish a fresh anchor after a gap.
Shared-core retention rules still apply to pending turns.
No historical catch-up. Deliberately ignored: conversations before activation; the partial turn when attaching mid-turn; events missed while unloaded/disconnected; and unobserved intervals after a crash. Skip incomplete turns and resume with new fully observed prompts/turns. Do not launch history/replay/backfill jobs to fill those intervals. Retries/deduplication for already-observed, privacy-screened pending records remain in scope.
The native context lacks message.list() while the HTTP client exposes it. That does not block this
prototype: it neither rereads V1-style transcripts nor reconstructs unobserved intervals. The existing
backfill CLI stays explicit/user-requested and must not be dispatched for a forked session. No implicit
server discovery or private-DB access is added to the collector.
For forks, src/v2/lifecycle.ts records the observed child/parent and before/through boundary as a
reference-only lifecycle fact. Subsequent child turns link through the same session_id; inherited
text/tool evidence is never replayed. Nested forks form immediate-parent links, without hydrating ancestors.
Bulk explicit backfill reports and excludes forked sessions before transcript reads; explicitly selecting
a fork is refused. Staged-revert backfill remains refused separately.
Reverts append distinct staged, cleared and committed facts. Staging is not described as a completed history rollback; cleared events do not include a target, so none is invented. Previously captured turns are never deleted or rewritten. Collection resumes with subsequent complete observed executions.
deliver/oc_lifecycle.py validates only structural IDs/enums/timestamps before persistence and emits
vonic_remember(mode="fact") with event_type="other", other_type="opencode_<type>" and the
reference under metadata.event.metadata.lifecycle. These records are not fabricated dialogue.
Lifecycle facts use a separate bounded local spool (10,000 records including dedupe tombstones), immutable
arguments and source-event-derived IDs. Existing core repository scope/auth/receipt helpers are reused;
unscoped writes are rejected. Pending facts retry at attachment (bounded batch) and on subsequent turns;
accepted receipts retain tombstones, terminal rejections are parked. No backend contract or vendored-core
file changed. See PLAN.md for remaining acceptance work.
See the investigation and future-only upstream proposal. No OpenCode core or installed binary has been modified.
See PLAN.md, DECISIONS.md, and the adapter reference. The original V1 README is archived, not current V2 guidance.
