ops: heartbeat v2 — programmatic health checks, zero tokens when healthy#127
Conversation
Greptile SummaryRewrote heartbeat extension to perform health checks programmatically in Node.js instead of consuming LLM tokens. When all 5 checks pass (session liveness, Slack bridge, stale worktrees, stuck todos, orphaned dev-agents), zero tokens are consumed. Only injects a targeted prompt when failures are detected. Major changes:
Issues found:
Token savings: Confidence Score: 4/5
Important Files Changed
Last reviewed commit: 2b2db3f |
| let depth = 0; | ||
| let jsonEnd = -1; | ||
| for (let i = 0; i < trimmed.length; i++) { | ||
| if (trimmed[i] === "{") depth++; | ||
| else if (trimmed[i] === "}") { | ||
| depth--; | ||
| if (depth === 0) { | ||
| jsonEnd = i + 1; | ||
| break; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
JSON parsing doesn't handle escaped braces in strings - will fail on todos with { or } in string values
| let depth = 0; | |
| let jsonEnd = -1; | |
| for (let i = 0; i < trimmed.length; i++) { | |
| if (trimmed[i] === "{") depth++; | |
| else if (trimmed[i] === "}") { | |
| depth--; | |
| if (depth === 0) { | |
| jsonEnd = i + 1; | |
| break; | |
| } | |
| } | |
| } | |
| // Find the closing brace for the top-level JSON object | |
| let depth = 0; | |
| let jsonEnd = -1; | |
| let inString = false; | |
| let escapeNext = false; | |
| for (let i = 0; i < trimmed.length; i++) { | |
| const char = trimmed[i]; | |
| if (escapeNext) { | |
| escapeNext = false; | |
| continue; | |
| } | |
| if (char === '\\') { | |
| escapeNext = true; | |
| continue; | |
| } | |
| if (char === '"') { | |
| inString = !inString; | |
| continue; | |
| } | |
| if (!inString) { | |
| if (char === "{") depth++; | |
| else if (char === "}") { | |
| depth--; | |
| if (depth === 0) { | |
| jsonEnd = i + 1; | |
| break; | |
| } | |
| } | |
| } | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: pi/extensions/heartbeat.ts
Line: 210-221
Comment:
JSON parsing doesn't handle escaped braces in strings - will fail on todos with `{` or `}` in string values
```suggestion
// Find the closing brace for the top-level JSON object
let depth = 0;
let jsonEnd = -1;
let inString = false;
let escapeNext = false;
for (let i = 0; i < trimmed.length; i++) {
const char = trimmed[i];
if (escapeNext) {
escapeNext = false;
continue;
}
if (char === '\\') {
escapeNext = true;
continue;
}
if (char === '"') {
inString = !inString;
continue;
}
if (!inString) {
if (char === "{") depth++;
else if (char === "}") {
depth--;
if (depth === 0) {
jsonEnd = i + 1;
break;
}
}
}
}
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Good catch — the naive brace counter broke on strings like "Fix {bug}". Fixed in bd63682: the scanner now tracks inString/escapeNext state to skip braces inside quoted values. Added 3 tests covering braces-in-strings and escaped quotes.
|
|
||
| if (!hasAgent) { | ||
| const title = todo.title || todoId; | ||
| const hoursStuck = Math.round(age / (60 * 60 * 1000) * 10) / 10; |
There was a problem hiding this comment.
Parentheses placed incorrectly - dividing by milliseconds after multiplying by 10
| const hoursStuck = Math.round(age / (60 * 60 * 1000) * 10) / 10; | |
| const hoursStuck = Math.round((age / (60 * 60 * 1000)) * 10) / 10; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: pi/extensions/heartbeat.ts
Line: 260
Comment:
Parentheses placed incorrectly - dividing by milliseconds after multiplying by 10
```suggestion
const hoursStuck = Math.round((age / (60 * 60 * 1000)) * 10) / 10;
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Fixed in bd63682. The original code happened to produce the correct result (JS evaluates age / 3600000 * 10 left-to-right since / and * are same precedence), but the explicit parens make the intent unambiguous.
|
Addressed the Problem: Fix: Now uses two-pronged matching:
Added 7 tests covering path context, boundary matching, backtick/quote contexts, and false-positive rejection. 60 tests total, all passing. |
|
Fixed in ea2a701. Bug: The Fix: Now catches the error and reports |
Rewrote heartbeat.ts to perform all health checks in pure Node.js: - Session liveness (alias + socket existence) - Slack bridge responsiveness (HTTP 400 check) - Stale worktrees (cross-ref with in-progress todos) - Stuck todos (in-progress >2h with no dev-agent) - Orphaned dev-agent sessions Only injects an LLM prompt when something actually fails. Saves ~$72/day on Opus vs the old approach that injected the full HEARTBEAT.md checklist every 10 minutes regardless of health. Updated tests to cover v2 functions (parseTodo, clampInt, getExpectedSessions, etc.). All 50 tests pass. Updated SKILL.md to document new heartbeat behavior.
- parseTodo JSON scanner now tracks inString/escapeNext state so
braces inside string values (e.g. "Fix {bug}") don't break parsing
- Added explicit parens to hoursStuck for clarity (same result, safer)
- Added 3 new tests for braces-in-strings and escaped quotes (53 total)
hasMatchingInProgressTodo used content.includes(worktreeName) which caused false positives — a worktree named 'fix' would match any todo containing the word 'fix' anywhere in its body. Stale worktrees would never be detected and cleaned up. Now checks for: 1. Path pattern: 'worktrees/<name>' (the stored path format) 2. Word boundary: name preceded/followed by whitespace, quotes, or backticks (catches 'fix/branch-name' in prose) Added 7 tests for boundary matching (60 total).
The catch block in checkSessions silently reported ok:true when readlinkSync threw (EINVAL, EACCES, etc.), masking broken sessions. Now reports ok:false with the error message so failures surface in the heartbeat alert.
ea2a701 to
53ca277
Compare
Summary
Rewrote
heartbeat.tsto perform all health checks in pure Node.js — zero LLM tokens consumed when everything is healthy (which is ~95% of the time).Before (v1)
HEARTBEAT.mdchecklist as an LLM promptlist_sessions,curl), respondsAfter (v2)
Checks performed (all in Node.js)
.aliassymlinks resolve to live.sockfileslocalhost:7890/sendreturns 400~/workspace/worktrees/with no matching in-progress todoin-progressfor >2h with no matching dev-agent sessiondev-agent-*sessions with no matching todoConfiguration
HEARTBEAT_EXPECTED_SESSIONS— comma-separated aliases to check (default:sentry-agent)HEARTBEAT_INTERVAL_MS/HEARTBEAT_ENABLED— unchanged from v1Tests
parseTodo,clampInt,getExpectedSessions, etc.)