One test walks create, send, stream, approval, cancel, reconnect, and paged
history against the real dispatcher, the real schemas, the real capability
gate, the real record store, journal, lease, adapter, and translation. Only
the Codex child is scripted, and it answers the same JSON-RPC calls the real
app-server does.
It is the first thing in the stack that proves the parts fit: that the
provider's echo of a user message reconciles into the bubble the client
already has instead of appending a second copy, that a client reconnecting
from its cursor replays only what it missed while a takeover advances the
fence and reaps the old child, and that the whole conversation reconstructs
from the durable journal alone with no page overlapping another.
The record store and the journals live under the profile's user-data path,
which is not final until Electron is ready, and a runtime that never serves a
structured session should not pay for a store it will never read. So the host
is built by the first `agentSession.create` or `.ensure` rather than at
startup, and the memoized slot clears on failure so the next call retries.
The capability gate runs first: building the store and spawning a child is an
observable side effect, and a client that cannot read structured sessions must
not be able to cause it.
Owner probing is scoped to this host. Checking another host's pid against this
machine's process table is exactly how a live owner gets declared dead, so a
foreign owner reports `indeterminate` and routes to manual recovery instead.
So does a reservation that named no process — freeing it needs positive proof
that nothing spawned under its token, and this host has no spawn-token process
scan yet.
Command, cwd, pinned account home, and the thread to resume all come back
out of the record the lease already wrote, never from the client-declared
identity: a caller that could name the thread could point a session at
someone else's conversation.
It refuses rather than guesses — a record pinned to another execution host or
a WSL distro is not this host's to spawn for, a record for another provider or
another account-home variable is not this adapter's to speak for, and a
workspace that no longer resolves surfaces instead of falling back to a
default cwd.
The adapter now writes what Codex streams straight into the session's
journal, so nothing between the child and the durable record has to remember
state: streamed text folds through the coalescing window, lifecycle events
flush it first, and every write is a highest-revision-wins upsert on the
durable key, which is what lets a completed body land on the row its own
deltas opened.
Journal first, observers second — a tap must never see a row the journal has
not been told about. A session with no sink still runs, unjournaled, which is
how the existing adapter tests keep working unchanged.
Starting or resuming the thread moved into its own module on the way past the
adapter's line cap; the check it carries is unchanged, and still refuses a
resume that lands on a different thread, since recording that would make the
durable handle chain lie about what the session proved.
Codex item ids are not stable across resume, so a journal keyed by them
cannot reconcile a recovered thread against a live one: the same message
would arrive under a new id and append a second copy of itself.
The durable key is instead the position the item holds — thread, turn, and
ordinal within that turn, counting message items only. Non-message items get
no ordinal at all rather than a number from a second counter, because a
counter a resumed history cannot reproduce is worse than no key: it looks
reconcilable and reconciles wrongly. Those rows fall back to a live-session
namespace that a recovered journal simply will not contain, which is correct,
since Codex does not persist them either.
Approvals and questions become journal rows the same way, naming the command
the item already announced so the user is not asked to approve an opaque id,
and one row per question rather than one row per request.
Providers emit one notification per token. Journaling each one writes a row
and wakes every subscriber per token, so one long answer costs thousands of
appends and thousands of stream frames on a phone.
Deltas accumulate and flush on a 60ms window, and the row carries a SNAPSHOT
of the text so far rather than the increment — which is also what makes
dropping intermediate frames safe for a reconnecting client. All streams share
one timer, so the deadline bounds latency the same way at one wakeup per
window instead of one per stream. Lifecycle bypasses the window by flushing
first, so nothing can be journaled ahead of the text that preceded it.
A provider starts streaming the moment its process exists, and that moment
is inside `adapter.acquire` — before the journal is open and before the host
has registered the session. The adapter now receives a deferred sink whose
writes queue in arrival order and drain once the journal is bound.
One sink lives for the session rather than for one acquisition, so an adapter
can hold it across a re-acquire without noticing the new journal object. It is
bound last in `onAttached` so nothing the provider streamed can interleave with
the attach result the client is about to read, and a refused attach that never
reached a journal closes it instead of leaving writes nothing will drain.
The host was one line under the 300-line cap before this, so the mutation
admission pipeline moved to its own module. That is a straight extraction:
recompute fingerprint, admit through the ledger, check the lease, run the
plan — the same route, now somewhere a new method cannot sit next to.
Six findings from the review of the structured Codex session, all of them
about a session that is live in the map while the thing it names is not:
- A prompt is addressed by its own approval id, not by the tool item it is
about. A shell bridge re-asks per command under one parent item, and the
second ask used to evict the first, leaving that turn blocked on a request
nobody could answer.
- A subagent runs its own thread over the same connection. Its `turn/started`
no longer satisfies the root thread's waiter, and every event now carries the
thread it actually came from.
- Publication is one step at the end of acquire: the process identity is in
hand, the child is still alive, and the attempt still owns the session. A
failure reaps only its own child, so a replacement already published keeps
running.
- Codex talks before its thread is named. Those events wait in the acquisition
window and are delivered in arrival order once the session is published,
instead of being dropped.
- A broken stdin pipe ends the transport rather than one write: the child is
reaped and the owner learns its lease is worthless.
- A death that reaches us through several listeners reports once, keeping the
first cause rather than the generic exit that follows it.
Implements the structured agent-session adapter for Codex: one long-lived
app-server child per session, started or resumed under the lease the host
already reserved, reporting the process identity the lease re-proves and the
provider handle link the journal binds to.
The thread the adapter reports is the one Codex actually opened, never the one
a client declared, and a resume that lands on a different thread is refused
rather than recorded as a resume. A turn is named from the start response when
Codex sends an id there and from the turn-started notification when it acks
first; when neither names it in time the submission settles unknown, which the
journal renders as delivery unconfirmed rather than failure. Only a refusal
Codex actually answered becomes a rejection.
Model, effort, and policy are per-turn overrides — there is no thread-settings
write — so an option set between turns is applied to the next turn/start, and
unlisted keys are refused instead of becoming arbitrary client-controlled
parameters.
Codex blocks a turn until every approval and tool-input request it sent is
answered, but the answer arrives much later and through a durable journal item
id rather than the live JSON-RPC request id. This holds the live requests for a
session and turns a chosen option back into the reply Codex expects.
Three cases the journal boundary forces: one request can carry several
questions but takes a single reply, so answers are buffered until the request is
complete; a request this build does not model is refused rather than left
blocking or silently approved; and a prompt is forgotten before its reply is
written, so a second answer finds nothing instead of replying twice.
The existing app-server client is request-scoped: one deadline for the whole
session, and every inbound message that is not a pending response is dropped. A
structured chat session needs the opposite — a child that outlives any single
call, per-request deadlines, and both traffic directions, because Codex streams
a turn as notifications and asks for approvals by sending requests back.
Adds a connection that spawns the child, completes the initialize handshake,
and stays open until closed, keeping the same environment overlay and reap
semantics as the short-lived client. A refusal is now distinguishable from a
call that never settled, so callers can tell "Codex declined" from "the child
died" instead of reporting both as failure.
Extends the cross-version harness, which covered the terminal stream only, with
the structured surface. The old side's advertised capabilities and registered
method names are read from the extracted release checkout rather than written by
hand: a build cannot advertise a string its own source never names, so "the old
side does not have it" stays a fact about a real release.
Ten methods on the runtime manifest, including a paged `agentSession.history`
over the journal cursor and a subscribe that streams cursor-qualified batches.
A declared client that did not advertise `agent-session.structured.v1` is told
the whole surface does not exist, so an old client never sees a session it
cannot render; an in-process caller negotiates nothing and is served.
One place where the lease, the journal, and the provider adapter meet: every
mutating call recomputes the fingerprint, admits through the durable operation
ledger, checks the lease, then acts, so no method can grow its own admission
rules. Journal recovery rehydrates an unreadable journal from provider history
into a fresh epoch and tells subscribers which reset they are seeing.
Attach adjudicates the leases it loaded from disk before granting any writer.
Without that step every lease stays unreconciled for the life of the process and
a restarted host refuses every persisted session forever.
Adds the request/refusal shapes every `agentSession.*` method speaks, the
four-field idempotency envelope and its lease admission, the item-key inverse an
upsert needs, and the `agent-session.structured.v1` capability. The protocol
version stays at 3: the surface is additive, and bumping it would strand every
paired device on the current release rather than degrade one feature.
Route every byte that can reach a PTY through the single-writer lease
adjudicator, so a session owned by one runtime cannot be typed into by
another. Nothing binds a PTY to a durable session record yet, so the gate
short-circuits and today's shells, legacy agent terminals, and unbound
sessions are untouched.
Gated paths: terminal.send RPC, the renderer IPC write paths
(pty:write / pty:writeAccepted), plugin sendTerminalText, the agent-prompt
paste, and ptyController.write as a backstop for runtime writes that never
passed a typed gate. Every yield inside a write - inter-chunk, the
text/suffix pause, the agent-prompt submit delay, deferred size
measurement - re-checks the fence, so a handoff mid-paste stops the
remaining chunks instead of interleaving two owners.
Refusals are typed and additive on the wire: an optional
agentSessionRefusal on RuntimeTerminalSend and on the existing
pty:writeUnavailable payload, refusal codes drawn only from the existing
agent-session error codes, and stream-input refusals mapped onto the
WriteUnavailable frame old clients already decode.
* Show attention badges on recent chats in Cmd+J palette
Keep current tabs visible only when they have a scannable badge
(working, permission, unread, done). Snapshot unread maps alongside
status maps to freeze recent-section membership on open instead of
churning with live updates. Unify badge logic across tab strip and
palette, and extract test fixtures for reuse between suites.
* Exclude idle current tabs from Cmd+J recent; add live attention badges
Current tabs no longer appear in Recent Chats when idle—only working, blocked, or
unread agents keep the current slot visible. `done` no longer admits current tabs;
the user watched it complete on screen, so that slot goes elsewhere.
Add live attention badges to recent rows (working/permission/unread/done), matching
the tab-bar ladder. Freeze recent-section membership at open-time to keep row order
and inclusion synchronized instead of changing live.
* minor fix