@inetafrica/open-claudia 2.2.12 → 2.2.15

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.2.15
4
+ - Fix compaction loop: `compactActiveSession` no longer holds `isCompacting=true` for the full duration of its two-step flow. The flag now clears after step 1 (the summarizer) finishes, so step 2 (the seed-the-fresh-session call) runs as a regular long-running task. Previously, the seed step could pick up where the prior conversation left off and do real work — dev servers, package installs — for hours, all while the bot reported "Compacting context, will pick this up next…" to every incoming message. After a `/restart` the in-memory flag reset but `lastSessionId` still pointed to the same huge session, triggering an auto-compact on the next message and looping the same trap. New behaviour: the summarizer-only phase shows the compaction message; once the summary is written the bot returns to its normal "Queued." reply for any messages that arrive while the seed continuation runs.
5
+ - Add `COMPACT_SUMMARY_TIMEOUT` (10 minutes) and thread it through `runClaudeCapture` via `opts.timeoutMs`. The summarizer is a single-shot summarisation call — if it hasn't returned in 10 minutes it's hung, not slow. Previously it could sit on the 6-hour `MAX_PROCESS_TIMEOUT` and lock the bot for a quarter of a day. The seed continuation keeps the full 6-hour budget since it can legitimately be a long-running agent task.
6
+
7
+ ## v2.2.14
8
+ - Dockerfile: bake `openssh-client` and `rsync` into the image. These were being installed at runtime via `sudo apt-get install` on pods that needed to push code over ssh or rsync to dev servers; baking them in means they survive pod restarts and `/upgrade` overlays. Companion change in the AgentSpace backend flips the bot-pod container `securityContext` to allow privilege escalation + adds `SETUID,SETGID,DAC_OVERRIDE,CHOWN,FOWNER` capabilities so the existing `claudia ALL=(ALL) NOPASSWD: /usr/bin/apt-get` sudoers rule (added in v2.2.7) actually works — without these, kernel `no_new_privs` blocks sudo from elevating. The same backend change also opens 22/TCP egress on the bot's NetworkPolicy so the in-pod ssh actually reaches dev hosts.
9
+
10
+ ## v2.2.13
11
+ - **Security fix**: `intro-flow.handleInbound` no longer auto-claims ownership on *any* first inbound message. Previously, if `people.json` had no owner record (fresh pod / fresh install), the very first inbound — including a `/start` tap from a t.me deep-link — would register the sender as the bot owner. In AgentSpace pods this meant anyone who guessed or was sent the bot username could take over a pod simply by clicking Start. The bootstrap path now requires either (a) pod mode (`AGENTSPACE_POD_TOKEN` set) with the inbound chat id pre-seeded in `TELEGRAM_CHAT_ID` env, or (b) local mode with the inbound message literally starting with `/auth`. Anything else gets a "no owner configured" reply and an `intro.bootstrap-refused` audit entry. Existing owner records are unaffected. Follow-up work needed in the AgentSpace backend provisioner to seed `TELEGRAM_CHAT_ID` at pod creation time from the provisioning user's Telegram chat id; until that lands, new pods will refuse all inbound until an operator manually seeds the env.
12
+
3
13
  ## v2.2.12
4
14
  - Cross-channel relay (`open-claudia send-to`, and by extension cron/wakeup-fired messages and any other caller of `relay.send`) now passes `parseMode: "Markdown"` when the resolved adapter is Telegram. Previously `relay.send` called `adapter.send(channelId, text)` with no opts, so the Telegram adapter never set `parse_mode` and every relayed message went out as plain text — `*bold*`, backticks, and `_italic_` rendered as literal characters. The main reply path already injected this via `core/runner.js:25-28`; relay was the missing twin. Non-telegram adapters are unaffected.
5
15
 
package/Dockerfile CHANGED
@@ -11,6 +11,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
11
11
  python3-pip \
12
12
  build-essential \
13
13
  sudo \
14
+ openssh-client \
15
+ rsync \
14
16
  && rm -rf /var/lib/apt/lists/*
15
17
 
16
18
  # Install Claude Code CLI
package/core/config.js CHANGED
@@ -74,6 +74,7 @@ const FILES_DIR = path.join(CONFIG_DIR, "files");
74
74
  const MAX_FILE_SIZE = 50 * 1024 * 1024;
75
75
  const MAX_VOICE_SIZE = 10 * 1024 * 1024;
76
76
  const MAX_PROCESS_TIMEOUT = 360 * 60 * 1000;
77
+ const COMPACT_SUMMARY_TIMEOUT = 10 * 60 * 1000;
77
78
 
78
79
  if (!WORKSPACE) { console.error("WORKSPACE not set"); process.exit(1); }
79
80
  if (!CLAUDE_PATH) { console.error("CLAUDE_PATH not set"); process.exit(1); }
@@ -200,6 +201,6 @@ module.exports = {
200
201
  PEOPLE_FILE, INTROS_FILE, AUDIT_FILE,
201
202
  STATE_FILE, SESSIONS_FILE,
202
203
  TEMP_DIR, FILES_DIR,
203
- MAX_FILE_SIZE, MAX_VOICE_SIZE, MAX_PROCESS_TIMEOUT,
204
+ MAX_FILE_SIZE, MAX_VOICE_SIZE, MAX_PROCESS_TIMEOUT, COMPACT_SUMMARY_TIMEOUT,
204
205
  FULL_PATH,
205
206
  };
@@ -112,11 +112,32 @@ async function handleInbound(envelope, sendFn) {
112
112
  people.seedOwnerFromLegacy();
113
113
  }
114
114
  if (!people.hasOwnerRecord()) {
115
+ // No owner record yet. Two failure modes we are guarding against:
116
+ // (a) AgentSpace pods: random Telegram users finding the bot username
117
+ // and auto-claiming ownership just by sending /start.
118
+ // (b) Local installs: any inbound (button taps, joins, automated
119
+ // crawls) claiming ownership before the actual operator messages.
120
+ // Pod mode: only chat ids pre-seeded into TELEGRAM_CHAT_ID may claim.
121
+ // Local mode: only an explicit "/auth" message may claim.
122
+ const podMode = !!process.env.AGENTSPACE_POD_TOKEN;
123
+ const allowedChatIds = String(process.env.TELEGRAM_CHAT_ID || "")
124
+ .split(",").map((s) => s.trim()).filter(Boolean);
125
+ if (podMode) {
126
+ if (allowedChatIds.length === 0 || !allowedChatIds.includes(String(channelId))) {
127
+ await sendFn("This bot has no owner configured yet. The AgentSpace operator needs to seed the owner chat id before the bot can be used.");
128
+ audit.log("intro.bootstrap-refused", { reason: "pod-mode-not-preseeded", adapter, channelId });
129
+ return true;
130
+ }
131
+ } else if (!/^\/auth\b/i.test(text)) {
132
+ await sendFn("This bot has no owner yet. Send /auth from the operator's chat to claim ownership.");
133
+ audit.log("intro.bootstrap-refused", { reason: "local-mode-non-auth-message", adapter, channelId });
134
+ return true;
135
+ }
115
136
  const displayName = displayNameFromEnvelope(envelope) || "Owner";
116
- const person = people.add({ name: displayName, isOwner: true, bio: "Auto-registered on first message" });
137
+ const person = people.add({ name: displayName, isOwner: true, bio: "Auto-registered as bootstrap owner" });
117
138
  try { people.linkHandle(person.id, { adapter, channelId, displayName, approvedBy: "bootstrap" }); } catch (e) {}
118
139
  try { bootstrapOwner({ chatId: channelId, name: displayName, username: usernameFromEnvelope(envelope) }); } catch (e) {}
119
- audit.log("people.bootstrap-owner", { personId: person.id, adapter, channelId });
140
+ audit.log("people.bootstrap-owner", { personId: person.id, adapter, channelId, mode: podMode ? "pod" : "local" });
120
141
  await sendFn(`Welcome, ${displayName}. You're registered as the bot owner. Send /start to begin.`);
121
142
  return true;
122
143
  }
package/core/runner.js CHANGED
@@ -6,7 +6,7 @@
6
6
  const { spawn } = require("child_process");
7
7
  const {
8
8
  CLAUDE_PATH, resolvedCursorPath, resolvedCodexPath,
9
- AUTO_COMPACT_TOKENS, MIN_COMPACT_INTERVAL_MS, MAX_PROCESS_TIMEOUT, botSubprocessEnv,
9
+ AUTO_COMPACT_TOKENS, MIN_COMPACT_INTERVAL_MS, MAX_PROCESS_TIMEOUT, COMPACT_SUMMARY_TIMEOUT, botSubprocessEnv,
10
10
  } = require("./config");
11
11
  const { currentState, saveState, recordSession, userOwnsClaudeSession } = require("./state");
12
12
  const { chatContext, currentChannelId, currentAdapter } = require("./context");
@@ -299,7 +299,7 @@ async function runClaudeCapture(prompt, cwd, opts = {}) {
299
299
  killProcessTree(proc.pid, "SIGTERM");
300
300
  setTimeout(() => killProcessTree(proc.pid, "SIGKILL"), 5000);
301
301
  }
302
- }, MAX_PROCESS_TIMEOUT);
302
+ }, opts.timeoutMs || MAX_PROCESS_TIMEOUT);
303
303
 
304
304
  proc.stdout.on("data", (data) => {
305
305
  streamBuffer += data.toString();
@@ -371,32 +371,33 @@ async function compactActiveSession(cwd, opts = {}) {
371
371
  if (state.isCompacting) return { compacted: false, reason: "Compaction already in progress." };
372
372
 
373
373
  state.isCompacting = true;
374
+ let summary;
374
375
  try {
375
376
  if (opts.notify) await send(opts.message || "Context is getting large, compacting first so this stays fast…");
376
- const summaryRun = await runClaudeCapture(compactSummaryPrompt(), cwd, { resumeSessionId: oldSessionId, skipAutoCompact: true, label: "compact-summary" });
377
- const summary = summaryRun.text || "No prior context was returned by the summarizer.";
377
+ const summaryRun = await runClaudeCapture(compactSummaryPrompt(), cwd, { resumeSessionId: oldSessionId, skipAutoCompact: true, label: "compact-summary", timeoutMs: COMPACT_SUMMARY_TIMEOUT });
378
+ summary = summaryRun.text || "No prior context was returned by the summarizer.";
378
379
  state[sessionKey] = null;
379
380
  state.sessionUsage = { turns: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, costUsd: 0, lastInputTokens: 0 };
380
381
  state.isFirstMessage = true;
381
382
  saveState();
382
-
383
- const seedRun = await runClaudeCapture(compactSeedPrompt(summary), cwd, { fresh: true, skipAutoCompact: true, label: "compact-seed" });
384
- const newSessionId = seedRun.sessionId || state[sessionKey];
385
- if (newSessionId) state[sessionKey] = newSessionId;
386
- state.isFirstMessage = false;
387
- state.lastCompactedAt = Date.now();
388
- state.sessionUsage = { turns: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, costUsd: 0, lastInputTokens: 0 };
389
- saveState();
390
-
391
- if (newSessionId && state.currentSession) {
392
- const title = `Compacted ${new Date().toLocaleDateString()}`;
393
- recordSession(state.userId, state.currentSession.name, newSessionId, title);
394
- }
395
- return { compacted: true, oldSessionId, newSessionId, summary };
396
383
  } finally {
397
384
  state.isCompacting = false;
398
385
  saveState();
399
386
  }
387
+
388
+ const seedRun = await runClaudeCapture(compactSeedPrompt(summary), cwd, { fresh: true, skipAutoCompact: true, label: "compact-seed" });
389
+ const newSessionId = seedRun.sessionId || state[sessionKey];
390
+ if (newSessionId) state[sessionKey] = newSessionId;
391
+ state.isFirstMessage = false;
392
+ state.lastCompactedAt = Date.now();
393
+ state.sessionUsage = { turns: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, costUsd: 0, lastInputTokens: 0 };
394
+ saveState();
395
+
396
+ if (newSessionId && state.currentSession) {
397
+ const title = `Compacted ${new Date().toLocaleDateString()}`;
398
+ recordSession(state.userId, state.currentSession.name, newSessionId, title);
399
+ }
400
+ return { compacted: true, oldSessionId, newSessionId, summary };
400
401
  }
401
402
 
402
403
  async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.2.12",
3
+ "version": "2.2.15",
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": {