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.
@@ -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
- const hadActive = !!proc?.inFlight;
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.4",
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": {