molecule-core/workspace-server
Hongming Wang d552c43b94 fix(a2a-proxy): close 60s context-canceled gap on long silent runs
Two compounding bugs caused the "context canceled" wave on 2026-04-26
(15+ failed user/agent A2A calls in 1hr across 6 workspaces, including
the user's "send it in the chat" message that the director never
received):

1. **a2a_proxy.go:applyIdleTimeout cancels the dispatch after 60s of
   broadcaster silence** for the workspace. Resets on any SSE event
   for the workspace, fires cancel() if no event arrives in time.
2. **registry.go:Heartbeat broadcast was conditional** —
   `if payload.CurrentTask != prevTask`. The runtime POSTs
   /registry/heartbeat every 30s, but if current_task hasn't changed
   the handler emits ZERO broadcasts. evaluateStatus only broadcasts
   on online/degraded transitions — also no-op when steady.

Net: a claude-code agent on a long packaging step or slow tool call
keeps the same current_task for >60s → no broadcasts → idle timer
fires → in-flight request cancelled mid-flight with the "context
canceled" error the user sees in the activity log.

Fix:

(a) Heartbeat handler always emits a `WORKSPACE_HEARTBEAT` BroadcastOnly
    event (no DB write — same path as TASK_UPDATED). At the existing 30s
    runtime cadence this resets the idle timer twice per minute.
    Cost is one in-memory channel send per active SSE subscriber + one
    WS hub fan-out per heartbeat — far below any noise floor.

(b) idleTimeoutDuration default bumped 60s → 5min as a safety net for
    any future regression where the heartbeat path goes silent (e.g.
    runtime crashed mid-request before its next heartbeat). Made
    env-overridable via A2A_IDLE_TIMEOUT_SECONDS for ops who want to
    tune (canary tests fail-fast, prod tenants with slow plugins want
    longer). Either fix alone closes today's gap; both together is
    defence in depth.

The runtime side already POSTs /registry/heartbeat every 30s via
workspace/heartbeat.py — no runtime change needed.

Test: TestHeartbeatHandler_AlwaysBroadcastsHeartbeat pins the property
that an SSE subscriber observes a WORKSPACE_HEARTBEAT broadcast on a
same-task heartbeat (the regression scenario). All 16 existing handler
tests still pass.

Doesn't fix: task #102 (single SDK session bottleneck) — peers will
still queue when busy. But this PR ensures the queue/wait flow
actually completes instead of being killed by the idle timer
mid-wait.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:45:44 -07:00
..
cmd/server feat(workspace-server): GHCR digest watcher closes runtime CD chain (#2114) 2026-04-26 13:36:26 -07:00
internal fix(a2a-proxy): close 60s context-canceled gap on long silent runs 2026-04-26 15:45:44 -07:00
migrations chore: second-pass review polish — symmetry + clearer test fixtures 2026-04-25 08:48:30 -07:00
pkg/provisionhook feat(#1957): wire gh-identity plugin into workspace-server 2026-04-24 15:01:41 +00:00
.ci-force chore: force Platform(Go) CI run on main — validate go vet clean 2026-04-21 15:43:19 +00:00
.gitignore feat(ws-server): pull env from CP on startup 2026-04-19 02:41:15 -07:00
.golangci.yaml chore(workspace-server): add golangci.yaml disabling errcheck 2026-04-24 07:16:54 +00:00
Dockerfile chore: extract ContextMenu Zustand fix + a2a_proxy local-docker SSRF bypass + workspace-server Dockerfile GID entrypoint 2026-04-22 20:00:16 -07:00
Dockerfile.tenant feat(terminal): remote path via aws ec2-instance-connect + pty 2026-04-21 18:13:29 -07:00
entrypoint-tenant.sh fix(security): add USER directive before ENTRYPOINT in all tenant images (#1155) 2026-04-20 23:51:33 +00:00
go.mod feat(#1957): wire gh-identity plugin into workspace-server 2026-04-24 15:01:41 +00:00
go.sum feat(#1957): wire gh-identity plugin into workspace-server 2026-04-24 18:28:18 +00:00