polygram 0.12.0-rc.12 → 0.12.0-rc.14
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/process/cli-process.js +92 -39
- 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 };
|
|
@@ -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
|
|
|
@@ -118,16 +120,17 @@ const STREAMING_HINT_RE = /esc to interrupt/i;
|
|
|
118
120
|
// — false positives surface as no-op telemetry, false negatives surface
|
|
119
121
|
// as the idle-ceiling timeout (~10min).
|
|
120
122
|
const UNKNOWN_PROMPT_HEURISTIC_RE = /(\?\s*$|\(y\/N\)|Yes\/No|❯\s|^\s*[12345]\.\s)/im;
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
// `
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
123
|
+
// rc.14: a previous rc (rc.11) had a BRIDGE_DEAD_RE here that matched the pane
|
|
124
|
+
// line "server:polygram-bridge no MCP server configured with that name" and
|
|
125
|
+
// treated it as a dead bridge to recover from. That was a MISDIAGNOSIS: this
|
|
126
|
+
// line is a BENIGN, persistent banner that `--dangerously-load-development-
|
|
127
|
+
// channels` + `--strict-mcp-config` prints on EVERY healthy session — the
|
|
128
|
+
// channel still delivers messages and the reply tool still works (reproduced
|
|
129
|
+
// 2026-06-01 with a test MCP server that demonstrably functions). The pane
|
|
130
|
+
// matcher therefore false-fired ~5s into every channels turn and KILLED
|
|
131
|
+
// healthy sessions (the Music-topic "mid-turn detach" regression). Real bridge
|
|
132
|
+
// loss is caught by the socket-close path (bridgeServer 'bridge-disconnected'
|
|
133
|
+
// → _handleBridgeDisconnected). There is no reliable pane signal — removed.
|
|
131
134
|
// Per-pattern rate limit so a dialog that lingers across multiple polls
|
|
132
135
|
// doesn't spam sendControl/event emissions. Aligned with the 5s poll cadence.
|
|
133
136
|
const MID_TURN_DEDUP_WINDOW_MS = 30_000;
|
|
@@ -520,6 +523,14 @@ class CliProcess extends Process {
|
|
|
520
523
|
override: _capOverride,
|
|
521
524
|
}).outBytes;
|
|
522
525
|
|
|
526
|
+
// 0.12.0-rc.13: per-chat/topic compaction warning (default OFF). Same
|
|
527
|
+
// topic→chat precedence as the file cap above. When enabled, the channels
|
|
528
|
+
// backend warns the chat as context fills (propose /compact at a break)
|
|
529
|
+
// and on auto-compaction (the event that detaches the bridge mid-turn).
|
|
530
|
+
const _compactionWarnRaw = topicConfig?.compactionWarnings ?? opts.chatConfig?.compactionWarnings;
|
|
531
|
+
this.compactionWarn = resolveCompactionWarnConfig({ compactionWarnings: _compactionWarnRaw });
|
|
532
|
+
this._compactionWarned = false; // proactive warn-once per climb; reset on PostCompact
|
|
533
|
+
|
|
523
534
|
// Parity audit P8 + rc.8 fs-guard (2026-05-26 shumorobot Music topic):
|
|
524
535
|
// `--session-id <id>` creates a NEW claude session with that id;
|
|
525
536
|
// `--resume <id>` resumes the EXISTING conversation. Lazy-respawn after
|
|
@@ -1738,6 +1749,38 @@ class CliProcess extends Process {
|
|
|
1738
1749
|
lastAssistantMessage: ev.lastAssistantMessage,
|
|
1739
1750
|
backend: this.backend,
|
|
1740
1751
|
});
|
|
1752
|
+
// 0.12.0-rc.13 proactive compaction warning: on turn-end, if enabled
|
|
1753
|
+
// for this chat and not already warned this climb, sample context
|
|
1754
|
+
// occupancy from the transcript and warn (propose /compact) BEFORE
|
|
1755
|
+
// claude auto-compacts mid-turn and detaches the bridge. Fire-and-
|
|
1756
|
+
// forget — transcript IO must never block the stop path.
|
|
1757
|
+
if (this.compactionWarn?.enabled && !this._compactionWarned && ev.transcriptPath) {
|
|
1758
|
+
this._maybeProactiveCompactionWarn(ev.transcriptPath);
|
|
1759
|
+
}
|
|
1760
|
+
return;
|
|
1761
|
+
|
|
1762
|
+
case 'PreCompact':
|
|
1763
|
+
// 0.12.0-rc.13: auto-compaction is the event that detaches the
|
|
1764
|
+
// channels MCP bridge mid-turn. Record it; and on the dangerous AUTO
|
|
1765
|
+
// case (manual /compact is the user's own deliberate action — never
|
|
1766
|
+
// nag), emit a reactive warning the chat layer posts. The proactive
|
|
1767
|
+
// warning (on Stop) tries to PREVENT this; this is the backstop.
|
|
1768
|
+
this._logEvent('cli-compaction-imminent', { trigger: ev.trigger });
|
|
1769
|
+
if (this.compactionWarn?.enabled && ev.trigger === 'auto') {
|
|
1770
|
+
this.emit('compaction-warn', {
|
|
1771
|
+
kind: 'reactive',
|
|
1772
|
+
trigger: 'auto',
|
|
1773
|
+
sessionId: this.claudeSessionId,
|
|
1774
|
+
backend: this.backend,
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
return;
|
|
1778
|
+
|
|
1779
|
+
case 'PostCompact':
|
|
1780
|
+
// Context just dropped — re-arm the proactive warn-once so the next
|
|
1781
|
+
// climb can warn again.
|
|
1782
|
+
this._compactionWarned = false;
|
|
1783
|
+
this._logEvent('cli-compaction-done', { trigger: ev.trigger });
|
|
1741
1784
|
return;
|
|
1742
1785
|
|
|
1743
1786
|
case 'Notification':
|
|
@@ -2320,6 +2363,37 @@ class CliProcess extends Process {
|
|
|
2320
2363
|
* Extracted as a separate async method so unit tests can drive it
|
|
2321
2364
|
* directly without waiting for the setInterval tick.
|
|
2322
2365
|
*/
|
|
2366
|
+
/**
|
|
2367
|
+
* 0.12.0-rc.13: proactive compaction warning. Read the transcript's current
|
|
2368
|
+
* context occupancy and, if past the per-chat threshold, emit a
|
|
2369
|
+
* 'compaction-warn' the chat layer turns into "you're ~N% full, run
|
|
2370
|
+
* /compact" — giving the user a window to compact on their terms BEFORE
|
|
2371
|
+
* claude auto-compacts mid-turn (which detaches the channels bridge). Warns
|
|
2372
|
+
* once per climb (this._compactionWarned), re-armed on PostCompact.
|
|
2373
|
+
* Fire-and-forget: swallows its own errors so transcript IO never breaks
|
|
2374
|
+
* the turn-end path.
|
|
2375
|
+
*/
|
|
2376
|
+
async _maybeProactiveCompactionWarn(transcriptPath) {
|
|
2377
|
+
try {
|
|
2378
|
+
if (!this.compactionWarn?.enabled || this._compactionWarned) return;
|
|
2379
|
+
const usage = await readContextTokens(transcriptPath);
|
|
2380
|
+
if (!usage) return;
|
|
2381
|
+
const pct = contextPct(usage.total) * 100;
|
|
2382
|
+
if (pct < this.compactionWarn.thresholdPct) return;
|
|
2383
|
+
if (this._compactionWarned) return; // re-check after the async gap
|
|
2384
|
+
this._compactionWarned = true;
|
|
2385
|
+
this.emit('compaction-warn', {
|
|
2386
|
+
kind: 'proactive',
|
|
2387
|
+
pct: Math.round(pct),
|
|
2388
|
+
totalTokens: usage.total,
|
|
2389
|
+
sessionId: this.claudeSessionId,
|
|
2390
|
+
backend: this.backend,
|
|
2391
|
+
});
|
|
2392
|
+
} catch (err) {
|
|
2393
|
+
this.logger.warn?.(`[${this.label}] compaction-warn sample failed: ${err.message}`);
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2323
2397
|
async _pollMidTurnDialogs() {
|
|
2324
2398
|
if (this.closed) return;
|
|
2325
2399
|
if (this.pendingTurns.size === 0) return; // no work to do when idle
|
|
@@ -2338,35 +2412,14 @@ class CliProcess extends Process {
|
|
|
2338
2412
|
}
|
|
2339
2413
|
if (!pane) return;
|
|
2340
2414
|
|
|
2341
|
-
//
|
|
2342
|
-
//
|
|
2343
|
-
//
|
|
2344
|
-
//
|
|
2345
|
-
//
|
|
2346
|
-
//
|
|
2347
|
-
//
|
|
2348
|
-
//
|
|
2349
|
-
// orphans and would hang until the wall-clock cap while this watchdog
|
|
2350
|
-
// logged cli-mid-turn-unknown-prompt every 30s and recovered nothing.
|
|
2351
|
-
// Detect it from the pane and route through the SAME recovery as a real
|
|
2352
|
-
// socket disconnect: reject pending turns (user gets the 🔌 resend
|
|
2353
|
-
// message via BRIDGE_DISCONNECTED) and emit 'bridge-disconnected' so
|
|
2354
|
-
// process-manager kills + lazy-respawns the dead instance (next msg
|
|
2355
|
-
// recovers the conversation via `claude --resume`). Gated on
|
|
2356
|
-
// pendingTurns.size>0 by the early return above, so it can't fire on an
|
|
2357
|
-
// idle session that merely has the string in scrollback.
|
|
2358
|
-
if (BRIDGE_DEAD_RE.test(pane)) {
|
|
2359
|
-
this.logger.warn?.(
|
|
2360
|
-
`[${this.label}] cli: channels MCP registration lost mid-turn ` +
|
|
2361
|
-
`(pane-detected) — recovering ${this.pendingTurns.size} orphaned turn(s)`,
|
|
2362
|
-
);
|
|
2363
|
-
this._logEvent('cli-bridge-detached-midturn', {
|
|
2364
|
-
pending_count: this.pendingTurns.size,
|
|
2365
|
-
session_id: this.claudeSessionId,
|
|
2366
|
-
});
|
|
2367
|
-
this._handleBridgeDisconnected('mcp-registration-lost');
|
|
2368
|
-
return;
|
|
2369
|
-
}
|
|
2415
|
+
// rc.14: removed the rc.11 pane-based "dead bridge" detection here. It
|
|
2416
|
+
// matched the BENIGN banner "server:polygram-bridge no MCP server
|
|
2417
|
+
// configured with that name" — a cosmetic line that
|
|
2418
|
+
// `--dangerously-load-development-channels` + `--strict-mcp-config` prints
|
|
2419
|
+
// on EVERY healthy session (channel still delivers; reply tool still
|
|
2420
|
+
// works). The matcher false-fired ~5s into every channels turn and killed
|
|
2421
|
+
// healthy sessions. Real bridge loss is the socket-close path
|
|
2422
|
+
// (_handleBridgeDisconnected), not anything observable in the pane.
|
|
2370
2423
|
|
|
2371
2424
|
const now = Date.now();
|
|
2372
2425
|
|
|
@@ -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.14",
|
|
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": {
|