fix(service): an inbox post with a picture marks the sandbox opened

Merged
DA
13d ago
+33-3

Summary

  • The ask: when post_to_inbox carries 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 counted post_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: opensSandbox now counts a post_to_inbox call whose blocks include 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_inbox tool in #3482, which had the same gap and fixes it there; this PR is the post_to_inbox half, 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. With tool-set.ts reverted, the new test fails (1 failed, 1 passed).
    • biome check on packages/service/src/agents/coder-host: clean.
    • tsc -p packages/service: clean.

Invariants

  • ./scripts/check.sh green. (CI)
  • JSON Schema goldens unchanged (LUKE_UPDATE_FIXTURES not 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 in chat-sandbox.test.ts covers a post with an image block, one with words alone, and one with no blocks.
  • Gateway envelope goldens unchanged. (CI)
  • No new as type assertion outside the allowlisted files. (CI via anti-slop)
  • No Effect.runPromise/runSync/runFork outside the runtime edges and the shims tools/anti-slop/effect-edges.json records. (CI via anti-slop/no-run-promise-outside-edges)
  • No new setTimeout/setInterval/new Promise/AbortController/fs.watch outside the files tools/anti-slop/effect-edges.json records. (CI via anti-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.md sentence naming a changed part is edited; root pair stays byte-identical. None names this part.
  • PRIVACY.md unchanged, or the change is a product decision called out in the PR body.
  • Package package.json deps match what the sources import by bare specifier; effect via catalog:. (CI) @luke/features is 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 renderer node: 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


Devin Review


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.