You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Writing text or Enter to a terminal does not prove the intended live coding process accepted a turn. Process replacement and manual input can race programmatic delivery.
Summary and scope
Track text and submission phases, fence delivery and status by the current runtime identity, isolate composer input, and contain interrupted or failed writes during shutdown and WebSocket input.
Recheck terminal input ownership after the durable pre-write hook and compensate an unsubmitted claim when manual input wins the race. The same compensation applies to staged-input collisions.
How this increment fits
Ordinary session input and runtime fencing are complete here. Bootstrap and delegation consume this delivery contract in later parts.
Stack and review boundary
Part 04 of 15 in the Agent Map review stack; review this increment against its predecessor.
Root checks ran against 023c09d98991ba60cec88f60b0aaf443a99681da. The final head changes only README terminology or commit ancestry; a complete tracked-file comparison confirms identical executable source and build inputs. The terminology gate was rerun on 3d105bcac3517691cefb893382a54633204f8229.
Regression coverage: Partial writes, text/Enter acknowledgement, runtime replacement, stale status events, manual-input preemption during durable writes, and interrupted shutdown.
See part 15 for integrated browser, native CLI, and Mac journey validation. The checks above were run independently on this PR’s own commit.
Linux tests run with ordinary user filesystem permissions; the sandbox's extra ambient capabilities are dropped. Hosted CI and automated review are separate from these recorded local results.
Compatibility and release impact
Compatibility: Breaking for embedders: SessionManager.write() can throw SESSION_INPUT_ISOLATION_REQUIRED when prior partial input cannot be cleared. Terminal-forwarding callers must handle the failure.
No secrets, credentials, private user data, or unsanitized logs are included.
This PR does not publicly disclose a suspected vulnerability.
AI assistance
Codex assembled the implementation, addressed reproduced defects, supplied tests and documentation, inspected the diff, and ran the checks above. Reviews are handled by hosted PR automation.
Checklist
Read CONTRIBUTING.md; implementation follows the requested 15-PR split.
Description reflects this PR's actual predecessor-relative diff.
Relevant tests accompany the changed behavior.
Root build, typecheck, lint, and test evidence matches the final implementation; any documentation-only update is identified above.
Release/documentation treatment is explained above.
No confidentiality findings: the changeset, comments and test fixtures are provider-neutral and
name no customer, partner or internal system. No frontend component code in the diff.
1. Raw keystrokes during beforeFirstWrite are not preempted — the exact prompt-merge this PR exists to prevent
The background-preemption fence reads terminalInputEpochs exactly once, at line 1571, and is
never re-read. Immediately after it comes await lifecycle.beforeFirstWrite() (line 1586) —
documented as "durable transition that must commit before the first PTY byte", i.e. an unbounded
I/O wait; the new test "waits for the durable pre-write hook before crossing the PTY boundary"
holds it open with a deferred to prove exactly that. The only fence after that await is this.closing || canWriteNow() (line 1592), and canWriteNow is caller-supplied and knows
nothing about terminal input. stagedInputs has no entry yet either, so the write() preemption
path at line 1478 cannot fire.
Failure scenario: a background submitInput(id, "automatic map bootstrap", true, undefined, true, { beforeFirstWrite }) passes the epoch check; while beforeFirstWrite is committing, the user
types fix the login bug into the same live composer. write() bumps the epoch, but nobody reads
it. submitInput resumes, writes its text onto the user's line, sleeps SUBMIT_DELAY_MS, sees staged.preempted === false, and writes \r. The agent receives fix the login bugautomatic map bootstrap as one turn.
Fix: re-check (this.terminalInputEpochs.get(id) ?? 0) !== initialTerminalInputEpoch in the
synchronous fence at line 1592, alongside this.closing (and route it through onNotSubmitted/SessionBackgroundInputPreemptedError(false) like the other pre-write bailouts).
2. SessionManager.write() gains a routine throw, shipped as a patch with no note and no exported error type
SessionManager is published type surface: src/index.ts exports HarnessServer, whose sessionManager: SessionManager field puts every method here in dist/index.d.ts. write()
previously returned boolean on a fenced composer; it now throws SessionInputIsolationError as
an ordinary, expected outcome — this PR had to add a try/catch in terminal-ws.ts precisely
because nothing was catching it before. setReady() also silently changed to no-op on exited
sessions, and submitInput() grew two positional parameters.
An embedder that calls server.sessionManager.write(...) from a synchronous socket/IPC handler now
gets an uncaught exception on a code path that used to be total. That is a behavior change a pinned
consumer breaks on, described in the changeset only as "handled consistently".
Two fixes, both needed:
Bump the changeset to minor and state the new throw explicitly, with the "catch and reconnect"
guidance terminal-ws.ts:88-96 already encodes.
Export SessionInputIsolationError (and SessionManagerClosingError, SessionBackgroundInputPreemptedError) from src/index.ts next to the existing HarnessError
block. As written they cross the published boundary but are unreachable by name, so consumers can
only string-match err.code — the same gap SessionInputGuardRejectedError already has; don't
widen it.
3. The staged-collision bailout is the one pre-Enter reject that skips onNotSubmitted
Every other bailout before the first byte compensates the caller: the isolation reject (1581), the beforeFirstWrite throw (1588), and the final fence (1593) all await onNotSubmitted(). The stagedInputs.has(id) collision throws bare — and it sits afterbeforeFirstWrite has already
committed its durable transition, so the caller is left with a committed pre-write record and no
positive not-submitted evidence to unwind it. A caller using submitInput directly (rather than submitInputTracked, which infers phase: "not-written") cannot distinguish this from an
ambiguous write and, per this PR's own contract, must fail closed and drop the turn.
Also note the error is SessionBackgroundInputPreemptedError even when the loser is a foreground
API submission and nothing was preempted — the collision is rejected, not the incumbent.
Verdict: Request changes — finding 1 is a live prompt-corruption path in the exact invariant
this increment is written to establish, and finding 2 ships a breaking behavior change to a
published API as a patch.
Own-PR delta is one commit, 023c09d9: session-manager.ts (+14), its tests (+55), and the
changeset. Everything else in the range is the rebased base branch (#822). No confidentiality
issues in the new prose; the changeset stays provider-neutral.
Fixed
Finding 1 (prompt merge during beforeFirstWrite) — resolved. session-manager.ts:1596-1604
re-reads terminalInputEpochs after the hook, before the first byte, with no await between
that check and pty.write, and routes the bailout through onNotSubmitted + SessionBackgroundInputPreemptedError(false). Covered by "preserves user keystrokes received
during a deferred pre-write hook".
Finding 3 (staged collision skipped compensation) — resolved at session-manager.ts:1663, with a test.
Not fixed
Finding 2, second half — error classes still unexported. The changeset now correctly says minor (0.14.0, pre-1.0) and calls out the write() throw as Breaking, but SessionInputIsolationError, SessionManagerClosingError and SessionBackgroundInputPreemptedError
(session-manager.ts:589/599/610) are still absent from packages/harness/src/index.ts, while SessionManager reaches dist/index.d.ts via HarnessServer.sessionManager
(server/index.ts:375). The changeset tells embedders to "handle this failure" using a name they
cannot import — the only option is string-matching err.code. Add them to the export block
alongside startServer.
Nit
The two bailouts beforebeforeFirstWrite — the canWrite reject (:1568) and the first epoch
fence (:1576) — still throw bare, while every bailout after them awaits onNotSubmitted. Same
inconsistency the new fix removed one line lower.
Correction to round 1: the SessionBackgroundInputPreemptedError-for-foreground-collision remark
was overstated; the error describes the losing caller, and it is only mislabelled when that
loser was itself a foreground submission.
Verdict: Approve once the three error classes are exported. The correctness fix is sound.
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
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.
Primary change type
Problem and motivation
Writing text or Enter to a terminal does not prove the intended live coding process accepted a turn. Process replacement and manual input can race programmatic delivery.
Summary and scope
Track text and submission phases, fence delivery and status by the current runtime identity, isolate composer input, and contain interrupted or failed writes during shutdown and WebSocket input.
Recheck terminal input ownership after the durable pre-write hook and compensate an unsubmitted claim when manual input wins the race. The same compensation applies to staged-input collisions.
How this increment fits
Ordinary session input and runtime fencing are complete here. Bootstrap and delegation consume this delivery contract in later parts.
Stack and review boundary
3d105bcac3517691cefb893382a54633204f8229; 2,228 changed lines across 11 files, counting additions and deletions including tests.fix/studio-onboarding-followups.Related work
Agent Map checkpoint SAP-3147; relevant work SAP-3148. This packaging follows the maintainer-approved 15-PR split.
Validation
Root checks ran against
023c09d98991ba60cec88f60b0aaf443a99681da. The final head changes only README terminology or commit ancestry; a complete tracked-file comparison confirms identical executable source and build inputs. The terminology gate was rerun on3d105bcac3517691cefb893382a54633204f8229.Tests and documentation
Regression coverage: Partial writes, text/Enter acknowledgement, runtime replacement, stale status events, manual-input preemption during durable writes, and interrupted shutdown.
See part 15 for integrated browser, native CLI, and Mac journey validation. The checks above were run independently on this PR’s own commit.
Linux tests run with ordinary user filesystem permissions; the sandbox's extra ambient capabilities are dropped. Hosted CI and automated review are separate from these recorded local results.
Compatibility and release impact
.changeset/ordinary-session-input.mdSecurity
AI assistance
Checklist