polygram 0.8.0-rc.2 → 0.8.0-rc.21

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.
@@ -29,19 +29,30 @@
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: ['✍', '🤔'] },
38
- DONE: { label: 'done', chain: ['👍'] },
39
- ERROR: { label: 'error', chain: ['🤯', '🤔'] },
40
- STALL: { label: 'stall', chain: ['🥱', '🤔'] },
41
- TIMEOUT: { label: 'timeout', chain: ['😨', '🤯'] },
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: ['✍', '🤔'] },
38
+ // 0.8.0-rc.11: terminal "your follow-up was incorporated into the
39
+ // in-flight turn" state. Used by polygram's autosteer block when a
40
+ // mid-turn user message is buffered for the next PostToolBatch
41
+ // injection.
42
+ AUTOSTEERED: { label: 'autosteered', chain: ['✍', '👀'] },
43
+ DONE: { label: 'done', chain: ['👍'] },
44
+ ERROR: { label: 'error', chain: ['🤯', '🤔'] },
45
+ STALL: { label: 'stall', chain: ['🥱', '🤔'] },
46
+ TIMEOUT: { label: 'timeout', chain: ['😨', '🤯'] },
42
47
  };
43
48
 
44
- const TERMINAL_STATES = new Set(['DONE', 'ERROR', 'TIMEOUT']);
49
+ // Terminal states bypass throttle, disarm stall promotion, and the
50
+ // reactor stays at this emoji until explicitly cleared. AUTOSTEERED
51
+ // is included so setState('AUTOSTEERED') flushes immediately
52
+ // (matters because the autosteer code path returns from
53
+ // handleMessage right after — we don't want the apply to be
54
+ // scheduled-and-cancelled by reactor.stop in the outer finally).
55
+ const TERMINAL_STATES = new Set(['DONE', 'ERROR', 'TIMEOUT', 'AUTOSTEERED']);
45
56
  const DEFAULT_THROTTLE_MS = 800;
46
57
  // 0.7.4 (item A): after this long with no setState() call (Claude is
47
58
  // silently chugging on a long tool / model latency), auto-flip to STALL
@@ -132,24 +143,40 @@ function createReactionManager({
132
143
  let stallTimer = null;
133
144
  let freezeTimer = null;
134
145
  let stopped = false;
146
+ // 0.8.0-rc.11: serialize Telegram setMessageReaction calls. Without
147
+ // this, multiple flush()es race at the network layer because each
148
+ // calls `await apply(emoji)` from a separate stack — Telegram
149
+ // processes them in arbitrary order and the FINAL visible state is
150
+ // whichever apply landed last. Symptom: 👀 stuck on autosteered
151
+ // messages when the QUEUED apply landed AFTER our explicit ✍ apply.
152
+ // Chaining all applies through `applyChain` guarantees they're sent
153
+ // to Telegram in setState() invocation order.
154
+ let applyChain = Promise.resolve();
135
155
  // States the auto-stall path may transition to. Once we've already
136
156
  // shown STALL or TIMEOUT we don't downgrade or rearm — only an
137
157
  // explicit setState() call (Claude resumed) can move us forward.
138
158
  const STALL_PROMOTABLE = new Set(['THINKING', 'CODING', 'WEB', 'TOOL', 'WRITING']);
139
159
 
140
160
  const flush = async (stateName) => {
141
- if (stopped) return;
161
+ if (stopped && !TERMINAL_STATES.has(stateName)) return;
142
162
  const spec = STATES[stateName];
143
163
  if (!spec) return;
144
164
  const emoji = resolveEmoji(spec.chain, availableEmojis);
145
165
  if (emoji === currentEmoji) return;
146
166
  currentEmoji = emoji;
147
167
  lastFlushTs = Date.now();
148
- try {
149
- await apply(emoji);
150
- } catch (err) {
151
- logError(`reaction apply failed (${stateName} → ${emoji}): ${err?.message || err}`);
152
- }
168
+ // Chain through applyChain so concurrent flushes are sent to
169
+ // Telegram serially in invocation order. Returning the chain
170
+ // promise lets callers await this specific flush completing.
171
+ const myApply = applyChain.then(async () => {
172
+ try {
173
+ await apply(emoji);
174
+ } catch (err) {
175
+ logError(`reaction apply failed (${stateName} → ${emoji}): ${err?.message || err}`);
176
+ }
177
+ });
178
+ applyChain = myApply;
179
+ return myApply;
153
180
  };
154
181
 
155
182
  const clearStallTimers = () => {
@@ -217,8 +244,16 @@ function createReactionManager({
217
244
  clearStallTimers();
218
245
  if (currentEmoji == null) return;
219
246
  currentEmoji = null;
220
- try { await apply(null); }
221
- catch (err) { logError(`reaction clear failed: ${err?.message || err}`); }
247
+ // Same applyChain serialization as flush — clear() is a state
248
+ // transition, just to "no emoji". Without chaining, a clear()
249
+ // racing with a pending apply (e.g. THINKING flush in flight)
250
+ // could land BEFORE that apply, leaving the emoji visible.
251
+ const myApply = applyChain.then(async () => {
252
+ try { await apply(null); }
253
+ catch (err) { logError(`reaction clear failed: ${err?.message || err}`); }
254
+ });
255
+ applyChain = myApply;
256
+ return myApply;
222
257
  };
223
258
 
224
259
  const stop = () => {
@@ -227,10 +262,26 @@ function createReactionManager({
227
262
  clearStallTimers();
228
263
  };
229
264
 
265
+ // 0.8.0-rc.16: heartbeat — re-arm stall/freeze timers without
266
+ // changing the visible emoji. Used by SDK pm's onStreamChunk
267
+ // callback so long text generation doesn't unfairly trip STALL
268
+ // (🥱) / TIMEOUT (😨) promotions after 10/30s of no explicit
269
+ // setState calls. Each text chunk is "I'm still here" evidence.
270
+ // Pre-rc.16, prolonged streaming with no tool use was reliably
271
+ // promoted to 🥱 within 10s even though the bot was producing
272
+ // output the whole time.
273
+ const heartbeat = () => {
274
+ if (stopped) return;
275
+ if (!STALL_PROMOTABLE.has(currentState)) return;
276
+ lastSetStateTs = Date.now();
277
+ armStallTimers();
278
+ };
279
+
230
280
  return {
231
281
  setState,
232
282
  clear,
233
283
  stop,
284
+ heartbeat,
234
285
  // Introspection for tests:
235
286
  get currentState() { return currentState; },
236
287
  get currentEmoji() { return currentEmoji; },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.8.0-rc.2",
3
+ "version": "0.8.0-rc.21",
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": {