@inetafrica/open-claudia 2.2.15 → 2.2.17
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 +7 -0
- package/channels/telegram/adapter.js +5 -2
- package/channels/telegram/format.js +98 -5
- package/core/actions.js +13 -0
- package/core/handlers.js +65 -5
- package/core/relay.js +1 -1
- package/core/runner.js +17 -8
- package/core/state.js +1 -1
- package/core/system-prompt.js +13 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.2.17
|
|
4
|
+
- Telegram output now uses `parse_mode: "HTML"` instead of legacy Markdown. The Telegram adapter normalizes both model-authored Telegram HTML and ordinary Markdown/CommonMark into Telegram's safe HTML subset before sending, so `<b>...</b>`, `**bold**`, backticks, links, headings, code blocks, strikethrough, and spoilers render cleanly instead of leaking literal markup or being rejected by Telegram. Messages still fall back to plain text if Telegram rejects the markup.
|
|
5
|
+
- Updated the Telegram system prompt to ask agents for short mobile-readable Telegram HTML (`<b>`, `<code>`, `<pre>`, `<a>`) with bullet-style layout. Relay sends and slash-command responses now use the same HTML parse mode as normal assistant replies.
|
|
6
|
+
|
|
7
|
+
## v2.2.16
|
|
8
|
+
- 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.
|
|
9
|
+
|
|
3
10
|
## v2.2.15
|
|
4
11
|
- 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
12
|
- 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.
|
|
@@ -10,6 +10,7 @@ const TelegramBot = require("node-telegram-bot-api");
|
|
|
10
10
|
const { TEMP_DIR, FILES_DIR, CONFIG_DIR } = require("../../core/config");
|
|
11
11
|
const { canonicalForChannel } = require("../../core/identity");
|
|
12
12
|
const { portableToInlineKeyboard } = require("../types");
|
|
13
|
+
const { telegramHtml } = require("./format");
|
|
13
14
|
|
|
14
15
|
class TelegramAdapter {
|
|
15
16
|
constructor({ id = "telegram", token, ownerChatId, chatIds }) {
|
|
@@ -204,6 +205,7 @@ class TelegramAdapter {
|
|
|
204
205
|
|
|
205
206
|
async send(channelId, text, opts = {}) {
|
|
206
207
|
const o = {};
|
|
208
|
+
const body = opts.parseMode === "HTML" ? telegramHtml(text) : text;
|
|
207
209
|
if (opts.parseMode) o.parse_mode = opts.parseMode;
|
|
208
210
|
const kb = this._normalizeKeyboard(opts.keyboard);
|
|
209
211
|
if (kb) o.reply_markup = kb;
|
|
@@ -211,7 +213,7 @@ class TelegramAdapter {
|
|
|
211
213
|
|
|
212
214
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
213
215
|
try {
|
|
214
|
-
const msg = await this.bot.sendMessage(channelId,
|
|
216
|
+
const msg = await this.bot.sendMessage(channelId, body, o);
|
|
215
217
|
return msg.message_id;
|
|
216
218
|
} catch (e) {
|
|
217
219
|
const errMsg = e.message || "";
|
|
@@ -239,13 +241,14 @@ class TelegramAdapter {
|
|
|
239
241
|
}
|
|
240
242
|
|
|
241
243
|
async edit(channelId, messageId, text, opts = {}) {
|
|
244
|
+
const body = opts.parseMode === "HTML" ? telegramHtml(text) : text;
|
|
242
245
|
const o = { chat_id: channelId, message_id: messageId };
|
|
243
246
|
if (opts.parseMode) o.parse_mode = opts.parseMode;
|
|
244
247
|
const kb = this._normalizeKeyboard(opts.keyboard);
|
|
245
248
|
if (kb) o.reply_markup = kb;
|
|
246
249
|
|
|
247
250
|
try {
|
|
248
|
-
await this.bot.editMessageText(
|
|
251
|
+
await this.bot.editMessageText(body, o);
|
|
249
252
|
} catch (e) {
|
|
250
253
|
const errMsg = e.message || "";
|
|
251
254
|
if (errMsg.includes("retry after")) return;
|
|
@@ -1,7 +1,18 @@
|
|
|
1
|
-
// Telegram
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Telegram formatting helpers.
|
|
2
|
+
//
|
|
3
|
+
// Telegram's legacy Markdown parser is too fragile for LLM output:
|
|
4
|
+
// CommonMark `**bold**`, underscores in identifiers, unescaped dots in
|
|
5
|
+
// MarkdownV2, and angle brackets in logs all commonly make Telegram reject
|
|
6
|
+
// the message or render markup literally. The bot now sends Telegram HTML and
|
|
7
|
+
// normalizes both model-authored Telegram HTML and ordinary Markdown into the
|
|
8
|
+
// safe subset Telegram accepts.
|
|
9
|
+
|
|
10
|
+
function escapeHtml(text) {
|
|
11
|
+
return String(text || "")
|
|
12
|
+
.replace(/&/g, "&")
|
|
13
|
+
.replace(/</g, "<")
|
|
14
|
+
.replace(/>/g, ">");
|
|
15
|
+
}
|
|
5
16
|
|
|
6
17
|
function stripMarkdown(text) {
|
|
7
18
|
return String(text || "")
|
|
@@ -9,4 +20,86 @@ function stripMarkdown(text) {
|
|
|
9
20
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
10
21
|
}
|
|
11
22
|
|
|
12
|
-
|
|
23
|
+
function stashCode(text) {
|
|
24
|
+
const blocks = [];
|
|
25
|
+
const stash = (html) => {
|
|
26
|
+
const token = `\u0000OCTGCODE${blocks.length}\u0000`;
|
|
27
|
+
blocks.push(html);
|
|
28
|
+
return token;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let out = String(text || "").replace(/```(?:[^\n`]*)\n?([\s\S]*?)```/g, (_, code) => {
|
|
32
|
+
return stash(`<pre>${escapeHtml(code.trimEnd())}</pre>`);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
out = out.replace(/`([^`\n]+)`/g, (_, code) => {
|
|
36
|
+
return stash(`<code>${escapeHtml(code)}</code>`);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return { text: out, blocks };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function restoreAllowedHtmlTags(text) {
|
|
43
|
+
let out = text;
|
|
44
|
+
|
|
45
|
+
// Allow model-authored Telegram HTML tags after escaping the rest of the text.
|
|
46
|
+
for (const tag of ["b", "strong", "i", "em", "u", "s", "strike", "del", "code", "pre", "blockquote"]) {
|
|
47
|
+
const open = new RegExp(`<${tag}>`, "gi");
|
|
48
|
+
const close = new RegExp(`</${tag}>`, "gi");
|
|
49
|
+
out = out.replace(open, `<${tag}>`).replace(close, `</${tag}>`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Telegram spoiler HTML.
|
|
53
|
+
out = out
|
|
54
|
+
.replace(/<span class="tg-spoiler">/gi, '<span class="tg-spoiler">')
|
|
55
|
+
.replace(/<\/span>/gi, "</span>");
|
|
56
|
+
|
|
57
|
+
// Safe links only. Quotes are not escaped by escapeHtml(), but href content
|
|
58
|
+
// is still constrained to http(s)/mailto/tg schemes to avoid broken markup.
|
|
59
|
+
out = out.replace(/<a href="([^"<>]+)">/gi, (_, href) => {
|
|
60
|
+
if (!/^(https?:|mailto:|tg:)/i.test(href)) return "";
|
|
61
|
+
return `<a href="${href}">`;
|
|
62
|
+
}).replace(/<\/a>/gi, "</a>");
|
|
63
|
+
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function convertMarkdownToHtml(text) {
|
|
68
|
+
let out = text;
|
|
69
|
+
|
|
70
|
+
// Markdown links: [label](https://example.com)
|
|
71
|
+
out = out.replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/g, (_, label, url) => {
|
|
72
|
+
return `<a href="${url}">${label}</a>`;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Headings become compact bold labels instead of literal # noise.
|
|
76
|
+
out = out.replace(/^\s{0,3}#{1,6}\s+(.+)$/gm, (_, title) => `<b>${title}</b>`);
|
|
77
|
+
|
|
78
|
+
// Bold. Handle CommonMark first, then Telegram Markdown v1 style.
|
|
79
|
+
out = out.replace(/\*\*([^*\n][\s\S]*?[^*\n])\*\*/g, "<b>$1</b>");
|
|
80
|
+
out = out.replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, "<b>$1</b>");
|
|
81
|
+
|
|
82
|
+
// Italic. Keep conservative to avoid mangling snake_case identifiers.
|
|
83
|
+
out = out.replace(/(?<!_)_([^_\n]+)_(?!_)/g, "<i>$1</i>");
|
|
84
|
+
|
|
85
|
+
// Strikethrough and Telegram spoiler syntax.
|
|
86
|
+
out = out.replace(/~~([^~\n]+)~~/g, "<s>$1</s>");
|
|
87
|
+
out = out.replace(/\|\|([^|\n]+)\|\|/g, '<span class="tg-spoiler">$1</span>');
|
|
88
|
+
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function telegramHtml(text) {
|
|
93
|
+
const { text: withoutCode, blocks } = stashCode(text);
|
|
94
|
+
let out = escapeHtml(withoutCode);
|
|
95
|
+
out = restoreAllowedHtmlTags(out);
|
|
96
|
+
out = convertMarkdownToHtml(out);
|
|
97
|
+
|
|
98
|
+
blocks.forEach((html, i) => {
|
|
99
|
+
out = out.replace(`\u0000OCTGCODE${i}\u0000`, html);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { escapeHtml, stripMarkdown, telegramHtml };
|
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/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) => {
|
|
@@ -743,7 +795,7 @@ register({
|
|
|
743
795
|
handler: async (env) => {
|
|
744
796
|
if (!authorized(env)) return;
|
|
745
797
|
await send("Bot mode: *direct* (default)\n\nSwitch to agent mode for non-blocking execution.\nIn agent mode, heavy tasks run in the background and you can keep chatting.", {
|
|
746
|
-
parseMode: "
|
|
798
|
+
parseMode: "HTML",
|
|
747
799
|
keyboard: { inline_keyboard: [[{ text: "Switch to Agent Mode", callback_data: "mode:agent" }]] },
|
|
748
800
|
});
|
|
749
801
|
},
|
|
@@ -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,8 +878,15 @@ register({
|
|
|
825
878
|
`Cost: $${u.costUsd.toFixed(4)}`,
|
|
826
879
|
`Last turn context: ${fmt(u.lastInputTokens)}`,
|
|
827
880
|
];
|
|
828
|
-
if (u.lastInputTokens > 200000)
|
|
829
|
-
|
|
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
|
+
}
|
|
889
|
+
send(lines.join("\n"), { parseMode: "HTML" });
|
|
830
890
|
},
|
|
831
891
|
});
|
|
832
892
|
|
package/core/relay.js
CHANGED
|
@@ -35,7 +35,7 @@ async function send({ text, target, from = null, kind = "relay" }) {
|
|
|
35
35
|
let messageId = null;
|
|
36
36
|
let errorMsg = null;
|
|
37
37
|
try {
|
|
38
|
-
const sendOpts = adapter.type === "telegram" ? { parseMode: "
|
|
38
|
+
const sendOpts = adapter.type === "telegram" ? { parseMode: "HTML" } : {};
|
|
39
39
|
const result = await adapter.send(resolved.channelId, String(text), sendOpts);
|
|
40
40
|
if (typeof result === "object" && result && "messageId" in result) { messageId = result.messageId; ok = !!messageId; }
|
|
41
41
|
else { messageId = result; ok = !!result; }
|
package/core/runner.js
CHANGED
|
@@ -22,9 +22,9 @@ const {
|
|
|
22
22
|
const { getClaudeOAuthToken, claudeAuthRecoveryMessage, isClaudeAuthErrorText, claudeUsageLimitMessage, isClaudeUsageLimitText, runClaudeAuthStatusDiagnostic, claudeSubprocessEnv } = require("./auth-flow");
|
|
23
23
|
const loopback = require("./loopback");
|
|
24
24
|
|
|
25
|
-
function
|
|
25
|
+
function telegramHtmlOpts(extra = {}) {
|
|
26
26
|
const adapter = currentAdapter();
|
|
27
|
-
return adapter?.type === "telegram" ? { ...extra, parseMode: "
|
|
27
|
+
return adapter?.type === "telegram" ? { ...extra, parseMode: "HTML" } : extra;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function chatEnvOverlay() {
|
|
@@ -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;
|
|
@@ -468,9 +476,9 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
468
476
|
const display = formatProgress(assistantText, toolUses, currentTool, elapsed, currentToolDetail);
|
|
469
477
|
if (display && display !== lastUpdate) {
|
|
470
478
|
if (!state.statusMessageId && assistantText) {
|
|
471
|
-
state.statusMessageId = await send(display.length > 4000 ? display.slice(-4000) : display,
|
|
479
|
+
state.statusMessageId = await send(display.length > 4000 ? display.slice(-4000) : display, telegramHtmlOpts({ replyTo: replyToMsgId }));
|
|
472
480
|
} else if (state.statusMessageId) {
|
|
473
|
-
await editMessage(state.statusMessageId, display.length > 4000 ? display.slice(-4000) : display,
|
|
481
|
+
await editMessage(state.statusMessageId, display.length > 4000 ? display.slice(-4000) : display, telegramHtmlOpts());
|
|
474
482
|
}
|
|
475
483
|
lastUpdate = display;
|
|
476
484
|
}
|
|
@@ -657,12 +665,12 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
657
665
|
const firstChunk = chunks[0];
|
|
658
666
|
|
|
659
667
|
if (state.statusMessageId && chunks.length === 1) {
|
|
660
|
-
await editMessage(state.statusMessageId, firstChunk,
|
|
668
|
+
await editMessage(state.statusMessageId, firstChunk, telegramHtmlOpts());
|
|
661
669
|
} else {
|
|
662
|
-
const sent = await send(firstChunk,
|
|
670
|
+
const sent = await send(firstChunk, telegramHtmlOpts({ replyTo: replyToMsgId }));
|
|
663
671
|
if (!sent) await send(firstChunk);
|
|
664
672
|
for (let i = 1; i < chunks.length; i++) {
|
|
665
|
-
await send(chunks[i],
|
|
673
|
+
await send(chunks[i], telegramHtmlOpts());
|
|
666
674
|
}
|
|
667
675
|
}
|
|
668
676
|
if (code !== 0 && code !== null) await send(`Exit code: ${code}`);
|
|
@@ -757,6 +765,7 @@ module.exports = {
|
|
|
757
765
|
getActiveBinary,
|
|
758
766
|
getActiveSessionKey,
|
|
759
767
|
shouldAutoCompact,
|
|
768
|
+
effectiveCompactThreshold,
|
|
760
769
|
formatProgress,
|
|
761
770
|
preflightClaudeAuthMessage,
|
|
762
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/core/system-prompt.js
CHANGED
|
@@ -78,28 +78,28 @@ function buildSystemPrompt() {
|
|
|
78
78
|
## Telegram formatting
|
|
79
79
|
Telegram is mobile-first and narrow. Optimize for readability in a small chat bubble.
|
|
80
80
|
|
|
81
|
-
Use Telegram
|
|
82
|
-
- Bold labels with
|
|
83
|
-
- Italic with
|
|
84
|
-
- Inline code with
|
|
85
|
-
-
|
|
81
|
+
Use Telegram HTML, not raw Markdown:
|
|
82
|
+
- Bold labels with <b>Status:</b> / <b>Done:</b> / <b>Blocked:</b> / <b>Next:</b>
|
|
83
|
+
- Italic with <i>...</i> only when genuinely useful.
|
|
84
|
+
- Inline code with <code>...</code> only for commands, file paths, IDs, short field names, and exact errors.
|
|
85
|
+
- Code blocks with <pre>...</pre> only for short commands or snippets. Do not paste long logs; save/send a file instead.
|
|
86
|
+
- Links may use <a href="https://example.com">label</a>, or just paste the URL.
|
|
86
87
|
|
|
87
88
|
Avoid formatting that renders badly or noisily in Telegram:
|
|
88
89
|
- No Markdown tables.
|
|
89
90
|
- No Markdown headings like #, ##, ###.
|
|
90
|
-
- No
|
|
91
|
-
-
|
|
92
|
-
- Do not wrap ordinary business words in backticks or quote marks. For example, write Payments completed, not \`Payments\` completed or 'Payments' completed.
|
|
91
|
+
- No raw **bold** / *bold* as your preferred output, even though the bot has a fallback converter.
|
|
92
|
+
- Do not wrap ordinary business words in backticks, <code>, or quote marks. For example, write Payments completed, not <code>Payments</code> completed or 'Payments' completed.
|
|
93
93
|
- Avoid large paragraphs. Use short sections, blank lines, and 3-7 concise bullets.
|
|
94
94
|
|
|
95
95
|
Good Telegram pattern:
|
|
96
|
-
|
|
96
|
+
<b>Status:</b> CRM sync is still running cleanly.
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
• Bills and Calls completed.
|
|
99
|
+
• Leads completed with 20 rows.
|
|
100
|
+
• Payments advanced to <code>2026-05-17 10:50</code>.
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
<b>Issue:</b> JSON parse error after Retention. I’m checking that stream next.
|
|
103
103
|
`
|
|
104
104
|
: adapter?.type === "kazee"
|
|
105
105
|
? `
|
package/package.json
CHANGED