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.
@@ -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
@@ -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",
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] tmux backend pinned to claude CLI v${CLAUDE_CLI_PINNED_VERSION}: ${binCheck.path}`,
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,