polygram 0.12.0-rc.11 → 0.12.0-rc.13
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/config.example.json +3 -1
- package/lib/compaction-warn.js +59 -0
- package/lib/context-usage.js +93 -0
- package/lib/handlers/dispatcher.js +23 -0
- package/lib/process/cli-process.js +73 -0
- package/lib/process/hook-event-tail.js +7 -0
- package/lib/process/hook-settings.js +7 -0
- package/lib/process-manager.js +6 -0
- package/lib/sdk/callbacks.js +36 -0
- package/package.json +1 -1
package/config.example.json
CHANGED
|
@@ -72,7 +72,9 @@
|
|
|
72
72
|
"effort": "medium",
|
|
73
73
|
"cwd": "/Users/you/admin-agent",
|
|
74
74
|
"timeout": 600,
|
|
75
|
-
"_comment_maxFileBytes": "OPTIONAL per-chat (or per-topic; topic wins) file-size cap in BYTES. There is NO fixed default — the default is backend-derived: cloud Telegram = 50MB send / 20MB receive; with a local Bot API server (bot.apiRoot set) = 2GB both ways. This key only LOWERS that ceiling for this chat (Telegram rejects anything above the backend limit regardless); omit it to use the full backend default. To set one, add e.g. \"maxFileBytes\": 104857600 (=100MB) — only meaningful when apiRoot is set, since cloud already clamps to 50/20MB."
|
|
75
|
+
"_comment_maxFileBytes": "OPTIONAL per-chat (or per-topic; topic wins) file-size cap in BYTES. There is NO fixed default — the default is backend-derived: cloud Telegram = 50MB send / 20MB receive; with a local Bot API server (bot.apiRoot set) = 2GB both ways. This key only LOWERS that ceiling for this chat (Telegram rejects anything above the backend limit regardless); omit it to use the full backend default. To set one, add e.g. \"maxFileBytes\": 104857600 (=100MB) — only meaningful when apiRoot is set, since cloud already clamps to 50/20MB.",
|
|
76
|
+
"_comment_compactionWarnings": "OPTIONAL per-chat (or per-topic; topic wins). CLI/channels backend (pm:'cli') only. Default OFF. When ON, polygram warns the chat as Claude's context fills so you can /compact on your terms BEFORE an auto-compaction interrupts a turn (auto-compaction detaches the channels MCP bridge mid-turn — see docs/0.12.0-compaction-warnings.md). Two forms: `true` (enable at the 75% default threshold) or `{ \"enabled\": true, \"thresholdPct\": 80 }` (custom 1-99 threshold). Proactive: at the threshold it posts 'context ~N% full, run /compact or /new at a break'. Reactive backstop: when claude auto-compacts anyway it posts 'compacting now, resend if quiet'. Manual /compact never warns. Requires the bot to allow /compact + /new commands.",
|
|
77
|
+
"compactionWarnings": true
|
|
76
78
|
},
|
|
77
79
|
|
|
78
80
|
"-1000000000001": {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compaction-warn — per-chat config resolution + warn-once state for the
|
|
3
|
+
* compaction warning (0.12.0-rc.13).
|
|
4
|
+
*
|
|
5
|
+
* The warning is OFF by default. A chat (or topic) opts in via
|
|
6
|
+
* `compactionWarnings`:
|
|
7
|
+
* true → enabled, default threshold
|
|
8
|
+
* { enabled: true, thresholdPct: 80 } → enabled, custom threshold
|
|
9
|
+
* false / absent / object w/o enabled → off
|
|
10
|
+
*
|
|
11
|
+
* `thresholdPct` is the context-fill % at which the PROACTIVE warning fires
|
|
12
|
+
* (propose /compact before claude auto-compacts mid-turn). Default 75 — below
|
|
13
|
+
* claude's own auto-compact threshold so the user gets a window to act.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const DEFAULT_THRESHOLD_PCT = 75;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {object|undefined} cfg resolved topic/chat config (getTopicConfig result)
|
|
22
|
+
* @returns {{enabled: boolean, thresholdPct: number}}
|
|
23
|
+
*/
|
|
24
|
+
function resolveCompactionWarnConfig(cfg) {
|
|
25
|
+
const raw = cfg?.compactionWarnings;
|
|
26
|
+
const off = { enabled: false, thresholdPct: DEFAULT_THRESHOLD_PCT };
|
|
27
|
+
|
|
28
|
+
if (raw === true) return { enabled: true, thresholdPct: DEFAULT_THRESHOLD_PCT };
|
|
29
|
+
if (raw && typeof raw === 'object' && raw.enabled === true) {
|
|
30
|
+
const t = Number(raw.thresholdPct);
|
|
31
|
+
const thresholdPct = (Number.isFinite(t) && t > 0 && t < 100) ? t : DEFAULT_THRESHOLD_PCT;
|
|
32
|
+
return { enabled: true, thresholdPct };
|
|
33
|
+
}
|
|
34
|
+
return off;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Per-session "have we already warned on this climb?" state. Warn ONCE per
|
|
39
|
+
* session until reset — without this the proactive warning would re-fire on
|
|
40
|
+
* every turn-end while the context stays high. Reset on a successful
|
|
41
|
+
* compaction (PostCompact → context dropped) or a fresh session so the next
|
|
42
|
+
* climb can warn again. Mirrors the autoResumeTracker shape.
|
|
43
|
+
*/
|
|
44
|
+
function createCompactionWarnTracker() {
|
|
45
|
+
const warned = new Set();
|
|
46
|
+
return {
|
|
47
|
+
shouldWarn(sessionKey) { return !warned.has(sessionKey); },
|
|
48
|
+
markWarned(sessionKey) { warned.add(sessionKey); },
|
|
49
|
+
reset(sessionKey) { warned.delete(sessionKey); },
|
|
50
|
+
resetAll() { warned.clear(); },
|
|
51
|
+
_size() { return warned.size; },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
resolveCompactionWarnConfig,
|
|
57
|
+
createCompactionWarnTracker,
|
|
58
|
+
DEFAULT_THRESHOLD_PCT,
|
|
59
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-usage — read live context occupancy from a Claude Code session
|
|
3
|
+
* transcript (JSONL).
|
|
4
|
+
*
|
|
5
|
+
* Used by the per-chat compaction warning (0.12.0-rc.13). polygram has no
|
|
6
|
+
* usage payload on the channels/CLI backend (hook events carry none — see
|
|
7
|
+
* the rc.13 spike), so the only source of "how full is the context" is the
|
|
8
|
+
* transcript itself. We read it ONCE per turn-end (Stop hook), not on a
|
|
9
|
+
* poll loop, so a single streamed pass is fine.
|
|
10
|
+
*
|
|
11
|
+
* What "occupancy" means: Claude's own context-% / auto-compact threshold is
|
|
12
|
+
* measured against what's fed INTO the model each turn —
|
|
13
|
+
* input_tokens + cache_read_input_tokens + cache_creation_input_tokens
|
|
14
|
+
* (cache_read dominates once the conversation is warm). output_tokens is the
|
|
15
|
+
* reply, not context, so it's excluded.
|
|
16
|
+
*
|
|
17
|
+
* We take the LAST main-thread (non-sidechain) assistant frame with a usage
|
|
18
|
+
* block. Subagents write to their own agent_transcript_path so sidechain
|
|
19
|
+
* frames don't normally appear here, but we skip them defensively: a format
|
|
20
|
+
* change that inlined a subagent's large usage would otherwise spike the
|
|
21
|
+
* parent's apparent context and trigger a false "you're full" warning.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const fs = require('node:fs');
|
|
27
|
+
const readline = require('node:readline');
|
|
28
|
+
|
|
29
|
+
// Standard Claude context window (sonnet/opus, non-beta). The warning is a
|
|
30
|
+
// heuristic ("you're getting full"), so an approximate denominator is fine;
|
|
31
|
+
// callers can pass a different window for 1M-beta sessions.
|
|
32
|
+
const DEFAULT_WINDOW_TOKENS = 200_000;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} transcriptPath
|
|
36
|
+
* @returns {Promise<{inputTokens:number, cacheReadTokens:number, cacheCreationTokens:number, total:number} | null>}
|
|
37
|
+
* null when the path is falsy/unreadable or no usable usage frame exists.
|
|
38
|
+
*/
|
|
39
|
+
async function readContextTokens(transcriptPath) {
|
|
40
|
+
if (!transcriptPath) return null;
|
|
41
|
+
|
|
42
|
+
let stream;
|
|
43
|
+
try {
|
|
44
|
+
stream = fs.createReadStream(transcriptPath, { encoding: 'utf8' });
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
let last = null;
|
|
51
|
+
// Resolve only once — error and close can both fire.
|
|
52
|
+
let done = false;
|
|
53
|
+
const finish = (v) => { if (!done) { done = true; resolve(v); } };
|
|
54
|
+
|
|
55
|
+
stream.on('error', () => finish(null));
|
|
56
|
+
|
|
57
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
58
|
+
// readline forwards the input stream's 'error' (e.g. ENOENT on open) to
|
|
59
|
+
// the interface; without this handler that re-emit is unhandled and
|
|
60
|
+
// crashes the process even though we resolved null on the stream error.
|
|
61
|
+
rl.on('error', () => finish(null));
|
|
62
|
+
rl.on('line', (line) => {
|
|
63
|
+
if (!line) return;
|
|
64
|
+
let o;
|
|
65
|
+
try { o = JSON.parse(line); } catch { return; } // skip partial/non-JSON lines
|
|
66
|
+
if (!o || o.type !== 'assistant' || o.isSidechain === true) return;
|
|
67
|
+
const u = o.message?.usage;
|
|
68
|
+
if (!u) return;
|
|
69
|
+
const inputTokens = Number(u.input_tokens) || 0;
|
|
70
|
+
const cacheReadTokens = Number(u.cache_read_input_tokens) || 0;
|
|
71
|
+
const cacheCreationTokens = Number(u.cache_creation_input_tokens) || 0;
|
|
72
|
+
const total = inputTokens + cacheReadTokens + cacheCreationTokens;
|
|
73
|
+
if (total > 0) last = { inputTokens, cacheReadTokens, cacheCreationTokens, total };
|
|
74
|
+
});
|
|
75
|
+
rl.on('close', () => finish(last));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Fraction (0..1) of the context window currently occupied. Clamps to 0 on
|
|
81
|
+
* non-positive / non-finite inputs so callers never see NaN/Infinity.
|
|
82
|
+
*
|
|
83
|
+
* @param {number} totalTokens
|
|
84
|
+
* @param {number} [windowTokens=DEFAULT_WINDOW_TOKENS]
|
|
85
|
+
* @returns {number}
|
|
86
|
+
*/
|
|
87
|
+
function contextPct(totalTokens, windowTokens = DEFAULT_WINDOW_TOKENS) {
|
|
88
|
+
if (!Number.isFinite(totalTokens) || totalTokens <= 0) return 0;
|
|
89
|
+
if (!Number.isFinite(windowTokens) || windowTokens <= 0) return 0;
|
|
90
|
+
return totalTokens / windowTokens;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { readContextTokens, contextPct, DEFAULT_WINDOW_TOKENS };
|
|
@@ -224,6 +224,29 @@ function createDispatcher({
|
|
|
224
224
|
chat_id: chatId, session_key: sessionKey, msg_id: msg.message_id,
|
|
225
225
|
error: resumeErr?.message?.slice(0, 200),
|
|
226
226
|
});
|
|
227
|
+
// Music topic incident (2026-06-01): a channels session whose
|
|
228
|
+
// context grew large enough to auto-/compact on resume loses its
|
|
229
|
+
// MCP bridge binding on EVERY resume ("no MCP server configured"),
|
|
230
|
+
// so the resumed turn re-detaches (BRIDGE_DISCONNECTED) and lands
|
|
231
|
+
// here. The persisted claude_session_id is then poisoned — every
|
|
232
|
+
// future message (manual resend OR post-cooldown auto-resume)
|
|
233
|
+
// re-resumes it and re-detaches, an endless "🔌 please resend"
|
|
234
|
+
// loop. Break it: drop the session row so the NEXT message spawns
|
|
235
|
+
// a FRESH session (no --resume). Gated on the ORIGINAL error being
|
|
236
|
+
// a bridge-detach AND auto-resume having failed — a one-off bridge
|
|
237
|
+
// crash that resumes cleanly takes the .then() path above and
|
|
238
|
+
// keeps its context; only a session that re-detaches on resume is
|
|
239
|
+
// treated as poison. We lose the poisoned conversation's history,
|
|
240
|
+
// but that session can't complete a turn anyway.
|
|
241
|
+
if (err.code === 'BRIDGE_DISCONNECTED' && typeof db.clearSessionId === 'function') {
|
|
242
|
+
dbWrite(
|
|
243
|
+
() => db.clearSessionId(sessionKey),
|
|
244
|
+
'clearSessionId: poisoned by bridge-detach on resume',
|
|
245
|
+
);
|
|
246
|
+
logEvent('session-reset-after-bridge-detach', {
|
|
247
|
+
chat_id: chatId, session_key: sessionKey, msg_id: msg.message_id,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
227
250
|
const fallbackText = errorReplyText(err);
|
|
228
251
|
if (fallbackText) {
|
|
229
252
|
tg(bot, 'sendMessage', {
|
|
@@ -53,6 +53,8 @@ const { createHookTail } = require('./hook-event-tail');
|
|
|
53
53
|
// /private/tmp drift — one of the original Music-topic failures).
|
|
54
54
|
const { DEFAULT_ATTACHMENT_BASE } = require('./channels-tool-dispatcher');
|
|
55
55
|
const { resolveFileCaps } = require('../attachments');
|
|
56
|
+
const { resolveCompactionWarnConfig } = require('../compaction-warn');
|
|
57
|
+
const { readContextTokens, contextPct } = require('../context-usage');
|
|
56
58
|
const { runStartupGate } = require('../tmux/startup-gate');
|
|
57
59
|
const { POLYGRAM_DISPLAY_HINT } = require('../telegram/display-hint');
|
|
58
60
|
|
|
@@ -520,6 +522,14 @@ class CliProcess extends Process {
|
|
|
520
522
|
override: _capOverride,
|
|
521
523
|
}).outBytes;
|
|
522
524
|
|
|
525
|
+
// 0.12.0-rc.13: per-chat/topic compaction warning (default OFF). Same
|
|
526
|
+
// topic→chat precedence as the file cap above. When enabled, the channels
|
|
527
|
+
// backend warns the chat as context fills (propose /compact at a break)
|
|
528
|
+
// and on auto-compaction (the event that detaches the bridge mid-turn).
|
|
529
|
+
const _compactionWarnRaw = topicConfig?.compactionWarnings ?? opts.chatConfig?.compactionWarnings;
|
|
530
|
+
this.compactionWarn = resolveCompactionWarnConfig({ compactionWarnings: _compactionWarnRaw });
|
|
531
|
+
this._compactionWarned = false; // proactive warn-once per climb; reset on PostCompact
|
|
532
|
+
|
|
523
533
|
// Parity audit P8 + rc.8 fs-guard (2026-05-26 shumorobot Music topic):
|
|
524
534
|
// `--session-id <id>` creates a NEW claude session with that id;
|
|
525
535
|
// `--resume <id>` resumes the EXISTING conversation. Lazy-respawn after
|
|
@@ -1738,6 +1748,38 @@ class CliProcess extends Process {
|
|
|
1738
1748
|
lastAssistantMessage: ev.lastAssistantMessage,
|
|
1739
1749
|
backend: this.backend,
|
|
1740
1750
|
});
|
|
1751
|
+
// 0.12.0-rc.13 proactive compaction warning: on turn-end, if enabled
|
|
1752
|
+
// for this chat and not already warned this climb, sample context
|
|
1753
|
+
// occupancy from the transcript and warn (propose /compact) BEFORE
|
|
1754
|
+
// claude auto-compacts mid-turn and detaches the bridge. Fire-and-
|
|
1755
|
+
// forget — transcript IO must never block the stop path.
|
|
1756
|
+
if (this.compactionWarn?.enabled && !this._compactionWarned && ev.transcriptPath) {
|
|
1757
|
+
this._maybeProactiveCompactionWarn(ev.transcriptPath);
|
|
1758
|
+
}
|
|
1759
|
+
return;
|
|
1760
|
+
|
|
1761
|
+
case 'PreCompact':
|
|
1762
|
+
// 0.12.0-rc.13: auto-compaction is the event that detaches the
|
|
1763
|
+
// channels MCP bridge mid-turn. Record it; and on the dangerous AUTO
|
|
1764
|
+
// case (manual /compact is the user's own deliberate action — never
|
|
1765
|
+
// nag), emit a reactive warning the chat layer posts. The proactive
|
|
1766
|
+
// warning (on Stop) tries to PREVENT this; this is the backstop.
|
|
1767
|
+
this._logEvent('cli-compaction-imminent', { trigger: ev.trigger });
|
|
1768
|
+
if (this.compactionWarn?.enabled && ev.trigger === 'auto') {
|
|
1769
|
+
this.emit('compaction-warn', {
|
|
1770
|
+
kind: 'reactive',
|
|
1771
|
+
trigger: 'auto',
|
|
1772
|
+
sessionId: this.claudeSessionId,
|
|
1773
|
+
backend: this.backend,
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
return;
|
|
1777
|
+
|
|
1778
|
+
case 'PostCompact':
|
|
1779
|
+
// Context just dropped — re-arm the proactive warn-once so the next
|
|
1780
|
+
// climb can warn again.
|
|
1781
|
+
this._compactionWarned = false;
|
|
1782
|
+
this._logEvent('cli-compaction-done', { trigger: ev.trigger });
|
|
1741
1783
|
return;
|
|
1742
1784
|
|
|
1743
1785
|
case 'Notification':
|
|
@@ -2320,6 +2362,37 @@ class CliProcess extends Process {
|
|
|
2320
2362
|
* Extracted as a separate async method so unit tests can drive it
|
|
2321
2363
|
* directly without waiting for the setInterval tick.
|
|
2322
2364
|
*/
|
|
2365
|
+
/**
|
|
2366
|
+
* 0.12.0-rc.13: proactive compaction warning. Read the transcript's current
|
|
2367
|
+
* context occupancy and, if past the per-chat threshold, emit a
|
|
2368
|
+
* 'compaction-warn' the chat layer turns into "you're ~N% full, run
|
|
2369
|
+
* /compact" — giving the user a window to compact on their terms BEFORE
|
|
2370
|
+
* claude auto-compacts mid-turn (which detaches the channels bridge). Warns
|
|
2371
|
+
* once per climb (this._compactionWarned), re-armed on PostCompact.
|
|
2372
|
+
* Fire-and-forget: swallows its own errors so transcript IO never breaks
|
|
2373
|
+
* the turn-end path.
|
|
2374
|
+
*/
|
|
2375
|
+
async _maybeProactiveCompactionWarn(transcriptPath) {
|
|
2376
|
+
try {
|
|
2377
|
+
if (!this.compactionWarn?.enabled || this._compactionWarned) return;
|
|
2378
|
+
const usage = await readContextTokens(transcriptPath);
|
|
2379
|
+
if (!usage) return;
|
|
2380
|
+
const pct = contextPct(usage.total) * 100;
|
|
2381
|
+
if (pct < this.compactionWarn.thresholdPct) return;
|
|
2382
|
+
if (this._compactionWarned) return; // re-check after the async gap
|
|
2383
|
+
this._compactionWarned = true;
|
|
2384
|
+
this.emit('compaction-warn', {
|
|
2385
|
+
kind: 'proactive',
|
|
2386
|
+
pct: Math.round(pct),
|
|
2387
|
+
totalTokens: usage.total,
|
|
2388
|
+
sessionId: this.claudeSessionId,
|
|
2389
|
+
backend: this.backend,
|
|
2390
|
+
});
|
|
2391
|
+
} catch (err) {
|
|
2392
|
+
this.logger.warn?.(`[${this.label}] compaction-warn sample failed: ${err.message}`);
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2323
2396
|
async _pollMidTurnDialogs() {
|
|
2324
2397
|
if (this.closed) return;
|
|
2325
2398
|
if (this.pendingTurns.size === 0) return; // no work to do when idle
|
|
@@ -51,6 +51,9 @@ const KNOWN_EVENT_NAMES = new Set([
|
|
|
51
51
|
'SubagentStop',
|
|
52
52
|
'Stop',
|
|
53
53
|
'Notification',
|
|
54
|
+
// 0.12.0-rc.13: compaction lifecycle (carry `trigger` + custom_instructions).
|
|
55
|
+
'PreCompact',
|
|
56
|
+
'PostCompact',
|
|
54
57
|
]);
|
|
55
58
|
|
|
56
59
|
/**
|
|
@@ -87,6 +90,10 @@ function normalizeHookEvent(raw) {
|
|
|
87
90
|
prompt: raw?.prompt ?? null,
|
|
88
91
|
stopHookActive: raw?.stop_hook_active ?? null,
|
|
89
92
|
lastAssistantMessage: raw?.last_assistant_message ?? null,
|
|
93
|
+
// PreCompact/PostCompact payload: trigger distinguishes auto vs manual
|
|
94
|
+
// compaction; custom_instructions is the `/compact <hint>` text (manual).
|
|
95
|
+
trigger: raw?.trigger ?? null,
|
|
96
|
+
customInstructions: raw?.custom_instructions ?? null,
|
|
90
97
|
receivedAtMs: raw?.polygram_received_at_ms ?? null,
|
|
91
98
|
raw,
|
|
92
99
|
};
|
|
@@ -44,6 +44,13 @@ const HOOK_EVENTS = [
|
|
|
44
44
|
'SubagentStop',
|
|
45
45
|
'Stop',
|
|
46
46
|
'Notification',
|
|
47
|
+
// 0.12.0-rc.13: compaction lifecycle. PreCompact fires when claude is about
|
|
48
|
+
// to (auto-)compact — the moment that detaches the channels MCP bridge.
|
|
49
|
+
// PostCompact fires after, when context has dropped (used to re-arm the
|
|
50
|
+
// per-chat compaction warning). Both confirmed supported by the pinned CLI
|
|
51
|
+
// (2.1.142) and carry a `trigger: auto|manual` field.
|
|
52
|
+
'PreCompact',
|
|
53
|
+
'PostCompact',
|
|
47
54
|
];
|
|
48
55
|
|
|
49
56
|
/**
|
package/lib/process-manager.js
CHANGED
|
@@ -41,6 +41,12 @@ const CALLBACK_TO_EVENT = {
|
|
|
41
41
|
onAssistantMessageStart: 'assistant-message-start',
|
|
42
42
|
onAutonomousAssistantMessage: 'autonomous-assistant-message',
|
|
43
43
|
onCompactBoundary: 'compact-boundary',
|
|
44
|
+
// 0.12.0-rc.13: per-chat compaction warning. CliProcess emits
|
|
45
|
+
// 'compaction-warn' {kind:'proactive'|'reactive', pct?} when (proactive)
|
|
46
|
+
// context crosses the chat's threshold at turn-end, or (reactive) claude is
|
|
47
|
+
// auto-compacting now. The callback posts a chat message proposing /compact
|
|
48
|
+
// — opt-in per chat. See docs/0.12.0-file-send.md / lib/compaction-warn.js.
|
|
49
|
+
onCompactionWarn: 'compaction-warn',
|
|
44
50
|
onQueueDrop: 'queue-drop',
|
|
45
51
|
onThinking: 'thinking',
|
|
46
52
|
// Tmux backend: TUI shows in-pane approval prompt. SDK backend
|
package/lib/sdk/callbacks.js
CHANGED
|
@@ -571,6 +571,42 @@ function createSdkCallbacks({
|
|
|
571
571
|
}
|
|
572
572
|
},
|
|
573
573
|
|
|
574
|
+
// 0.12.0-rc.13: per-chat compaction warning. CliProcess emits
|
|
575
|
+
// 'compaction-warn' when context crosses the chat's threshold at turn-end
|
|
576
|
+
// (proactive) or claude is auto-compacting now (reactive). Post a chat
|
|
577
|
+
// message proposing /compact so the user can compact on their terms BEFORE
|
|
578
|
+
// an auto-compaction interrupts a turn (and detaches the channels bridge).
|
|
579
|
+
// Opt-in per chat (lib/compaction-warn.js) — CliProcess only emits when
|
|
580
|
+
// enabled, so no extra config gate is needed here. Best-effort send.
|
|
581
|
+
onCompactionWarn: (sessionKey, payload /* , entry */) => {
|
|
582
|
+
try {
|
|
583
|
+
const chatId = getChatIdFromKey(sessionKey);
|
|
584
|
+
const threadIdRaw = getThreadIdFromKey(sessionKey);
|
|
585
|
+
const threadId = threadIdRaw ? parseInt(threadIdRaw, 10) : null;
|
|
586
|
+
const kind = payload?.kind === 'reactive' ? 'reactive' : 'proactive';
|
|
587
|
+
logEvent('compaction-warn', {
|
|
588
|
+
chat_id: chatId,
|
|
589
|
+
session_key: sessionKey,
|
|
590
|
+
kind,
|
|
591
|
+
pct: payload?.pct ?? null,
|
|
592
|
+
backend: payload?.backend ?? 'cli',
|
|
593
|
+
});
|
|
594
|
+
if (!bot) return;
|
|
595
|
+
const text = kind === 'reactive'
|
|
596
|
+
? '🗜️ Auto-compacting now — context filled up. If this turn goes quiet, please resend. (Tip: running `/compact` at a natural break avoids mid-task compactions.)'
|
|
597
|
+
: `📚 Heads up — this chat's context is ~${payload?.pct ?? '?'}% full. To avoid an auto-compaction that can interrupt a turn, run \`/compact\` (optionally with a hint, e.g. \`/compact keep the recent decisions\`) at a natural break — or \`/new\` for a fresh start.`;
|
|
598
|
+
tg(bot, 'sendMessage', {
|
|
599
|
+
chat_id: chatId,
|
|
600
|
+
text,
|
|
601
|
+
...(threadId ? { message_thread_id: threadId } : {}),
|
|
602
|
+
}, { source: 'compaction-warn', botName }).catch((err) => {
|
|
603
|
+
logger.error?.(`[${botName}] compaction-warn send failed: ${err.message}`);
|
|
604
|
+
});
|
|
605
|
+
} catch (err) {
|
|
606
|
+
logger.error?.(`[${botName}] compaction-warn handler: ${err.message}`);
|
|
607
|
+
}
|
|
608
|
+
},
|
|
609
|
+
|
|
574
610
|
// 0.10.0 rc.42 #8: tmux backend hook-tail error observability.
|
|
575
611
|
// Persistent failures of the hook ndjson tail degrade H3 idle-
|
|
576
612
|
// ceiling accuracy and H4 Stop-synth coverage with no surface
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.12.0-rc.
|
|
3
|
+
"version": "0.12.0-rc.13",
|
|
4
4
|
"description": "Telegram daemon for Claude Code that preserves the OpenClaw per-chat session model. Migration path for OpenClaw users moving to Claude Code.",
|
|
5
5
|
"main": "lib/ipc/client.js",
|
|
6
6
|
"bin": {
|