Skip to content

fix(channel): fail workspace calls fast when the workspace host stops answering - #376

Merged
iamnbutler merged 2 commits into
mainfrom
workspace-liveness
Oct 15, 2026
Merged

iamnbutler merged 2 commits into
mainfrom
workspace-liveness

Conversation

@iamnbutler

Copy link
Copy Markdown
Contributor

Fixes #372.

A frozen or unreachable workspace host kept its socket attached to the hosted channel, so the instructions prompt section waited on AGENTS.md reads and blocked model-only runs until the host returned.

  • Liveness: hosts send {"ping":true} on the workspace socket every 5s. The object answers {"pong":true} through setWebSocketAutoResponse, so hibernation is unaffected.
  • Link fails fast: Link treats a socket whose getWebSocketAutoResponseTimestamp is older than 15s (three intervals) as not responding. New calls fail immediately with "The workspace is not responding". Calls already in flight fail on a watchdog that runs only while calls are pending, and each one gets a cancel for when the host resumes. Desktop and browser actions are refused without being dispatched.
  • Instructions degrade: instructions now treats only a missing file or directory as absent. Any other read failure throws, so pi keeps the instructions it last showed and reports the error, where before it silently dropped them.
  • Host reconnects: once a service has answered a ping, a host that then misses three pongs closes the socket and reconnects. It counts unanswered pings rather than elapsed time, so a host waking from sleep keeps its live socket.

Compatibility

  • An older host never pings, so its socket has no auto-response timestamp and is treated as answering, the same as today. I checked this: a host built from main sat idle for 30s against the new Worker and its tools still worked.
  • A new host against an older Worker: the ping is a JSON object that the old Link.receive ignores. No pong comes back, so the host never starts watching for pongs. This was not run, only read from the code.
  • ace-channel-study needs a redeploy from main to get liveness.

Smoke test (wrangler dev plus a source host on its own port and data dir, openai/gpt-4.1-mini, with an AGENTS.md instruction the model has to follow):

  • Host live: the run answered in 1.7s and followed AGENTS.md.
  • Ask sent right after SIGSTOP of the host: answered in 16s. The pending read failed at the stale mark, and the kept instructions still applied.
  • Ask sent 20s into the freeze: answered in 1.5s. A shell tool reported "The workspace is not responding" within 2s.
  • After SIGCONT: tools worked again on the same socket, with no reconnect.
  • After SIGSTOP of workerd: the host logged workspace.drop (stale) about 16s later and reconnected after SIGCONT. Tools worked afterward.

Built in Ace

No — built with Claude Code outside Ace by an overnight issue watcher, because this Mac's Ace Canary is a participant in the running two-person study (#218); dogfooding gap tracked in #5.

… answering

A frozen or unreachable workspace host kept its socket attached, so the
instructions prompt section waited on AGENTS.md reads and blocked model-only
runs until the host returned (#372).

- Hosts ping the workspace socket every 5s; the hosted object answers through
  setWebSocketAutoResponse, so hibernation still works.
- Link treats a socket whose last ping is older than three intervals as not
  responding: new calls fail at once and calls in flight fail (with a cancel
  sent for when the host resumes).
- The instructions section throws when the workspace can't be read, so pi
  keeps the last-shown instructions and reports why, instead of dropping them.
- Hosts reconnect when a service that has answered pings misses three.

Hosts that predate pings have no auto-response timestamp and are treated as
answering, as before. An older service ignores the ping frame, and a host
only watches for pongs once one has arrived.

Why: packages/channel/src/workspace.ts#class Link | a socket with no auto-response timestamp belongs to a host that predates pings and must never be declared stale
Why: packages/channel/src/workspace.ts#const PING | PING and PONG are matched byte-for-byte by setWebSocketAutoResponse and must stay JSON objects an older Link.receive ignores
Why: packages/channel/src/context.ts#function absent | only a missing file means no instructions; any other read failure must throw so pi keeps the last-shown section
Why: apps/host/src/workspace.ts#function link | staleness counts unanswered pings, not elapsed time, so a host resuming from sleep does not drop a live socket, and only after a first pong so older services are not reconnected forever
@iamnbutler
iamnbutler merged commit ba50839 into main Oct 15, 2026
1 check passed
iamnbutler added a commit that referenced this pull request Oct 15, 2026
…ay (#381)

* fix(app): say a hosted channel's tools fail while its workspace is away

Why: apps/app/src/channel.tsx#function workspace | the hosted details must describe what tools do while the workspace is unreachable: since #376 they fail fast

* lane: sync hosted-tools-copy memory
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.

Hosted channel: model-only runs wait on an unresponsive workspace

1 participant