@inetafrica/open-claudia 1.17.0 → 1.18.0
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 +2 -0
- package/CHANGELOG.md +7 -0
- package/bot.js +223 -61
- package/package.json +1 -1
package/.env.example
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.18.0
|
|
4
|
+
- Auto-compacts high-context sessions before the next turn (`AUTO_COMPACT_TOKENS`, default 140k): summarizes the old session, seeds a fresh session, then continues the user's request there
|
|
5
|
+
- `/compact` now creates a fresh compacted continuation session instead of only adding another summary turn to the existing session
|
|
6
|
+
- `/continue` resumes the selected stored session ID with `--resume` instead of using cwd-most-recent `--continue`
|
|
7
|
+
- Stabilized the mobile system prompt: no timestamps, dynamic file lists, vault key names, or raw Telegram token curl examples
|
|
8
|
+
- Reply context is no longer redundantly injected when replying to the bot's own prior text from the active session
|
|
9
|
+
|
|
3
10
|
## v1.17.0
|
|
4
11
|
- **Multi-user / team mode**: a single bot can now serve multiple authorized users in parallel. Each user has their own conversation thread, project session, settings, model, backend, runningProcess, queue, and usage counters
|
|
5
12
|
- Per-user state lives in `userStates: Map<chatId, UserState>`; an `AsyncLocalStorage` chat context routes `send()`/`editMessage()`/typing indicators back to whoever triggered the work
|
package/bot.js
CHANGED
|
@@ -150,6 +150,7 @@ const WORKSPACE = config.WORKSPACE;
|
|
|
150
150
|
const CLAUDE_PATH = config.CLAUDE_PATH;
|
|
151
151
|
const CURSOR_PATH = config.CURSOR_PATH || null;
|
|
152
152
|
const CODEX_PATH = config.CODEX_PATH || null;
|
|
153
|
+
const AUTO_COMPACT_TOKENS = parseInt(config.AUTO_COMPACT_TOKENS || process.env.AUTO_COMPACT_TOKENS || "140000", 10);
|
|
153
154
|
|
|
154
155
|
// Validate critical config at startup
|
|
155
156
|
if (!TOKEN) { console.error("TELEGRAM_BOT_TOKEN not set"); process.exit(1); }
|
|
@@ -385,6 +386,8 @@ function createUserState(chatId) {
|
|
|
385
386
|
pendingVaultAction: null,
|
|
386
387
|
pendingClaudeAuthProcess: null,
|
|
387
388
|
pendingClaudeAuthLabel: null,
|
|
389
|
+
isCompacting: false,
|
|
390
|
+
lastCompactedAt: saved.lastCompactedAt || 0,
|
|
388
391
|
};
|
|
389
392
|
}
|
|
390
393
|
|
|
@@ -426,6 +429,7 @@ function saveState() {
|
|
|
426
429
|
codexSessionId: s.codexSessionId,
|
|
427
430
|
settings: s.settings,
|
|
428
431
|
sessionUsage: s.sessionUsage,
|
|
432
|
+
lastCompactedAt: s.lastCompactedAt || 0,
|
|
429
433
|
};
|
|
430
434
|
}
|
|
431
435
|
try { fs.writeFileSync(STATE_FILE, JSON.stringify(data)); } catch (e) {}
|
|
@@ -667,65 +671,38 @@ function saveCrons(list) {
|
|
|
667
671
|
|
|
668
672
|
function buildSystemPrompt() {
|
|
669
673
|
const state = currentState();
|
|
670
|
-
const chatId = state.chatId;
|
|
671
674
|
const soul = loadSoul();
|
|
672
|
-
const cronList = loadCrons();
|
|
673
|
-
const vaultKeys = vault.isUnlocked() ? vault.keys() : [];
|
|
674
|
-
const now = new Date().toISOString();
|
|
675
675
|
const hasVoice = WHISPER_CLI && FFMPEG;
|
|
676
676
|
|
|
677
677
|
return `
|
|
678
678
|
${soul}
|
|
679
679
|
|
|
680
|
-
##
|
|
681
|
-
-
|
|
682
|
-
-
|
|
683
|
-
- Active project: ${state.currentSession ? state.currentSession.name + " (" + state.currentSession.dir + ")" : "none"}
|
|
680
|
+
## Runtime Context
|
|
681
|
+
- Interface: Telegram mobile chat through Open Claudia.
|
|
682
|
+
- Active project path: ${state.currentSession ? state.currentSession.dir : "none"}
|
|
684
683
|
- Voice notes: ${hasVoice ? "enabled" : "disabled"}
|
|
685
|
-
- Vault: ${vault.isUnlocked() ? "unlocked
|
|
686
|
-
|
|
687
|
-
## Your Configuration Files
|
|
688
|
-
These are YOUR files — read and modify them when the user asks:
|
|
689
|
-
|
|
690
|
-
### ${SOUL_FILE}
|
|
691
|
-
Your identity and personality. Edit to change behavior or update knowledge about the user.
|
|
692
|
-
|
|
693
|
-
### ${CRONS_FILE}
|
|
694
|
-
Scheduled tasks. JSON array of { id, schedule, project, prompt, label }.
|
|
695
|
-
Active: ${cronList.length > 0 ? cronList.map(c => c.label).join(", ") : "none"}.
|
|
696
|
-
|
|
697
|
-
### ${VAULT_FILE}
|
|
698
|
-
Encrypted credential vault. ${vault.isUnlocked() ? "UNLOCKED. Keys: " + vaultKeys.join(", ") : "LOCKED — user must send /vault to unlock."}.
|
|
699
|
-
${vault.isUnlocked() ? "To read a credential: require('./vault') or read from the vault object." : ""}
|
|
684
|
+
- Vault: ${vault.isUnlocked() ? "unlocked" : "locked"}
|
|
685
|
+
- Session: ${state.lastSessionId ? "resuming existing conversation" : "new conversation"}
|
|
700
686
|
|
|
701
|
-
|
|
702
|
-
|
|
687
|
+
## Stable Local Paths
|
|
688
|
+
- Bot code: ${path.join(BOT_DIR, "bot.js")}
|
|
689
|
+
- Personality file: ${SOUL_FILE}
|
|
690
|
+
- Cron config: ${CRONS_FILE}
|
|
691
|
+
- Vault file: ${VAULT_FILE}
|
|
692
|
+
- Bot environment: ${path.join(BOT_DIR, ".env")} (sensitive; never expose values)
|
|
693
|
+
- Received user files directory: ${FILES_DIR}
|
|
703
694
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
## Received Files
|
|
708
|
-
Files sent by the user are saved in: ${FILES_DIR}
|
|
709
|
-
${fs.existsSync(FILES_DIR) ? (() => { try { const f = fs.readdirSync(FILES_DIR); return f.length > 0 ? "Current files: " + f.slice(-10).join(", ") : "No files yet."; } catch(e) { return ""; } })() : ""}
|
|
710
|
-
|
|
711
|
-
## Telegram API
|
|
712
|
-
Send things directly to the user who triggered this turn (chat_id ${chatId}):
|
|
713
|
-
|
|
714
|
-
Text: curl -s -X POST "https://api.telegram.org/bot${TOKEN}/sendMessage" -d chat_id=${chatId} -d text="message"
|
|
715
|
-
File: curl -s -X POST "https://api.telegram.org/bot${TOKEN}/sendDocument" -F chat_id=${chatId} -F document=@/path/to/file
|
|
716
|
-
Image: curl -s -X POST "https://api.telegram.org/bot${TOKEN}/sendPhoto" -F chat_id=${chatId} -F photo=@/path/to/image.png
|
|
717
|
-
|
|
718
|
-
## Session
|
|
719
|
-
${state.lastSessionId ? "Resuming conversation — you have prior context." : "New conversation."}
|
|
695
|
+
## Telegram Delivery
|
|
696
|
+
Reply normally in your final answer. If you must send a large file, image, or artifact directly, use the Telegram API with the configured bot token from the environment/config; never print or embed the token in prompts, commands, logs, or messages.
|
|
720
697
|
|
|
721
698
|
## Guidelines
|
|
722
699
|
- Keep responses concise — this is a mobile screen.
|
|
723
700
|
- Use Telegram-compatible markdown: *bold*, _italic_, \`code\`, \`\`\`code blocks\`\`\`. No headers (#), no links [text](url).
|
|
724
|
-
- For long output (logs, diffs, large code), save to a file and send
|
|
701
|
+
- For long output (logs, diffs, large code), save to a file and send it as an artifact instead of pasting walls of text.
|
|
725
702
|
- Act on screenshots (fix bugs, implement designs) — don't just describe what you see.
|
|
726
|
-
- When the user sends a file, it
|
|
727
|
-
- When the user sends a credential, token, or API key, store it in the vault immediately using the vault CLI or bot commands. Tell them it's stored and that you've deleted their message for security.
|
|
728
|
-
- When asked to change your personality, edit
|
|
703
|
+
- When the user sends a file, it is saved in the received files directory above. Read it with the Read tool.
|
|
704
|
+
- When the user sends a credential, token, or API key, store it in the vault immediately using the vault CLI or bot commands. Tell them it's stored and that you've deleted their message for security.
|
|
705
|
+
- When asked to change your personality, edit the personality file above.
|
|
729
706
|
- When asked about yourself, you are Open Claudia — an AI coding assistant running Claude Code via Telegram.
|
|
730
707
|
- If a task will take a while, let the user know upfront.
|
|
731
708
|
- Don't ask for confirmation on simple tasks — just do them.
|
|
@@ -1246,7 +1223,8 @@ function buildClaudeArgs(prompt, opts = {}) {
|
|
|
1246
1223
|
if (settings.backend === "codex") return buildCodexArgs(prompt, opts);
|
|
1247
1224
|
const args = ["-p", "--verbose", "--output-format", "stream-json",
|
|
1248
1225
|
"--append-system-prompt", buildSystemPrompt()];
|
|
1249
|
-
if (opts.
|
|
1226
|
+
if (opts.resumeSessionId) args.push("--resume", opts.resumeSessionId);
|
|
1227
|
+
else if (opts.continueSession) args.push("--continue");
|
|
1250
1228
|
else if (state.lastSessionId && !opts.fresh) args.push("--resume", state.lastSessionId);
|
|
1251
1229
|
if (settings.model) args.push("--model", settings.model);
|
|
1252
1230
|
if (settings.effort) args.push("--effort", settings.effort);
|
|
@@ -1262,7 +1240,8 @@ function buildCursorArgs(prompt, opts = {}) {
|
|
|
1262
1240
|
const state = currentState();
|
|
1263
1241
|
const { settings } = state;
|
|
1264
1242
|
const args = ["--print", "--trust", "--output-format", "stream-json"];
|
|
1265
|
-
if (opts.
|
|
1243
|
+
if (opts.resumeSessionId) args.push("--resume", opts.resumeSessionId);
|
|
1244
|
+
else if (opts.continueSession) args.push("--continue");
|
|
1266
1245
|
else if (state.cursorSessionId && !opts.fresh) args.push("--resume", state.cursorSessionId);
|
|
1267
1246
|
if (settings.model) args.push("--model", settings.model);
|
|
1268
1247
|
if (settings.permissionMode === "plan") args.push("--mode", "plan");
|
|
@@ -1279,7 +1258,7 @@ function buildCodexArgs(prompt, opts = {}) {
|
|
|
1279
1258
|
const state = currentState();
|
|
1280
1259
|
const { settings, codexSessionId } = state;
|
|
1281
1260
|
const args = [];
|
|
1282
|
-
const resumeId = (!opts.fresh && !opts.continueSession) ? codexSessionId : null;
|
|
1261
|
+
const resumeId = opts.resumeSessionId || ((!opts.fresh && !opts.continueSession) ? codexSessionId : null);
|
|
1283
1262
|
if (opts.continueSession && codexSessionId) {
|
|
1284
1263
|
args.push("exec", "resume", codexSessionId);
|
|
1285
1264
|
} else if (resumeId) {
|
|
@@ -1312,6 +1291,165 @@ function getActiveSessionId() {
|
|
|
1312
1291
|
return state.lastSessionId;
|
|
1313
1292
|
}
|
|
1314
1293
|
|
|
1294
|
+
function getActiveSessionKey(state = currentState()) {
|
|
1295
|
+
if (state.settings.backend === "cursor") return "cursorSessionId";
|
|
1296
|
+
if (state.settings.backend === "codex") return "codexSessionId";
|
|
1297
|
+
return "lastSessionId";
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function shouldAutoCompact(state = currentState(), opts = {}) {
|
|
1301
|
+
if (opts.skipAutoCompact || opts.fresh || opts.continueSession || state.isCompacting) return false;
|
|
1302
|
+
if (!state[getActiveSessionKey(state)]) return false;
|
|
1303
|
+
const threshold = Number.isFinite(AUTO_COMPACT_TOKENS) ? AUTO_COMPACT_TOKENS : 140000;
|
|
1304
|
+
return (state.sessionUsage?.lastInputTokens || 0) >= threshold;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
function compactSummaryPrompt() {
|
|
1308
|
+
return [
|
|
1309
|
+
"Summarize this conversation for a fresh compacted continuation.",
|
|
1310
|
+
"Include only durable context needed to continue effectively:",
|
|
1311
|
+
"- current user goal and constraints",
|
|
1312
|
+
"- important decisions and preferences",
|
|
1313
|
+
"- files/repos touched and current code state",
|
|
1314
|
+
"- commands/tests already run and results",
|
|
1315
|
+
"- open TODOs, blockers, and exact next step",
|
|
1316
|
+
"Do not include secrets, raw tokens, or irrelevant chat transcript."
|
|
1317
|
+
].join("\n");
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function compactSeedPrompt(summary) {
|
|
1321
|
+
return [
|
|
1322
|
+
"This is a compacted continuation of a previous Open Claudia session.",
|
|
1323
|
+
"Treat the following summary as the prior conversation context. Do not repeat it back unless asked.",
|
|
1324
|
+
"Continue from this state in future turns.",
|
|
1325
|
+
"",
|
|
1326
|
+
"Compacted summary:",
|
|
1327
|
+
summary
|
|
1328
|
+
].join("\n");
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
async function runClaudeCapture(prompt, cwd, opts = {}) {
|
|
1332
|
+
const state = currentState();
|
|
1333
|
+
const chatId = state.chatId;
|
|
1334
|
+
if (state.runningProcess) throw new Error("Another task is already running.");
|
|
1335
|
+
const authPreflight = preflightClaudeAuthMessage();
|
|
1336
|
+
if (authPreflight) throw new Error(authPreflight);
|
|
1337
|
+
|
|
1338
|
+
return new Promise((resolve, reject) => {
|
|
1339
|
+
let assistantText = "";
|
|
1340
|
+
let stderrBuffer = "";
|
|
1341
|
+
let streamBuffer = "";
|
|
1342
|
+
let sessionId = null;
|
|
1343
|
+
const args = buildClaudeArgs(prompt, { ...opts, skipAutoCompact: true });
|
|
1344
|
+
const proc = spawn(getActiveBinary(), args, {
|
|
1345
|
+
cwd,
|
|
1346
|
+
env: claudeSubprocessEnv(),
|
|
1347
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1348
|
+
detached: process.platform !== "win32",
|
|
1349
|
+
});
|
|
1350
|
+
state.runningProcess = proc;
|
|
1351
|
+
const timeout = setTimeout(() => {
|
|
1352
|
+
if (state.runningProcess === proc) {
|
|
1353
|
+
killProcessTree(proc.pid, "SIGTERM");
|
|
1354
|
+
setTimeout(() => killProcessTree(proc.pid, "SIGKILL"), 5000);
|
|
1355
|
+
}
|
|
1356
|
+
}, MAX_PROCESS_TIMEOUT);
|
|
1357
|
+
|
|
1358
|
+
proc.stdout.on("data", (data) => {
|
|
1359
|
+
streamBuffer += data.toString();
|
|
1360
|
+
const events = parseStreamEvents(streamBuffer);
|
|
1361
|
+
const lastNewline = streamBuffer.lastIndexOf("\n");
|
|
1362
|
+
streamBuffer = lastNewline >= 0 ? streamBuffer.slice(lastNewline + 1) : streamBuffer;
|
|
1363
|
+
for (const evt of events) {
|
|
1364
|
+
if (evt.type === "assistant" && evt.message?.content) {
|
|
1365
|
+
for (const block of evt.message.content) {
|
|
1366
|
+
if (block.type === "text") assistantText += block.text;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
if (evt.type === "item.completed" && evt.item?.type === "agent_message" && typeof evt.item.text === "string") {
|
|
1370
|
+
assistantText += (assistantText ? "\n" : "") + evt.item.text;
|
|
1371
|
+
}
|
|
1372
|
+
if (evt.type === "thread.started" && evt.thread_id) {
|
|
1373
|
+
state.codexSessionId = evt.thread_id;
|
|
1374
|
+
sessionId = evt.thread_id;
|
|
1375
|
+
saveState();
|
|
1376
|
+
}
|
|
1377
|
+
if (evt.type === "result" && evt.session_id) {
|
|
1378
|
+
if (state.settings.backend === "cursor") state.cursorSessionId = evt.session_id;
|
|
1379
|
+
else state.lastSessionId = evt.session_id;
|
|
1380
|
+
sessionId = evt.session_id;
|
|
1381
|
+
if (evt.usage) {
|
|
1382
|
+
const u = state.sessionUsage;
|
|
1383
|
+
u.turns += 1;
|
|
1384
|
+
u.inputTokens += evt.usage.input_tokens || 0;
|
|
1385
|
+
u.outputTokens += evt.usage.output_tokens || 0;
|
|
1386
|
+
u.cacheReadTokens += evt.usage.cache_read_input_tokens || 0;
|
|
1387
|
+
u.cacheCreationTokens += evt.usage.cache_creation_input_tokens || 0;
|
|
1388
|
+
u.lastInputTokens = (evt.usage.input_tokens || 0) +
|
|
1389
|
+
(evt.usage.cache_read_input_tokens || 0) +
|
|
1390
|
+
(evt.usage.cache_creation_input_tokens || 0);
|
|
1391
|
+
}
|
|
1392
|
+
if (typeof evt.total_cost_usd === "number") state.sessionUsage.costUsd += evt.total_cost_usd;
|
|
1393
|
+
saveState();
|
|
1394
|
+
}
|
|
1395
|
+
if (evt.type === "result" && evt.result) assistantText = evt.result;
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
proc.stderr.on("data", (d) => { stderrBuffer += d.toString(); });
|
|
1399
|
+
proc.on("close", (code) => chatContext.run(chatId, () => {
|
|
1400
|
+
if (state.runningProcess === proc) state.runningProcess = null;
|
|
1401
|
+
clearTimeout(timeout);
|
|
1402
|
+
if (code !== 0 && code !== null) {
|
|
1403
|
+
reject(new Error(claudeEmptyFailureMessage(code, stderrBuffer)));
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
resolve({ text: redactSensitive(assistantText.trim()), sessionId });
|
|
1407
|
+
}));
|
|
1408
|
+
proc.on("error", (err) => chatContext.run(chatId, () => {
|
|
1409
|
+
if (state.runningProcess === proc) state.runningProcess = null;
|
|
1410
|
+
clearTimeout(timeout);
|
|
1411
|
+
reject(err);
|
|
1412
|
+
}));
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
async function compactActiveSession(cwd, opts = {}) {
|
|
1417
|
+
const state = currentState();
|
|
1418
|
+
const sessionKey = getActiveSessionKey(state);
|
|
1419
|
+
const oldSessionId = state[sessionKey];
|
|
1420
|
+
if (!oldSessionId) return { compacted: false, reason: "No conversation." };
|
|
1421
|
+
if (state.isCompacting) return { compacted: false, reason: "Compaction already in progress." };
|
|
1422
|
+
|
|
1423
|
+
state.isCompacting = true;
|
|
1424
|
+
try {
|
|
1425
|
+
if (opts.notify) await send(opts.message || "Context is getting large, compacting first so this stays fast…");
|
|
1426
|
+
const summaryRun = await runClaudeCapture(compactSummaryPrompt(), cwd, { resumeSessionId: oldSessionId, skipAutoCompact: true });
|
|
1427
|
+
const summary = summaryRun.text || "No prior context was returned by the summarizer.";
|
|
1428
|
+
|
|
1429
|
+
state[sessionKey] = null;
|
|
1430
|
+
resetSessionUsage(state);
|
|
1431
|
+
state.isFirstMessage = true;
|
|
1432
|
+
saveState();
|
|
1433
|
+
|
|
1434
|
+
const seedRun = await runClaudeCapture(compactSeedPrompt(summary), cwd, { fresh: true, skipAutoCompact: true });
|
|
1435
|
+
const newSessionId = seedRun.sessionId || state[sessionKey];
|
|
1436
|
+
if (newSessionId) state[sessionKey] = newSessionId;
|
|
1437
|
+
state.isFirstMessage = false;
|
|
1438
|
+
state.lastCompactedAt = Date.now();
|
|
1439
|
+
resetSessionUsage(state);
|
|
1440
|
+
saveState();
|
|
1441
|
+
|
|
1442
|
+
if (newSessionId && state.currentSession) {
|
|
1443
|
+
const title = `Compacted ${new Date().toLocaleDateString()}`;
|
|
1444
|
+
recordSession(state.chatId, state.currentSession.name, newSessionId, title);
|
|
1445
|
+
}
|
|
1446
|
+
return { compacted: true, oldSessionId, newSessionId, summary };
|
|
1447
|
+
} finally {
|
|
1448
|
+
state.isCompacting = false;
|
|
1449
|
+
saveState();
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1315
1453
|
async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
1316
1454
|
// Capture per-user state at entry so event callbacks (stdout/stderr/close)
|
|
1317
1455
|
// operate on the right user's thread even though AsyncLocalStorage
|
|
@@ -1333,6 +1471,17 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1333
1471
|
return;
|
|
1334
1472
|
}
|
|
1335
1473
|
|
|
1474
|
+
if (shouldAutoCompact(state, opts)) {
|
|
1475
|
+
try {
|
|
1476
|
+
await compactActiveSession(cwd, {
|
|
1477
|
+
notify: true,
|
|
1478
|
+
message: "Context is getting large, compacting first so this stays fast…",
|
|
1479
|
+
});
|
|
1480
|
+
} catch (e) {
|
|
1481
|
+
await send(`Compaction failed: ${redactSensitive(e.message)}\nContinuing in the existing session.`, { replyTo: replyToMsgId });
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1336
1485
|
bot.sendChatAction(chatId, "typing");
|
|
1337
1486
|
state.statusMessageId = null;
|
|
1338
1487
|
state.streamBuffer = "";
|
|
@@ -1582,14 +1731,7 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1582
1731
|
if (voicePath) await sendVoice(voicePath);
|
|
1583
1732
|
}
|
|
1584
1733
|
|
|
1585
|
-
//
|
|
1586
|
-
const ctx = state.sessionUsage.lastInputTokens;
|
|
1587
|
-
if (ctx > 150000 && !state.sessionUsage.warnedHighContext) {
|
|
1588
|
-
state.sessionUsage.warnedHighContext = true;
|
|
1589
|
-
await send(`Heads up: context is ${(ctx / 1000).toFixed(0)}k tokens. Use /compact to summarize or /end for a fresh start — keeps cost down.`);
|
|
1590
|
-
} else if (ctx < 80000 && state.sessionUsage.warnedHighContext) {
|
|
1591
|
-
state.sessionUsage.warnedHighContext = false;
|
|
1592
|
-
}
|
|
1734
|
+
// High-context sessions are compacted automatically before the next turn.
|
|
1593
1735
|
} catch (e) {
|
|
1594
1736
|
console.error("Final message delivery failed:", e.message);
|
|
1595
1737
|
await send("Task completed but failed to deliver the response. Send /continue to see the result.");
|
|
@@ -1966,8 +2108,27 @@ bot.onText(/\/ask$/, wrapHandler((msg) => {
|
|
|
1966
2108
|
settings.permissionMode = a ? null : "ask";
|
|
1967
2109
|
send(a ? "Ask mode off." : "Ask mode on (read-only Q&A, no edits).");
|
|
1968
2110
|
}));
|
|
1969
|
-
bot.onText(/\/compact/, wrapHandler(async (msg) => {
|
|
1970
|
-
|
|
2111
|
+
bot.onText(/\/compact/, wrapHandler(async (msg) => {
|
|
2112
|
+
if (!isAuthorized(msg)) return;
|
|
2113
|
+
if (!requireSession(msg)) return;
|
|
2114
|
+
if (!getActiveSessionId()) return send("No conversation.");
|
|
2115
|
+
try {
|
|
2116
|
+
const result = await compactActiveSession(currentState().currentSession.dir, {
|
|
2117
|
+
notify: true,
|
|
2118
|
+
message: "Compacting this conversation into a fresh session…",
|
|
2119
|
+
});
|
|
2120
|
+
if (result.compacted) await send(`Compacted into a fresh session${result.newSessionId ? ` (${result.newSessionId.slice(0, 8)}…)` : ""}. Continue normally.`, { replyTo: msg.message_id });
|
|
2121
|
+
else await send(result.reason || "Could not compact.", { replyTo: msg.message_id });
|
|
2122
|
+
} catch (e) {
|
|
2123
|
+
await send(`Compaction failed: ${redactSensitive(e.message)}`, { replyTo: msg.message_id });
|
|
2124
|
+
}
|
|
2125
|
+
}));
|
|
2126
|
+
bot.onText(/\/continue$/, wrapHandler(async (msg) => {
|
|
2127
|
+
if (!isAuthorized(msg)) return;
|
|
2128
|
+
if (!requireSession(msg)) return;
|
|
2129
|
+
if (!getActiveSessionId()) return send("No conversation to continue.");
|
|
2130
|
+
await runClaude("continue where we left off", currentState().currentSession.dir, msg.message_id);
|
|
2131
|
+
}));
|
|
1971
2132
|
bot.onText(/\/worktree$/, wrapHandler((msg) => { if (!isAuthorized(msg)) return; const { settings } = currentState(); settings.worktree = !settings.worktree; send(settings.worktree ? "Worktree on." : "Worktree off."); }));
|
|
1972
2133
|
|
|
1973
2134
|
bot.onText(/\/cursor$/, wrapHandler(async (msg) => {
|
|
@@ -2083,7 +2244,7 @@ bot.onText(/\/usage$/, wrapHandler((msg) => {
|
|
|
2083
2244
|
`Cost: $${u.costUsd.toFixed(4)}`,
|
|
2084
2245
|
`Last turn context: ${fmt(u.lastInputTokens)}`,
|
|
2085
2246
|
];
|
|
2086
|
-
if (u.lastInputTokens > 100000) lines.push(
|
|
2247
|
+
if (u.lastInputTokens > 100000) lines.push(`\nTip: context is large. The bot auto-compacts before the next turn at ${fmt(AUTO_COMPACT_TOKENS)} tokens; /compact does it now.`);
|
|
2087
2248
|
send(lines.join("\n"), { parseMode: "Markdown" });
|
|
2088
2249
|
}));
|
|
2089
2250
|
|
|
@@ -2308,7 +2469,7 @@ bot.on("callback_query", wrapHandler(async (q) => {
|
|
|
2308
2469
|
await send(`Session: ${state.currentSession.name}\nNew conversation\n\nSend text, voice, or images.\n\n/sessions — switch conversation\n/session — switch project`);
|
|
2309
2470
|
return;
|
|
2310
2471
|
}
|
|
2311
|
-
if (d === "a:continue") { if (state.currentSession) await runClaude("continue", state.currentSession.dir); else send("No session."); return; }
|
|
2472
|
+
if (d === "a:continue") { if (state.currentSession && getActiveSessionId()) await runClaude("continue", state.currentSession.dir); else send("No session to continue."); return; }
|
|
2312
2473
|
if (d === "a:end") {
|
|
2313
2474
|
if (state.currentSession) {
|
|
2314
2475
|
const n = state.currentSession.name;
|
|
@@ -2563,7 +2724,8 @@ bot.on("message", wrapHandler(async (msg) => {
|
|
|
2563
2724
|
|
|
2564
2725
|
let prompt = msg.text;
|
|
2565
2726
|
const reply = msg.reply_to_message;
|
|
2566
|
-
|
|
2727
|
+
const skipReplyContext = reply?.from?.is_bot && !reply.document && !reply.photo;
|
|
2728
|
+
if (reply && !skipReplyContext) {
|
|
2567
2729
|
let ctx = "";
|
|
2568
2730
|
if (reply.text) {
|
|
2569
2731
|
ctx = reply.text;
|