@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 +10 -0
- package/Dockerfile +2 -0
- package/core/config.js +2 -1
- package/core/intro-flow.js +23 -2
- package/core/runner.js +19 -18
- package/package.json +1 -1
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
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
|
};
|
package/core/intro-flow.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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