@inetafrica/open-claudia 1.14.7 → 1.14.8

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/bot.js +80 -25
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.14.8
4
+ - Fix: `/stop` and the 6h hard-timeout now walk the full descendant
5
+ process tree, not just the Claude CLI's process group. Background
6
+ bashes started with `run_in_background: true` (and other detached
7
+ children) used to survive the kill and keep running indefinitely —
8
+ e.g. a polling `until` loop with a broken grep would burn CPU and
9
+ network for hours after the agent was already dead.
10
+ - Fix: on bot startup, sweep stale `claude login` / `claude setup-token`
11
+ processes older than 30 minutes. These were left over from previous
12
+ bot crashes (blocked on stdin forever) and could hold Keychain locks.
13
+
3
14
  ## v1.14.0
4
15
  - Claude Code auth management from Telegram: `/auth_status`, `/login`, `/setup_token`, `/use_oauth_token`, and `/clear_oauth_token`
5
16
  - Claude subprocesses now receive `CLAUDE_CODE_OAUTH_TOKEN` from config/env/vault when available, avoiding launchd/macOS Keychain auth failures
package/bot.js CHANGED
@@ -7,16 +7,51 @@ const cron = require("node-cron");
7
7
  const Vault = require("./vault");
8
8
  const CONFIG_DIR = require("./config-dir");
9
9
 
10
+ // ── Process tree helpers ───────────────────────────────────────────
11
+ // Background tools (e.g. Bash run_in_background, /stop on a dev server)
12
+ // often spawn into their own session/group, so a single PGID kill on the
13
+ // Claude CLI doesn't reach them. We walk the descendant tree explicitly
14
+ // and signal each PID. Without this, runaway poll-loops survive
15
+ // indefinitely after the parent CLI is SIGTERMed.
16
+ function descendantPids(rootPid) {
17
+ const seen = new Set();
18
+ const queue = [Number(rootPid)];
19
+ while (queue.length) {
20
+ const pid = queue.shift();
21
+ if (!pid || seen.has(pid)) continue;
22
+ seen.add(pid);
23
+ try {
24
+ const out = execSync(`pgrep -P ${pid}`, { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
25
+ for (const line of out.split("\n")) {
26
+ const child = Number(line.trim());
27
+ if (child) queue.push(child);
28
+ }
29
+ } catch (e) {
30
+ // pgrep exits 1 when no children — ignore.
31
+ }
32
+ }
33
+ seen.delete(Number(rootPid));
34
+ return [...seen];
35
+ }
36
+
37
+ function killProcessTree(rootPid, signal = "SIGTERM") {
38
+ if (!rootPid) return;
39
+ const descendants = descendantPids(rootPid);
40
+ // Kill the process group first (covers the common case quickly).
41
+ try { process.kill(-rootPid, signal); } catch (e) {}
42
+ try { process.kill(rootPid, signal); } catch (e) {}
43
+ // Then mop up any descendants that escaped the group (detached
44
+ // children with their own session — e.g. bash run_in_background).
45
+ for (const pid of descendants) {
46
+ try { process.kill(pid, signal); } catch (e) {}
47
+ }
48
+ }
49
+
10
50
  // ── Graceful shutdown & error handling ─────────────────────────────
11
51
  async function gracefulShutdown(signal) {
12
52
  console.log(`Received ${signal}, shutting down gracefully...`);
13
- // Stop any running Claude process
14
53
  if (typeof runningProcess !== "undefined" && runningProcess) {
15
- try {
16
- process.kill(-runningProcess.pid, "SIGTERM");
17
- } catch (e) {
18
- try { runningProcess.kill("SIGTERM"); } catch (e2) {}
19
- }
54
+ killProcessTree(runningProcess.pid, "SIGTERM");
20
55
  }
21
56
  // Clean up temp media files older than 1 hour
22
57
  try {
@@ -1178,17 +1213,13 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
1178
1213
  const startTime = Date.now();
1179
1214
  let longRunningNotified = false;
1180
1215
 
1181
- // Hard timeout to prevent runaway processes
1216
+ // Hard timeout to prevent runaway processes. We walk the descendant
1217
+ // tree (not just the PGID) so that detached background bashes — like
1218
+ // `run_in_background: true` poll loops — get cleaned up too.
1182
1219
  const processTimeout = setTimeout(async () => {
1183
1220
  if (runningProcess === proc) {
1184
- try {
1185
- process.kill(-proc.pid, "SIGTERM");
1186
- setTimeout(() => {
1187
- try { process.kill(-proc.pid, "SIGKILL"); } catch (e) {}
1188
- }, 5000);
1189
- } catch (e) {
1190
- try { proc.kill("SIGKILL"); } catch (e2) {}
1191
- }
1221
+ killProcessTree(proc.pid, "SIGTERM");
1222
+ setTimeout(() => killProcessTree(proc.pid, "SIGKILL"), 5000);
1192
1223
  await send(`Task timed out after ${MAX_PROCESS_TIMEOUT / 60000} minutes. Stopped.`);
1193
1224
  }
1194
1225
  }, MAX_PROCESS_TIMEOUT);
@@ -1697,16 +1728,10 @@ bot.onText(/\/stop/, async (msg) => {
1697
1728
  if (!isAuthorized(msg)) return;
1698
1729
  if (runningProcess) {
1699
1730
  const pid = runningProcess.pid;
1700
- try {
1701
- // Kill entire process group to catch child processes (dev servers, etc.)
1702
- process.kill(-pid, "SIGTERM");
1703
- } catch (e) {
1704
- try { runningProcess.kill("SIGTERM"); } catch (e2) {}
1705
- }
1706
- // Force kill after 3 seconds if still alive
1707
- setTimeout(() => {
1708
- try { process.kill(-pid, "SIGKILL"); } catch (e) {}
1709
- }, 3000);
1731
+ // Walk the descendant tree so detached children (background bashes,
1732
+ // dev servers) get cleaned up, not just the PGID.
1733
+ killProcessTree(pid, "SIGTERM");
1734
+ setTimeout(() => killProcessTree(pid, "SIGKILL"), 3000);
1710
1735
  runningProcess = null;
1711
1736
  if (streamInterval) clearTimeout(streamInterval);
1712
1737
  messageQueue = [];
@@ -2206,6 +2231,36 @@ try {
2206
2231
  console.warn("Setup check skipped:", e.message);
2207
2232
  }
2208
2233
 
2234
+ // Sweep stale `claude login` / `claude setup-token` processes that
2235
+ // survived a previous bot crash. They're blocked on stdin forever and
2236
+ // can hold keychain locks / resources. Anything older than 30 minutes
2237
+ // is assumed to be abandoned.
2238
+ try {
2239
+ const out = execSync(
2240
+ `ps -axo pid,etime,command | awk '/claude (login|setup-token)/ && !/awk/'`,
2241
+ { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] },
2242
+ );
2243
+ for (const line of out.split("\n")) {
2244
+ const m = line.trim().match(/^(\d+)\s+(\S+)\s/);
2245
+ if (!m) continue;
2246
+ const pid = Number(m[1]);
2247
+ const elapsed = m[2]; // [DD-]HH:MM:SS or MM:SS
2248
+ const minutes = elapsed.includes("-")
2249
+ ? Number(elapsed.split("-")[0]) * 24 * 60 + 60 // any "DD-" form is ≥1 day
2250
+ : elapsed.split(":").length === 3
2251
+ ? Number(elapsed.split(":")[0]) * 60 + Number(elapsed.split(":")[1])
2252
+ : Number(elapsed.split(":")[0]);
2253
+ if (minutes >= 30) {
2254
+ try {
2255
+ process.kill(pid, "SIGTERM");
2256
+ console.log(`Swept stale auth process pid=${pid} elapsed=${elapsed}`);
2257
+ } catch (e) {}
2258
+ }
2259
+ }
2260
+ } catch (e) {
2261
+ // ps/awk not available or no matches — fine.
2262
+ }
2263
+
2209
2264
  initCrons();
2210
2265
  console.log("Claude Code Telegram bot running");
2211
2266
  console.log(`Workspace: ${WORKSPACE}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "1.14.7",
3
+ "version": "1.14.8",
4
4
  "description": "Your always-on AI coding assistant — Claude Code via Telegram",
5
5
  "main": "bot.js",
6
6
  "bin": {