Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
442 changes: 221 additions & 221 deletions dist/artifact-btV3Nd6h.js → dist/artifact-evjF_miE.js

Large diffs are not rendered by default.

34 changes: 17 additions & 17 deletions dist/main.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/post.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions packages/gateway/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,24 @@ When the agent calls OpenCode's `question` tool, OpenCode emits `question.asked`

**Restart limitation:** pending questions are in memory only. A gateway restart loses them; see [Known limitations](#known-limitations).

## V2 run release invariant

A run on OpenCode V2 (`execute/run-core-v2.ts`) is released by the directory, not by its own accounting. Stock v2.2.0 gives no way to prove that nothing the run started can still wake one of its sessions: a shell's `DELETE` wakes its owner, a subagent's completion and an enqueued notice wake a session in a step the event stream does not show, and an `interrupt` reply is a delivery receipt. The ledger and the notices still drive ownership, approvals, event routing, wait reasons and progress. They no longer decide a release.

**The invariant.** The run's checkout is released only after the run's directory has shown no active session, no running shell and no queued inbox item for at least 5 s. Concretely (`packages/runtime/src/agent/directory-quiescence-v2.ts`):

- **A reading** has three parts, all bounded and carrying the signal. (a) `GET /api/session/active` (global), each entry located with `GET /api/session/:id` and kept when its stored directory is this one: root, children, grandchildren and resumed descendants alike, whatever the ledger says. (b) `GET /api/shell` for the directory, owned or not. (c) `GET /api/session/:id/inbox` for the sessions that matter to this run: its root, every session it owns, and every session under the directory whose `time.updated` is at or after the run's start minus 10 minutes (`DIRECTORY_INBOX_RECENT_MARGIN_MS`).
- **Why (c) is bounded.** The workspace's server is long-lived and the sessions of earlier runs pile up under the directory. A session last updated before the cut-off already passed its own release gate, and anything of it that is running shows up in (a) or (b). Admitting an inbox item bumps the session's `time.updated` (`packages/core/src/session/projector.ts`, the `InboxEnqueued` projection), and every wake goes through an admit, so a notice queued during this run always lands inside the bound. Terminal events and claim bookkeeping do not bump it, which is why (a) does not use it. The list is read newest-first and the walk stops at the first page that reaches the cut-off, so the 2000-session page bound applies to the recent sessions only. A server that does not return the list in newest-first order, or an entry with no readable time, makes the walk keep paging instead of cutting it short.
- **The gate** (`createDirectoryStabilityGate`) passes after 11 consecutive quiet readings at least 500 ms apart that span at least 5 s (`DIRECTORY_QUIESCENCE_REQUIRED_OBSERVATIONS`, `_MIN_SPACING_MS`, `_MIN_SPAN_MS`). Any activity, and any read that fails, times out or comes back partial, resets the count; a reading the run could not take is never read as quiet. The wait asks whether the evidence it is for is still current after every reading, the one that would pass included; when it is not, the count is dropped (`awaitDirectoryStability`), so root activity and a fresh idle that land during the last reading make the retry take a whole new window. needs the existing evidence (root idle, a terminal reply, no queued notice) and the gate. Every success waits, including a run that adopted nothing. Activity does not fail the run: it keeps draining until the directory is quiet or the wall clock ends it.
- **A stop** (`throwWithBarrier`, `settle-owned-sessions-v2.ts`) repeats until the gate passes within `DEFAULT_SETTLE_TIMEOUT_MS`: `DELETE` every running shell of the directory, interrupt every active session of it (root first), read again. A stop the gate does not confirm in the bound is unconfirmed: the run quarantines, and the quarantine watch (`startQuarantineWatchV2`) repeats the kill and interrupt for the whole hold. The DELETEs and the interrupts of one stop each have a bound of their own (`STOP_DELETE_TIMEOUT_MS`, `STOP_INTERRUPT_TIMEOUT_MS`) derived from the barrier's or the hold's life, not from the reading's, and the interrupts get a fresh one after the DELETEs, so a DELETE that never answers costs the interrupts the wait and nothing else; interrupts are issued together, root first. An observed move of an owned session always quarantines, because the destination's sessions and shells cannot be read from this directory, and the moved session is an explicit target: the barrier and the watch interrupt it while it is on the active list, wherever the server stores it now (`knownSessionIds`, no placement lookup).
- **A degraded reading keeps what it found.** A reading that cannot be completed (a branch timed out, one placement lookup failed, the caller aborted) is `unreadable` for the release, and still carries the shells, active sessions placed in the directory and queued inboxes that its reads did find. The barrier and the watch stop those; a failed lookup of one active session does not discard the placements that succeeded.

**The residual.** An upstream producer that is paused for longer than the window and wakes a session later is not seen. Quarantine on an unconfirmed stop and the next run's acquire check (`repo-quiescence-v2.ts`) mitigate it; it is tracked upstream.

**The consequence.** Every session and shell in a gateway checkout directory is treated as the run's. Nothing else shares that directory, so on a stop they are killed or interrupted, whoever started them. The kill of a live shell wakes its owner, so a stop with a live shell needs a full quiet window after the kill and quarantines if the owner cannot be stopped.

**Cost.** A reading costs one request per session running anywhere on the server (to place it), one shell list, a few list pages (one when the recent sessions fit in a page), and one inbox read per recent or owned session, 8 in flight. None of it grows with the history stored under the directory. A reading does fail, and so never reads as quiet, if more than 2000 sessions of the directory were updated since the run began. The acquire check (`repo-quiescence-v2.ts`) uses the same placement of the active sessions, so it does not list the directory either.

## Known limitations

- **`add-project` is Discord-only.** The orchestration runs inside the slash
Expand Down
Loading
Loading