fix(service): an inbox post with a picture marks the sandbox opened
Merged
DA
13d ago+33+33+01234567890123456789-3-3-0123456789
Summary
- The ask: when
post_to_inboxcarries an image block, count that call as opening the sandbox, so the turn's end records the branch and stops the sandbox. - Why it was missed: the service reads an image block's picture from the agent's sandbox, which opens the sandbox, but
opensSandbox(coder-host/tool-set.ts) never countedpost_to_inbox. - Effect of the gap: a turn whose only sandbox use was posting a picture to the inbox never set
openedAt. The turn-end hook (apps/dev/hooks/store.ts) then skipped recording the branch and stopping the sandbox, so the sandbox kept running until it timed out. - The fix:
opensSandboxnow counts apost_to_inboxcall whoseblocksinclude an image block, using the same block-type check a chat post with a picture already gets. A post with words alone, or with no blocks, still opens nothing. - Found while reviewing the
reply_inboxtool in #3482, which had the same gap and fixes it there; this PR is thepost_to_inboxhalf, on its own as the orchestrator asked.
Evidence
- Platform-independent checks:
vitest run packages/service/src/agents/coder-host/chat-sandbox.test.ts: 2 passed. Withtool-set.tsreverted, the new test fails (1 failed, 1 passed).biome checkonpackages/service/src/agents/coder-host: clean.tsc -p packages/service: clean.
Invariants
-
./scripts/check.shgreen. (CI) - JSON Schema goldens unchanged (
LUKE_UPDATE_FIXTURESnot run, or diff explained line by line). (CI) - Every behavior change has, in this PR, the test that fails without it; no change-detector test added. (manual; root
AGENTS.md"Testing") The new test inchat-sandbox.test.tscovers a post with an image block, one with words alone, and one with no blocks. - Gateway envelope goldens unchanged. (CI)
- No new
astype assertion outside the allowlisted files. (CI via anti-slop) - No
Effect.runPromise/runSync/runForkoutside the runtime edges and the shimstools/anti-slop/effect-edges.jsonrecords. (CI viaanti-slop/no-run-promise-outside-edges) - No new
setTimeout/setInterval/new Promise/AbortController/fs.watchoutside the filestools/anti-slop/effect-edges.jsonrecords. (CI viaanti-slop/no-raw-async-primitives) - No Effect, Stream, or Layer written as a statement and dropped; a description a caller walks past runs nothing. (CI via
pnpm discarded-effect) - Test count equals the pre-PR count or the delta is listed. (manual:
vitest run --reporter=json) +1. - Each hand-rolled file the PR replaces is deleted, or the part that remains is a permanent adaptor named in root
AGENTS.md's "The permanent adaptors" with the reason it stays. Nothing is replaced. - Every
CLAUDE.md/AGENTS.mdsentence naming a changed part is edited; root pair stays byte-identical. None names this part. -
PRIVACY.mdunchanged, or the change is a product decision called out in the PR body. - Package
package.jsondeps match what the sources import by bare specifier;effectviacatalog:. (CI)@luke/featuresis already a dependency of@luke/service. - Barrel/door rule: no Node-reaching Effect module (
@effect/platform-node,effect/sql) behind a barrel the renderer or a web function opens. (CI: existing renderernode:grep plus a new grep for those specifiers in renderer bundles)
Notes
- Blockers or follow-up verification: none. #3482 makes the same change for
reply_inbox, in the same function; whichever lands second rebases.
Made with Luke
Summary by cubic
opensSandbox now counts a post_to_inbox call that carries an image block, so a turn whose only sandbox use is posting a picture to the inbox records its branch and stops its sandbox at turn end, instead of leaving it running until timeout.