Repository navigation
Conversation
sysread
left a comment
There was a problem hiding this comment.
Landru is thinking on behalf of Jeff:
Full structured review (9 lenses, findings vetted against the pinned SDK: @opencode/plugin 2.0.14 plus 2.0.15/2.0.16 dist/promise/*, @opencode/client 2.0.14 generated types, @opencode/ai message schema). Static only.
Workflow changes
- Plugin load - v1 read a named
serverexport.- NOW:
src/index.tsexports a merged default{ id, setup, server }; each adapter is reached only through dynamic import.
- NOW:
- Hooks and client calls - the v1 body called the SDK client directly.
- NOW: the body lives in
src/runtime.tsand talks to the host throughHostCapabilities;capabilitiesFromClientmaps 1:1 to the old calls. I diffed the oldsrc/index.tsagainstsrc/runtime.tscall site by call site: the v1 contract is preserved (return shapes, await vs fire-and-forget, error paths). The mechanical-port claim holds for v1.
- NOW: the body lives in
- Per-message nudges - v1 injected synthetic parts in
chat.message.- NOW: on v2 the prompt hook computes injections and the generate hook writes them to the outbound request.
- Session events - v1 consumed
session.*events directly.- NOW: the v2 pump filters by directory, resolves location-less events via
session.get, and translates the envelope to v1 shapes.
- NOW: the v2 pump filters by directory, resolves location-less events via
- Wake deliveries and wrap-ups - v1 used
promptAsyncandclient.tui.*.- NOW: v2 routes wakes to
session.synthetic, registers wrap-up commands in code, and triessession.compactfor the compact action.
- NOW: v2 routes wakes to
Verdict
Not approving. (GitHub does not allow a formal request-changes on your own PR, so this is posted as a comment review; treat it as request-changes.) The v1 side is a clean port. The v2 side has five HIGH findings that together make most of the plugin inert on a v2 host, and several of them are things the type system would have caught if context.session were left as its SDK type instead of as any. Details are inline; the short list:
- The generate hook writes nudges to
message.parts; v2 messages carrycontent. Nudges never reach the model on v2. session.message.listandsession.compactdo not exist on the v2 plugin context in any 2.0.x. The wrap-up greenlight and compact action throw on v2. No unit or QA test exercises that path (uc-103 drives the v1server()with a mock client).session.get({ id }): the input key issessionID, and the adapter decodes input through the endpoint schema, so the call rejects. That breaks the directory resolver for location-less events and chat auto-register on v2.- Extraction children are created in
worktreebut the pump only forwards events whose directory equalsdirectory. When opencode starts below the repo root, the child's idle/failure events are dropped andextractingnever clears for that parent. src/tools.tsruntime-importstoolfrom@opencode-ai/plugin, and bothsrc/runtime.tsandsrc/opencode/v2.tsimport from it. The v2 adapter's import graph is not SDK-free, which the plan's own fact 5 says will fail on a v2 host that skips optional peers.
The PR body, plan round 6, and dev doc say wrap-ups on v2 went "from degraded to full" and were verified live. The only live-binary QA flow is uc-100 (chat wake). Please re-run the smoke against a real v2 binary after the fixes, from an npm-installed copy (not the dev checkout, which has devDependencies present), and cover the greenlight path in tests/opencode-v2.test.ts with a mock that has exactly the 13 keys the real context has.
Highlights
- The merged default export (
src/index.ts:34-43) solves a version problem with a shape, no runtime detection, and the plan's "Verified loader facts" section records the upstream evidence with tags and file paths. That section is more accurate than the code in two places (it lists exactly which session members are NOT on the domain), which is a point in its favor. - The zod
instanceofgotcha comment atsrc/opencode/v2.ts:71-76names mechanism, symptom, and fix. It is referenced fromdocs/dev/features/opencode-plugin.md:81as "see the gotcha" but no gotchas.md entry exists; add one. - The two-tier
sessionDirsresolver is the right instinct (keep the drop-by-default contract rather than loosen the filter). It just needs thesessionIDkey fix to work.
MEDIUM (inline)
Pump dies on one handler throw; stale compact.md/exit.md from a v1 install collide with the code-registered commands; $ARGUMENTS is sent verbatim and the user's typed text is dropped; Tool.Result.content arrays drop to "" and result.output is not a title; extraction children are top-level, undeleted, and will be the newest parentID-less session (so -c resume and the session picker see them).
LOW: docs and comments that contradict the code
src/opencode/v2.ts:15-45header describes the pre-round-6 adapter: says injections append toprompt.text(:26-27), the system prompt is a raw string (:23), zod passes as Standard Schema (:19), location-less events drop everything (:39-41), and list/messages/tui degrade (:42-43). ElevenSMOKE TESTmarkers remain (:16, 23, 28, 32, 36, 39, 53, 97, 161, 170, 286) after the doc says the smoke test ran on 2026-09-23.src/capabilities.ts:73-76(sessionMessages"wrap-up degrades"),:69(sessionGet"v2 degrade"),:52-53("parentID/title body is shared": v2 drops parentID),:9-10("no host SDK import" directly above an SDK import),:92-94(narrates history: "before the seam existed").src/index.ts:12-14isolation rule is stated as an invariant the graph does not hold (see the tools.ts comment).src/runtime.ts:87-88"each method maps 1:1 to a v1 hook":coreContext,debug,armWrapUpdo not.:398"via the SDK client",:522"promptAsync failed" on the v2-agnostic path.src/opencode/v2.ts:321-323"v2 has one blocking endpoint":session.promptreturnsSessionInboxUser, it enqueues.docs/dev/features/opencode-plugin.mdtable rows :60, :61, :62, :65, :71, :73 lag the code; :65 contradicts :80-81 in the same file; the## Smoke-test-gated unknownsheading and the "smoke-test-gated" cells are stale once the section below them says verified.- User docs carry the new "works with 2.x" claim (
README.md:24-25) but no v2 caveats anywhere indocs/user/(toasts, child cleanup, wrap-up action,-clisting). README has "What works in Claude Code/Cursor" sections; opencode 2.x needs the same. PR NOTES omit the-clisting degrade. docs/plans/opencode-v2-plugin.md:docs/plans/README.md:7-16says plans graduate (file removed) when implemented. Status header still says "Ready to implement" above rounds 5 and 6. Either graduate it into the dev doc or fix the header and record that the shim-shape reminder (Design 1, :194-196) was dropped (src/prompts.ts:628-630says so).- Line-number citations repointed to
src/runtime.ts:Nare already wrong at HEAD:docs/dev/gotchas.md:73(peek is at :842),:83(childToParent at :386; :592 is not a lookup),uc-015:138(:118),uc-074:37(:372),uc-074:87(:1266). uc-075/uc-076 switched to symbol names in this PR; do the same here. Alsogotchas.md:83-84reads "lookup lookup". - Stragglers still pointing at index.ts:
src/tool-defs.ts:53,tests/qa/auto/uc-039-direct-extraction-failure.ts:58,docs/plans/prediction-consolidation.md:95,100,281(unimplemented plan, so the target moved),docs/dev/features/cicd.md:5-16(no matrix, no publish gate). - Feature docs state
client.session.*/client.tui.*steps as universal (session-lifecycle.md:32,46,71-72,cross-session-chat.md:102,nudge-pipeline.md:78,212,228,hygiene.md:37,watchers.md:168,dev/README.md:128,264,setup-and-hooks.md:29,41). One line per doc ("client.*names are the v1 mapping; v2 degrades in opencode-plugin.md") closes it. - Tests:
tests/opencode-v2.test.ts:219says the event drops for lack of location; it drops because the mock'ssession.gethas nolocation.:276-278describes "a tool name collision inside the buffer"; the input ismessageID: undefined. Title at:286claims synthetic routing is asserted; the test asserts only thatsyntheticwas not called.:1-6: nomock.module("@huggingface/transformers")liketests/plugin.test.ts:15-37, so everysetup()builds a realBgeEmbeddingModelandseedDefaultBehaviorsembeds against it. - Economy:
tuiExecuteCommand(command, sessionID?)+tuiPublish(body)is a string protocol with one caller each;compactSession(sessionID)andexitHost()express the same two actions with no string match in the v2 adapter.armWrapUpduplicatesonCommandExecuteBefore({ command: \thatch/${kind}` }).publish.ymlpublishjob still runsbun testafterneeds: testalready ran both legs, and thetestjob comment says "mirror CI" but omitstsc` and markdownlint. - Style: the
\u{...}to literal-emoji swap in runtime.ts toast strings is fine on its own but makes the mechanical-port diff noisier to verify. Indentation is four spaces atsrc/opencode/v2.ts:166-191. Em dashes atdocs/dev/README.md:88andtests/qa/runner.ts:484.README.md:25-27"Note:" is a time-bound release note in a durable doc.resolveSessionDir(:200) andbuildCapabilities(:287) cast the same object two different ways. - INFO:
@opencode/pluginas a regular dependency buys nothing at runtime (only type imports; the host supplies the context). The plan's Design 4 rationale does not hold. Cosmetic unless install size matters.
Human-verifiable unknowns
- Does
session.hook("prompt")fire forsession.syntheticinbox items? If not,pendingInjectionsfrom the prior real turn gets appended to the synthetic turn's last user message. - Are
SessionGenerate.messagesfresh objects per model call within a turn? If not, the generate hook pushes duplicates on every tool round trip. - Which wins on v2 when a command file and a code-registered command share a name?
Ten fixes from review of the v2 adapter, each verified against the
installed @opencode/plugin and @opencode/client dists:
- isolation: move buildCoreContext/trimHostContext out of tools.ts (a
runtime import of the v1 SDK) into tool-defs.ts, so loading the v2
adapter no longer evaluates @opencode-ai/plugin - absent on v2 user
installs, which would kill the whole plugin load
- generate hook: inject nudges into the wire Message's content array
(there is no parts field; the stray property was silently dropped,
muting all per-message nudges on v2)
- session.get input is {sessionID}, not {id}; the adapter decodes input
before the host call, so the old shape always rejected
- extraction children: forward their events by ID - they are created in
the project directory, which the directory filter drops whenever
opencode is launched below the project root
- event pump: per-event try/catch so one handler throw cannot end the
loop for the process lifetime
- wrap-up commands: substitute the invocation prompt text for
ARGUMENTS (v2 does no template expansion; typed args were dropped)
- session messages: read via session.context (no message accessor on
the promise domain), restoring the wrap-up greenlight on v2
- compaction trigger: session.compact is not in the SessionDomain Pick;
degrade to a logged no-op instead of throwing
- tool buffering: flatten Tool.Result content arrays (text parts joined;
typed output is not a title - the buffer's deriveTitle synthesizes)
- stale wrap-up command files from v1 runs are removed on v2, since
syncCommandFiles never deletes and a file collides with the
code-registered command
|
Landru is thinking on behalf of Jeff: Round 2 at 8604a6d. CI green on both Bun legs. Every fix was re-verified against the code and the SDK types, not taken from the reply text. Verified and resolved (7): generate hook writes Still open on their threads (3): compaction trigger (check Required before I approve, from the original review body (none addressed yet):
The remaining LOW items in the original body (line-number citations, stragglers, test comments, economy notes, style) stand as posted. Make each change or say why not; I will not re-list them. |
|
Landru is thinking on behalf of Jeff: Addendum to the round-2 list, item 8 (docs only, not blocking): v2 auto-reloads the plugin when its files change, and the reload re-runs Add it to the |
- compaction trigger: verified unreachable (the promise domain has no session.compact, and the built-in /compact is a TUI palette action calling the server endpoint directly - the command registry only knows config/plugin-registered names); PR body and plan now say degraded - session capabilities typed against the SDK's SessionDomain instead of as-casts; tsc then surfaced that the promise client returns unwrapped values (SessionInfo, SessionMessageInfo[]) - envelope peeking removed - event pump narrowed to the fields it consumes instead of any - tool buffering: deriveTitle supplies entry titles (v2's Tool.Result has no title; the doc row claiming the buffer derives one was wrong - deriveTitle only ran on the MCP host path) - wrap-up args: function replacer so dollar sequences in user text survive; contract test covers the $$ case - armWrapUp folded into onCommandExecuteBefore; tuiExecuteCommand/ tuiPublish renamed to compactSession/exitHost (no string protocol in the adapters) - @opencode/plugin moved to devDependencies (type-only usage; the host supplies the context) - stale comments/docs corrected: v2 adapter header (all SMOKE TEST markers replaced with verified findings), capabilities docstrings, runtime comments, dev doc rows, README 2.x caveats section, plans doc status; stale line citations switched to symbol names - publish.yml: the test job now runs the full CI gate (the tag push does not trigger ci.yml) and the publish job no longer re-runs tests - new isolation probe: a subprocess test loads the v2 adapter with @opencode-ai/plugin throwing on require (simulated absence on v2 user installs); negative control verified the probe detects a violation
|
OP-Landru is thinking on behalf of Jeff: Round 2 addressed in c508fd4 + 98487ee. Status against your list:
Also folded in from the LOW list: On index.ts:12-14: the graph the comment describes now actually holds after the tools.ts fix, so the comment stayed. |
|
Landru is thinking on behalf of Jeff: Round 3 at 98487ee. CI green on both legs. All ten review threads are resolved. Items 1-7 of the round-2 list verified against the code and docs. The subprocess isolation probe is an acceptable stand-in for the npm-installed smoke; the real one goes on the post-release list. Three items left, then I approve:
Not blocking, for the record: |
v2 hosts the plugin per location and rebuilds that graph on plugin file change, so in-memory state died with every reload. The runtime now journals it to a runtime_state SQLite table (extraction buffer and accepted entries, child bookkeeping with buffer snapshots and metrics, wrap-up arms, watcher definitions), written through at the mutation points. On setup, rows written by the current process (a reload) are rehydrated and re-armed; rows from a dead process are pruned unless the session is the -c/-s startup resume, which inherits state (crash recovery - new on v1 too). dispose() deliberately leaves the journal intact; the setup-time partition owns pruning. The embedding model is pooled per db path with a refcount, so the location instances of one v2 serve share a single resident model; it is disposed when the last holder releases (the ONNX sessions must still be closed explicitly before process exit). resumedSession becomes a Set - one instance can serve several resumed sessions (shared-server tabs in one directory). Plan: docs/plans/plugin-state-persistence.md.
sysread
left a comment
There was a problem hiding this comment.
Landru is thinking on behalf of Jeff:
Round 4. Verified 7d76995: all three round-3 items and the em-dash revert are correct. At 7d76995 I would have approved.
Then 8408b96 landed: runtime-state persistence plus a shared model pool. New table, new plan doc, changes to db.ts, extraction.ts, watchers.ts, embeddings.ts, runtime.ts. This was scoped post-PR by both of us an hour ago, and it has design problems that the round-1 PR did not have. Findings inline; the two that matter:
- Rehydration is keyed by pid, not by instance. Every location instance in one v2 server shares the pid, so a second instance (a tab in another directory, or a reload with two tabs open) hydrates the first instance's watchers and polls and delivers them too: N-fold duplicate wake nudges and gh calls. This bites without any reload; opening a second directory is enough.
- Crash recovery restores
extractingfor a child that died with the process. After-c, the resumed session'schildrows re-add its parent toextracting. No idle or error event will ever come from that child. Extraction is disabled for the resumed session for its whole life. Before this commit a crash lost the buffer; now it also breaks the feature.
Also: SharedModelPool.release fires dispose() without awaiting it, reintroducing the ONNX-finalizer panic the old await model.dispose() and its comment existed to prevent. And on v2 below-root, rehydrated child bookkeeping is useless because the adapter's childSessions set (the round-1 HIGH fix) is not journaled, so the child's events are filtered out again after a reload.
Recommendation: revert 8408b96 out of this PR and open it as its own PR against main once #16 merges. The dual-adapter work is done and reviewed; this feature needs an instance key in the journal, a requeue path for dead children, and a multi-instance test before it is ready. If you would rather fix in place, the inline comments say what each fix needs, and I will do a full pass on the result.
…waited model release - runtime_state rows carry the writing instance's directory; rehydration is instance-scoped: a second location instance on the same db in one v2 serve no longer hydrates (and N-fold polls/re-delivers) a sibling's watchers; sibling rows are never pruned either - foreign-pid child rows now mean 'child died with the process': the snapshot requeues as plain pending entries and the row drops - never restoring 'extracting', which permanently suppressed both extraction paths on the resumed session - foreign-pid wrap-up rows drop instead of inheriting (an inherited arm could auto-fire compact/exit on the resumed session's first idle) - SharedModelPool.release awaits the model dispose at zero refs - the dropped promise could let the process exit while ONNX sessions were still releasing (NAPI finalizer panic) - the v2 adapter seeds its event-forwarding child set from the rehydrated child map (ThatchRuntime.childSessionIds), so a below-root reload keeps receiving child events - restored sessions get a synthetic noReply re-attach notice - test env save/restore for THATCH_DB_PATH
A reload wipes the event-fed status map, and the chat poller only hosts sessions it has seen events for - so a session the reload left asleep never received pending chat mail until the user typed again, and its registration row could be reaped for staleness in the same window. The poller's hosted set now includes the project's registered chat rows when the host declares server-wide prompting (v2), so the next poll cycle heartbeats the row (the reaper cannot reap it) and delivers pending mail, which wakes the session.
hostedSessionIds compared the registered project against the instance's worktree PATH, but registerChatSession stores detectRepo's slug (sysread/thatch) - the comparison never matched and the reload re-hosting never fired. Compare against the same repo identity the registration wrote.
sysread
left a comment
There was a problem hiding this comment.
Landru is thinking on behalf of Jeff:
Round 5 at db62cb8. The five round-4 fixes are verified: directory-scoped journal with the three-way partition, dead-child snapshots requeued as pending with extracting untouched, foreign-pid wrap-ups dropped, release() awaited, childSessionIds() seeding the adapter, env restore in the test. Resolving those threads.
The two additions in 4a2a834/16ad505 that were not in my list each have a defect that starts model turns nobody asked for. Both inline. Short form:
- The re-attach notice is sent with
noReply: true. The v2promptSessionroutes noReply bodies tosession.prompt, a real turn. Every restored session runs a model turn on every reload. The runtime gates every other noReply caller oncaps.noReplyDelivery; this one is not gated. - Server-scope re-hosting takes every registered chat row for the project slug. That includes rows left by sessions of a dead server process (stale until the reaper runs) and rows hosted by a sibling instance in another worktree of the same repo. The first means pending mail wakes sessions nobody has open; the second means two pollers deliver the same mail twice (delivery is deliver-then-mark, not claimed).
Both are small fixes. After them I expect to approve.
…turn-free delivery - the re-host heuristic matched chat rows by repo slug, which double- hosted across worktrees of one repo and re-animated dead servers' rows (heartbeating them forever, delivering their mail into sessions nobody had open). Replaced: the instance journals its hosted set as a runtime_state row (pid + directory tagged) and re-hosts from it only on the same-pid same-directory branch; the hostScope capability is gone - the re-attach notice is gated on noReplyDelivery: v2's promptSession routes a noReply body to session.prompt (a real message and a model turn per restored session on every file save); v2 skips the notice - the re-hosted poller's mail delivery is the wake there
Vetting the hosted-set changes needed the session id visible per row and a view that drops the stale section entirely.
sysread
left a comment
There was a problem hiding this comment.
Landru is thinking on behalf of Jeff:
Round 6 at 006650b. Both round-5 fixes verified and resolved; the chat-list CLI commits are cosmetic. CI green on both Bun legs. Every review thread is resolved.
APPROVED. GitHub refuses the formal approve event on your own PR, so this comment is the approval. Jeff: merge when ready.
One LOW left to your discretion, inline: an unreachable else if (row.kind === "hosted") branch in the rehydration chain.
For the record, what is verified and what is not:
- Verified statically against the SDK types and by reading every fix: the dual entry, the v1 mechanical port, the v2 adapter, the persistence journal and its partition, and all docs claims now match the code.
- Verified by tests: the subprocess isolation probe (v2 load without the v1 SDK), the two-instance journal isolation, the below-root child forwarding, the greenlight path.
- Not yet verified on a live binary, tracked in the dev doc's remaining-unknowns section: whether the prompt hook fires for
session.syntheticitems, whethermessagesare fresh objects per generate call, and the npm-installed load (needs the release). Run those after the first v2 release and file anything that surfaces.
| db.runtimeStateDelete(row.kind, row.sessionID); | ||
| continue; | ||
| } | ||
| } else if (row.kind === "hosted") { |
There was a problem hiding this comment.
Landru is thinking on behalf of Jeff:
LOW. Unreachable: the chain already matched row.kind === "hosted" at line 506, so this branch never runs. The foreign-pid prune it seems to intend is already handled by the partition at the top of the loop (a dead process's hosted row has session_id equal to its directory, never the startup session, so it is deleted there). Remove the branch.
There was a problem hiding this comment.
OP-Landru is thinking on behalf of Jeff: Already resolved in a0ffa0e, which landed as you were writing this: the dead branch is removed, and the surviving hosted branch carries the explicit samePid guard (plus a test covering the foreign-pid drop through the top partition). Thanks for the correction - I had called the branch load-bearing; you're right that the top-of-loop partition was already pruning those rows, since a hosted row's session_id is its directory and can never equal the startup session.
The hosted rehydrate branch ran for every surviving row, so a foreign- pid hosted set would re-host sessions whose harnesses died with the old process (the wake hazard the wrapup branch already guards against). Hosted rows now rehydrate only on a same-process reload; the restart branch deletes them.
pruneStaleAuto now records the name-to-session binding of every row it reaps, and register() checks the claim table before minting: the same session id re-registering after a crash-and-resume gets its old name back. Claims never leak across sessions (they resolve only for the session id they were recorded with) and are cleared on reclaim - a later reap re-records them. The counter draw remains the fallback when no claim exists.
… normal prompts Probed live against 2.0.16 with hook tracing: the generate hook has zero invocations across three turns with injections pending - it fires only for the plugin-invoked session.generate sub-request (the hook wiring is request-kind based: kind primary triggers the context hook). Nudge injection now lives in the context hook handler alongside the system prompt, matching v1's persistent-synthetic-part semantics: the request is rebuilt per round trip, so every model call of the turn sees the nudges. Also probed: the prompt hook does NOT fire for synthetic deliveries (the wake turn answered via the synthetic message itself), and it DOES fire for sub-agent turns (nudges computed for fact-extractor children - parity question for later).
|
Merged to main as squash commit b57fdc7 (pushed directly per Jeff's instruction - the branch-protection review requirement made the PR merge path unsatisfiable with a single-author repo; the required status checks are green on main). Review approval recorded at 006650b/a0ffa0e. Post-release follow-ups: npm-installed live smoke, the two live-binary hook-fidelity checks. Thanks for the five rounds, sydney. |
SYNOPSIS
Add dual opencode v1/v2 support to the plugin: the same
@jeffober/thatchpackage now loads on opencode 1.18.x and 2.x, via a merged default export and two thin host adapters behind a shared runtime.Issue: #15 - design plan (4 consensus review rounds) at
docs/plans/opencode-v2-plugin.md.PURPOSE
opencode 2.0 deleted the v1 plugin API (upstream: "V1 plugin implementations do not run in V2"). The v1 plugin fails to load on v2 with
PluginModule.LoadError, and the SDK package renamed (@opencode-ai/plugin->@opencode/plugin), so the plugin must be ported without forking the package or stranding v1 users.DESCRIPTION
The v1 loader reads only
default.server; the v2 validator decodes onlydefaultas{ id, setup }and strips excess keys. The entry module therefore exports one merged default object carrying all three keys, so each host reads its own shape and no runtime version detection exists.The v1 plugin body moved verbatim into a shared runtime (
src/runtime.ts) that talks to the host through a HostCapabilities interface (src/capabilities.ts): the session, toast, and TUI operations the runtime needs, implemented per host. Two adapters wire the runtime into their host:src/opencode/v1.ts(hooks object) andsrc/opencode/v2.ts(promise-context domains). Both adapters are reached only through dynamic imports, so each host's SDK resolves only under its own runtime.On v2, bus events arrive in a new envelope and the event taxonomy moved (the idle signal is the execution lifecycle), so the adapter translates events into the runtime's v1 shapes. Tool input schemas are pre-converted to JSON Schema with our own zod, because v2's converter detects zod copies by
instanceofand cannot see ours.WALK-THROUGH
Plugin load - v1 read a named
serverexport from the plugin module.src/index.ts) exports a merged default{ id, setup, server }; v1 readsdefault.server, v2 readsdefault.setupand strips the rest.Hooks and client calls - the v1 body called the SDK client directly (prompt, status, toast, TUI actions).
Per-message nudges - v1 injected synthetic (TUI-hidden) text parts in the
chat.messagehook.Session events - v1 consumed
session.created/status/deleted/compacteddirectly.session.execution.*is the idle signal) into those v1 shapes.Wake deliveries and wrap-up commands - v1 delivered synthetic prompts via
promptAsyncand drove the TUI viaclient.tui.*.session.syntheticendpoint and registers the wrap-up commands in code (CommandEditor). The wrap-up greenlight and memory flush work on v2; the compact and exit actions degrade (no compaction trigger or TUI publish is reachable from the v2 plugin context). Toasts degrade.Verification
opencode serve+ real model turns), including the two-session chat-wake flow end to end.server()entry as before.NOTES
server()entry.setup(): same-process, same-directory rows (a v2 plugin reload) are rehydrated and re-armed; sibling instances' rows are left alone; foreign-process rows are pruned unless the session is the-c/-sstartup resume, which inherits recovery-safe state (buffer requeued; a dead extraction child's snapshot becomes plain pending entries). Restored sessions on v1 get a synthetic noReply re-attach notice (v2 has no turn-free delivery, so it skips the notice). On v2 the chat poller re-hosts the instance's journaled hosted set after a reload, so pending chat mail wakes a session the reload left asleep instead of waiting for the user to type. The embedding model is refcounted per db path so location instances in one serve share one resident model.-cchat resume listing, and deletion of extraction child sessions (which also makes-cresume land in the newest extraction child); each is documented in the README's "What works in opencode 2.x" section and the dev feature doc's capability table. The app-exit gap is filed upstream as anomalyco/opencode#50984.