@inetafrica/open-claudia 2.6.55 → 2.6.56

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 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,8 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.6.56
4
+ - **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.
5
+
3
6
  ## v2.6.55
4
7
  - **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
8
  - **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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.6.55",
3
+ "version": "2.6.56",
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": {