polygram 0.8.0-rc.39 → 0.8.0-rc.40
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.
- package/.claude-plugin/plugin.json +1 -1
- package/lib/status-reactions.js +77 -8
- package/package.json +1 -1
- package/polygram.js +20 -0
|
@@ -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.
|
|
4
|
+
"version": "0.8.0-rc.40",
|
|
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",
|
package/lib/status-reactions.js
CHANGED
|
@@ -131,6 +131,12 @@ function classifyToolName(name) {
|
|
|
131
131
|
// nothing and leave the user wondering whether the bot is alive.
|
|
132
132
|
const GENERIC_FALLBACKS = ['👍', '👀', '🔥'];
|
|
133
133
|
|
|
134
|
+
// Module-scope sentinel for flush()'s `fromStateOverride` parameter.
|
|
135
|
+
// Distinguishes "caller didn't pass fromState" from "caller passed
|
|
136
|
+
// null (= reactor had no prior state)". Module-scoped so we don't
|
|
137
|
+
// allocate a fresh symbol on every flush call.
|
|
138
|
+
const FROM_STATE_UNSET = Symbol('flush.fromState-unset');
|
|
139
|
+
|
|
134
140
|
/**
|
|
135
141
|
* Resolve the best-available emoji from a chain given an allowlist.
|
|
136
142
|
* If allowlist is null/undefined, assume default-available set and
|
|
@@ -160,6 +166,20 @@ function resolveEmoji(chain, allowlist) {
|
|
|
160
166
|
* from getChat().available_reactions. Null/undefined = assume defaults.
|
|
161
167
|
* @param {number} [deps.throttleMs] minimum ms between non-terminal changes.
|
|
162
168
|
* @param {(msg: string) => void} [deps.logError]
|
|
169
|
+
* @param {(transition: object) => void} [deps.onStateChange]
|
|
170
|
+
* rc.39: called after every visible state transition (state OR
|
|
171
|
+
* emoji change). Payload shape:
|
|
172
|
+
* { fromState, toState, fromEmoji, toEmoji, source, ts }
|
|
173
|
+
* where `source` is one of:
|
|
174
|
+
* 'manual' — explicit setState() call
|
|
175
|
+
* 'cascade-deeper' — auto-promotion to THINKING_DEEPER
|
|
176
|
+
* 'cascade-deepest' — auto-promotion to THINKING_DEEPEST
|
|
177
|
+
* 'stall-timer' — auto STALL after stallMs
|
|
178
|
+
* 'freeze-timer' — auto FREEZE after freezeMs
|
|
179
|
+
* 'clear' — clear() call (toEmoji=null)
|
|
180
|
+
* Used by polygram to emit `reactor-state` events to the events
|
|
181
|
+
* table for forensic post-hoc reconstruction of any reaction
|
|
182
|
+
* anomaly. Must be cheap + sync — fired in the hot setState path.
|
|
163
183
|
*/
|
|
164
184
|
function createReactionManager({
|
|
165
185
|
apply,
|
|
@@ -170,6 +190,7 @@ function createReactionManager({
|
|
|
170
190
|
thinkingDeeperMs = DEFAULT_THINKING_DEEPER_MS,
|
|
171
191
|
thinkingDeepestMs = DEFAULT_THINKING_DEEPEST_MS,
|
|
172
192
|
logError = () => {},
|
|
193
|
+
onStateChange = null,
|
|
173
194
|
} = {}) {
|
|
174
195
|
if (typeof apply !== 'function') throw new Error('apply function required');
|
|
175
196
|
let currentState = null;
|
|
@@ -202,14 +223,37 @@ function createReactionManager({
|
|
|
202
223
|
'CODING', 'WEB', 'TOOL', 'WRITING',
|
|
203
224
|
]);
|
|
204
225
|
|
|
205
|
-
const flush = async (stateName) => {
|
|
226
|
+
const flush = async (stateName, source = 'manual', fromStateOverride = FROM_STATE_UNSET) => {
|
|
206
227
|
if (stopped && !TERMINAL_STATES.has(stateName)) return;
|
|
207
228
|
const spec = STATES[stateName];
|
|
208
229
|
if (!spec) return;
|
|
209
230
|
const emoji = resolveEmoji(spec.chain, availableEmojis);
|
|
210
231
|
if (emoji === currentEmoji) return;
|
|
232
|
+
// For telemetry: prefer the caller-supplied fromState override
|
|
233
|
+
// (setState/cascade timers swap currentState BEFORE calling flush;
|
|
234
|
+
// we want the PRE-swap value in the event, not the post-swap
|
|
235
|
+
// self-loop). Sentinel symbol distinguishes "no override" from
|
|
236
|
+
// a legitimate `null` (= reactor had no prior state, e.g. very
|
|
237
|
+
// first setState in a fresh handleMessage).
|
|
238
|
+
const fromState = fromStateOverride === FROM_STATE_UNSET ? currentState : fromStateOverride;
|
|
239
|
+
const fromEmoji = currentEmoji;
|
|
211
240
|
currentEmoji = emoji;
|
|
212
241
|
lastFlushTs = Date.now();
|
|
242
|
+
// rc.39: emit telemetry on the visible-change moment. Fired
|
|
243
|
+
// synchronously so the events.table row's ts ordering matches
|
|
244
|
+
// the ordering of setMessageReaction calls. Wrapped in try/catch
|
|
245
|
+
// so a buggy onStateChange can't break the reactor.
|
|
246
|
+
if (typeof onStateChange === 'function') {
|
|
247
|
+
try {
|
|
248
|
+
onStateChange({
|
|
249
|
+
fromState, toState: stateName,
|
|
250
|
+
fromEmoji, toEmoji: emoji,
|
|
251
|
+
source, ts: lastFlushTs,
|
|
252
|
+
});
|
|
253
|
+
} catch (err) {
|
|
254
|
+
logError(`onStateChange threw: ${err?.message || err}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
213
257
|
// Chain through applyChain so concurrent flushes are sent to
|
|
214
258
|
// Telegram serially in invocation order. Returning the chain
|
|
215
259
|
// promise lets callers await this specific flush completing.
|
|
@@ -249,9 +293,12 @@ function createReactionManager({
|
|
|
249
293
|
if (currentState !== 'THINKING') return;
|
|
250
294
|
// Promote without going through setState — we want the visual
|
|
251
295
|
// change but NOT to reset the deepest timer (which keeps
|
|
252
|
-
// counting from the original THINKING start).
|
|
296
|
+
// counting from the original THINKING start). Pass the PRE-swap
|
|
297
|
+
// state to flush() so the telemetry event records the actual
|
|
298
|
+
// transition (THINKING → THINKING_DEEPER), not a self-loop.
|
|
299
|
+
const before = currentState;
|
|
253
300
|
currentState = 'THINKING_DEEPER';
|
|
254
|
-
flush('THINKING_DEEPER');
|
|
301
|
+
flush('THINKING_DEEPER', 'cascade-deeper', before);
|
|
255
302
|
}, thinkingDeeperMs);
|
|
256
303
|
deeperTimer.unref?.();
|
|
257
304
|
deepestTimer = setTimeout(() => {
|
|
@@ -259,8 +306,9 @@ function createReactionManager({
|
|
|
259
306
|
if (stopped) return;
|
|
260
307
|
// Promote from THINKING or THINKING_DEEPER (NOT from CODING etc).
|
|
261
308
|
if (currentState !== 'THINKING' && currentState !== 'THINKING_DEEPER') return;
|
|
309
|
+
const before = currentState;
|
|
262
310
|
currentState = 'THINKING_DEEPEST';
|
|
263
|
-
flush('THINKING_DEEPEST');
|
|
311
|
+
flush('THINKING_DEEPEST', 'cascade-deepest', before);
|
|
264
312
|
}, thinkingDeepestMs);
|
|
265
313
|
deepestTimer.unref?.();
|
|
266
314
|
};
|
|
@@ -275,13 +323,13 @@ function createReactionManager({
|
|
|
275
323
|
// promotable state in the interim.
|
|
276
324
|
if (stopped || TERMINAL_STATES.has(currentState)) return;
|
|
277
325
|
if (!STALL_PROMOTABLE.has(currentState)) return;
|
|
278
|
-
flush('STALL');
|
|
326
|
+
flush('STALL', 'stall-timer');
|
|
279
327
|
}, stallMs);
|
|
280
328
|
stallTimer.unref?.();
|
|
281
329
|
freezeTimer = setTimeout(() => {
|
|
282
330
|
freezeTimer = null;
|
|
283
331
|
if (stopped || TERMINAL_STATES.has(currentState)) return;
|
|
284
|
-
flush('TIMEOUT');
|
|
332
|
+
flush('TIMEOUT', 'freeze-timer');
|
|
285
333
|
}, freezeMs);
|
|
286
334
|
freezeTimer.unref?.();
|
|
287
335
|
};
|
|
@@ -289,6 +337,12 @@ function createReactionManager({
|
|
|
289
337
|
const setState = (stateName) => {
|
|
290
338
|
if (stopped) return;
|
|
291
339
|
if (!STATES[stateName]) return;
|
|
340
|
+
// rc.39: capture pre-swap state for telemetry. Pre-rc.39 the
|
|
341
|
+
// currentState was set BEFORE flush, so the onStateChange event's
|
|
342
|
+
// fromState always equalled toState (looked like a self-loop).
|
|
343
|
+
// Pass `before` through to flush so the audit trail records the
|
|
344
|
+
// real transition.
|
|
345
|
+
const before = currentState;
|
|
292
346
|
currentState = stateName;
|
|
293
347
|
lastSetStateTs = Date.now();
|
|
294
348
|
|
|
@@ -298,7 +352,7 @@ function createReactionManager({
|
|
|
298
352
|
if (pendingTimer) { clearTimeout(pendingTimer); pendingTimer = null; }
|
|
299
353
|
clearStallTimers();
|
|
300
354
|
clearDeepeningTimers();
|
|
301
|
-
return flush(stateName);
|
|
355
|
+
return flush(stateName, 'manual', before);
|
|
302
356
|
}
|
|
303
357
|
|
|
304
358
|
// Any explicit setState resets the stall clock — Claude clearly is
|
|
@@ -326,7 +380,7 @@ function createReactionManager({
|
|
|
326
380
|
// round-trip. User sees 👀 → 🤔 → 🔥 progress, smoothly
|
|
327
381
|
// paced by network latency.
|
|
328
382
|
if (pendingTimer) { clearTimeout(pendingTimer); pendingTimer = null; }
|
|
329
|
-
return flush(stateName);
|
|
383
|
+
return flush(stateName, 'manual', before);
|
|
330
384
|
};
|
|
331
385
|
|
|
332
386
|
const clear = async () => {
|
|
@@ -334,7 +388,22 @@ function createReactionManager({
|
|
|
334
388
|
clearStallTimers();
|
|
335
389
|
clearDeepeningTimers();
|
|
336
390
|
if (currentEmoji == null) return;
|
|
391
|
+
const fromState = currentState;
|
|
392
|
+
const fromEmoji = currentEmoji;
|
|
337
393
|
currentEmoji = null;
|
|
394
|
+
// rc.39: emit telemetry on clear (toEmoji=null is the "we let go
|
|
395
|
+
// of the message" signal). Mirrors the flush() path.
|
|
396
|
+
if (typeof onStateChange === 'function') {
|
|
397
|
+
try {
|
|
398
|
+
onStateChange({
|
|
399
|
+
fromState, toState: null,
|
|
400
|
+
fromEmoji, toEmoji: null,
|
|
401
|
+
source: 'clear', ts: Date.now(),
|
|
402
|
+
});
|
|
403
|
+
} catch (err) {
|
|
404
|
+
logError(`onStateChange threw: ${err?.message || err}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
338
407
|
// Same applyChain serialization as flush — clear() is a state
|
|
339
408
|
// transition, just to "no emoji". Without chaining, a clear()
|
|
340
409
|
// racing with a pending apply (e.g. THINKING flush in flight)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.8.0-rc.
|
|
3
|
+
"version": "0.8.0-rc.40",
|
|
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
|
@@ -2425,6 +2425,26 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
2425
2425
|
},
|
|
2426
2426
|
availableEmojis,
|
|
2427
2427
|
logError: (m) => console.error(`[${label}] ${m}`),
|
|
2428
|
+
// rc.39: emit reactor-state events for forensic post-hoc
|
|
2429
|
+
// reconstruction of any reaction anomaly (stuck reactions, dual
|
|
2430
|
+
// emojis, unexpected ERROR transitions, etc.). Sync callback —
|
|
2431
|
+
// logEvent is best-effort and never throws. One row per visible
|
|
2432
|
+
// change moment; cascade/stall/freeze auto-promotions get
|
|
2433
|
+
// their own `source` value so we can tell apart manual setState
|
|
2434
|
+
// calls from timer-driven transitions.
|
|
2435
|
+
onStateChange: ({ fromState, toState, fromEmoji, toEmoji, source, ts }) => {
|
|
2436
|
+
logEvent('reactor-state', {
|
|
2437
|
+
chat_id: chatId,
|
|
2438
|
+
msg_id: msg.message_id,
|
|
2439
|
+
session_key: sessionKey,
|
|
2440
|
+
from_state: fromState,
|
|
2441
|
+
to_state: toState,
|
|
2442
|
+
from_emoji: fromEmoji,
|
|
2443
|
+
to_emoji: toEmoji,
|
|
2444
|
+
source,
|
|
2445
|
+
ts,
|
|
2446
|
+
});
|
|
2447
|
+
},
|
|
2428
2448
|
});
|
|
2429
2449
|
// rc.32: skip QUEUED (👀) entirely for first-message-in-chain. Go
|
|
2430
2450
|
// straight to THINKING (🤔). The 👀 → 🤔 two-hop didn't add
|