Summary
When my Claude Code agent emits the configured completionSignal as the very last line of its iteration, Sandcastle does not terminate the iteration. The run hangs until idleTimeoutSeconds elapses (10 minutes in my case) and then fails with AgentIdleTimeoutError — even though the work is complete and committed.
The commit the agent made is real and lands on the target branch; the only loss is that the run is reported as a failure and result.commits is empty (because commit collection runs in the failure path differently than the completion path).
Environment
@ai-hero/sandcastle@0.5.8
- Node
v24.14.1
- Windows 11 + Docker Desktop 26.0.0
- Agent:
claudeCode("claude-opus-4-7", { effort: "high" })
- Sandbox:
docker()
branchStrategy: { type: "branch", branch: "agent/slice-1-submit" }
Configuration
const COMPLETION_SIGNAL = "<promise>SLICE_DONE_4e9c2b</promise>";
await run({
name: "afk-issue-1",
agent: claudeCode("claude-opus-4-7", { effort: "high" }),
sandbox: docker(),
branchStrategy: { type: "branch", branch: "agent/slice-1-submit" },
prompt: `...When the slice is fully implemented, all checks are green, and your commits are on the branch, output exactly this string on its own line as the very last thing you say: ${COMPLETION_SIGNAL}...`,
hooks: { sandbox: { onSandboxReady: [{ command: "pnpm install", timeoutMs: 600_000 }] } },
maxIterations: 12,
completionSignal: COMPLETION_SIGNAL,
idleTimeoutSeconds: 600,
});
Observed log
The relevant tail of agent-…afk-issue-1.log:
All three checks pass. Slice 1 is committed on `agent/slice-1-submit`:
- `convex/schema.ts` — `sites` and `submissions` tables (plus `authTables` for Convex Auth)
- `convex/auth.ts` + `middleware.ts` — anonymous Convex Auth gating the dashboard
- ...
<promise>SLICE_DONE_4e9c2b</promise>
Agent idle for 1 minute
Agent idle for 2 minutes
Agent idle for 3 minutes
...
Agent idle for 9 minutes
Agent idle for 600 seconds — no output received. Consider increasing the idle timeout with --idle-timeout.
The marker <promise>SLICE_DONE_4e9c2b</promise> is on its own line, with no preceding or trailing characters on that line, exactly matching completionSignal.
run() then throws AgentIdleTimeoutError and result.commits is [], even though git log on the worktree confirms a real commit landed during the iteration.
Expected
After the agent emits the configured completion signal as a substring of its output, Sandcastle should:
- Stop the iteration loop early (per the README's "Early termination" semantics).
- Set
result.completionSignal to the matched string.
- Collect commits via the success path so they appear in
result.commits.
Hypothesis
It looks like the matcher isn't seeing output that arrives at the very end of an agent message — possibly a buffering issue on Claude Code's stdout stream after the agent's last assistant turn, or a race between the agent's "I'm done" signal and the substring scanner.
I'd be happy to provide the full agent log if helpful.
Summary
When my Claude Code agent emits the configured
completionSignalas the very last line of its iteration, Sandcastle does not terminate the iteration. The run hangs untilidleTimeoutSecondselapses (10 minutes in my case) and then fails withAgentIdleTimeoutError— even though the work is complete and committed.The commit the agent made is real and lands on the target branch; the only loss is that the run is reported as a failure and
result.commitsis empty (because commit collection runs in the failure path differently than the completion path).Environment
@ai-hero/sandcastle@0.5.8v24.14.1claudeCode("claude-opus-4-7", { effort: "high" })docker()branchStrategy: { type: "branch", branch: "agent/slice-1-submit" }Configuration
Observed log
The relevant tail of
agent-…afk-issue-1.log:The marker
<promise>SLICE_DONE_4e9c2b</promise>is on its own line, with no preceding or trailing characters on that line, exactly matchingcompletionSignal.run()then throwsAgentIdleTimeoutErrorandresult.commitsis[], even thoughgit logon the worktree confirms a real commit landed during the iteration.Expected
After the agent emits the configured completion signal as a substring of its output, Sandcastle should:
result.completionSignalto the matched string.result.commits.Hypothesis
It looks like the matcher isn't seeing output that arrives at the very end of an agent message — possibly a buffering issue on Claude Code's stdout stream after the agent's last assistant turn, or a race between the agent's "I'm done" signal and the substring scanner.
I'd be happy to provide the full agent log if helpful.