polygram 0.10.0-rc.47 → 0.10.0-rc.50
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/error/classify.js +14 -0
- package/lib/process/tmux-process.js +74 -0
- package/lib/process/turn-phase.js +6 -1
- package/lib/sdk/callbacks.js +151 -9
- package/lib/telegram/api.js +11 -0
- package/package.json +1 -1
- package/polygram.js +8 -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.10.0-rc.
|
|
4
|
+
"version": "0.10.0-rc.50",
|
|
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",
|
package/lib/error/classify.js
CHANGED
|
@@ -74,6 +74,19 @@ const PATTERNS = {
|
|
|
74
74
|
// image-failure verb co-located with "image" or "photo".
|
|
75
75
|
imageProcess: /(could not process|cannot process|failed to (process|load|decode)|unsupported|invalid|corrupt(?:ed)?)[^\n]{0,80}\b(image|photo)\b|\b(image|photo)\b[^\n]{0,80}(could not process|failed to (process|load|decode)|is (invalid|corrupted|unsupported))/i,
|
|
76
76
|
|
|
77
|
+
// rc.50: lib/process-manager.js `_awaitLruSlot` rejects when no
|
|
78
|
+
// backend slot is available within `lruWaitMs` (default 5 min).
|
|
79
|
+
// Symptom of an upstream wedge (an inFlight tmux process can't be
|
|
80
|
+
// LRU-evicted; if its turn is wedged the slot stays held for the
|
|
81
|
+
// whole idle-ceiling). Production 2026-05-24 (shumorobot Music):
|
|
82
|
+
// hit twice during the wedged-"yes" turn that took 30 min to
|
|
83
|
+
// surface. Pre-rc.50 the user got `Hit a snag: lru wait timed
|
|
84
|
+
// out after 300000ms` which is opaque; now they get a hint that
|
|
85
|
+
// it's a busy/queued condition and a retry will probably work.
|
|
86
|
+
// Placed BEFORE the generic `timeout` pattern so the LRU phrasing
|
|
87
|
+
// wins over the broader timeout match.
|
|
88
|
+
lruWaitTimeout: /lru wait timed out/i,
|
|
89
|
+
|
|
77
90
|
// rc.47: tmux backend's TMUX_TURN_TIMEOUT after H3 idle-ceiling fired.
|
|
78
91
|
// Production wedge 2026-05-24 msg 1020: a Bash tool's `PreToolUse`
|
|
79
92
|
// fired but `PostToolUse` never came — claude waited forever for the
|
|
@@ -115,6 +128,7 @@ const USER_MESSAGES = {
|
|
|
115
128
|
missingToolInput: '⚠️ Session history looks corrupted. Try /new.',
|
|
116
129
|
imageProcess: '🖼 One of the images in this conversation can\'t be re-processed by Claude — likely an older one in the history. Starting a fresh session for this chat.',
|
|
117
130
|
tmuxToolWedge: '🔧 A tool didn\'t return in time — I cut it off. Most often this is a Bash command waiting on something external (a server, a file lock, an interactive prompt). Try resending; if it happens again, break the task into smaller steps.',
|
|
131
|
+
lruWaitTimeout: '⏳ Other chats are busy — couldn\'t free up a backend slot in time. Try resending in a moment; if it keeps happening, an operator may need to raise the warm-process cap.',
|
|
118
132
|
timeout: '⏳ I went quiet too long without finishing. Try resending or simplifying.',
|
|
119
133
|
format: '⚠️ Invalid request format. Try rephrasing or /new.',
|
|
120
134
|
// Used both for in-flight retry attempts AND for the post-retry-failed
|
|
@@ -251,6 +251,26 @@ const DEFAULT_READY_DEBUG_QUIET_MS = 1000;
|
|
|
251
251
|
// combined with the "Resuming the full session" rationale.
|
|
252
252
|
const SESSION_AGE_PROMPT_RE = /Resuming the full session.*Resume from summary/s;
|
|
253
253
|
|
|
254
|
+
// rc.48 (production 2026-05-24 post-rc.47 deploy, shumorobot HOME):
|
|
255
|
+
// rc.43's session-age menu auto-dismiss + rc.44's deadline reset
|
|
256
|
+
// gave `/compact` a fresh `readyTimeoutMs` (120 s) budget — but on
|
|
257
|
+
// an 8h+ aged session compact STILL took >120 s (observed pane:
|
|
258
|
+
// `✶ Compacting conversation… (1m 58s) ▰▰…▱ 73%` at the timeout
|
|
259
|
+
// moment). The single one-shot reset is too coarse.
|
|
260
|
+
//
|
|
261
|
+
// Fix: while `/compact` is in progress AND making observable progress
|
|
262
|
+
// (the captured signature changes between polls — elapsed time,
|
|
263
|
+
// progress bar, percentage), keep extending the deadline. Compact
|
|
264
|
+
// that genuinely stalls (same signature across multiple polls) is
|
|
265
|
+
// still allowed to time out so a real wedge isn't masked.
|
|
266
|
+
//
|
|
267
|
+
// Match captures the distinctive header `Compacting conversation…`
|
|
268
|
+
// (with ellipsis or three dots) plus everything up to the
|
|
269
|
+
// percentage, giving a signature that changes as compact advances:
|
|
270
|
+
// elapsed wall-time (1m 58s → 1m 59s), progress bar density
|
|
271
|
+
// (▰…▱ → ▰▰…▱), AND percentage (73% → 74%).
|
|
272
|
+
const COMPACT_PROGRESS_RE = /Compacting (?:conversation|context)(?:…|\.\.\.)[\s\S]{0,300}?\d+%/i;
|
|
273
|
+
|
|
254
274
|
// R7: sentinel returned by _awaitTurnComplete when its poll loop is
|
|
255
275
|
// stopped by the caller's absolute-deadline abort (rather than by a
|
|
256
276
|
// real READY quiescence or its own internal timeout). _runTurn maps
|
|
@@ -2674,6 +2694,12 @@ class TmuxProcess extends Process {
|
|
|
2674
2694
|
// prompt this wait, so we don't fire Enter every poll if claude
|
|
2675
2695
|
// is slow to re-render after dismissing it.
|
|
2676
2696
|
let sessionAgePromptDismissed = false;
|
|
2697
|
+
// rc.48: track the most recently observed `/compact` progress
|
|
2698
|
+
// signature so we can extend the deadline ONLY when compact is
|
|
2699
|
+
// making real progress. A genuinely-stalled compact (same
|
|
2700
|
+
// signature N polls in a row) will not extend, so the wedge
|
|
2701
|
+
// safety net is preserved.
|
|
2702
|
+
let lastCompactSignature = null;
|
|
2677
2703
|
if (this.pollScheduler) this.pollScheduler.acquire();
|
|
2678
2704
|
try {
|
|
2679
2705
|
while (this._now() < deadline) {
|
|
@@ -2716,6 +2742,13 @@ class TmuxProcess extends Process {
|
|
|
2716
2742
|
// the menu but the post-dismiss wait still timed out.
|
|
2717
2743
|
// Reset the deadline so compact has a full
|
|
2718
2744
|
// `readyTimeoutMs` budget from dismissal time.
|
|
2745
|
+
//
|
|
2746
|
+
// rc.48: the rc.44 one-shot reset isn't enough either —
|
|
2747
|
+
// observed on shumorobot post-rc.47 deploy with an even
|
|
2748
|
+
// older session, compact ran >120 s and still timed out.
|
|
2749
|
+
// The new compact-progress block below extends the deadline
|
|
2750
|
+
// each poll that observes progress, with the rc.44 reset
|
|
2751
|
+
// here as the initial budget for the FIRST compact tick.
|
|
2719
2752
|
deadline = this._now() + this.readyTimeoutMs;
|
|
2720
2753
|
// Reset readiness clock + prev-pane so the menu's content
|
|
2721
2754
|
// doesn't satisfy the byte-stability check while claude is
|
|
@@ -2725,6 +2758,45 @@ class TmuxProcess extends Process {
|
|
|
2725
2758
|
await this._waitForNextTick();
|
|
2726
2759
|
continue;
|
|
2727
2760
|
}
|
|
2761
|
+
// rc.48: `/compact` (triggered by the session-age dismissal,
|
|
2762
|
+
// OR by any future code path that runs compact during startup)
|
|
2763
|
+
// shows a progress UI in the pane. If we can SEE compact making
|
|
2764
|
+
// progress (signature changes between polls), extend the
|
|
2765
|
+
// deadline so compact has unbounded time to finish AS LONG AS
|
|
2766
|
+
// it's progressing. A stalled compact (same signature across
|
|
2767
|
+
// multiple polls) is still bounded by the existing deadline —
|
|
2768
|
+
// the wedge safety net is preserved.
|
|
2769
|
+
const compactMatch = lastBuf.match(COMPACT_PROGRESS_RE);
|
|
2770
|
+
if (compactMatch) {
|
|
2771
|
+
const currentSignature = compactMatch[0];
|
|
2772
|
+
if (currentSignature !== lastCompactSignature) {
|
|
2773
|
+
// Compact made observable progress (elapsed time advanced,
|
|
2774
|
+
// progress bar grew, or percentage incremented). Extend
|
|
2775
|
+
// the deadline and emit a forensics event so a soak can
|
|
2776
|
+
// count how often this kicks in.
|
|
2777
|
+
lastCompactSignature = currentSignature;
|
|
2778
|
+
deadline = this._now() + this.readyTimeoutMs;
|
|
2779
|
+
readySinceAt = null;
|
|
2780
|
+
prevBuf = null;
|
|
2781
|
+
this.emit('compact-progress', {
|
|
2782
|
+
sessionId: this.claudeSessionId,
|
|
2783
|
+
backend: 'tmux',
|
|
2784
|
+
});
|
|
2785
|
+
}
|
|
2786
|
+
// Whether progressing or stalled, skip the ready-hint check
|
|
2787
|
+
// this poll — compact's progress UI is on the pane, not the
|
|
2788
|
+
// ready hint. The next poll re-evaluates.
|
|
2789
|
+
await this._waitForNextTick();
|
|
2790
|
+
continue;
|
|
2791
|
+
} else if (lastCompactSignature != null) {
|
|
2792
|
+
// Compact finished (no longer visible in pane). Clear the
|
|
2793
|
+
// tracker; the normal ready-check below resumes. Do NOT
|
|
2794
|
+
// reset the deadline here — the existing deadline still has
|
|
2795
|
+
// its last-extended budget, which is the right window for
|
|
2796
|
+
// claude to repaint the ready hint after compact returns
|
|
2797
|
+
// control.
|
|
2798
|
+
lastCompactSignature = null;
|
|
2799
|
+
}
|
|
2728
2800
|
// Ready ⇔ the hint is on the pane AND the pane is identical to
|
|
2729
2801
|
// the previous poll (the MCP-loading repaint storm has
|
|
2730
2802
|
// stopped). The first poll has no previous buffer to compare,
|
|
@@ -3354,4 +3426,6 @@ module.exports = {
|
|
|
3354
3426
|
CLAUDE_CLI_PINNED_VERSION,
|
|
3355
3427
|
// rc.43 — exported for unit-test coverage of the menu pattern.
|
|
3356
3428
|
SESSION_AGE_PROMPT_RE,
|
|
3429
|
+
// rc.48 — exported for unit-test coverage of the /compact progress pattern.
|
|
3430
|
+
COMPACT_PROGRESS_RE,
|
|
3357
3431
|
};
|
|
@@ -117,7 +117,12 @@ const ALLOWED_TRANSITIONS = Object.freeze({
|
|
|
117
117
|
// PASTED_UNCONFIRMED intermediate. rc.35 production caught this as
|
|
118
118
|
// log noise once Commit 3 (`_awaitSettle`) started consuming
|
|
119
119
|
// predicate fields more strictly.
|
|
120
|
-
|
|
120
|
+
// rc.49 (shumorobot HOME 2026-05-24): SUBMITTED is reachable
|
|
121
|
+
// directly from QUEUED when the TUI's `jsonl:user-message` event
|
|
122
|
+
// races ahead of the `paste:returned` event-loop callback that
|
|
123
|
+
// normally lands first and advances QUEUED → PASTED_UNCONFIRMED.
|
|
124
|
+
// Symmetric to the rc.35→rc.36 PASTE_PARKED edge; same fix shape.
|
|
125
|
+
[TurnPhase.QUEUED]: new Set([TurnPhase.PASTED_UNCONFIRMED, TurnPhase.PASTE_PARKED, TurnPhase.SUBMITTED, TurnPhase.FAILED]),
|
|
121
126
|
[TurnPhase.PASTED_UNCONFIRMED]: new Set([TurnPhase.PASTE_PARKED, TurnPhase.SUBMITTED, TurnPhase.STREAMING, TurnPhase.TOOL_RUNNING, TurnPhase.SUBAGENT_RUNNING, TurnPhase.APPROVAL_PENDING, TurnPhase.QUIET, TurnPhase.DONE, TurnPhase.FAILED]),
|
|
122
127
|
[TurnPhase.PASTE_PARKED]: new Set([TurnPhase.SUBMITTED, TurnPhase.STREAMING, TurnPhase.TOOL_RUNNING, TurnPhase.SUBAGENT_RUNNING, TurnPhase.APPROVAL_PENDING, TurnPhase.QUIET, TurnPhase.DONE, TurnPhase.FAILED]),
|
|
123
128
|
[TurnPhase.SUBMITTED]: new Set([TurnPhase.STREAMING, TurnPhase.TOOL_RUNNING, TurnPhase.SUBAGENT_RUNNING, TurnPhase.APPROVAL_PENDING, TurnPhase.QUIET, TurnPhase.DONE, TurnPhase.FAILED]),
|
package/lib/sdk/callbacks.js
CHANGED
|
@@ -39,6 +39,21 @@ function createSdkCallbacks({
|
|
|
39
39
|
getChatIdFromKey,
|
|
40
40
|
getThreadIdFromKey,
|
|
41
41
|
logger = console,
|
|
42
|
+
// rc.50 (production 2026-05-24, shumorobot HOME): the
|
|
43
|
+
// autonomous-wakeup path (`onAutonomousAssistantMessage` below)
|
|
44
|
+
// was bypassing parseResponse + sanitizeReply + deliverReplies
|
|
45
|
+
// entirely, so `[sticker:pumped]` showed up as literal text and
|
|
46
|
+
// `No response requested.` leaked through (the rc.45 sanitizer
|
|
47
|
+
// only protected the regular reply path). Inject the full
|
|
48
|
+
// pipeline so autonomous messages get the same treatment as
|
|
49
|
+
// streamed bot replies. All optional — fallback below preserves
|
|
50
|
+
// the pre-rc.50 raw-sendMessage behavior when any dep is unwired
|
|
51
|
+
// (e.g. an old caller of createSdkCallbacks).
|
|
52
|
+
parseResponse = null,
|
|
53
|
+
sanitizeAssistantReply = null,
|
|
54
|
+
chunkMarkdownText = null,
|
|
55
|
+
deliverReplies = null,
|
|
56
|
+
chunkBudget = 3500,
|
|
42
57
|
} = {}) {
|
|
43
58
|
// rc.9: typing-indicator state for autosteer NEW-TURN extraction.
|
|
44
59
|
// Keyed by sessionKey. extra-turn-started installs a 4-second
|
|
@@ -202,6 +217,25 @@ function createSdkCallbacks({
|
|
|
202
217
|
// ScheduleWakeup case where the agent self-fires without an
|
|
203
218
|
// inbound user message. Best-effort send: failures are logged
|
|
204
219
|
// but don't propagate.
|
|
220
|
+
//
|
|
221
|
+
// rc.50 (production 2026-05-24): pre-fix this called
|
|
222
|
+
// `tg(bot, 'sendMessage', { text: <raw> })` directly, bypassing
|
|
223
|
+
// parseResponse + sanitizeReply + deliverReplies. Two
|
|
224
|
+
// user-visible bugs as a result:
|
|
225
|
+
// - `[sticker:NAME]` and `[react:EMOJI]` tags appeared as
|
|
226
|
+
// literal text in the chat (no sticker bubble fired, no
|
|
227
|
+
// reaction set).
|
|
228
|
+
// - The CLI canned-string leak `No response requested.`
|
|
229
|
+
// reached Telegram (the rc.45 sanitizer was wired into the
|
|
230
|
+
// regular reply path only — this path was unprotected).
|
|
231
|
+
// Fix: route the autonomous text through the same pipeline as
|
|
232
|
+
// bot-reply-stream — parseResponse → sanitize → chunk →
|
|
233
|
+
// deliverReplies → send inline stickers/sticker. No user-msg to
|
|
234
|
+
// reply to or react against; inline reactions are logged-and-
|
|
235
|
+
// dropped instead of applied. Optional deps: if the caller of
|
|
236
|
+
// createSdkCallbacks didn't inject the pipeline pieces, fall
|
|
237
|
+
// back to the pre-rc.50 raw-sendMessage path (preserves
|
|
238
|
+
// back-compat for older test harnesses).
|
|
205
239
|
onAutonomousAssistantMessage: (sessionKey, msg /* , entry */) => {
|
|
206
240
|
try {
|
|
207
241
|
// Backend-shape normalization: SDK emits the raw SDKMessage
|
|
@@ -218,21 +252,129 @@ function createSdkCallbacks({
|
|
|
218
252
|
logger.error?.(`[${botName}] autonomous wakeup: bot not ready, dropping ${text.length} chars`);
|
|
219
253
|
return;
|
|
220
254
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
255
|
+
|
|
256
|
+
// Pre-rc.50 fallback: any pipeline dep missing → raw send.
|
|
257
|
+
// Used in tests or partial-wire scenarios. Production wires
|
|
258
|
+
// all five (parseResponse, sanitizeAssistantReply,
|
|
259
|
+
// chunkMarkdownText, deliverReplies, chunkBudget).
|
|
260
|
+
const haveFullPipeline = (typeof parseResponse === 'function')
|
|
261
|
+
&& (typeof sanitizeAssistantReply === 'function')
|
|
262
|
+
&& (typeof chunkMarkdownText === 'function')
|
|
263
|
+
&& (typeof deliverReplies === 'function');
|
|
264
|
+
|
|
265
|
+
if (!haveFullPipeline) {
|
|
266
|
+
const params = {
|
|
267
|
+
chat_id: chatId,
|
|
268
|
+
text,
|
|
269
|
+
...(Number.isInteger(threadId) && { message_thread_id: threadId }),
|
|
270
|
+
};
|
|
271
|
+
tg(bot, 'sendMessage', params,
|
|
272
|
+
{ source: 'autonomous-wakeup', botName }).catch((err) => {
|
|
273
|
+
logger.error?.(`[${botName}] autonomous wakeup send failed: ${err.message}`);
|
|
274
|
+
});
|
|
275
|
+
logEvent('autonomous-wakeup-message', {
|
|
276
|
+
chat_id: chatId,
|
|
277
|
+
session_key: sessionKey,
|
|
278
|
+
thread_id: threadIdRaw,
|
|
279
|
+
text_len: text.length,
|
|
280
|
+
pipeline: 'raw-fallback',
|
|
230
281
|
});
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Full pipeline (mirrors polygram.js handleMessage's
|
|
286
|
+
// bot-reply-stream branch). Don't await — keep the pm-sdk
|
|
287
|
+
// event loop unblocked; surface failures via .catch.
|
|
288
|
+
(async () => {
|
|
289
|
+
const parsed = parseResponse(text);
|
|
290
|
+
if (parsed.text) {
|
|
291
|
+
const sanitized = sanitizeAssistantReply(parsed.text);
|
|
292
|
+
if (sanitized.replaced) {
|
|
293
|
+
logEvent('canned-reply-suppressed', {
|
|
294
|
+
chat_id: chatId,
|
|
295
|
+
msg_id: null, // no inbound msg
|
|
296
|
+
original: sanitized.original,
|
|
297
|
+
source: 'autonomous-wakeup',
|
|
298
|
+
});
|
|
299
|
+
parsed.text = sanitized.text;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Text first (so any follow-up stickers read as
|
|
304
|
+
// punctuation, matching the bot-reply-stream order).
|
|
305
|
+
if (parsed.text) {
|
|
306
|
+
const chunks = chunkMarkdownText(parsed.text, chunkBudget);
|
|
307
|
+
await deliverReplies({
|
|
308
|
+
bot,
|
|
309
|
+
send: (b, method, params, m) => tg(b, method, params, m),
|
|
310
|
+
chatId,
|
|
311
|
+
threadId,
|
|
312
|
+
chunks,
|
|
313
|
+
replyToMessageId: null, // no inbound msg
|
|
314
|
+
meta: { source: 'autonomous-wakeup', botName },
|
|
315
|
+
logger,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Solo-sticker path (parseResponse returns a single
|
|
320
|
+
// sticker when the WHOLE text was just `[sticker:NAME]`).
|
|
321
|
+
if (parsed.sticker) {
|
|
322
|
+
await tg(bot, 'sendSticker', {
|
|
323
|
+
chat_id: chatId,
|
|
324
|
+
sticker: parsed.sticker,
|
|
325
|
+
...(Number.isInteger(threadId) && { message_thread_id: threadId }),
|
|
326
|
+
}, {
|
|
327
|
+
source: 'autonomous-wakeup-sticker', botName,
|
|
328
|
+
stickerName: parsed.stickerLabel || null,
|
|
329
|
+
}).catch((err) => {
|
|
330
|
+
logger.error?.(`[${botName}] autonomous-wakeup sendSticker failed: ${err.message}`);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Inline stickers (parsed.stickers[] when the text
|
|
335
|
+
// embedded one or more `[sticker:NAME]` tags). Send
|
|
336
|
+
// sequentially so order matches the order in the text.
|
|
337
|
+
for (const s of (parsed.stickers || [])) {
|
|
338
|
+
try {
|
|
339
|
+
await tg(bot, 'sendSticker', {
|
|
340
|
+
chat_id: chatId,
|
|
341
|
+
sticker: s.fileId,
|
|
342
|
+
...(Number.isInteger(threadId) && { message_thread_id: threadId }),
|
|
343
|
+
}, {
|
|
344
|
+
source: 'autonomous-wakeup-inline-sticker', botName,
|
|
345
|
+
stickerName: s.name,
|
|
346
|
+
});
|
|
347
|
+
} catch (err) {
|
|
348
|
+
logger.error?.(`[${botName}] autonomous-wakeup inline sendSticker(${s.name}) failed: ${err.message}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Reactions: parsed.reaction (solo) and parsed.reactions[]
|
|
353
|
+
// (inline). Autonomous-wakeup has no target msg to react
|
|
354
|
+
// against (the message isn't replying to anything). Log
|
|
355
|
+
// and drop so this surfaces in forensics if a future agent
|
|
356
|
+
// starts using react-tags in autonomous output.
|
|
357
|
+
const allReactions = [
|
|
358
|
+
...(parsed.reaction ? [parsed.reaction] : []),
|
|
359
|
+
...(parsed.reactions || []),
|
|
360
|
+
];
|
|
361
|
+
if (allReactions.length > 0) {
|
|
362
|
+
logEvent('autonomous-wakeup-reactions-dropped', {
|
|
363
|
+
chat_id: chatId,
|
|
364
|
+
session_key: sessionKey,
|
|
365
|
+
dropped: allReactions,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
})().catch((err) => {
|
|
369
|
+
logger.error?.(`[${botName}] autonomous wakeup pipeline failed: ${err.message}`);
|
|
370
|
+
});
|
|
371
|
+
|
|
231
372
|
logEvent('autonomous-wakeup-message', {
|
|
232
373
|
chat_id: chatId,
|
|
233
374
|
session_key: sessionKey,
|
|
234
375
|
thread_id: threadIdRaw,
|
|
235
376
|
text_len: text.length,
|
|
377
|
+
pipeline: 'full',
|
|
236
378
|
});
|
|
237
379
|
} catch (err) {
|
|
238
380
|
logger.error?.(`[${botName}] autonomous wakeup handler: ${err.message}`);
|
package/lib/telegram/api.js
CHANGED
|
@@ -93,6 +93,17 @@ const METHODS_WITHOUT_MSG = new Set([
|
|
|
93
93
|
'deleteMessage',
|
|
94
94
|
'editMessageReplyMarkup',
|
|
95
95
|
'editMessageText',
|
|
96
|
+
// rc.49 (production wedge 2026-05-24): sendChatAction is the
|
|
97
|
+
// typing indicator. lib/sdk/callbacks.js fires it every 4s during
|
|
98
|
+
// a turn. Telegram returns `true` (boolean, not a message object)
|
|
99
|
+
// — `res?.message_id ?? 0` evaluates to 0, so markOutboundSent
|
|
100
|
+
// would UPDATE the row to (chat_id, msg_id=0). The 2nd+ tick in
|
|
101
|
+
// the same chat then UNIQUE-collides on (chat_id, 0), leaving
|
|
102
|
+
// pending rows stranded. shumorobot post-rc.48 deploy: shutdown
|
|
103
|
+
// drain reported "marked 236 stale pending rows as failed" — all
|
|
104
|
+
// 236 had source='extra-turn-typing' and text=''. Excluding
|
|
105
|
+
// sendChatAction from the row-tracking set is the surgical fix.
|
|
106
|
+
'sendChatAction',
|
|
96
107
|
]);
|
|
97
108
|
|
|
98
109
|
// Derive the row's `text` column. sendSticker has no text/caption, so we
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.10.0-rc.
|
|
3
|
+
"version": "0.10.0-rc.50",
|
|
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
|
@@ -2130,6 +2130,14 @@ async function main() {
|
|
|
2130
2130
|
classifyToolName, announce, shouldAnnounce, contextHintShown,
|
|
2131
2131
|
extractAssistantText, getChatIdFromKey, getThreadIdFromKey,
|
|
2132
2132
|
logger: console,
|
|
2133
|
+
// rc.50: full reply-pipeline deps for the autonomous-wakeup path
|
|
2134
|
+
// (see `onAutonomousAssistantMessage` in lib/sdk/callbacks.js).
|
|
2135
|
+
// Mirrors what bot-reply-stream uses in handleMessage so
|
|
2136
|
+
// [sticker:NAME] / [react:EMOJI] tags get processed and the
|
|
2137
|
+
// rc.45 canned-string sanitizer fires.
|
|
2138
|
+
parseResponse, sanitizeAssistantReply,
|
|
2139
|
+
chunkMarkdownText, deliverReplies,
|
|
2140
|
+
chunkBudget: TG_CHUNK_BUDGET,
|
|
2133
2141
|
});
|
|
2134
2142
|
// 0.10.0: sdkCallbacks (the polygram-side lifecycle handlers — status
|
|
2135
2143
|
// reactor, stream chunk → bubble edit, etc.) move from the underlying
|