Skip to content

exec+sdk: stateful shell sessions with reattach#188

Merged
motatoes merged 3 commits intomainfrom
feat/exec-shell-sessions
Apr 24, 2026
Merged

exec+sdk: stateful shell sessions with reattach#188
motatoes merged 3 commits intomainfrom
feat/exec-shell-sessions

Conversation

@motatoes
Copy link
Copy Markdown
Contributor

Summary

  • New exec.shell() on both TS + Python SDKs: a long-lived bash exec session with a stable sessionId, where cwd, exported env vars, and shell functions persist across .run() calls. Terminal-tab ergonomics, single server-side process.
  • New exec.reattachShell(sessionId) / exec.reattach_shell(session_id): revisits an open shell from a different client (or a different process invocation). No backend changes required — the worker already sends 0x04 scrollback-end markers and exec sessions survive client disconnect.
  • Per-run() framing uses dual sentinels: a DONE marker on stdout and an EXIT marker on stderr. A run resolves only once both markers are observed. Without this, the stderr sentinel frequently lands before the user's stdout is drained (stdout/stderr are read on separate goroutines in the agent) and the trailing stdout is silently dropped into the next run.
  • ShellImpl gates on scrollback completion: inbound chunks before the 0x04 marker are ignored, so reattaching to a shell that has prior runs in its scrollback doesn't mis-parse old sentinels as a current completion.

Incidental fixes found while wiring this up

  • TS Exec.attach() now awaits ws.onopen before returning. The previous code returned while the WS was still CONNECTING, and the first sendStdin() was silently dropped by the readyState !== OPEN guard in sendStdin.
  • TS: fixed a bash syntax error in the command wrapper ({ cmd\n; }{ cmd\n}). The stray ; after the newline was producing bash: syntax error near unexpected token ';' and bash exited with code 2 on the first run.
  • Python Sandbox.exec now pairs URL + credential correctly. Previously the property passed the sandbox JWT to whichever URL was available; when falling back to _api_url (control plane), the JWT was rejected with an InvalidStatus handshake. Control-plane requests now use the API key and include the /api prefix.

Terminal-tab semantics

exit N inside a run() kills the shell — same as closing a terminal tab — and rejects the pending run with ShellClosedError. Tests updated to match (step 5 originally asserted exit 42 || true; ( exit 7 ) returned 7; now uses a subshell so the outer shell stays alive).

Test plan

  • npx tsx sdks/typescript/examples/test-shell.ts (hits app.opencomputer.dev): all assertions pass through step 15, including reattach preserving cwd/env and exit N closing the shell.
  • python sdks/python/examples/test_shell.py: same assertions, Python side.
  • sandbox.exec.list() still sees the shell session alongside regular exec sessions; shell.sessionId is the same ID.
  • Existing exec.run() / exec.start() / exec.background() behavior unchanged.

🤖 Generated with Claude Code

Adds `exec.shell()` to both SDKs: a long-lived bash session behind a
single exec-session ID. `cwd`, exported env vars, and shell functions
persist across `.run()` calls (terminal-tab ergonomics), and the shell
can be reattached from a different client via `exec.reattachShell(id)`.

Framing protocol: each `run()` sends bash a `{ cmd\n}` brace group
followed by a DONE marker on stdout and an EXIT marker on stderr. Both
must be observed before the run resolves — the agent reads stdout/stderr
on separate goroutines, so the stderr sentinel can otherwise land before
the stdout is drained and the user's output gets dropped.

Other fixes surfaced while bringing this up end-to-end:
- TS `Exec.attach()` awaits `ws.onopen` before returning, so the first
  `sendStdin()` isn't silently dropped by the `readyState !== OPEN` guard.
- `ShellImpl` ignores inbound chunks until the worker's `0x04`
  scrollback-end marker, so reattach doesn't mis-parse historical
  sentinels from prior runs as a current completion.
- Python `Sandbox.exec` pairs URL + credential: direct-worker URL uses
  the sandbox JWT, control-plane fallback uses the API key and includes
  the `/api` prefix. Sending the JWT to the control-plane WS endpoint
  yields an InvalidStatus handshake; the bare URL (no /api) yields HTTP
  200 from the control plane's HTML fallback.

Docs and examples updated; new end-to-end shell tests in both SDKs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mintlify
Copy link
Copy Markdown

mintlify Bot commented Apr 24, 2026

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
opencomputer 🟢 Ready View Preview Apr 24, 2026, 7:12 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 24, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
opensandbox Ready Ready Preview, Comment Apr 24, 2026 9:15pm

Request Review

- test step 7 now prints each stdout/stderr chunk with an arrival
  timestamp and gap-since-last-chunk, so callers can see exactly when
  bytes land instead of only that they landed. The hard streaming-span
  assertion is dropped — upstream hops (CF/ALB, agent→worker gRPC) may
  legally coalesce small frames, making the timing brittle to assert on.

- Adds a standalone stream demo in both SDKs (stream_demo.py /
  stream-demo.ts) that simulates ~6s of apt-install-style output
  through shell.run() and prints per-chunk timing + a final summary
  (exit code, wall time, chunk counts, byte totals). Useful for eyeball
  verification of streaming behavior in real deployments.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…veats

Adds a when-to-use-what table and rules of thumb at the top of Running
Commands so readers don't have to infer the purpose of each primitive
from its signature.

Expands the Shell section with:
- reattachShell() usage (stable sessionId across SDK invocations)
- terminal-tab exit semantics (exit N closes the shell)
- streaming output caveats (bash-builtin stdio buffering; upstream frame
  coalescing), with a pointer to the stream-demo scripts

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@breardon2011 breardon2011 left a comment

Choose a reason for hiding this comment

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

Approve

@motatoes motatoes merged commit eed4de6 into main Apr 24, 2026
3 checks 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