feat: wake a cluster agent's sandbox from the console instead of explaining - #1077
Conversation
There was a problem hiding this comment.
Requesting changes for one lifecycle regression in the session console. The dock intentionally keeps every panel mounted while only the selected panel is visible, and Sessions is the default tab. The new Files hook therefore wakes a sleeping cluster sandbox while the user is merely opening/reading a session, before they select Files. That makes the pod start implicit and can create avoidable cluster churn; the wake should be gated on the Files tab actually becoming active.
I verified the checkout as the trusted synthetic merge of dc90bfa3b4acdadced85b2a69de54f08cfdf255c and f935fd3c7163007288a37b3e017f5a56a3fec7e2, inspected the complete 26-file patch and relevant workspace/placement lifecycle code, and found no trailing-whitespace errors. I could not rerun the TypeScript/Vitest checks because this isolated checkout has no installed dependencies and the sandbox denies /dev/null, which prevents the Git/pnpm wrappers from starting.
sent by review-bot (Codex · gpt-5.6-sol) · open in session
| const { dirs, root, expanded, toggleDir, loadMoreDir } = useWorkspaceTree(agentId, sessionId, refreshTick + wakeTick) | ||
| const { git, outcome, primaryBranch } = useWorkspaceGitStatus(agentId, sessionId, refreshTick + wakeTick) | ||
| const retryRoot = useCallback(() => setWakeTick((tick) => tick + 1), []) | ||
| const wake = useSandboxWake(agentId, workspaceRootReadState(root), retryRoot) |
There was a problem hiding this comment.
FilesPanel is mounted even while hidden: SessionDetailView renders every DockPanel, defaults to Sessions, and DockPanel only applies display: none. Consequently this hook sees the hidden root read’s asleep refusal and immediately POSTs the wake just from opening a session page. That turns the intended explicit Files action into an implicit pod start. Please pass the tab’s active state into this panel/hook (or otherwise defer the automatic wake until Files is selected) while keeping the panel mounted if its status and UI state need to survive tab changes.
…box up without a turn A cluster agent's files live on its pod's volume and are only readable through a running sandbox, so a console read that refuses with `sandbox-unavailable` needs an explicit way to start it — a GET must never wake anything (#1070). `agent/wake` (C→D REQ → `agent/wake/ok`) is the first half of what a turn does before it prompts: claim the duty if this member does not hold it (the same activation rendezvous a relay trigger uses), then bring the sandbox to Running and bind its shim channel — and deliberately not the second half: no host is started and no ACP session opened. The daemon answers what it observed, never a promise: `running` when the channel is already bound, `starting` when the bind was kicked off (coalesced per agent, so a burst of wakes costs one resume), `unsupported` on a daemon that runs no sandboxes. Only a `--k8s` daemon advertises `agent-wake-v1`; elsewhere there is nothing to wake and the control plane answers without sending a frame the daemon would ignore. The memory reader is deliberately left as it is: managed memory (`memory/`, `channels/`) is written by the daemon's own memory provider under the member's state root, not on the sandbox volume, so reading it "through the sandbox" would read nothing. That is the storage gap #955 M1 already records, and it wants its own fix rather than a nicer refusal here.
… resolved through dispatch The console's "start this agent's sandbox" (#1070). Authorized like every other agent write (viewers refused, `canEdit` on the agent), the route resolves the DISPATCH daemon rather than the serving one: a pool agent whose lease lapsed is served by nobody for one horizon, and the wake is exactly the trigger that gives it a holder again — it reaches a live member, which claims the agent the way a turn would. The frame is org-scoped explicitly, since that member's connection has no entry for an agent it does not hold yet. Answers with what the daemon observed: 202 `running` / `starting` (poll the workspace read), 200 `unsupported` when the daemon does not advertise the wake (a machine-placed agent, or a daemon that runs no sandboxes) — nothing is sent in that case. Wakes are debounced per agent in `AgentWakeCoordinator`: one in flight is joined by every caller, and one settled within 30 s is answered from its result, so a Files and a Memory tab opened together cost the daemon one frame. A failed wake is not remembered.
When the workspace root read refuses with the sleeping-sandbox code (or the agent is known to run on the pool and the tab is opened), the panel presses the wake once, shows a calm "Starting the agent's sandbox…" line instead of an error, and re-issues the read with backoff (2 s → 10 s) until it answers or 90 s pass. Then the existing not-available copy returns with a Start button that presses the wake again; a daemon with nothing to wake (`unsupported`) keeps the copy without one. A refused press (403/404) gives up at once; any other failure lets the read decide, since the wake may simply have raced. `useSandboxWake` is one hook shared by the agent page's file browser and the dock's Files panel, with `SandboxWakeNotice` carrying the two shared notices so the copy cannot drift. The Memory tab is not wired: its reads never refuse as asleep, because managed memory lives on the member's state root rather than the sandbox volume (the storage gap noted in the daemon commit).
The session dock keeps every panel mounted (a hidden one is `display:none`), so the Files panel's hook saw its hidden root read refuse as asleep and pressed the wake just from opening a session page — a pod start as a side effect of a page whose reader had not asked for the files. `useSandboxWake` takes an `active` option: while false it neither presses nor polls, and a poll interrupted by hiding resumes on the next activation. The dock's Files panel passes its tab's selected state through; the agent page's file browser is on screen whenever it is mounted and keeps the default. The Start button still presses when the panel is visible.
|
Addressed the dock finding: the session dock keeps every panel mounted, so a hidden Files tab could press the wake just from opening a session page. |
f935fd3 to
5feda2b
Compare
There was a problem hiding this comment.
Approved. The prior lifecycle blocker is resolved: SessionDetailView now passes the Files tab’s selected state into FilesPanel, and useSandboxWake gates both the automatic POST and polling on that state. Because the dock keeps hidden panels mounted, this prevents merely opening a session from waking a pod while preserving the panel’s status and UI state; the added hook and panel tests cover hidden→visible activation and hiding during polling.
I verified the checkout at exact head 5feda2b666919554a8713358c77dfe10e0d95820, inspected the fix commit and the complete 27-file base-to-head patch, confirmed every changed working file matches the head tree, checked all useSandboxWake callers after the options-signature change, and found no trailing-whitespace errors. I could not independently rerun TypeScript/Vitest because this isolated checkout has no installed dependencies and the sandbox denies /dev/null, which prevents the Git/pnpm wrappers from starting.
sent by review-bot (Codex · gpt-5.6-sol) · open in session
|
Confirmed in sent by |
Summary
Files of a cluster agent live on its pod's volume and are only readable through a running sandbox, so the console now wakes the sandbox instead of explaining why it cannot read — an explicit
POST /agents/:id/wake, pressed once by the Files surfaces and followed by a polled read. A GET never wakes anything.Closes #1070.
Decision (from the issue)
API
POST /orgs/:orgId/agents/:id/wake(wakeAgent, tagged Agents)canEditon the agent; a restricted agent the caller cannot see is 404.placementResolver.dispatchDaemon(agent); 503agent has no live daemonwhen nothing can be reached.agent/wakeonly to a daemon advertisingagent-wake-v1; otherwise 200{ state: 'unsupported' }— a machine-placed agent, or a daemon that runs no sandboxes, gets no frame at all.{ state: 'running' | 'starting' }with what the daemon observed.AgentWakeCoordinator): a wake in flight is joined; one settled within 30 s answers repeat callers from its result; a failure is not remembered.Daemon
agent/wake→agent/wake/ok(AgentWakeReq/AgentWakeOk,state: running | starting | unsupported) and theagent-wake-v1feature, advertised only under--k8s.createAgentWaker: if the member does not hold the agent, claim the duty (claimDutyForTrigger, the same rendezvous a relay trigger uses); if the sandbox channel is bound already,running; else kick offplane.ensureChannel(sandbox → Running + shim bind, no host, no ACP session), coalesced per agent, and answerstarting. A lost claim refuses withunknown-agent(BAD_PAYLOAD), which the CP maps to 503.workspaceFilesForserves on, so a woken sandbox is exactly a readable one; the launch it records gets a full idle window before the sweep may suspend it again.Console
useSandboxWake(agentId, read, retry, sandboxed): presses the wake once when the root read refuses withWORKSPACE_SANDBOX_UNAVAILABLE(or on open for an agent placed on the pool), then re-issues the read with backoff (2 s → 10 s) until it answers or 90 s pass. Then the existing not-available copy returns with a Start button that presses again;unsupportedkeeps the copy without one; a 403/404 on the press gives up at once, any other press failure lets the read decide.WorkspaceFiles, which now also gets the sandbox-specific copy) and the dock's Files panel; the two notices live inSandboxWakeNoticeso the copy cannot drift. The "starting" line is calm (spinner, secondary text), not an error. One responsive tree, so mobile follows.Memory location (issue Gap 2, question 2)
Verified rather than asserted: managed memory (
<agent.dir>/memory/,channels/) is written by the daemon's own memory provider — the memory MCP tools and session-start injection run in the daemon process — under the member's state root, not on the sandbox volume. Under--k8sthat root is the pod's ephemeral disk, so the content is member-local and a duty move loses it; native memory only moves under a private runtime HOME for the local OS sandbox, not for the cluster one. That is the storage gap #955 M1 already lists ("managed agent memory … durable user content with no cloud home"). Per the issue's own guidance this PR does not paper over it: the memory reader still readsagent.dir(a woken sandbox has no memory to read through), and the Memory tab is not wired to the wake, since its reads never refuse as asleep. Moving memory onto the agent's volume (or another durable home) is a separate change; once it lands, the memory reader gets the same one-resolutionsandbox-unavailablerefusal and the Memory tab takes the same hook.Test plan
packages/protocol— typecheck,vitest(frame registration).packages/daemon—test/cp/agent-wake.test.ts(unsupported without a plane, running vs starting, one bind per burst, claim-then-wake, lost claim refuses, failed bind forgotten),client-dispatch(agent/wake→agent/wake/ok, unsupported without a waker, BAD_PAYLOAD reason),daemon-k8s-mode(feature advertised under--k8s),daemon-workspace-git-review-feature(not advertised locally). Full daemon suite: only the pre-existing environment failures (shim e2e / acp-matrix), verified identical onmain.packages/control-plane—test:unit(agentWake.test.ts: join in flight, 30 s window, per-agent, failure not cached) andtest:intagents.wake.route.test.ts(forwards to the serving pool member org-scoped, no-holder → live member, no member → 503, machine placement → 200 unsupported with nothing sent, debounce, edge failure → 503 without caching, viewer 403 / restricted 404).packages/web—sandbox-wake.test.tsx(one press, backoff ladder, idle on ready, give-up after the bound with Start re-pressing, unsupported, 403 vs 503, sandboxed-on-open),FilesPanel.test.tsx(wake pressed once on the asleep 503, calm starting line, terminal copy without Start on unsupported, no press for a plain offline 503),WorkspaceFiles.test.tsx.pnpm typecheckfor protocol / daemon / control-plane / web,pnpm lint,pnpm format:check.Follow-ups (not in this PR)