polygram 0.8.0-rc.32 β†’ 0.8.0-rc.34

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.8.0-rc.32",
4
+ "version": "0.8.0-rc.34",
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 and a history skill.",
6
6
  "keywords": [
7
7
  "telegram",
@@ -29,12 +29,21 @@
29
29
  // are progressively safer. All endings in this list are in Telegram's
30
30
  // default available reactions as of 2026-04.
31
31
  const STATES = {
32
- QUEUED: { label: 'queued', chain: ['πŸ‘€', 'πŸ€”'] },
33
- THINKING: { label: 'thinking', chain: ['πŸ€”'] },
34
- CODING: { label: 'coding', chain: ['πŸ‘¨β€πŸ’»', '✍', 'πŸ€”'] },
35
- WEB: { label: 'web', chain: ['⚑', 'πŸ”₯', 'πŸ€”'] },
36
- TOOL: { label: 'tool', chain: ['πŸ”₯', 'πŸ€”'] },
37
- WRITING: { label: 'writing', chain: ['✍', 'πŸ€”'] },
32
+ QUEUED: { label: 'queued', chain: ['πŸ‘€', 'πŸ€”'] },
33
+ // rc.32: progressive-deepening cascade. setState('THINKING') auto-
34
+ // promotes through THINKING_DEEPER β†’ THINKING_DEEPEST as time passes
35
+ // without a state change. Thresholds calibrated to Ivan DM 14-day
36
+ // production: 8% turns finish <5s, 33% in 5-15s (so 8s catches the
37
+ // meaty band), 25% in 15-30s (20s threshold catches them mid-flow),
38
+ // 17% over 60s (existing 45s STALL still fires for the truly long).
39
+ // All three emoji on Telegram's curated standard reaction list.
40
+ THINKING: { label: 'thinking', chain: ['πŸ€”'] },
41
+ THINKING_DEEPER: { label: 'thinking-deeper', chain: ['🀨', 'πŸ€”'] },
42
+ THINKING_DEEPEST: { label: 'thinking-deepest', chain: ['🧐', 'πŸ€“', 'πŸ€”'] },
43
+ CODING: { label: 'coding', chain: ['πŸ‘¨β€πŸ’»', '✍', 'πŸ€”'] },
44
+ WEB: { label: 'web', chain: ['⚑', 'πŸ”₯', 'πŸ€”'] },
45
+ TOOL: { label: 'tool', chain: ['πŸ”₯', 'πŸ€”'] },
46
+ WRITING: { label: 'writing', chain: ['✍', 'πŸ€”'] },
38
47
  // 0.8.0-rc.11: terminal "your follow-up was incorporated into the
39
48
  // in-flight turn" state. Used by polygram's autosteer block when a
40
49
  // mid-turn user message is buffered for the next PostToolBatch
@@ -70,6 +79,14 @@ const DEFAULT_STALL_MS = 45_000;
70
79
  // which 180s captures while letting routine work breathe. Pm has its
71
80
  // own 5-minute hard idle timeout that actually rejects stuck turns.
72
81
  const DEFAULT_FREEZE_MS = 180_000;
82
+ // rc.32: thinking-deepening cascade thresholds. Calibrated to Ivan DM
83
+ // 14-day production data (445 turns) β€” 8% finish <5s, 33% in 5-15s,
84
+ // 25% in 15-30s. 8s catches the meaty band, 20s catches the
85
+ // mid-bracket. STALL still fires at 45s for the genuinely-long 17%.
86
+ // CODING / TOOL / WEB / WRITING reset the cascade (any state-change
87
+ // in setState clears the deepening timers).
88
+ const DEFAULT_THINKING_DEEPER_MS = 8_000;
89
+ const DEFAULT_THINKING_DEEPEST_MS = 20_000;
73
90
 
74
91
  // Tool name β†’ state classifier. Case-insensitive substring match so we
75
92
  // don't have to enumerate every existing or future tool. Order matters:
@@ -139,6 +156,8 @@ function createReactionManager({
139
156
  throttleMs = DEFAULT_THROTTLE_MS,
140
157
  stallMs = DEFAULT_STALL_MS,
141
158
  freezeMs = DEFAULT_FREEZE_MS,
159
+ thinkingDeeperMs = DEFAULT_THINKING_DEEPER_MS,
160
+ thinkingDeepestMs = DEFAULT_THINKING_DEEPEST_MS,
142
161
  logError = () => {},
143
162
  } = {}) {
144
163
  if (typeof apply !== 'function') throw new Error('apply function required');
@@ -149,6 +168,8 @@ function createReactionManager({
149
168
  let pendingTimer = null;
150
169
  let stallTimer = null;
151
170
  let freezeTimer = null;
171
+ let deeperTimer = null; // rc.32: THINKING β†’ THINKING_DEEPER (8s)
172
+ let deepestTimer = null; // rc.32: THINKING β†’ THINKING_DEEPEST (20s)
152
173
  let stopped = false;
153
174
  // 0.8.0-rc.11: serialize Telegram setMessageReaction calls. Without
154
175
  // this, multiple flush()es race at the network layer because each
@@ -162,7 +183,13 @@ function createReactionManager({
162
183
  // States the auto-stall path may transition to. Once we've already
163
184
  // shown STALL or TIMEOUT we don't downgrade or rearm β€” only an
164
185
  // explicit setState() call (Claude resumed) can move us forward.
165
- const STALL_PROMOTABLE = new Set(['THINKING', 'CODING', 'WEB', 'TOOL', 'WRITING']);
186
+ // rc.32: THINKING_DEEPER and THINKING_DEEPEST are stall-promotable
187
+ // too β€” if the model is stuck in a deep-thinking phase past 45s,
188
+ // STALL still fires.
189
+ const STALL_PROMOTABLE = new Set([
190
+ 'THINKING', 'THINKING_DEEPER', 'THINKING_DEEPEST',
191
+ 'CODING', 'WEB', 'TOOL', 'WRITING',
192
+ ]);
166
193
 
167
194
  const flush = async (stateName) => {
168
195
  if (stopped && !TERMINAL_STATES.has(stateName)) return;
@@ -191,6 +218,42 @@ function createReactionManager({
191
218
  if (freezeTimer) { clearTimeout(freezeTimer); freezeTimer = null; }
192
219
  };
193
220
 
221
+ // rc.32: thinking-deepening cascade. When state is THINKING, schedule
222
+ // auto-promotion at 8s (β†’ THINKING_DEEPER, 🀨) and 20s (β†’ THINKING_DEEPEST,
223
+ // 🧐). Any other setState (CODING / TOOL / WEB / WRITING / terminal) clears
224
+ // these. heartbeat() does NOT re-arm them β€” heartbeat is for keeping
225
+ // STALL/TIMEOUT at bay during silent activity, not for resetting
226
+ // visible deepening progression.
227
+ const clearDeepeningTimers = () => {
228
+ if (deeperTimer) { clearTimeout(deeperTimer); deeperTimer = null; }
229
+ if (deepestTimer) { clearTimeout(deepestTimer); deepestTimer = null; }
230
+ };
231
+ const armDeepeningTimers = () => {
232
+ clearDeepeningTimers();
233
+ if (stopped) return;
234
+ if (currentState !== 'THINKING') return;
235
+ deeperTimer = setTimeout(() => {
236
+ deeperTimer = null;
237
+ if (stopped) return;
238
+ if (currentState !== 'THINKING') return;
239
+ // Promote without going through setState β€” we want the visual
240
+ // change but NOT to reset the deepest timer (which keeps
241
+ // counting from the original THINKING start).
242
+ currentState = 'THINKING_DEEPER';
243
+ flush('THINKING_DEEPER');
244
+ }, thinkingDeeperMs);
245
+ deeperTimer.unref?.();
246
+ deepestTimer = setTimeout(() => {
247
+ deepestTimer = null;
248
+ if (stopped) return;
249
+ // Promote from THINKING or THINKING_DEEPER (NOT from CODING etc).
250
+ if (currentState !== 'THINKING' && currentState !== 'THINKING_DEEPER') return;
251
+ currentState = 'THINKING_DEEPEST';
252
+ flush('THINKING_DEEPEST');
253
+ }, thinkingDeepestMs);
254
+ deepestTimer.unref?.();
255
+ };
256
+
194
257
  const armStallTimers = () => {
195
258
  clearStallTimers();
196
259
  if (stopped) return;
@@ -223,6 +286,7 @@ function createReactionManager({
223
286
  if (TERMINAL_STATES.has(stateName)) {
224
287
  if (pendingTimer) { clearTimeout(pendingTimer); pendingTimer = null; }
225
288
  clearStallTimers();
289
+ clearDeepeningTimers();
226
290
  return flush(stateName);
227
291
  }
228
292
 
@@ -230,6 +294,12 @@ function createReactionManager({
230
294
  // doing *something*. Re-arm only if the new state is promotable
231
295
  // (no point arming over QUEUED/STALL/TIMEOUT itself).
232
296
  armStallTimers();
297
+ // rc.32: arm/clear deepening cascade. Only THINKING starts the
298
+ // 8s/20s progression; any other state (CODING / TOOL / etc)
299
+ // clears it (the visible state has moved on, deepening would be
300
+ // a regression).
301
+ if (stateName === 'THINKING') armDeepeningTimers();
302
+ else clearDeepeningTimers();
233
303
 
234
304
  // 0.8.0-rc.24: drop the 800ms throttle. Pre-rc.24, when a tool-
235
305
  // using turn fired QUEUED β†’ THINKING β†’ TOOL within a few ms,
@@ -251,6 +321,7 @@ function createReactionManager({
251
321
  const clear = async () => {
252
322
  if (pendingTimer) { clearTimeout(pendingTimer); pendingTimer = null; }
253
323
  clearStallTimers();
324
+ clearDeepeningTimers();
254
325
  if (currentEmoji == null) return;
255
326
  currentEmoji = null;
256
327
  // Same applyChain serialization as flush β€” clear() is a state
@@ -269,6 +340,7 @@ function createReactionManager({
269
340
  stopped = true;
270
341
  if (pendingTimer) { clearTimeout(pendingTimer); pendingTimer = null; }
271
342
  clearStallTimers();
343
+ clearDeepeningTimers();
272
344
  };
273
345
 
274
346
  // 0.8.0-rc.16: heartbeat β€” re-arm stall/freeze timers without
@@ -306,5 +378,7 @@ module.exports = {
306
378
  DEFAULT_THROTTLE_MS,
307
379
  DEFAULT_STALL_MS,
308
380
  DEFAULT_FREEZE_MS,
381
+ DEFAULT_THINKING_DEEPER_MS,
382
+ DEFAULT_THINKING_DEEPEST_MS,
309
383
  GENERIC_FALLBACKS,
310
384
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.8.0-rc.32",
3
+ "version": "0.8.0-rc.34",
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
@@ -2429,7 +2429,20 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
2429
2429
  // 0.7.4 (item G) voice-ack guard preserved: if πŸ‘‚ is up from
2430
2430
  // voice transcription, don't overwrite it. Let onFirstStream
2431
2431
  // promote to πŸ€” when Claude actually starts work.
2432
- if (!voiceAck.ackEmitted) {
2432
+ //
2433
+ // rc.32 second guard: skip THINKING if we're going to autosteer
2434
+ // this message. Pre-fix, autosteer messages briefly showed
2435
+ // πŸ€” β†’ ✍ (THINKING set here, then AUTOSTEERED set inside the
2436
+ // autosteer block ~50 lines below). The flash was visible to
2437
+ // users. Detect the autosteer pre-condition (SDK pm active AND
2438
+ // session in-flight AND chat hasn't opted out) and skip.
2439
+ const willAutosteer = pm.has(sessionKey)
2440
+ && pm.get(sessionKey)?.inFlight
2441
+ && pm.isSdkFor(sessionKey)
2442
+ && (chatConfig.autosteer != null
2443
+ ? chatConfig.autosteer !== false
2444
+ : config.bot?.autosteer !== false);
2445
+ if (!voiceAck.ackEmitted && !willAutosteer) {
2433
2446
  reactor.setState('THINKING');
2434
2447
  }
2435
2448
 
@@ -2524,7 +2537,19 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
2524
2537
  // (queue-head transition) flipped to THINKING the moment we wrote
2525
2538
  // stdin, even though Claude could spend hundreds of ms loading.
2526
2539
  // Result: long flat πŸ€” with nothing happening; users assumed stall.
2527
- onFirstStream: () => reactor.setState('THINKING'),
2540
+ //
2541
+ // rc.33: also stop typing here. Telegram's "typing…" indicator
2542
+ // shows for ~5s after the LAST sendChatAction tick. If we stop
2543
+ // typing at turn-end (~5-10s post final reply), the indicator
2544
+ // persists 5s past when the user sees the reply β€” confusing
2545
+ // ("bot replied AND is still typing?"). Stopping at the first
2546
+ // chunk means typing fades naturally as the streamer emits the
2547
+ // bubble. Reactions (THINKING/CODING/etc) take over as the
2548
+ // "still working" signal for long agent turns.
2549
+ onFirstStream: () => {
2550
+ reactor.setState('THINKING');
2551
+ stopTyping();
2552
+ },
2528
2553
  });
2529
2554
  const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
2530
2555