polygram 0.12.0-rc.4 → 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/package.json +1 -1
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/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": {
|