Skip to content

Commit d27347d

Browse files
ziggyclaude
andcommitted
fix: heartbeat keepalive + session validation path fix
Two remaining gaps in v0.7.4: 1. Heartbeat: task_notification SDK events only fire on completion, not during sub-agent execution. The stderr idle reset was therefore insufficient for long Task waits. Agent-runner now writes a keepalive marker to stdout every 2 minutes — container-runner resets idle timer on any stdout data, so long Task waits no longer trigger false stall timeouts. 2. Session validation path: the leading slash in the CWD was being stripped (.replace(/^-/, '')) when constructing the session .jsonl path, causing validation to always miss the file. Claude Code keeps the leading dash in the project directory name (-Users-ziggy-nanoclaw-groups-main), fixed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d41e32c commit d27347d

3 files changed

Lines changed: 33 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## v0.7.5 (2026-04-04) — Heartbeat + session validation path fix
4+
5+
### Fixes
6+
- **Agent-runner heartbeat** — writes a keepalive marker to stdout every 2 minutes. The container-runner already resets the idle timer on any stdout data, so this prevents false stall timeouts during Task sub-agent waits (which produce no other stdout). The `task_notification` SDK event only fires on completion, not during execution — meaning the stderr-based idle reset in v0.7.4 was insufficient for long Task waits specifically.
7+
- **Session validation path bug fixed** — the leading slash in the CWD was being stripped when constructing the session file path, causing validation to always miss the file and silently do nothing. Claude Code keeps the leading dash (e.g. `-Users-ziggy-nanoclaw-groups-main`), and the path is now constructed correctly.
8+
39
## v0.7.4 (2026-04-04) — Idle timeout architecture fix
410

511
### Fixes

container/agent-runner/src/index.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,8 +366,11 @@ function isSessionResumable(sessionId: string): boolean {
366366
if (!configDir || !sessionId) return true;
367367

368368
// Claude Code stores sessions under projects/{sanitized_cwd}/{sessionId}.jsonl
369+
// Sanitization: replace all slashes with dashes. The leading slash becomes a
370+
// leading dash — do NOT strip it, that's how Claude Code names the directory.
371+
// e.g. /Users/ziggy/nanoclaw/groups/main → -Users-ziggy-nanoclaw-groups-main
369372
const cwd = process.env.GHOSTCLAW_GROUP_DIR || process.cwd();
370-
const sanitizedCwd = cwd.replace(/\//g, '-').replace(/^-/, '');
373+
const sanitizedCwd = cwd.replace(/\//g, '-');
371374
const sessionFile = path.join(configDir, 'projects', sanitizedCwd, `${sessionId}.jsonl`);
372375

373376
try {
@@ -593,6 +596,21 @@ async function runQuery(
593596
return { newSessionId, lastAssistantUuid, closedDuringQuery, resultCount };
594597
}
595598

599+
const HEARTBEAT_MARKER = '---GHOSTCLAW_HEARTBEAT---';
600+
const HEARTBEAT_INTERVAL_MS = 2 * 60 * 1000; // 2 minutes
601+
602+
/**
603+
* Write a periodic heartbeat to stdout so the container-runner's idle timer
604+
* resets during long-running operations (e.g. Task sub-agent waits) that
605+
* produce no other stdout. The marker is not an output — container-runner
606+
* resets the idle timer on any stdout data before checking for output markers.
607+
*/
608+
function startHeartbeat(): NodeJS.Timeout {
609+
return setInterval(() => {
610+
process.stdout.write(`${HEARTBEAT_MARKER}\n`);
611+
}, HEARTBEAT_INTERVAL_MS);
612+
}
613+
596614
async function main(): Promise<void> {
597615
let containerInput: ContainerInput;
598616

@@ -644,6 +662,10 @@ async function main(): Promise<void> {
644662
prompt += '\n' + pending.join('\n');
645663
}
646664

665+
// Heartbeat: keeps the container-runner's idle timer alive during long-running
666+
// operations (Task sub-agent waits, slow tool calls) that produce no stdout.
667+
const heartbeat = startHeartbeat();
668+
647669
// Query loop: run query → wait for IPC message → run new query → repeat
648670
let resumeAt: string | undefined;
649671
try {
@@ -705,6 +727,7 @@ async function main(): Promise<void> {
705727
prompt = nextMessage;
706728
}
707729
} catch (err) {
730+
clearInterval(heartbeat);
708731
const errorMessage = err instanceof Error ? err.message : String(err);
709732
log(`Agent error: ${errorMessage}`);
710733
writeOutput({
@@ -715,6 +738,8 @@ async function main(): Promise<void> {
715738
});
716739
process.exit(1);
717740
}
741+
742+
clearInterval(heartbeat);
718743
}
719744

720745
main();

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ghostclaw",
3-
"version": "0.7.4",
3+
"version": "0.7.5",
44
"description": "Personal AI assistant. Bare metal, Telegram-first, no containers.",
55
"type": "module",
66
"main": "dist/index.js",

0 commit comments

Comments
 (0)