polygram 0.10.0-rc.44 → 0.10.0-rc.45

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "polygram",
4
- "version": "0.10.0-rc.44",
4
+ "version": "0.10.0-rc.45",
5
5
  "description": "Telegram integration for Claude Code that preserves the OpenClaw per-chat session model. Migration target for OpenClaw users. Multi-bot, multi-chat, per-topic isolation; SQLite transcripts; inline-keyboard approvals. Bundles /polygram:status|logs|pair-code|approvals admin commands plus history (transcript queries) and polygram-send (out-of-turn IPC sends with file-upload validation) skills.",
6
6
  "keywords": [
7
7
  "telegram",
@@ -1569,9 +1569,26 @@ class TmuxProcess extends Process {
1569
1569
  _heartbeat(turn, source) {
1570
1570
  if (!turn) return;
1571
1571
  turn.lastActivityAt = this._now();
1572
- // No event emission heartbeats are high-frequency; the
1573
- // phase-change event is the consumer-visible surface. Source tag
1574
- // is reserved for future debug telemetry.
1572
+ // rc.45 (shumorobot main topic 2026-05-23): the reactor's
1573
+ // cascade timers (THINKING_DEEPER at 12 s, THINKING_DEEPEST at
1574
+ // 30 s, STALL at 45 s) were firing during long pure-thinking
1575
+ // turns because no signal reset the clock — hooks don't fire
1576
+ // during thinking, JSONL `assistant-chunk` doesn't fire either.
1577
+ // But capture-pane DOES see claude's continuous "esc to
1578
+ // interrupt" indicator throughout the thinking phase, and the
1579
+ // existing `_heartbeat(turn, 'capture:streaming')` call from
1580
+ // line 2620 already runs on every poll while claude is busy.
1581
+ // Route that signal to the reactor too — the cascade clock is
1582
+ // reset and the user sees a stable 🤔 instead of escalating to
1583
+ // 🥱 (STALL) on a healthy long-thinking turn.
1584
+ //
1585
+ // The SDK backend gets this for free via typed `thinking`
1586
+ // events that flow through onStreamChunk → reactor.heartbeat.
1587
+ // tmux had no equivalent until this hookup.
1588
+ const reactor = turn.context?.reactor;
1589
+ if (reactor && typeof reactor.heartbeat === 'function') {
1590
+ try { reactor.heartbeat(); } catch { /* swallow — heartbeat is best-effort */ }
1591
+ }
1575
1592
  void source;
1576
1593
  }
1577
1594
 
@@ -53,24 +53,43 @@ const SANITIZED_REPLACEMENT =
53
53
  '_(the model returned no actual reply — try rephrasing or asking again)_';
54
54
 
55
55
  /**
56
- * Inspect an outbound assistant text. If the FULL TRIMMED text
57
- * matches a known CLI-context canned string, return the honest
58
- * replacement and a `replaced` flag so the caller can log the
59
- * substitution. Otherwise return the original text unchanged.
56
+ * Inspect an outbound assistant text. Replaces any occurrence of a
57
+ * known CLI-context canned string (as a SUBSTRING) with the honest
58
+ * fallback, and returns a `replaced` flag so the caller can log the
59
+ * substitution.
60
+ *
61
+ * Why substring-replace, not exact-match (rc.45 production discovery):
62
+ * claude can emit MULTIPLE assistant-message blocks within one turn,
63
+ * and polygram concatenates them into the final `parsed.text`. If
64
+ * claude's first block was a substantive reply and the second was
65
+ * the canned `No response requested.`, the combined `parsed.text`
66
+ * matches neither canned string exactly — so the previous exact-
67
+ * match sanitizer didn't fire and the canned string still reached
68
+ * Telegram as a separate bubble (shumorobot Music topic and main
69
+ * topic, 2026-05-23, msg 999).
70
+ *
71
+ * Substring replacement risk: a legitimate reply that DISCUSSES the
72
+ * canned phrase ("the model leaks 'No response requested.' here")
73
+ * will have the inner phrase replaced. Cost is cosmetic (slightly
74
+ * ugly italicised replacement embedded in prose), not catastrophic.
75
+ * The CANNED_STRINGS allowlist stays narrow to keep this rare.
60
76
  *
61
77
  * @param {string} text — the assistant text about to be sent.
62
78
  * @returns {{ text: string, replaced: boolean, original?: string }}
63
79
  */
64
80
  function sanitizeAssistantReply(text) {
65
81
  if (typeof text !== 'string') return { text, replaced: false };
66
- const trimmed = text.trim();
67
- if (!trimmed) return { text, replaced: false };
68
- if (CANNED_STRINGS.has(trimmed)) {
69
- return {
70
- text: SANITIZED_REPLACEMENT,
71
- replaced: true,
72
- original: trimmed,
73
- };
82
+ if (!text) return { text, replaced: false };
83
+ let mutated = text;
84
+ let firstHit = null;
85
+ for (const canned of CANNED_STRINGS) {
86
+ if (mutated.includes(canned)) {
87
+ if (firstHit == null) firstHit = canned;
88
+ mutated = mutated.split(canned).join(SANITIZED_REPLACEMENT);
89
+ }
90
+ }
91
+ if (firstHit != null) {
92
+ return { text: mutated, replaced: true, original: firstHit };
74
93
  }
75
94
  return { text, replaced: false };
76
95
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.10.0-rc.44",
3
+ "version": "0.10.0-rc.45",
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": {