@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 CHANGED
@@ -3,6 +3,8 @@ TELEGRAM_CHAT_ID=your-chat-id
3
3
  WORKSPACE=/path/to/your/workspace
4
4
  CLAUDE_PATH=/path/to/claude
5
5
  CURSOR_PATH=
6
+ CODEX_PATH=
7
+ AUTO_COMPACT_TOKENS=140000
6
8
  WHISPER_CLI=
7
9
  WHISPER_MODEL=
8
10
  FFMPEG=
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
- ## Current Context
681
- - Date/time: ${now}
682
- - Platform: Telegram (mobile app)
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 (" + vaultKeys.length + " keys)" : "locked"}
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
- ### ${path.join(BOT_DIR, "bot.js")}
702
- The bot code. Read to understand capabilities. Only modify if explicitly asked.
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
- ### ${path.join(BOT_DIR, ".env")}
705
- Configuration (Telegram token, paths, etc). Sensitive don't expose values.
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 via the Telegram API curl above don't paste walls of text.
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's saved in ${FILES_DIR}. Read it with the Read tool.
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. Don't tell them to use /vault manually — handle it for them.
728
- - When asked to change your personality, edit ${SOUL_FILE}.
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.continueSession) args.push("--continue");
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.continueSession) args.push("--continue");
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
- // Suggest /compact when context gets large (once per threshold crossing)
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) => { if (!isAuthorized(msg)) return; if (!requireSession(msg)) return; if (!getActiveSessionId()) return send("No conversation."); await runClaude("Summarize: key decisions, code state, next steps.", currentState().currentSession.dir, msg.message_id); }));
1970
- bot.onText(/\/continue$/, wrapHandler(async (msg) => { if (!isAuthorized(msg)) return; if (!requireSession(msg)) return; await runClaude("continue where we left off", currentState().currentSession.dir, msg.message_id, { continueSession: true }); }));
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("\nTip: context is large. Use /compact to summarize, or /end for a clean slate.");
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
- if (reply) {
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "description": "Your always-on AI coding assistant — Claude Code, Cursor Agent, and OpenAI Codex via Telegram",
5
5
  "main": "bot.js",
6
6
  "bin": {