@inetafrica/open-claudia 2.2.13 → 2.2.16
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/actions.js +13 -0
- package/core/config.js +2 -1
- package/core/handlers.js +63 -3
- package/core/runner.js +29 -19
- package/core/state.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.2.16
|
|
4
|
+
- New `/compactwindow` slash command (alias `/autocompact`) lets each user override the auto-compact token threshold from chat instead of editing `AUTO_COMPACT_TOKENS` in `.env` and restarting. Quick-pick buttons for 200k / 300k / 380k / 500k / Off / Default, or free-form `/compactwindow 250k` / `/compactwindow 0.5m` / `/compactwindow off` / `/compactwindow default`. Stored per-user as `settings.compactWindow` (persists across restarts via `state.json`); `null` falls back to the env default, `0` disables auto-compact entirely (manual `/compact` still works). `/status` now reports the effective window, and `/usage`'s "context is large" tip reflects the user's override.
|
|
5
|
+
|
|
6
|
+
## v2.2.15
|
|
7
|
+
- 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.
|
|
8
|
+
- 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.
|
|
9
|
+
|
|
10
|
+
## v2.2.14
|
|
11
|
+
- 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.
|
|
12
|
+
|
|
3
13
|
## v2.2.13
|
|
4
14
|
- **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.
|
|
5
15
|
|
package/Dockerfile
CHANGED
package/core/actions.js
CHANGED
|
@@ -257,6 +257,19 @@ async function handleAction(envelope) {
|
|
|
257
257
|
if (d.startsWith("m:")) { state.settings.model = d.slice(2) === "default" ? null : d.slice(2); await send(`Model: ${state.settings.model || "default"}`); return; }
|
|
258
258
|
if (d.startsWith("e:")) { const e = d.slice(2); state.settings.effort = e === "default" ? null : e; await send(`Effort: ${state.settings.effort || "default"}`); return; }
|
|
259
259
|
if (d.startsWith("b:")) { const b = d.slice(2); state.settings.budget = b === "none" ? null : parseFloat(b); await send(`Budget: ${state.settings.budget ? "$" + state.settings.budget : "none"}`); return; }
|
|
260
|
+
if (d.startsWith("cw:")) {
|
|
261
|
+
const v = d.slice(3);
|
|
262
|
+
if (v === "default") state.settings.compactWindow = null;
|
|
263
|
+
else if (v === "off") state.settings.compactWindow = 0;
|
|
264
|
+
else {
|
|
265
|
+
const n = parseInt(v, 10);
|
|
266
|
+
if (Number.isFinite(n) && n > 0) state.settings.compactWindow = n;
|
|
267
|
+
}
|
|
268
|
+
saveState();
|
|
269
|
+
const { formatCompactWindow } = require("./handlers");
|
|
270
|
+
await send(`Auto-compact window: ${formatCompactWindow(state)}`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
260
273
|
if (d.startsWith("be:")) {
|
|
261
274
|
const be = d.slice(3);
|
|
262
275
|
if (be === "cursor" && !resolvedCursorPath) { await send("Cursor Agent CLI not found."); return; }
|
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/handlers.js
CHANGED
|
@@ -24,7 +24,7 @@ const { runDoctorChecks, formatDoctorReport } = require("./doctor");
|
|
|
24
24
|
const jobs = require("./jobs");
|
|
25
25
|
const scheduler = require("./scheduler");
|
|
26
26
|
const {
|
|
27
|
-
runClaude, compactActiveSession, getActiveSessionId,
|
|
27
|
+
runClaude, compactActiveSession, getActiveSessionId, effectiveCompactThreshold,
|
|
28
28
|
} = require("./runner");
|
|
29
29
|
const {
|
|
30
30
|
getClaudeOAuthToken, claudeSubprocessEnv, saveClaudeOAuthToken,
|
|
@@ -119,7 +119,7 @@ register({
|
|
|
119
119
|
if (!authorized(env)) return;
|
|
120
120
|
send([
|
|
121
121
|
"Session: /session /sessions /projects /continue /status /stop /end",
|
|
122
|
-
"Settings: /model /effort /budget /plan /compact /worktree /mode",
|
|
122
|
+
"Settings: /model /effort /budget /plan /compact /compactwindow /worktree /mode",
|
|
123
123
|
"Identity: /whoami /link",
|
|
124
124
|
"Team: /people /intros /auth (owner)",
|
|
125
125
|
"Automation: /cron /vault /soul",
|
|
@@ -671,6 +671,58 @@ register({
|
|
|
671
671
|
},
|
|
672
672
|
});
|
|
673
673
|
|
|
674
|
+
function parseCompactWindow(raw) {
|
|
675
|
+
const s = String(raw || "").trim().toLowerCase();
|
|
676
|
+
if (!s) return { value: undefined };
|
|
677
|
+
if (s === "default" || s === "auto" || s === "reset") return { value: null };
|
|
678
|
+
if (s === "off" || s === "disable" || s === "disabled") return { value: 0 };
|
|
679
|
+
const m = s.match(/^(\d+(?:\.\d+)?)\s*([km]?)$/);
|
|
680
|
+
if (!m) return { error: `Couldn't parse "${raw}". Use a number like 250000, 250k, off, or default.` };
|
|
681
|
+
let n = parseFloat(m[1]);
|
|
682
|
+
if (m[2] === "k") n *= 1000;
|
|
683
|
+
else if (m[2] === "m") n *= 1000000;
|
|
684
|
+
n = Math.round(n);
|
|
685
|
+
if (n === 0) return { value: 0 };
|
|
686
|
+
if (n < 10000) return { error: "Compaction window must be at least 10k tokens (or use 'off' to disable)." };
|
|
687
|
+
return { value: n };
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function formatCompactWindow(state) {
|
|
691
|
+
const fmt = (n) => n >= 1000 ? (n / 1000).toFixed(0) + "k" : String(n);
|
|
692
|
+
const cw = state.settings?.compactWindow;
|
|
693
|
+
if (cw === 0) return "off";
|
|
694
|
+
if (cw == null) return `default (${fmt(AUTO_COMPACT_TOKENS)})`;
|
|
695
|
+
return fmt(cw);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
register({
|
|
699
|
+
name: "compactwindow", aliases: ["autocompact"],
|
|
700
|
+
description: "Set auto-compact token threshold",
|
|
701
|
+
args: "[<tokens> | off | default]",
|
|
702
|
+
handler: async (env, { tail }) => {
|
|
703
|
+
if (!authorized(env)) return;
|
|
704
|
+
const state = currentState();
|
|
705
|
+
if (tail) {
|
|
706
|
+
const { value, error } = parseCompactWindow(tail);
|
|
707
|
+
if (error) return send(error);
|
|
708
|
+
state.settings.compactWindow = value;
|
|
709
|
+
saveState();
|
|
710
|
+
return send(`Auto-compact window: ${formatCompactWindow(state)}`);
|
|
711
|
+
}
|
|
712
|
+
send(
|
|
713
|
+
`Auto-compact window: ${formatCompactWindow(state)}\n\n` +
|
|
714
|
+
"When the last turn's input context exceeds this, the next reply triggers a compaction. " +
|
|
715
|
+
"Type /compactwindow <n> for a custom value (e.g. 250k).",
|
|
716
|
+
{ keyboard: { inline_keyboard: [
|
|
717
|
+
[{ text: "200k", callback_data: "cw:200000" }, { text: "300k", callback_data: "cw:300000" }, { text: "380k", callback_data: "cw:380000" }, { text: "500k", callback_data: "cw:500000" }],
|
|
718
|
+
[{ text: "Off", callback_data: "cw:off" }, { text: "Default", callback_data: "cw:default" }],
|
|
719
|
+
] } },
|
|
720
|
+
);
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
module.exports.formatCompactWindow = formatCompactWindow;
|
|
725
|
+
|
|
674
726
|
register({
|
|
675
727
|
name: "worktree", description: "Toggle isolated git branch",
|
|
676
728
|
handler: async (env) => {
|
|
@@ -787,6 +839,7 @@ register({
|
|
|
787
839
|
`Project: ${state.currentSession.name}`,
|
|
788
840
|
`Backend: ${backendLabel}`,
|
|
789
841
|
`Model: ${settings.model || "default"} | Effort: ${settings.effort || "default"}`,
|
|
842
|
+
`Auto-compact: ${formatCompactWindow(state)}`,
|
|
790
843
|
`Vault: ${vault.isUnlocked() ? "unlocked" : "locked"} | Crons: ${activeCrons.size}`,
|
|
791
844
|
state.runningProcess ? "Working..." : "Ready.",
|
|
792
845
|
].join("\n"));
|
|
@@ -825,7 +878,14 @@ register({
|
|
|
825
878
|
`Cost: $${u.costUsd.toFixed(4)}`,
|
|
826
879
|
`Last turn context: ${fmt(u.lastInputTokens)}`,
|
|
827
880
|
];
|
|
828
|
-
if (u.lastInputTokens > 200000)
|
|
881
|
+
if (u.lastInputTokens > 200000) {
|
|
882
|
+
const threshold = effectiveCompactThreshold(currentState());
|
|
883
|
+
if (threshold === 0) {
|
|
884
|
+
lines.push(`\nTip: context is large. Auto-compact is off (/compactwindow); /compact does it now.`);
|
|
885
|
+
} else {
|
|
886
|
+
lines.push(`\nTip: context is large. The bot auto-compacts after the next reply at ${fmt(threshold)} tokens (/compactwindow); /compact does it now.`);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
829
889
|
send(lines.join("\n"), { parseMode: "Markdown" });
|
|
830
890
|
},
|
|
831
891
|
});
|
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");
|
|
@@ -87,10 +87,18 @@ function getActiveSessionKey(state = currentState()) {
|
|
|
87
87
|
return "lastSessionId";
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
function effectiveCompactThreshold(state = currentState()) {
|
|
91
|
+
const override = state.settings?.compactWindow;
|
|
92
|
+
if (override === 0) return 0;
|
|
93
|
+
if (Number.isFinite(override) && override > 0) return override;
|
|
94
|
+
return Number.isFinite(AUTO_COMPACT_TOKENS) ? AUTO_COMPACT_TOKENS : 380000;
|
|
95
|
+
}
|
|
96
|
+
|
|
90
97
|
function shouldAutoCompact(state = currentState(), opts = {}) {
|
|
91
98
|
if (opts.skipAutoCompact || opts.fresh || opts.continueSession || state.isCompacting) return false;
|
|
92
99
|
if (!state[getActiveSessionKey(state)]) return false;
|
|
93
|
-
const threshold =
|
|
100
|
+
const threshold = effectiveCompactThreshold(state);
|
|
101
|
+
if (threshold === 0) return false;
|
|
94
102
|
if ((state.sessionUsage?.lastInputTokens || 0) < threshold) return false;
|
|
95
103
|
const minInterval = Number.isFinite(MIN_COMPACT_INTERVAL_MS) ? MIN_COMPACT_INTERVAL_MS : 1800000;
|
|
96
104
|
if (state.lastCompactedAt && (Date.now() - state.lastCompactedAt) < minInterval) return false;
|
|
@@ -299,7 +307,7 @@ async function runClaudeCapture(prompt, cwd, opts = {}) {
|
|
|
299
307
|
killProcessTree(proc.pid, "SIGTERM");
|
|
300
308
|
setTimeout(() => killProcessTree(proc.pid, "SIGKILL"), 5000);
|
|
301
309
|
}
|
|
302
|
-
}, MAX_PROCESS_TIMEOUT);
|
|
310
|
+
}, opts.timeoutMs || MAX_PROCESS_TIMEOUT);
|
|
303
311
|
|
|
304
312
|
proc.stdout.on("data", (data) => {
|
|
305
313
|
streamBuffer += data.toString();
|
|
@@ -371,32 +379,33 @@ async function compactActiveSession(cwd, opts = {}) {
|
|
|
371
379
|
if (state.isCompacting) return { compacted: false, reason: "Compaction already in progress." };
|
|
372
380
|
|
|
373
381
|
state.isCompacting = true;
|
|
382
|
+
let summary;
|
|
374
383
|
try {
|
|
375
384
|
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
|
-
|
|
385
|
+
const summaryRun = await runClaudeCapture(compactSummaryPrompt(), cwd, { resumeSessionId: oldSessionId, skipAutoCompact: true, label: "compact-summary", timeoutMs: COMPACT_SUMMARY_TIMEOUT });
|
|
386
|
+
summary = summaryRun.text || "No prior context was returned by the summarizer.";
|
|
378
387
|
state[sessionKey] = null;
|
|
379
388
|
state.sessionUsage = { turns: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, costUsd: 0, lastInputTokens: 0 };
|
|
380
389
|
state.isFirstMessage = true;
|
|
381
390
|
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
391
|
} finally {
|
|
397
392
|
state.isCompacting = false;
|
|
398
393
|
saveState();
|
|
399
394
|
}
|
|
395
|
+
|
|
396
|
+
const seedRun = await runClaudeCapture(compactSeedPrompt(summary), cwd, { fresh: true, skipAutoCompact: true, label: "compact-seed" });
|
|
397
|
+
const newSessionId = seedRun.sessionId || state[sessionKey];
|
|
398
|
+
if (newSessionId) state[sessionKey] = newSessionId;
|
|
399
|
+
state.isFirstMessage = false;
|
|
400
|
+
state.lastCompactedAt = Date.now();
|
|
401
|
+
state.sessionUsage = { turns: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, costUsd: 0, lastInputTokens: 0 };
|
|
402
|
+
saveState();
|
|
403
|
+
|
|
404
|
+
if (newSessionId && state.currentSession) {
|
|
405
|
+
const title = `Compacted ${new Date().toLocaleDateString()}`;
|
|
406
|
+
recordSession(state.userId, state.currentSession.name, newSessionId, title);
|
|
407
|
+
}
|
|
408
|
+
return { compacted: true, oldSessionId, newSessionId, summary };
|
|
400
409
|
}
|
|
401
410
|
|
|
402
411
|
async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
@@ -756,6 +765,7 @@ module.exports = {
|
|
|
756
765
|
getActiveBinary,
|
|
757
766
|
getActiveSessionKey,
|
|
758
767
|
shouldAutoCompact,
|
|
768
|
+
effectiveCompactThreshold,
|
|
759
769
|
formatProgress,
|
|
760
770
|
preflightClaudeAuthMessage,
|
|
761
771
|
claudeEmptyFailureMessage,
|
package/core/state.js
CHANGED
|
@@ -43,7 +43,7 @@ const savedState = (() => {
|
|
|
43
43
|
const userStates = new Map();
|
|
44
44
|
|
|
45
45
|
function freshSettings() {
|
|
46
|
-
return { model: null, effort: null, budget: null, permissionMode: null, worktree: false, backend: "claude" };
|
|
46
|
+
return { model: null, effort: null, budget: null, permissionMode: null, worktree: false, backend: "claude", compactWindow: null };
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
function freshUsage() {
|
package/package.json
CHANGED