Skip to content

feat(opencode): full driver — typed expansion, SSE observed state, native delivery - #325

Merged
schickling-assistant merged 15 commits into
schickling/hs-06-claudefrom
schickling/hs-07-opencode
Aug 24, 2026
Merged

feat(opencode): full driver — typed expansion, SSE observed state, native delivery#325
schickling-assistant merged 15 commits into
schickling/hs-06-claudefrom
schickling/hs-07-opencode

Conversation

@schickling

@schickling schickling commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

The full OpenCode driver — the fourth harness at parity, per the measured surface research (v1.18.19: the TUI is a server; captures reproduced in the shipped experiment record).

  • Typed driver: Driver::OpenCode + expand_opencode + st2 driver opencode-session; expansion stays pure (the wrapper allocates the port at runtime).

  • Wrapper (src/opencode_session.rs): port + per-seat OPENCODE_SERVER_PASSWORD allocation, presence lease, SSE /event reader with reconnect, and a pure EventMachine: any busy session ⇒ active (dedicated seat aggregates); permission.asked/question.askedblockedOn: human with id-matched exits on the matching replied/rejected event; ProviderAuthError ⇒ ended; other errors ⇒ idle with reason; idle needs positive /session/status evidence (the endpoint omits idle sessions). Disconnect stops heartbeats; the terminal record lands before SIGKILL escalation.

  • Native delivery (replaces screen-scraped DING for driver seats): Codex-style durable FIFO — Attempted persisted before transport, POST /session/{id}/prompt_async with a stable caller messageID, receipt only by reading the message back; stale attempts reconcile by read-back before any resend. /tui/* endpoints are never a transport (they return true with no TUI attached — measured).

  • Fail-closed gates: SUPPORTED_OPENCODE_VERSIONS + a live /doc OpenAPI subset check over exactly the arms st2 depends on; gate failure runs presence-only.

  • Docs: .experiments/2026-08-23-opencode-surface.md + spec section updated to the implemented design. Open: a live permission.asked capture is still owed (DQ-H6).

  • Cycle-2: pending questions re-seed across SSE reconnects from their own measured listing endpoint; blocked observations carry ask kinds (permission/question); a lone [opencode] (or [pi]) driver block is now a spec candidate; the restarted wrapper opens a fresh transition.

  • Cycle-3: the reconnect seed is atomic (status level AND both pending-ask listings must succeed, else evidence stays off and it retries); an unrecognized session.status word is no longer level evidence; the /doc gate names every consumed arm including the question exit events and both pending listings.

  • Cycle-4: pre-settled sessions are recovered from the session listing for pending deliveries; the status seed trusts exactly busy/retry/idle and fails closed on unknown words; the stop path records the grace-window reap's real exit over its escalation cover.

  • Cycle-8: an SSE disconnect marks the observation stream interrupted (no continuity claim across the outage); an unknown status word on a tracked-busy session poisons the projection (observations withheld, evidence dropped, immediate reseed — while a sticky terminal still outranks the poison); and a pending-listing entry without a readable id fails the whole seed instead of being skipped.

Part of the #268 stack (#319#326).

🤖 Generated with Claude Code

Refinement pass (live captures + review): the DQ-H6 capture landed both blocked pairs live and corrected the producer twice — exit events spell the ask id requestID (the schema-derived id-only extraction would have held blockedOn: human forever after a real grant; verbatim captured pairs are now fixture tests), and GET /event is chunk-encoded over HTTP/1.1, so the SSE reader requests HTTP/1.0, which the server streams raw. Review fixes: evidence (and heartbeats) turn on only after a successful level seed, retried while the connection lives; the seed re-enters permission asks already pending at (re)connect under their ids, so blockedOn survives an SSE drop; an indeterminate delivery read-back retries the read-back and never re-POSTs (measured: a same-messageID re-POST appends duplicate parts); the record-write volume defect the smoke measured (679 writes/221 s) is closed by the envelope's restatement guard; and ptySession records the wrapper's runtime ID.

@schickling schickling changed the title schickling/hs 07 opencode feat(opencode): full driver — typed expansion, SSE observed state, native delivery Aug 23, 2026
@schickling
schickling marked this pull request as ready for review August 23, 2026 18:18

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cebddb797a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/opencode_session.rs
Comment thread src/opencode_session.rs Outdated
@schickling
schickling force-pushed the schickling/hs-07-opencode branch from cebddb7 to b2df2fd Compare August 23, 2026 19:23

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b2df2fdcc1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/opencode_session.rs Outdated
Comment thread crates/agent-spec/src/spec.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cd5b00923d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/opencode_session.rs Outdated
Comment thread src/opencode_session.rs Outdated
Comment thread src/opencode_session.rs
@schickling
schickling force-pushed the schickling/hs-07-opencode branch from cd5b009 to 1278d64 Compare August 23, 2026 22:29

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1278d64e59

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/opencode_session.rs Outdated
Comment thread src/opencode_session.rs Outdated
@schickling
schickling force-pushed the schickling/hs-07-opencode branch from 1278d64 to 95a041f Compare August 23, 2026 23:01

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 95a041f94d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/agent-spec/src/spec.rs
@schickling
schickling force-pushed the schickling/hs-07-opencode branch from 95a041f to 26ebfff Compare August 23, 2026 23:35

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 26ebfff096

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/opencode_session.rs Outdated
Comment thread src/opencode_session.rs Outdated
@schickling
schickling force-pushed the schickling/hs-07-opencode branch from 26ebfff to f8392cc Compare August 24, 2026 00:05

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f8392cc483

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/opencode_session.rs
Comment thread src/opencode_session.rs Outdated
@schickling
schickling force-pushed the schickling/hs-07-opencode branch from f8392cc to 8ffc60e Compare August 24, 2026 00:37

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8ffc60e269

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/opencode_session.rs
Comment thread src/opencode_session.rs Outdated
@schickling
schickling force-pushed the schickling/hs-07-opencode branch from 8ffc60e to 52fe79e Compare August 24, 2026 04:30

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 52fe79e1e5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/opencode_session.rs Outdated
Comment on lines +723 to +725
_ => {
if self.busy.contains_key(&session_id) {
self.poisoned = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Invalidate idle evidence for every unknown session status

When a successful seed has established idle and a newly created or previously untracked session then emits a future status such as paused, this condition does not poison the projection because the session is absent from busy; the wrapper therefore continues heartbeating a definite idle state even though that session's activity is unknown. The new untracked-session branch is fresh evidence beyond the earlier tracked-busy case. Invalidate existing level evidence whenever any session moves to an unreadable status, not only when that session was already tracked as busy.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fca92a5 on schickling/hs-07-opencode: an unrecognized session.status word now poisons the projection for ANY session -- tracked or untracked -- since an untracked session in an unreadable state makes standing idle evidence a fabrication; withholding holds until a fresh level seed rebuilds the picture (sticky terminals still outrank the poison).

Posted on behalf of @schickling
field value
agent_identity unknown
agent_persona generalist
agent_supervisor unavailable
agent_tool OMP
agent_tool_version 18.0.3
agent_runtime OMP 18.0.3
tooling_profile dotfiles@f33cd9c-dirty

Comment thread src/opencode_session.rs Outdated
)
.with_ownership(session, seq)
};
let mut child = spawn_provider(&argv, &password)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record a terminal launch error when spawning fails

If the OpenCode executable is missing, loses execute permission, or otherwise fails in spawn_provider, this ? returns after harness_state::claim has already replaced the prior record, but no real terminal observation is written. The durable record is consequently left as the claim placeholder ended (superseded) rather than launch-error, misclassifying the failed launch; handle the spawn error and write through writer before returning it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fca92a5 on schickling/hs-07-opencode: a failed spawn_provider no longer returns through ? past the claim; the wrapper writes an honest ended / launch-error / exit unknown terminal through its adopted ownership before propagating, matching the Claude and pi launch paths.

Posted on behalf of @schickling
field value
agent_identity unknown
agent_persona generalist
agent_supervisor unavailable
agent_tool OMP
agent_tool_version 18.0.3
agent_runtime OMP 18.0.3
tooling_profile dotfiles@f33cd9c-dirty

Comment thread src/opencode_session.rs Outdated
Comment on lines +1138 to +1139
std::fs::write(&temp, serde_json::to_vec(value)?)?;
if let Err(error) = std::fs::rename(&temp, path) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fsync the attempted receipt before sending

When the host crashes after this rename and after prompt_async is accepted, but before the unsynced receipt reaches stable storage, the OpenCode message can survive in its SQLite store while delivery-state.json disappears after reboot. The pump then posts the same stable messageID without first reconciling it, and the measured OpenCode behavior appends the text parts again. Since the transport relies on Attempted being durable before transport, sync the temporary file before renaming and sync the parent directory before allowing the POST.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fca92a5 on schickling/hs-07-opencode: atomic_json fsyncs the temp file before the rename and syncs the parent directory after it, so once delivery proceeds to prompt_async the Attempted receipt survives a host crash and the pump cannot re-POST duplicate parts.

Posted on behalf of @schickling
field value
agent_identity unknown
agent_persona generalist
agent_supervisor unavailable
agent_tool OMP
agent_tool_version 18.0.3
agent_runtime OMP 18.0.3
tooling_profile dotfiles@f33cd9c-dirty

schickling and others added 2 commits August 24, 2026 16:43
…ved state, native delivery

OpenCode's TUI is also a server, so the driver needs no screen scraping:
the opencode-session wrapper allocates a loopback port and per-seat
password, owns the presence lease and the harness-state writer, projects
the /event SSE stream (busy/idle/retry, id-matched permission and
question edges, session errors) with evidence-gated heartbeats, and
delivers inbox messages over POST prompt_async with a stable caller
messageID whose only accepted receipt is the message read back durably —
never the /tui/* endpoints, which acknowledge input with no TUI attached.
Delivery is fail-closed behind SUPPORTED_OPENCODE_VERSIONS plus a live
/doc OpenAPI subset check that names whatever went missing.

Includes the scoped watch_delivery_inputs watcher (inbox subtree + status
only) pending merge unification with the codex slice, the measured
2026-08-23 OpenCode surface experiment record, and the spec.md producer
section rewritten to the implemented design.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ID, SSE needs HTTP/1.0

Live capture on 1.18.19: permission.asked/question.asked carry
properties.id but their replied/rejected exits carry properties.requestID,
so the shipped id extraction never released blockedOn:human after a real
grant; and GET /event over HTTP/1.1 is chunk-encoded, which the
line-oriented SSE reader cannot parse safely, while HTTP/1.0 streams raw.
Verbatim captured event pairs are now fixture tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
schickling and others added 13 commits August 24, 2026 16:43
Config-file permission settings ask headless (the PATCH path was the
earlier failure); verbatim captured pairs recorded, the requestID and
HTTP/1.0 corrections documented, and prompt_async's repeated-messageID
parts-append behavior measured.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-only retries, runtime-id sessions

Review-pass fixes: evidence (and with it heartbeats) turns on only after a
successful level seed, with the seed retried while the SSE connection
lives — a transiently failed seed no longer resumed heartbeats on
whatever the disk last said; the seed also recovers permission asks
already pending at (re)connect under their own ids so blockedOn survives
an SSE drop and the ordinary id-matched exit still releases it (pending
questions have no verified listing endpoint on 1.18.19 — stated in code);
an indeterminate delivery read-back retries the read-back and never
re-POSTs, because a same-messageID re-POST appends duplicate parts
(measured); and ptySession records the wrapper's runtime ID.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s, and driver-block discovery

Pending questions recover across an SSE reconnect from their own listing
endpoint (measured on 1.18.19) exactly like permissions; blocked
observations carry the machine-readable ask kind; the restarted wrapper
opens a fresh transition; and a lone driver block of any provider —
opencode and the silently-missing pi included — is a spec candidate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ly recognized level evidence

The /doc subset names the question exit arms and both pending-ask
listings (a release renaming an exit would otherwise hold blockedOn
forever); the reconnect seed fails — keeping heartbeats off and retrying
— unless the status level AND both ask listings succeed; and an
unrecognized session.status word is no longer level evidence, so a
future word cannot prove idle on a quiet server.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d status words, and keep real stop exits

A pending delivery whose session settled before the observer connected
recovers its binding from the session listing (idle sessions are
invisible to events and /session/status) and retries until one exists;
the reconnect seed matches exactly busy/retry and fails closed on an
unknown word; the stop path rewrites its escalation-cover record with
the exit the grace-window reap actually observed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ver re-exports

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s nothing

Found by the pre-submit self-review: the written claim sat after
spawn_provider, so a claim failure error-aborted the launch around a
running child.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…from the listing

The reconnect seed builds a fresh machine and swaps it in only after
every level read validates: a mid-seed failure leaves no half-seeded
asks, and a successful re-seed clears stale busy/blocked entries whose
exits passed while the stream was down — the level surface at seed time
is the whole truth. The seed no longer touches delivery targeting at
all: status-map iteration order is not recency, and targets resolve only
through the session listing's newest entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d level evidence

The SSE socket carries a read timeout of at least twice the measured
heartbeat cadence, so a stalled connection surfaces as a disconnect
(evidence off, reconnect and reseed) instead of keeping evidence alive
forever; a /session/status response that is not the documented object
shape fails the seed rather than fabricating definite idle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Found by the cycle-7 self-review's error-arm sweep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ord on a tracked session poisons the projection, and an unreadable pending entry fails the seed

Three honesty holes in the projection:
- An SSE drop now marks the writer interrupted, so the first post-reseed
  observation opens a fresh transition instead of claiming continuity across
  an interval nobody observed.
- A future status word on a session the machine tracks as busy poisons the
  whole projection (observations withheld, evidence dropped, immediate
  reseed): that busy entry could never be trusted to clear.
- A pending-ask listing entry without a readable id fails the seed instead of
  being silently skipped: seeding around it would restore evidence on a
  picture that drops a human block.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ended does not depend on the busy map an unknown status word made
untrustworthy; withholding it while forcing the reseed would silently lose
the terminal to the fresh machine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e record, and the receipt survives a crash

An unrecognized session.status word poisoned only sessions tracked busy;
an untracked — possibly newly created — session in a state this version
cannot read left definite idle heartbeating on top of unknown activity.
Any unreadable status word now withholds the projection until a level
seed rebuilds it.

A failed spawn_provider returned through ? after the claim had already
replaced the prior record, leaving the exitless ended (superseded)
placeholder as a false takeover; the wrapper now writes an honest
ended/launch-error terminal before returning.

The Attempted receipt was renamed without fsync, so a crash between the
rename and prompt_async acceptance could lose it and re-POST duplicate
parts. The temp file is synced before the rename and the directory entry
afterwards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
agent-identity: unknown
agent-persona: generalist
agent-supervisor: unavailable
agent-tool: OMP
agent-tool-version: 18.0.3
agent-runtime: OMP 18.0.3
tooling-profile: dotfiles@f33cd9c-dirty
@schickling-assistant
schickling-assistant merged commit 802fe21 into main Aug 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants