polygram 0.12.0-rc.3 → 0.12.0-rc.6
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/lib/handlers/abort.js +38 -1
- package/lib/process/cli-process.js +57 -0
- package/lib/process/factory.js +0 -4
- package/package.json +1 -1
- package/polygram.js +1 -8
package/lib/handlers/abort.js
CHANGED
|
@@ -42,13 +42,37 @@ function createHandleAbort({
|
|
|
42
42
|
const threadId = msg.message_thread_id?.toString();
|
|
43
43
|
const sessionKey = getSessionKey(chatId, threadId, chatConfig);
|
|
44
44
|
const proc = pm.has(sessionKey) ? pm.get(sessionKey) : null;
|
|
45
|
-
|
|
45
|
+
let hadActive = !!proc?.inFlight;
|
|
46
46
|
|
|
47
47
|
// Mark BEFORE killing: the 'close' event fires almost immediately
|
|
48
48
|
// after interrupt, and the surrounding handleMessage's catch
|
|
49
49
|
// needs to see the flag to skip the generic error-reply.
|
|
50
50
|
if (hadActive) markSessionAborted(sessionKey);
|
|
51
51
|
|
|
52
|
+
// "Stop" incident (shumorobot Music, 2026-05-31 13:08): on the
|
|
53
|
+
// CliProcess/channels backend a turn resolves on the quiet-window
|
|
54
|
+
// after claude's last reply tool call (inFlight → false), but claude
|
|
55
|
+
// can still be working (subagent, long Bash). Keying the ack on
|
|
56
|
+
// inFlight alone made "Stop" say "Nothing to stop" while a subagent
|
|
57
|
+
// download churned. probeBusyState() reads the TUI "esc to interrupt"
|
|
58
|
+
// hint — the truthful signal — so detection, the abort mark, and the
|
|
59
|
+
// ack all agree. The probe result is logged below (forensics) so the
|
|
60
|
+
// heuristic can be refined against real states later. Channels analog
|
|
61
|
+
// of the (deleted) tmux hasBackgroundShell branch; typeof-guarded so
|
|
62
|
+
// it's a no-op on backends without it.
|
|
63
|
+
let busyProbe = null;
|
|
64
|
+
if (!hadActive && proc && typeof proc.probeBusyState === 'function') {
|
|
65
|
+
try {
|
|
66
|
+
busyProbe = await proc.probeBusyState();
|
|
67
|
+
if (busyProbe?.busy) {
|
|
68
|
+
hadActive = true;
|
|
69
|
+
markSessionAborted(sessionKey);
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
logger.error?.(`[${botName}] busy-probe failed: ${err.message}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
52
76
|
// Bug 1 (incident 2026-05-18): "Stop" was turn-scoped — it only
|
|
53
77
|
// looked at an in-flight TURN. But the agent can leave a DETACHED
|
|
54
78
|
// background shell running (a `run_in_background:true` Bash) that
|
|
@@ -87,6 +111,19 @@ function createHandleAbort({
|
|
|
87
111
|
chat_id: chatId, user_id: msg.from?.id || null,
|
|
88
112
|
had_active: hadActive,
|
|
89
113
|
killed_background_shell: killedBackgroundShell,
|
|
114
|
+
// "Stop" incident forensics: the raw busy-probe signals at decision
|
|
115
|
+
// time. Lets us query, across real aborts, where the esc-hint /
|
|
116
|
+
// inFlight / pending-turn signals agreed vs diverged and refine the
|
|
117
|
+
// heuristic later. null when no probe ran (turn was already inFlight,
|
|
118
|
+
// or the backend has no probeBusyState).
|
|
119
|
+
busy_probe: busyProbe ? {
|
|
120
|
+
busy: busyProbe.busy,
|
|
121
|
+
streaming: busyProbe.streaming,
|
|
122
|
+
in_flight: busyProbe.inFlight,
|
|
123
|
+
pending_turns: busyProbe.pendingTurns,
|
|
124
|
+
captured: busyProbe.captured,
|
|
125
|
+
pane_tail: busyProbe.paneTail,
|
|
126
|
+
} : null,
|
|
90
127
|
trigger: cleanText.slice(0, 40),
|
|
91
128
|
});
|
|
92
129
|
|
|
@@ -1394,6 +1394,63 @@ class CliProcess extends Process {
|
|
|
1394
1394
|
this._interruptGraceTimer.unref?.();
|
|
1395
1395
|
}
|
|
1396
1396
|
|
|
1397
|
+
/**
|
|
1398
|
+
* Is claude actually still working, regardless of the resolved-turn flag?
|
|
1399
|
+
*
|
|
1400
|
+
* "Stop" incident (shumorobot Music, 2026-05-31 13:08): the channels
|
|
1401
|
+
* backend resolves a turn on the quiet-window after claude's last reply
|
|
1402
|
+
* tool call (inFlight → false), but claude can keep working afterwards
|
|
1403
|
+
* (a subagent, a long Bash). The abort handler keyed its ack on inFlight
|
|
1404
|
+
* alone, so "Stop" said "Nothing to stop" one second after the bot said
|
|
1405
|
+
* "On it — downloading…" while a subagent churned.
|
|
1406
|
+
*
|
|
1407
|
+
* The TUI prints "esc to interrupt" (STREAMING_HINT_RE) continuously
|
|
1408
|
+
* whenever claude is busy — capture-pane is the truthful signal, the
|
|
1409
|
+
* channels analog of the (deleted) tmux hasBackgroundShell() probe.
|
|
1410
|
+
*
|
|
1411
|
+
* Returns a STRUCTURED probe (not just a boolean) so the abort path can
|
|
1412
|
+
* log the raw signals — pane tail + flags — to the events DB. That lets
|
|
1413
|
+
* us later characterize which states the heuristic gets right/wrong and
|
|
1414
|
+
* refine it (e.g. add signals beyond the esc-hint) without guessing.
|
|
1415
|
+
*
|
|
1416
|
+
* Never throws — a failed capture returns captured:false, busy:false.
|
|
1417
|
+
*
|
|
1418
|
+
* @returns {Promise<{busy:boolean, streaming:boolean, inFlight:boolean,
|
|
1419
|
+
* pendingTurns:number, captured:boolean, paneTail:(string|null)}>}
|
|
1420
|
+
*/
|
|
1421
|
+
async probeBusyState() {
|
|
1422
|
+
const base = {
|
|
1423
|
+
busy: false, streaming: false,
|
|
1424
|
+
inFlight: this.inFlight, pendingTurns: this.pendingTurns.size,
|
|
1425
|
+
captured: false, paneTail: null,
|
|
1426
|
+
};
|
|
1427
|
+
if (this.closed || !this.tmuxSession || typeof this.runner?.captureWide !== 'function') {
|
|
1428
|
+
return base;
|
|
1429
|
+
}
|
|
1430
|
+
let pane;
|
|
1431
|
+
try {
|
|
1432
|
+
pane = await this.runner.captureWide(this.tmuxSession);
|
|
1433
|
+
} catch (err) {
|
|
1434
|
+
this.logger.warn?.(`[${this.label}] channels: probeBusyState captureWide failed: ${err.message}`);
|
|
1435
|
+
return base;
|
|
1436
|
+
}
|
|
1437
|
+
if (!pane) return base;
|
|
1438
|
+
const streaming = STREAMING_HINT_RE.test(pane);
|
|
1439
|
+
return {
|
|
1440
|
+
...base,
|
|
1441
|
+
busy: streaming,
|
|
1442
|
+
streaming,
|
|
1443
|
+
captured: true,
|
|
1444
|
+
paneTail: pane.slice(-200),
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/** Boolean shorthand for probeBusyState().busy (abort-path convenience). */
|
|
1449
|
+
async isBusy() {
|
|
1450
|
+
const { busy } = await this.probeBusyState();
|
|
1451
|
+
return busy;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1397
1454
|
async kill(reason = 'kill') {
|
|
1398
1455
|
if (this.closed) return;
|
|
1399
1456
|
// Parity P19: re-entry guard for concurrent kill() calls. Mirrors
|
package/lib/process/factory.js
CHANGED
|
@@ -91,10 +91,6 @@ function _maybeWarnR12Migration({ rawPm, canonical, chatId, threadId, chatCfg, t
|
|
|
91
91
|
* @param {number} [opts.queryCloseTimeoutMs]
|
|
92
92
|
* @param {object} [opts.tmuxRunner] — required when ANY chat routes to 'cli'
|
|
93
93
|
* @param {string} [opts.botName] — required when ANY chat routes to 'cli'
|
|
94
|
-
* @param {object} [opts.pollScheduler] — DEPRECATED in 0.12 — was used by the
|
|
95
|
-
* removed tmux backend to share one setInterval across all chats; CliProcess's
|
|
96
|
-
* per-session pongWatchdog handles its own cadence. Param kept for caller
|
|
97
|
-
* back-compat; ignored. Will be removed in 0.13.
|
|
98
94
|
* @param {Function} [opts.toolDispatcher] — required when ANY chat routes to 'cli'.
|
|
99
95
|
* async ({sessionKey, chatId, threadId, toolName, text, files}) => {ok, error?}.
|
|
100
96
|
* Called when Claude's reply (or react/edit_message) tool fires inside a
|
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.6",
|
|
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": {
|
package/polygram.js
CHANGED
|
@@ -2243,19 +2243,13 @@ async function main() {
|
|
|
2243
2243
|
const binCheck = verifyPinnedClaudeBin(CLAUDE_CLI_PINNED_VERSION);
|
|
2244
2244
|
if (binCheck.ok) {
|
|
2245
2245
|
console.log(
|
|
2246
|
-
`[polygram]
|
|
2246
|
+
`[polygram] CliProcess pinned to claude CLI v${CLAUDE_CLI_PINNED_VERSION}: ${binCheck.path}`,
|
|
2247
2247
|
);
|
|
2248
2248
|
pinnedClaudeBin = binCheck.path;
|
|
2249
2249
|
} else {
|
|
2250
2250
|
console.warn(`[polygram] WARNING: ${binCheck.reason}`);
|
|
2251
2251
|
}
|
|
2252
2252
|
}
|
|
2253
|
-
// O1 optimization: shared poll-tick scheduler. N TmuxProcess
|
|
2254
|
-
// instances share ONE setInterval instead of spawning N independent
|
|
2255
|
-
// setTimeout chains. Idle when no chats are in flight (zero timers
|
|
2256
|
-
// running). Configurable via config.bot.tmuxPollIntervalMs.
|
|
2257
|
-
const tmuxPollIntervalMs = config.bot?.tmuxPollIntervalMs || 250;
|
|
2258
|
-
const pollScheduler = new PollScheduler({ intervalMs: tmuxPollIntervalMs });
|
|
2259
2253
|
// 0.11.0: channels backend wiring. Used when a chat opts in via
|
|
2260
2254
|
// `pm: 'channels'` config. Falls back to SDK gracefully if the pinned
|
|
2261
2255
|
// claude binary isn't present (see factory.js — channelsClaudeBin
|
|
@@ -2281,7 +2275,6 @@ async function main() {
|
|
|
2281
2275
|
logger: console,
|
|
2282
2276
|
tmuxRunner,
|
|
2283
2277
|
botName: BOT_NAME,
|
|
2284
|
-
pollScheduler,
|
|
2285
2278
|
// channels backend
|
|
2286
2279
|
toolDispatcher: channelsToolDispatcher,
|
|
2287
2280
|
channelsClaudeBin,
|