@inetafrica/open-claudia 2.6.55 → 2.6.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +3 -0
- package/CHANGELOG.md +6 -0
- package/bot-agent.js +42 -0
- package/core/runner.js +75 -23
- package/package.json +3 -2
- package/test-runner-watchdog-static.js +21 -0
package/.env.example
CHANGED
|
@@ -21,6 +21,9 @@ CLAUDE_MODEL=claude-fable-5
|
|
|
21
21
|
CURSOR_PATH=
|
|
22
22
|
CODEX_PATH=
|
|
23
23
|
AUTO_COMPACT_TOKENS=280000
|
|
24
|
+
# Inactivity watchdog: kill a heavy turn's child if it emits no output for
|
|
25
|
+
# this many ms, so a hung tool call can't wedge the bot. Default 600000 (10min).
|
|
26
|
+
OC_TURN_IDLE_MS=600000
|
|
24
27
|
USAGE_ALERT_CONTEXT_TOKENS=120000
|
|
25
28
|
USAGE_ALERT_RATE_MULTIPLIER=1.75
|
|
26
29
|
USAGE_ALERT_BASELINE_TURNS=20
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.6.57
|
|
4
|
+
- **Move the inactivity watchdog into the live runner.** Deep review found v2.6.56 put the watchdog in the legacy `bot-agent.js` monolith, but the LaunchAgent runs `bot.js`, which delegates heavy turns through `core/runner.js`. That meant the installed live path still only had the 6-hour hard timeout and could still wedge on a silent child. This release ports the watchdog to `core/runner.js`, updates activity on child stdout/stderr, terminates the process tree after `OC_TURN_IDLE_MS` of silence, suppresses the confusing partial/no-output final message, clears stuck busy state with a failsafe, and drains any queued follow-up messages instead of stranding them. Adds a static regression test that fails if the watchdog only exists in `bot-agent.js` again.
|
|
5
|
+
|
|
6
|
+
## v2.6.56
|
|
7
|
+
- **Stop a single hung child from wedging the whole bot.** Agent mode is single-flight: a heavy turn spawns one child and sets a global `runningProcess` flag; while it's set, every new message is demoted to a lightweight side-chat instead of a full turn. That flag is *only* ever cleared by the child's `close`/`error` events — so a child that hangs (a wedged tool call that never returns) pins it forever. That's exactly what happened: one turn went silent, its child never exited, and from then on the bot stayed alive (health checks passed) but answered nothing of substance — every later message fell through to a side-chat that also couldn't make progress. The fix is an **inactivity watchdog** in `runClaude` (`bot-agent.js`): track `lastActivity`, bumped on every `stdout`/`stderr` chunk from the child, and inside the existing progress-update loop (which already ticks every 2–5s while a task runs) kill the child if it produces no output for longer than the idle limit — SIGTERM to the process group, SIGKILL 3s later, plus a 10s failsafe that force-clears the flag if `close` somehow never fires. The watchdog short-circuits the close handler so the user gets one clear "task stalled — stopped it, I'm responsive again, send /continue" message instead of a confusing "(no output)". Default idle limit is 10 minutes, tunable via `OC_TURN_IDLE_MS`. Deliberately scoped to the wedge only — the v2.6.55 polling fix is healthy and untouched.
|
|
8
|
+
|
|
3
9
|
## v2.6.55
|
|
4
10
|
- **Make fixing a tool as cheap as creating one.** The tool subsystem was structurally tilted toward sprawl: creating a tool was a trusting one-liner with no validation, while *fixing* one had no verb at all — so the path of least resistance was always another near-duplicate. This release closes that gap from both ends. **A first-class fix verb:** `open-claudia tool set <name> [--desc|--pack|--requires|--usage]` rewrites a tool's header metadata in place (new `updateToolHeader()` in `core/tools.js`) — it refreshes only the recognised field lines, preserving the marker, any hand-written comments, and the immutable `createdAt`, and never touches the body. `open-claudia tool edit <name>` prints the source path (stdout) so the agent's Edit/Write or the user's `$EDITOR` can open it directly, with the metadata-only hint on stderr. **An add-time safety net** (all advisory, never blocking — a tool the agent just wrote is usually right): `tool add`/`tool set` now lint the source and surface (1) hardcoded secrets — `sk-ant-`/`sk-proj-`/`sk-`/`AKIA`/`gh*_`/`xox*`/PEM/inline-Bearer literals that should be `$KEYRING` refs, mirroring the redactor; (2) requires/keyring mismatches — keyring keys the script reads but `--requires` omits (with a ready-to-paste `tool set` fix), and declared keys it never reads; (3) syntax errors via the script's own interpreter (`bash -n` / `node --check` / `py_compile`, silent when the interpreter or type is unknown — never cries wolf); and (4) a dangling `--pack` link to a pack that doesn't exist. **Immutable creation telemetry:** the `.usage.json` sidecar now stamps `createdAt` once at add-time (surfaced on every tool record); the nightly dream's "cold since creation" check ages by this stamp instead of file mtime, so a body edit can no longer reset a never-run tool's staleness clock (falls back to mtime for tools created before the stamp existed). Extends `test-tools.js` (createdAt immutability, `envRefsIn`, `lintToolSource`, `checkSyntax`, `updateToolHeader`).
|
|
5
11
|
- **Stop the phantom-outage restart storm — just keep polling.** The Telegram adapter was killing the bot over network blips that weren't real. A side-by-side network monitor (probing 1.1.1.1 *and* api.telegram.org every 60s) showed the connection healthy throughout, yet `bot.log` had logged 20k+ `EFATAL: read ETIMEDOUT` → "Network lost" events: the long-poll's pooled keep-alive socket goes stale on a macOS sleep/wake or an idle-NAT eviction, and every reused poll then times out instantly. The old handler treated *any* such timeout as a lost network — loudly stopping/restarting polling and, after 6 quick attempts (a sleep/wake error burst exhausts those in seconds), calling `process.exit(1)` for launchd to relaunch. That voluntary suicide *was* the "why does it keep restarting" the bot kept doing. Three changes in `channels/telegram/adapter.js`: (1) **fresh socket per poll** (`keepAlive:false`) so there's no pooled socket to go stale — the root of the ETIMEDOUT storm; (2) **gentle, throttled recovery** — a transient timeout no longer screams or tears down on the first hit (with keep-alive off the next poll self-heals on a new connection); it logs at most once a minute and only actively restarts polling if errors persist; (3) **never exit over a network blip** — only a `409 Conflict` (a second poller on the same token) exits immediately; a genuinely wedged loop now has to fail unbroken for a full 10 minutes before a clean relaunch, versus the old ~90s hair-trigger. The diagnostic `netmon.sh` that proved the network was fine stays running.
|
package/bot-agent.js
CHANGED
|
@@ -1259,6 +1259,16 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1259
1259
|
const startTime = Date.now();
|
|
1260
1260
|
let longRunningNotified = false;
|
|
1261
1261
|
|
|
1262
|
+
// Inactivity watchdog. The single-flight `runningProcess` flag is only
|
|
1263
|
+
// cleared by this child's close/error events, so a child that hangs
|
|
1264
|
+
// (e.g. a wedged tool call) pins the flag forever and demotes every later
|
|
1265
|
+
// message to a side-chat. Track the last time the child produced output;
|
|
1266
|
+
// if it goes silent past the idle limit, kill it so the flag clears and
|
|
1267
|
+
// the bot responds again. Tune with OC_TURN_IDLE_MS (default 10min).
|
|
1268
|
+
const idleLimitMs = Math.max(60000, parseInt(process.env.OC_TURN_IDLE_MS, 10) || 10 * 60 * 1000);
|
|
1269
|
+
let lastActivity = Date.now();
|
|
1270
|
+
let watchdogTripped = false;
|
|
1271
|
+
|
|
1262
1272
|
let lastUpdate = "";
|
|
1263
1273
|
// Adaptive update interval: 2s for first 2min, then 5s to avoid rate limits
|
|
1264
1274
|
const scheduleUpdate = () => {
|
|
@@ -1268,6 +1278,29 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1268
1278
|
};
|
|
1269
1279
|
const updateProgress = async () => {
|
|
1270
1280
|
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
1281
|
+
// Inactivity watchdog: kill a child that has gone silent past the idle limit.
|
|
1282
|
+
if (!watchdogTripped && runningProcess === proc && Date.now() - lastActivity > idleLimitMs) {
|
|
1283
|
+
watchdogTripped = true;
|
|
1284
|
+
const idleMin = Math.floor((Date.now() - lastActivity) / 60000);
|
|
1285
|
+
const pid = proc.pid;
|
|
1286
|
+
console.error(`[watchdog] child pid ${pid} idle ${idleMin}min (limit ${Math.floor(idleLimitMs / 60000)}min) — killing`);
|
|
1287
|
+
try { process.kill(-pid, "SIGTERM"); } catch (e) {
|
|
1288
|
+
try { proc.kill("SIGTERM"); } catch (e2) {}
|
|
1289
|
+
}
|
|
1290
|
+
setTimeout(() => { try { process.kill(-pid, "SIGKILL"); } catch (e) {} }, 3000);
|
|
1291
|
+
// Failsafe: if close never fires, force-clear so the bot recovers anyway.
|
|
1292
|
+
setTimeout(() => {
|
|
1293
|
+
if (runningProcess === proc) {
|
|
1294
|
+
runningProcess = null; runningProcessPrompt = null;
|
|
1295
|
+
clearTimeout(streamInterval); streamInterval = null;
|
|
1296
|
+
}
|
|
1297
|
+
}, 10000);
|
|
1298
|
+
try {
|
|
1299
|
+
await send(`Task stalled — no activity for ${idleMin}min, so I stopped it and I'm responsive again. Send /continue to resume, or just re-ask.`);
|
|
1300
|
+
} catch (e) {}
|
|
1301
|
+
statusMessageId = null;
|
|
1302
|
+
return; // stop the progress loop; close (or failsafe) clears state
|
|
1303
|
+
}
|
|
1271
1304
|
try {
|
|
1272
1305
|
bot.sendChatAction(CHAT_ID, "typing").catch(() => {});
|
|
1273
1306
|
const display = formatProgress(assistantText, toolUses, currentTool, elapsed, currentToolDetail);
|
|
@@ -1292,6 +1325,7 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1292
1325
|
scheduleUpdate();
|
|
1293
1326
|
|
|
1294
1327
|
proc.stdout.on("data", (data) => {
|
|
1328
|
+
lastActivity = Date.now();
|
|
1295
1329
|
streamBuffer += data.toString();
|
|
1296
1330
|
const events = parseStreamEvents(streamBuffer);
|
|
1297
1331
|
const lastNewline = streamBuffer.lastIndexOf("\n");
|
|
@@ -1367,6 +1401,7 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1367
1401
|
|
|
1368
1402
|
let stderrBuffer = "";
|
|
1369
1403
|
proc.stderr.on("data", (d) => {
|
|
1404
|
+
lastActivity = Date.now();
|
|
1370
1405
|
const chunk = d.toString();
|
|
1371
1406
|
stderrBuffer += chunk;
|
|
1372
1407
|
console.error("STDERR:", redactSensitive(chunk));
|
|
@@ -1375,6 +1410,13 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1375
1410
|
proc.on("close", async (code) => {
|
|
1376
1411
|
runningProcess = null; runningProcessPrompt = null;
|
|
1377
1412
|
clearTimeout(streamInterval); streamInterval = null;
|
|
1413
|
+
if (watchdogTripped) {
|
|
1414
|
+
// The inactivity watchdog already notified the user and killed the
|
|
1415
|
+
// child; don't also deliver partial / "(no output)" text.
|
|
1416
|
+
statusMessageId = null;
|
|
1417
|
+
if (settings.budget) settings.budget = null;
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1378
1420
|
if (settings.backend !== "cursor" && isClaudeAuthErrorText(stderrBuffer)) {
|
|
1379
1421
|
await send(claudeAuthRecoveryMessage(stderrBuffer.trim().slice(-800)), { replyTo: replyToMsgId });
|
|
1380
1422
|
return;
|
package/core/runner.js
CHANGED
|
@@ -1067,6 +1067,17 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1067
1067
|
const startTime = Date.now();
|
|
1068
1068
|
let longRunningNotified = false;
|
|
1069
1069
|
|
|
1070
|
+
// Inactivity watchdog. The live bot runs through this modular runner
|
|
1071
|
+
// (`bot.js` → `core/runner.js`), so the guard must live here, not only in
|
|
1072
|
+
// the legacy monolith. `state.runningProcess` is single-flight and normally
|
|
1073
|
+
// clears only on child close/error; a silent wedged child can otherwise pin it
|
|
1074
|
+
// forever and leave every later message queued. Treat child stdout/stderr as
|
|
1075
|
+
// liveness and stop the process group if it goes quiet past the idle limit.
|
|
1076
|
+
const idleLimitMs = Math.max(60000, parseInt(config.OC_TURN_IDLE_MS || process.env.OC_TURN_IDLE_MS || "", 10) || 10 * 60 * 1000);
|
|
1077
|
+
let lastActivity = Date.now();
|
|
1078
|
+
let watchdogTripped = false;
|
|
1079
|
+
let watchdogNotice = null;
|
|
1080
|
+
|
|
1070
1081
|
const processTimeout = setTimeout(() => {
|
|
1071
1082
|
if (state.runningProcess === proc) {
|
|
1072
1083
|
killProcessTree(proc.pid, "SIGTERM");
|
|
@@ -1081,8 +1092,60 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1081
1092
|
const interval = elapsed > 120 ? 5000 : 2000;
|
|
1082
1093
|
state.streamInterval = setTimeout(() => chatContext.run(store, updateProgress), interval);
|
|
1083
1094
|
};
|
|
1095
|
+
|
|
1096
|
+
const drainQueuedMessages = async () => {
|
|
1097
|
+
if (state.messageQueue.length > 0 && state.currentSession) {
|
|
1098
|
+
const drained = state.messageQueue.splice(0);
|
|
1099
|
+
if (drained.length === 1) {
|
|
1100
|
+
const only = drained[0];
|
|
1101
|
+
await runClaude(only.prompt, state.currentSession.dir, only.replyToMsgId, only.opts);
|
|
1102
|
+
} else {
|
|
1103
|
+
const fmtTime = (ts) => {
|
|
1104
|
+
const d = new Date(ts);
|
|
1105
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
1106
|
+
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
1107
|
+
const ss = String(d.getSeconds()).padStart(2, "0");
|
|
1108
|
+
return `${hh}:${mm}:${ss}`;
|
|
1109
|
+
};
|
|
1110
|
+
const numbered = drained.map((m, i) => `[${i + 1}] (${fmtTime(m.queuedAt || Date.now())}) ${m.prompt}`).join("\n\n");
|
|
1111
|
+
const batched =
|
|
1112
|
+
`While you were working the user sent these ${drained.length} follow-up messages (oldest first):\n\n` +
|
|
1113
|
+
`${numbered}\n\n` +
|
|
1114
|
+
`Treat each as a distinct follow-up request. Add them to your plan and handle them; ` +
|
|
1115
|
+
`if any contradicts an earlier one, prefer the newer one and call out the conflict.`;
|
|
1116
|
+
const last = drained[drained.length - 1];
|
|
1117
|
+
await runClaude(batched, state.currentSession.dir, last.replyToMsgId, last.opts);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
|
|
1084
1122
|
const updateProgress = async () => {
|
|
1085
1123
|
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
1124
|
+
if (!watchdogTripped && state.runningProcess === proc && Date.now() - lastActivity > idleLimitMs) {
|
|
1125
|
+
watchdogTripped = true;
|
|
1126
|
+
const idleMin = Math.max(1, Math.floor((Date.now() - lastActivity) / 60000));
|
|
1127
|
+
watchdogNotice = `Task stalled — no activity for ${idleMin}min, so I stopped it and I'm responsive again. Send /continue to resume, or just re-ask.`;
|
|
1128
|
+
console.error(`[watchdog] child pid ${proc.pid} idle ${idleMin}min (limit ${Math.floor(idleLimitMs / 60000)}min) — killing`);
|
|
1129
|
+
try { killProcessTree(proc.pid, "SIGTERM"); }
|
|
1130
|
+
catch (e) { try { proc.kill("SIGTERM"); } catch (e2) {} }
|
|
1131
|
+
setTimeout(() => { try { killProcessTree(proc.pid, "SIGKILL"); } catch (e) {} }, 3000);
|
|
1132
|
+
setTimeout(() => chatContext.run(store, async () => {
|
|
1133
|
+
if (state.runningProcess === proc) {
|
|
1134
|
+
state.runningProcess = null;
|
|
1135
|
+
state.preparingRun = false;
|
|
1136
|
+
stopTyping();
|
|
1137
|
+
clearTimeout(state.streamInterval); state.streamInterval = null;
|
|
1138
|
+
clearTimeout(processTimeout);
|
|
1139
|
+
state.statusMessageId = null;
|
|
1140
|
+
if (settings.budget) settings.budget = null;
|
|
1141
|
+
saveState();
|
|
1142
|
+
await drainQueuedMessages();
|
|
1143
|
+
}
|
|
1144
|
+
}), 10000);
|
|
1145
|
+
try { await send(watchdogNotice, { replyTo: replyToMsgId }); } catch (e) {}
|
|
1146
|
+
state.statusMessageId = null;
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1086
1149
|
try {
|
|
1087
1150
|
if (adapter && channelId) adapter.typing(channelId).catch(() => {});
|
|
1088
1151
|
const display = formatProgress(assistantText, toolUses, currentTool, elapsed, currentToolDetail);
|
|
@@ -1106,6 +1169,7 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1106
1169
|
scheduleUpdate();
|
|
1107
1170
|
|
|
1108
1171
|
proc.stdout.on("data", (data) => {
|
|
1172
|
+
lastActivity = Date.now();
|
|
1109
1173
|
state.streamBuffer += data.toString();
|
|
1110
1174
|
const events = parseStreamEvents(state.streamBuffer);
|
|
1111
1175
|
const lastNewline = state.streamBuffer.lastIndexOf("\n");
|
|
@@ -1257,6 +1321,7 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1257
1321
|
|
|
1258
1322
|
let stderrBuffer = "";
|
|
1259
1323
|
proc.stderr.on("data", (d) => {
|
|
1324
|
+
lastActivity = Date.now();
|
|
1260
1325
|
const chunk = d.toString();
|
|
1261
1326
|
stderrBuffer += chunk;
|
|
1262
1327
|
console.error("STDERR:", redactSensitive(chunk));
|
|
@@ -1269,6 +1334,15 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1269
1334
|
clearTimeout(state.streamInterval); state.streamInterval = null;
|
|
1270
1335
|
clearTimeout(processTimeout);
|
|
1271
1336
|
|
|
1337
|
+
if (watchdogTripped) {
|
|
1338
|
+
const note = watchdogNotice || "Task stalled; watchdog stopped the child process.";
|
|
1339
|
+
appendProjectTranscript("system-note", note, { exitCode: code, kind: "watchdog" }, state, getActiveSessionId);
|
|
1340
|
+
if (settings.budget) settings.budget = null;
|
|
1341
|
+
state.statusMessageId = null;
|
|
1342
|
+
await drainQueuedMessages();
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1272
1346
|
if (settings.backend === "claude" && isClaudeAuthErrorText(stderrBuffer)) {
|
|
1273
1347
|
const authText = claudeAuthRecoveryMessage(stderrBuffer.trim().slice(-800));
|
|
1274
1348
|
appendProjectTranscript("system-note", authText, { kind: "auth-error" }, state, getActiveSessionId);
|
|
@@ -1465,29 +1539,7 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1465
1539
|
}
|
|
1466
1540
|
}
|
|
1467
1541
|
|
|
1468
|
-
|
|
1469
|
-
const drained = state.messageQueue.splice(0);
|
|
1470
|
-
if (drained.length === 1) {
|
|
1471
|
-
const only = drained[0];
|
|
1472
|
-
await runClaude(only.prompt, state.currentSession.dir, only.replyToMsgId, only.opts);
|
|
1473
|
-
} else {
|
|
1474
|
-
const fmtTime = (ts) => {
|
|
1475
|
-
const d = new Date(ts);
|
|
1476
|
-
const hh = String(d.getHours()).padStart(2, "0");
|
|
1477
|
-
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
1478
|
-
const ss = String(d.getSeconds()).padStart(2, "0");
|
|
1479
|
-
return `${hh}:${mm}:${ss}`;
|
|
1480
|
-
};
|
|
1481
|
-
const numbered = drained.map((m, i) => `[${i + 1}] (${fmtTime(m.queuedAt || Date.now())}) ${m.prompt}`).join("\n\n");
|
|
1482
|
-
const batched =
|
|
1483
|
-
`While you were working the user sent these ${drained.length} follow-up messages (oldest first):\n\n` +
|
|
1484
|
-
`${numbered}\n\n` +
|
|
1485
|
-
`Treat each as a distinct follow-up request. Add them to your plan and handle them; ` +
|
|
1486
|
-
`if any contradicts an earlier one, prefer the newer one and call out the conflict.`;
|
|
1487
|
-
const last = drained[drained.length - 1];
|
|
1488
|
-
await runClaude(batched, state.currentSession.dir, last.replyToMsgId, last.opts);
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1542
|
+
await drainQueuedMessages();
|
|
1491
1543
|
}));
|
|
1492
1544
|
|
|
1493
1545
|
proc.on("error", (err) => chatContext.run(store, async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.57",
|
|
4
4
|
"description": "Your always-on AI coding assistant — Claude Code, Cursor Agent, and OpenAI Codex via Telegram or Kazee Chat",
|
|
5
5
|
"main": "bot.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"setup": "node setup.js",
|
|
11
11
|
"start": "node bot.js",
|
|
12
|
-
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tool-graph.js"
|
|
12
|
+
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tool-graph.js"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"bot.js",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
".env.example",
|
|
31
31
|
"README.md",
|
|
32
32
|
"CHANGELOG.md",
|
|
33
|
+
"test-runner-watchdog-static.js",
|
|
33
34
|
"test-usage-accounting.js",
|
|
34
35
|
"test-recall-engine.js",
|
|
35
36
|
"test-recall-graph.js",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const root = __dirname;
|
|
7
|
+
const runner = fs.readFileSync(path.join(root, 'core', 'runner.js'), 'utf8');
|
|
8
|
+
const monolith = fs.readFileSync(path.join(root, 'bot-agent.js'), 'utf8');
|
|
9
|
+
|
|
10
|
+
assert.match(runner, /OC_TURN_IDLE_MS/, 'live core/runner.js must read OC_TURN_IDLE_MS');
|
|
11
|
+
assert.match(runner, /watchdogTripped/, 'live core/runner.js must guard close handling after watchdog trip');
|
|
12
|
+
assert.match(runner, /lastActivity\s*=\s*Date\.now\(\)/, 'live core/runner.js must track last child activity');
|
|
13
|
+
assert.match(runner, /proc\.stdout\.on\("data",[\s\S]{0,120}\blastActivity\s*=\s*Date\.now\(\)/, 'stdout must refresh lastActivity');
|
|
14
|
+
assert.match(runner, /proc\.stderr\.on\("data",[\s\S]{0,120}\blastActivity\s*=\s*Date\.now\(\)/, 'stderr must refresh lastActivity');
|
|
15
|
+
assert.match(runner, /killProcessTree\(proc\.pid,\s*"SIGTERM"\)/, 'watchdog must terminate the child process tree first');
|
|
16
|
+
assert.match(runner, /killProcessTree\(proc\.pid,\s*"SIGKILL"\)/, 'watchdog must force-kill the child process tree after grace');
|
|
17
|
+
assert.match(runner, /await drainQueuedMessages\(\)/, 'watchdog path must not strand queued follow-up messages');
|
|
18
|
+
|
|
19
|
+
assert.match(monolith, /OC_TURN_IDLE_MS/, 'legacy bot-agent.js watchdog should remain for monolith users');
|
|
20
|
+
|
|
21
|
+
console.log('runner watchdog static OK');
|