-
Notifications
You must be signed in to change notification settings - Fork 2
feat(opencode): full driver — typed expansion, SSE observed state, native delivery #325
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
schickling-assistant
merged 15 commits into
schickling/hs-06-claude
from
schickling/hs-07-opencode
Aug 24, 2026
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
78a7cb7
feat(opencode): full driver — typed expansion, session wrapper, obser…
schickling 91fbfc4
fix(opencode): match the measured wire — replied events carry request…
schickling ca97147
docs(vrs): resolve DQ-H6 with the live blocked-pair capture
schickling d865843
fix(opencode): seed-gated evidence, reconnect ask recovery, read-back…
schickling b3746ec
fix(opencode): question re-seeding, session-boundary starts, ask kind…
schickling 6f01253
fix(opencode): gate every consumed arm, seed atomically, and count on…
schickling 12ae566
fix(opencode): recover pre-settled delivery targets, trust only pinne…
schickling 5f95070
fix(opencode,agent-spec): written ownership claim and the missing dri…
schickling 1a79a8d
fix(opencode): claim before the provider spawns — a failed claim leak…
schickling 019b6a8
fix(opencode): atomic whole-truth seeding, and delivery targets only …
schickling 2c12456
fix(opencode): a silence horizon on the stream, and only object-shape…
schickling 3489c55
fix(opencode): a failed liveness check ends the record honestly
schickling 57a65f7
fix(opencode): a disconnect breaks continuity, an unreadable status w…
schickling c2ca285
fix(opencode): a sticky terminal outranks the poison
schickling fca92a5
fix(opencode): every unreadable status poisons, spawn failures end th…
schickling-assistant File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
129 changes: 129 additions & 0 deletions
129
docs/vrs/05-harness-state/.experiments/2026-08-23-opencode-surface.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| # OpenCode's server surface, measured for the driver | ||
|
|
||
| 2026-08-23, OpenCode 1.18.19 (`/home/schickling/.nix-profile/bin/opencode`), Linux, isolated | ||
| `XDG_DATA_HOME`/`XDG_CONFIG_HOME`, headless `opencode serve --port 43123 --print-logs`. The free | ||
| anonymous model (`opencode/big-pickle`) answered prompts with no credentials, so every claim below | ||
| is reproducible without an API key. | ||
|
|
||
| ## What was established | ||
|
|
||
| **The TUI is a server.** `opencode` (TUI, the default command) starts a server on `--port` / | ||
| `--hostname` exactly like `opencode serve`; `opencode attach <url>` exists for the reverse | ||
| direction. A driver therefore launches the interactive seat with a wrapper-allocated loopback port | ||
| and speaks HTTP to its own child — no screen scraping anywhere in the driver path. | ||
|
|
||
| **Observation** rides `GET /event` (SSE). First event is `server.connected`, `server.heartbeat` is | ||
| periodic, and a connect replays ~45 `plugin.added` events — subscribers must tolerate noise and | ||
| duplicates. The API self-describes at `GET /doc` (OpenAPI 3.1, 94 event schemas). Measured and | ||
| schema-verified signals, as projected by the driver: | ||
|
|
||
| - `session.status` with a three-arm status union `busy | idle | retry` (measured firing at turn | ||
| start and end; `retry` carries `attempt`/`next`/`message`); | ||
| - `session.idle` fires beside the idle status (measured); | ||
| - `permission.asked` / `permission.replied` and `question.asked` / `question.replied|rejected` | ||
| carry stable `^per` / `^que` ids — the blocked-on-human exit edge is id-matched, with none of the | ||
| Claude batching ambiguity (schema-verified; a live `permission.asked` capture is still owed — | ||
| with `{"permission":{"bash":"ask"}}` PATCHed into config, the free model's bash ran without | ||
| asking in one run and emitted no tool part in another); | ||
| - `session.error` is an eight-arm union; `ProviderAuthError` is terminal for the seat, the others | ||
| leave the session promptable; | ||
| - `GET /session/status` returns `{sessionID: status}` and **omits idle sessions** — measured `{}` | ||
| when idle, so absence-of-key is the idle proof only over a proven-live server. | ||
|
|
||
| **Delivery** is `POST /session/{id}/prompt_async` (measured: returns 200 immediately, empty body), | ||
| which accepts a caller-supplied `messageID` (`^msg`) — idempotent and receipt-correlatable. The | ||
| receipt is the message read back (`GET /session/{id}/message/{messageID}` / the `message.updated` | ||
| event). Prompts sent mid-turn queue natively. **`/tui/append-prompt` and `/tui/submit-prompt` | ||
| returned `true` on a headless server with no TUI attached** — they are broadcast, not receipt, and | ||
| must never count as delivery. Auth is `OPENCODE_SERVER_PASSWORD` + basic auth (user `opencode`), | ||
| unsecured by default on loopback. | ||
|
|
||
| **Sessions** are `ses_*`; storage moved to sqlite (`$XDG_DATA_HOME/opencode/opencode.db`, | ||
| `opencode db` exists) — the API is the only sane read path. Exact resume is `--session <id>` / | ||
| `--continue`, forking is `--fork`. No native incarnation concept: st2's runtime generation, the | ||
| pinned port, and the wrapper pid supply fencing. | ||
|
|
||
| **Pinning**: `opencode --version` prints the bare version; `Session.version` also rides the wire. | ||
| Because the server serves its own OpenAPI document, a live `/doc` subset check at wrapper start | ||
| covers the shape while a version list covers the semantics — the hybrid of the Codex | ||
| `SUPPORTED_CODEX_CLI_VERSIONS` pattern and the pi type-check pattern. | ||
|
|
||
| ## Reproduction | ||
|
|
||
| ``` | ||
| XDG_DATA_HOME=$S/data XDG_CONFIG_HOME=$S/config opencode serve --port 43123 --print-logs | ||
| curl -s http://127.0.0.1:43123/doc | jq '.paths | keys' | ||
| curl -sN http://127.0.0.1:43123/event # SSE capture | ||
| curl -s -XPOST http://127.0.0.1:43123/session # create ses_… | ||
| curl -s -XPOST http://127.0.0.1:43123/session/<id>/prompt_async \ | ||
| -H 'content-type: application/json' \ | ||
| -d '{"messageID":"msg0000000000000000000000000","parts":[{"type":"text","text":"hi"}]}' | ||
| curl -s http://127.0.0.1:43123/session/<id>/message/msg0000000000000000000000000 | ||
| curl -s http://127.0.0.1:43123/session/status # {} idle · {"ses_…":{"type":"busy"}} mid-turn | ||
| curl -s -XPOST http://127.0.0.1:43123/tui/append-prompt -d '{"text":"x"}' # true, no TUI attached | ||
| ``` | ||
|
|
||
| Original captures: session `ses_fd078983affefGxfpkGr2u44LJ`, files `serve.log`, `openapi.json`, | ||
| `events{,2,3,4}.sse`, `session.json`, `prompt-response.json` (session scratchpad, not committed). | ||
|
|
||
| ## Follow-up capture: the blocked-on-human pairs, live (2026-08-23, second run) | ||
|
|
||
| The permission prompt fires headless after all — the first run's failure was the *write path*, not | ||
| the surface: permissions set via `PATCH /config` did not take effect for asks, while the same | ||
| `{"permission":{"bash":"ask","edit":"ask","webfetch":"ask"}}` in `$XDG_CONFIG_HOME/opencode/ | ||
| opencode.json` asks reliably with the free model and no TUI. | ||
|
|
||
| Reproduction (isolated env as above, port 43217; session `ses_fd0241376ffe3KDznnEB55qvKi`): | ||
|
|
||
| ``` | ||
| # config file (not PATCH) carries the ask settings, then: | ||
| curl -s -XPOST :43217/session/<id>/prompt_async -d '{"parts":[{"type":"text", | ||
| "text":"Use the bash tool to run exactly: echo capture-test-42. Do not answer without running it."}]}' | ||
| curl -s :43217/permission # pending: [{"id":"per_02fdc246b001BB5pclAd62tzpJ","permission":"bash",…}] | ||
| curl -s -XPOST :43217/permission/per_…/reply -d '{"reply":"once"}' # → true; pending clears; turn completes | ||
| # question: prompt "you MUST use your question tool…", then | ||
| curl -s :43217/question # pending: [{"id":"que_02fdd3e83001GwptE1fgJam0jB",…}] | ||
| curl -s -XPOST :43217/question/que_…/reply -d '{"answers":[["Yes"]]}' | ||
| ``` | ||
|
|
||
| Captured event frames (verbatim, now fixture tests in `src/opencode_session.rs`): | ||
|
|
||
| ``` | ||
| data: {"id":"evt_02fdc246b0020Xw65txB3nXBC4","type":"permission.asked","properties":{"id":"per_02fdc246b001BB5pclAd62tzpJ","sessionID":"ses_fd0241376ffe3KDznnEB55qvKi","permission":"bash","patterns":["echo capture-test-42"],"metadata":{"command":"echo capture-test-42"},"always":["echo *"],"tool":{"messageID":"msg_02fdc0989001nfz93uTCTLeO6O","callID":"call_6614fd927fe74d86ab089078"}}} | ||
| data: {"id":"evt_02fdc8342001TQBwhszchZw1U6","type":"permission.replied","properties":{"sessionID":"ses_fd0241376ffe3KDznnEB55qvKi","requestID":"per_02fdc246b001BB5pclAd62tzpJ","reply":"once"}} | ||
| data: {"type":"question.asked","properties":{"id":"que_02fdd3e83001GwptE1fgJam0jB",…}} | ||
| data: {"type":"question.replied","properties":{"sessionID":"…","requestID":"que_02fdd3e83001GwptE1fgJam0jB","answers":[["Yes"]]}} | ||
| ``` | ||
|
|
||
| **Two corrections to the schema-derived design, both shipped:** | ||
|
|
||
| 1. **Exit events spell the id `requestID`.** Entry events carry `properties.id`; `permission.replied` | ||
| and `question.replied|rejected` carry `properties.requestID`. The extraction that only knew `/id` | ||
| would have held `blockedOn: human` forever after a real grant. | ||
| 2. **`GET /event` over HTTP/1.1 is `Transfer-Encoding: chunked`** — chunk-size lines interleave into | ||
| the line-oriented SSE read and a `data:` line can split across chunks (silent event loss). The | ||
| same server streams raw SSE over an HTTP/1.0 request, so the producer requests HTTP/1.0. | ||
| JSON endpoints (`/config`, and `/doc` at 478 KB) responded `Content-Length` in every probe, so | ||
| the one-shot request path is unaffected. | ||
|
|
||
| Also measured while live: `prompt_async` with a repeated caller `messageID` yields **one** user | ||
| message (read-back receipt correlation holds; no duplicate delivery), but the second POST appends | ||
| its `parts` again into that message — a resend after a *transiently failed* read-back duplicates | ||
| text inside the message, not the message. The pump's read-back-before-resend rule is therefore | ||
| load-bearing, not just polite. | ||
|
|
||
| ## Limits | ||
|
|
||
| - A v2 surface (`/api/event`, `/api/session/{id}/wait`, `permission.v2.*`) coexists with the | ||
| legacy one probed here; the driver pins the legacy arms via the `/doc` check. | ||
| - Docs move fast (the site showed "Last updated Aug 23, 2026"); the `/doc` gate is the defense. | ||
| - The chunked/1.0 behavior and the `requestID` spelling are measured on 1.18.19 only; both sit | ||
| behind `SUPPORTED_OPENCODE_VERSIONS` and the `/doc` subset gate. | ||
|
|
||
| ## VRS Impact | ||
|
|
||
| Resolves `DQ-H6` in full: the OpenCode producer is evented (server SSE), uniquely offers an | ||
| id-matched blocked-on-human exit edge, and both blocked pairs are now captured live with the two | ||
| wire corrections above landed as code plus verbatim fixture tests. Feeds the OpenCode producer | ||
| section of `spec.md` (mapping table, aggregate-session rule, receipt semantics, the two-gate | ||
| fail-closed rule) and requirement `OHS-R08`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.