feat(sdk): Phase 2a surface — PTY, stdin, events, lifecycle, capabilities, re-attach (CORE-58) - #583
Conversation
commands.run gains pty (tty + initial size; output arrives merged on the pty channel) and stdin (string/bytes = write-then-close, subprocess semantics; true = keep open for the handle). CommandHandle gains writeStdin/closeStdin/stdinStatus/resize with an offset-tracked cursor that advances only on success, so retried writes are deduplicated by the daemon (offset-idempotent). commands.get(id) re-attaches a handle by execution id and seeds the stdin cursor from the daemon's accepted count. A one-shot feed racing the process's early exit is swallowed when the execution has exited — the exit result is the truth.
commands.run gains pty=PtySize(cols, rows) (tty + initial size; output arrives merged on the pty channel) and stdin (str/bytes = write-then-close, subprocess semantics; True = keep open for the handle). CommandHandle gains write_stdin/close_stdin/stdin_status/resize with an offset-tracked cursor that advances only on success, so retried writes are deduplicated by the daemon (offset-idempotent). commands.get(id) re-attaches a handle by execution id and seeds the stdin cursor from the daemon's accepted count. A one-shot feed racing the process's early exit is swallowed when the execution has exited. Sync tree regenerated via gen_sync.py.
The attach loop shared by CommandHandle.output and waitForExit's result collection now tracks the per-channel delivered offsets and, when the transport drops mid-stream, re-attaches from them — the daemon replays nothing already delivered, so consumers see one seamless, gapless stream. Only consecutive dead dials count against the retry budget (delivered output resets it); exhaustion surfaces as the new ConnectionLostError (subclass of ConnectionFailedError), which also gains the mid-stream teardown syscall shapes (ECONNRESET/EPIPE/ ECONNABORTED) in the connection-failure classifier. Daemon-typed stream errors are never retried.
The attach loop shared by CommandHandle.output and wait_for_exit's result collection now tracks the per-channel delivered offsets and, when the transport drops mid-stream, re-attaches from them — the daemon replays nothing already delivered, so consumers see one seamless, gapless stream. Only consecutive dead dials count against the retry budget (delivered output resets it); exhaustion surfaces as the new ConnectionLostError (subclass of ConnectionFailedError). A truncated streaming body — no terminal EndStreamResponse — now raises ConnectionLostError too: truncation means the connection died mid-body. Daemon-typed stream errors are never retried. Sync tree regenerated.
sandbox.events() yields typed lifecycle events (keepalives filtered; clean server end ends the iterator; a mid-stream drop after frames flowed surfaces as ConnectionLostError — re-subscribing is the caller's call since missed events cannot be replayed). sandbox.setLifecycle takes a tri-state update: an omitted knob is absent on the wire (unchanged), null is an explicit zero/UNSPECIFIED (restore the daemon default), a value replaces — presence-honest against the proto's optional fields. arcbox.capabilities() exposes the daemon handshake (version, protocol, features, nested_virt), cached per client with failed fetches uncached; the SDK does not gate on it. Stale CORE-21-stub comments dropped — Pause/Resume are implemented daemon-side.
sandbox.events() yields typed SandboxEvent values through the Async/EventStream pair (keepalives filtered; clean server end ends the iterator; the context-manager form cancels the subscription on early exit; a mid-stream drop surfaces as ConnectionLostError). sandbox.set_lifecycle is tri-state via the exported UNCHANGED sentinel: omitted (UNCHANGED) leaves a knob alone, None restores the daemon default — the same meaning None already has on create — and a value replaces, mapped presence-honestly onto the proto's optional fields. arcbox.capabilities() exposes the daemon handshake, cached per client with failed fetches uncached; the SDK does not gate on it. Stale CORE-21-stub comments dropped. Sync tree regenerated.
Both live-daemon loops now cover the capabilities handshake (protocol, features, nested_virt supported), foreground stdin echo, background offset-idempotent stdin writes, commands.get re-attach with replayed output, a PTY stty-size roundtrip plus a live resize, and the setLifecycle tri-state against Inspect's ttl_deadline; a second sandbox with a 4s idle timeout and the PAUSE policy proves events() observes PAUSING (reason idle_timeout) then PAUSED — the Python flavor through the genuinely blocking sync stream. README feature tables updated to the phase 2a surface, with ListExecutions/WaitForPort/path verbs/ Template CRUD recorded as 2b (the daemon answers Unimplemented).
|
Your Claude subscription has hit its usage limit. It resets at 12am (UTC). Re-trigger Pullfrog after the reset, or add an Add repo secret → · Model settings → · Setup docs → · Ask in Discord →
|
|
Your Claude subscription has hit its usage limit. It resets at 12am (UTC). Re-trigger Pullfrog after the reset, or add an Add repo secret → · Model settings → · Setup docs → · Ask in Discord →
|
Greptile SummaryThe PR extends the Python and TypeScript SDKs with the daemon-supported Phase 2a execution and lifecycle surface.
Confidence Score: 5/5The PR appears safe to merge because no blocking failure remains in the eligible follow-up scope. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| sdk/python/src/arcbox/_async/commands.py | Adds asynchronous PTY and stdin controls, execution lookup, and offset-resumable output and result collection. |
| sdk/python/src/arcbox/_sync/commands.py | Mirrors the asynchronous command additions on the generated synchronous SDK surface. |
| sdk/typescript/src/commands.ts | Adds the TypeScript command execution, stdin, PTY, re-attachment, and resumable-stream APIs. |
| sdk/python/src/arcbox/_async/sandbox.py | Adds asynchronous capabilities discovery, lifecycle updates, and typed lifecycle event streaming. |
| sdk/typescript/src/sandbox.ts | Adds TypeScript capabilities caching, lifecycle mutation, and lifecycle event streaming. |
| sdk/python/src/arcbox/_types.py | Defines the new Python public DTOs, event types, PTY size, stdin status, and unchanged-value sentinel. |
| sdk/typescript/src/types.ts | Defines the corresponding TypeScript Phase 2a public data contracts. |
| sdk/python/src/arcbox/_async/_client.py | Classifies truncated unary and streaming Connect responses as connection-loss errors. |
| sdk/typescript/test/reattach.test.ts | Exercises transport-loss recovery and exact-offset output re-attachment behavior. |
| sdk/python/tests/test_stdin_pty.py | Covers stdin cursor semantics, one-shot feeding, PTY configuration, and process cleanup behavior. |
Sequence Diagram
sequenceDiagram
participant App
participant SDK
participant Daemon
App->>SDK: run(command, PTY/stdin options)
SDK->>Daemon: StartExecution(execution_id, tty, stdin)
Daemon-->>SDK: Execution
opt One-shot stdin
SDK->>Daemon: WriteStdin(offset, data)
SDK->>Daemon: "WriteStdin(offset, eof=true)"
end
SDK->>Daemon: AttachExecution(stdout_offset, stderr_offset)
loop Stream execution events
Daemon-->>SDK: Output(channel, offset, data)
SDK->>SDK: Advance delivered channel offset
end
alt Transport loss
SDK->>Daemon: AttachExecution(last delivered offsets)
Daemon-->>SDK: Replay remaining output
else Execution exits
Daemon-->>SDK: Exited(result)
SDK-->>App: CommandResult
end
Reviews (2): Last reviewed commit: "fix(sdk): treat server-signaled unavaila..." | Re-trigger Greptile
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bc5f8c864a
ℹ️ 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".
…ked stdin feeds Review findings (Codex): the daemon losing its upstream stream ends the HTTP stream cleanly with a Connect unavailable error frame, which the Python decoder types as ConnectionFailedError — neither httpx.HTTPError nor ConnectionLostError — so the attach resume loop and the events stream-death reclassification both missed it. Both now catch ConnectionFailedError (daemon-typed errors still map to other classes and are never retried), matching the TypeScript predicate. And a one-shot stdin feed that fails while the process still runs now best-effort SIGKILLs it before surfacing the feed error: a thrown run() returns no handle, so the fed process (cat waiting on input) would otherwise keep the sandbox RUNNING with no way to reach it — both languages. Sync tree regenerated.
|
Your Claude subscription has hit its usage limit. It resets at 12am (UTC). Re-trigger Pullfrog after the reset, or add an Add repo secret → · Model settings → · Setup docs → · Ask in Discord →
|

Extends both SDKs (
@arcbox/sandbox,arcbox) with the daemon-ready Phase 2a surface. Scope was verified against the connect handlers: everything here is served today; anything answeringUNIMPLEMENTEDstays in 2b.Surface
commands.rungainspty: {cols, rows}/pty=PtySize(cols, rows)(tty +tty_sizeon StartExecution; output arrives merged on theptychannel);CommandHandle.resize(cols, rows)drives ResizeExecutionTty.writeStdin/closeStdin/stdinStatus(write_stdin/close_stdin/stdin_status). The handle tracks the stdin cursor and advances it only on success, so a retried lost write lands at the same offset and the daemon's dedup makes it idempotent — never a double feed. Foregroundrungainsstdin: string|bytes(write-then-close,subprocesssemantics: a feed racing the process's early exit is noise when the execution has exited);stdin: truekeeps it open for background handles.outputandwaitForExit's result collection tracks per-channel delivered offsets and re-attaches from them on transport death, so consumers see one seamless, gapless stream. Only consecutive dead dials count against the retry budget (3; delivered output resets it); exhaustion surfaces as the newConnectionLostError(subclass ofConnectionFailedError). Daemon-typed stream errors are never retried.commands.get(id)— re-attach a handle by execution id (WaitExecution poll validates existence and seeds the stdin cursor fromExecution.stdin).sandbox.events()— typed lifecycle events, keepalives filtered, deterministic termination; a mid-stream drop surfaces asConnectionLostError(reconnect deferred to 2b by design — missed events cannot be replayed). Python sync reads genuinely block (httpx sync stream), no polling.setLifecycletri-state — presence-honest against the proto'soptionalfields:null= restore default (no TTL / no idle detection / default action), value = replace.UNCHANGEDsentinel, exported) = unchanged,None= restore default — the same meaningNonealready has oncreate— value = replace.arcbox.capabilities()— GetCapabilities mapped to a DTO, cached per client (failed fetches uncached). The SDK does not gate on it; fail-fast stays daemon-side.Deferred to 2b (daemon evidence)
commands.list()—ListExecutionsanswersUNIMPLEMENTED(app/arcbox-api/src/connect/process.rs, "CORE-58 phase 2"); likewiseWaitForPort, the filesystem path verbs (connect/filesystem.rs), and Template CRUD (connect/template.rs). README feature tables record this.Validation
cargo test -p arcbox-e2e --test sdk_ts -- --ignored→ vitest 3/3;--test sdk_py→ pytest 4/4. Covers the capabilities handshake, stdin echo, offset-idempotent background stdin,get(id)re-attach with replayed output, PTYstty sizeroundtrip (40 120) plus live resize (50 200), setLifecycle againstttl_deadline, andevents()observing the idle auto-pause (pausingwithreason=idle_timeout→paused) — the Python flavor through the blocking sync stream.