polygram 0.10.0-rc.6 → 0.10.0-rc.7
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.
|
|
4
|
+
"version": "0.10.0-rc.7",
|
|
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",
|
|
@@ -69,9 +69,15 @@ function createAutosteerHandlers({
|
|
|
69
69
|
if (!entry?.inFlight) return { autosteered: false };
|
|
70
70
|
|
|
71
71
|
const priority = priorityFor(chatConfig, config);
|
|
72
|
+
// rc.7: pass the autosteered msg_id through to the backend so the
|
|
73
|
+
// tmux backend can route an extra-turn reply back to Telegram if
|
|
74
|
+
// the TUI dequeues the paste as a fresh user turn (NEW-TURN path).
|
|
75
|
+
// SDK backend ignores msgId — its PostToolBatch fold path
|
|
76
|
+
// guarantees one combined reply via the primary pm.send.
|
|
72
77
|
const ok = pm.injectUserMessage(sessionKey, {
|
|
73
78
|
content: prompt,
|
|
74
79
|
priority,
|
|
80
|
+
msgId: msg.message_id,
|
|
75
81
|
});
|
|
76
82
|
if (!ok) return { autosteered: false };
|
|
77
83
|
|
|
@@ -167,6 +167,29 @@ class TmuxProcess extends Process {
|
|
|
167
167
|
// getContextUsage() so polygram's post-turn auto-hint works on
|
|
168
168
|
// the tmux backend just like SDK.
|
|
169
169
|
this._lastUsage = null;
|
|
170
|
+
|
|
171
|
+
// rc.7: autosteer fold-vs-queue tracking. When polygram autosteers
|
|
172
|
+
// a user message via injectUserMessage({content, msgId}), we
|
|
173
|
+
// record (content, msgId) here. The TUI's queue may consume that
|
|
174
|
+
// paste in two ways (see SessionLogParser docs):
|
|
175
|
+
//
|
|
176
|
+
// A) FOLD — JSONL emits `attachment.type="queued_command"`.
|
|
177
|
+
// We pop the matching entry. The primary turn's reply
|
|
178
|
+
// already covers both messages — no extra delivery needed.
|
|
179
|
+
//
|
|
180
|
+
// B) NEW TURN — JSONL emits a top-level `user` message with
|
|
181
|
+
// matching content. We pop the matching entry and start an
|
|
182
|
+
// `_extraTurnState` accumulator. Subsequent assistant-chunks
|
|
183
|
+
// (while no _turnState is active — i.e. primary pm.send
|
|
184
|
+
// already returned) feed the accumulator; the next `result`
|
|
185
|
+
// event flushes it as an `'extra-turn-reply'` { msgId, text }
|
|
186
|
+
// event for polygram to route to Telegram.
|
|
187
|
+
//
|
|
188
|
+
// FIFO matching by content text — multiple pending autosteers are
|
|
189
|
+
// resolved in submission order, which matches how Telegram
|
|
190
|
+
// delivers messages anyway.
|
|
191
|
+
this._pendingAutosteers = []; // Array<{ content, msgId }>
|
|
192
|
+
this._extraTurnState = null; // null | { msgId, text }
|
|
170
193
|
}
|
|
171
194
|
|
|
172
195
|
get cost() { return 3; }
|
|
@@ -468,6 +491,14 @@ class TmuxProcess extends Process {
|
|
|
468
491
|
|
|
469
492
|
_completeTurn() {
|
|
470
493
|
this.inFlight = false;
|
|
494
|
+
// rc.7: clear _turnState so JSONL events arriving BETWEEN turns
|
|
495
|
+
// (autonomous assistant messages, queue-dequeued user prompts
|
|
496
|
+
// becoming fresh turns) don't incorrectly route to the just-
|
|
497
|
+
// completed turn. Without this, the rc.7 autosteer extra-turn
|
|
498
|
+
// path can't distinguish "primary turn done, watch for queue
|
|
499
|
+
// dequeue" from "still in primary turn". The next send() call
|
|
500
|
+
// freshly sets _turnState so existing callers are unaffected.
|
|
501
|
+
this._turnState = null;
|
|
471
502
|
// Shift the HEAD pending (just-completed turn). After this, the
|
|
472
503
|
// queue contains only items queued while inFlight (each carrying
|
|
473
504
|
// their own resolve/reject pair). If any, re-enter send() on the
|
|
@@ -560,6 +591,13 @@ class TmuxProcess extends Process {
|
|
|
560
591
|
? `${this._turnState.text}\n\n${ev.text}`
|
|
561
592
|
: ev.text;
|
|
562
593
|
this.emit('stream-chunk', this._turnState.text);
|
|
594
|
+
} else if (this._extraTurnState) {
|
|
595
|
+
// rc.7: autosteer NEW-TURN extraction in progress. Accumulate
|
|
596
|
+
// this chunk into the extra-turn buffer. It will be flushed
|
|
597
|
+
// when the next 'result' event fires (see below).
|
|
598
|
+
this._extraTurnState.text = this._extraTurnState.text
|
|
599
|
+
? `${this._extraTurnState.text}\n\n${ev.text}`
|
|
600
|
+
: ev.text;
|
|
563
601
|
} else {
|
|
564
602
|
// No turn in flight — this is an autonomous assistant message
|
|
565
603
|
// (claude self-initiated; typically ScheduleWakeup firing).
|
|
@@ -614,10 +652,51 @@ class TmuxProcess extends Process {
|
|
|
614
652
|
if (this._turnState && this._turnState.resolveResult) {
|
|
615
653
|
this._turnState.resultEvent = ev;
|
|
616
654
|
this._turnState.resolveResult(ev);
|
|
655
|
+
} else if (this._extraTurnState) {
|
|
656
|
+
// rc.7: extra-turn (autosteer NEW-TURN) just completed. Flush
|
|
657
|
+
// the accumulated text as 'extra-turn-reply' so polygram can
|
|
658
|
+
// send it to Telegram as a reply to the autosteered msgId.
|
|
659
|
+
// Only flush on the 'success' subtype (end_turn) — non-terminal
|
|
660
|
+
// result subtypes like 'tool_use' shouldn't close the
|
|
661
|
+
// extra-turn (the model may still emit more chunks).
|
|
662
|
+
if (ev.subtype === 'success') {
|
|
663
|
+
const flushed = this._extraTurnState;
|
|
664
|
+
this._extraTurnState = null;
|
|
665
|
+
this.emit('extra-turn-reply', {
|
|
666
|
+
msgId: flushed.msgId,
|
|
667
|
+
text: flushed.text,
|
|
668
|
+
sessionId: this.claudeSessionId,
|
|
669
|
+
backend: 'tmux',
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// If no turn in flight and no extra-turn, result event just
|
|
674
|
+
// marks the end of an autonomous message segment — already
|
|
675
|
+
// handled by the assistant-chunk branch above.
|
|
676
|
+
} else if (ev.type === 'queue-folded') {
|
|
677
|
+
// rc.7: TUI consumed an autosteered paste inside the current
|
|
678
|
+
// turn (FOLD). Pop the matching pending autosteer so a later
|
|
679
|
+
// 'user-message' with the same content doesn't spuriously
|
|
680
|
+
// trigger extra-turn extraction. The primary turn's reply
|
|
681
|
+
// already covers both messages — no extra delivery needed.
|
|
682
|
+
const idx = this._pendingAutosteers.findIndex((a) => a.content === ev.prompt);
|
|
683
|
+
if (idx >= 0) {
|
|
684
|
+
this._pendingAutosteers.splice(idx, 1);
|
|
685
|
+
}
|
|
686
|
+
} else if (ev.type === 'user-message') {
|
|
687
|
+
// rc.7: top-level user message in JSONL — may be the TUI
|
|
688
|
+
// dequeuing an autosteered paste as a fresh user turn
|
|
689
|
+
// (NEW-TURN path). Match by content. If found AND no primary
|
|
690
|
+
// turn is currently in flight (the autosteered second turn
|
|
691
|
+
// runs AFTER the first turn returned), start an extra-turn
|
|
692
|
+
// accumulator. Subsequent assistant-chunks + the next 'result'
|
|
693
|
+
// event will flush it as 'extra-turn-reply'.
|
|
694
|
+
const idx = this._pendingAutosteers.findIndex((a) => a.content === ev.text);
|
|
695
|
+
if (idx >= 0 && !this._turnState && !this._extraTurnState) {
|
|
696
|
+
const { msgId } = this._pendingAutosteers[idx];
|
|
697
|
+
this._pendingAutosteers.splice(idx, 1);
|
|
698
|
+
this._extraTurnState = { msgId, text: '' };
|
|
617
699
|
}
|
|
618
|
-
// If no turn in flight, the result event simply marks the end of
|
|
619
|
-
// an autonomous message segment — already handled by the
|
|
620
|
-
// assistant-chunk branch above.
|
|
621
700
|
} else if (ev.type === 'last-prompt') {
|
|
622
701
|
// Fallback complete signal. If 'result' didn't fire (rare; some
|
|
623
702
|
// claude versions may write last-prompt instead of stop_reason),
|
|
@@ -996,10 +1075,15 @@ class TmuxProcess extends Process {
|
|
|
996
1075
|
* Inject text into the in-flight turn. Fire-and-forget paste; errors
|
|
997
1076
|
* surface via 'inject-fail' event, never as a thrown exception.
|
|
998
1077
|
*
|
|
1078
|
+
* @param {object} [opts.msgId] — rc.7: when provided, registers
|
|
1079
|
+
* the (content, msgId) for autosteer fold-vs-queue tracking so
|
|
1080
|
+
* the NEW-TURN case (queue dequeued as a fresh user turn) can
|
|
1081
|
+
* route its reply back to Telegram via 'extra-turn-reply'.
|
|
1082
|
+
*
|
|
999
1083
|
* @returns {boolean} false if no live turn (caller falls through to
|
|
1000
1084
|
* pm.send queue path) OR if content sanitized to empty.
|
|
1001
1085
|
*/
|
|
1002
|
-
injectUserMessage({ content, priority = 'next', shouldQuery } = {}) {
|
|
1086
|
+
injectUserMessage({ content, priority = 'next', shouldQuery, msgId } = {}) {
|
|
1003
1087
|
if (!this.inFlight || this.closed) return false;
|
|
1004
1088
|
// Mirror R2-F1: sanitize even though pasteText also sanitizes.
|
|
1005
1089
|
// We need to detect empty-after-sanitize here so caller can fall
|
|
@@ -1020,7 +1104,17 @@ class TmuxProcess extends Process {
|
|
|
1020
1104
|
this._turnState.pendingSteerCausesNewBubble = true;
|
|
1021
1105
|
}
|
|
1022
1106
|
|
|
1023
|
-
|
|
1107
|
+
// rc.7: register the autosteer for fold-vs-queue tracking. The
|
|
1108
|
+
// JSONL parser will later emit either 'queue-folded' (FOLD) or
|
|
1109
|
+
// 'user-message' (NEW TURN) with the same content, and
|
|
1110
|
+
// _handleSessionEvent matches by content to resolve which path
|
|
1111
|
+
// ran. msgId is optional — when omitted, the inject is still
|
|
1112
|
+
// delivered but no extra-turn extraction is attempted.
|
|
1113
|
+
if (msgId != null) {
|
|
1114
|
+
this._pendingAutosteers.push({ content: safe, msgId });
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
this.emit('inject-user-message', { text_len: safe.length, priority, shouldQuery, msgId });
|
|
1024
1118
|
return true;
|
|
1025
1119
|
}
|
|
1026
1120
|
|
package/lib/process-manager.js
CHANGED
|
@@ -48,6 +48,14 @@ const CALLBACK_TO_EVENT = {
|
|
|
48
48
|
// onApprovalRequired to route tmux prompts through the SAME
|
|
49
49
|
// approval card UI used by SDK's canUseTool flow.
|
|
50
50
|
onApprovalRequired: 'approval-required',
|
|
51
|
+
// rc.7: tmux backend autosteer "NEW-TURN" extra reply. Fires when
|
|
52
|
+
// the TUI's queue dequeued an autosteered paste as a fresh user
|
|
53
|
+
// turn (because primary turn ended before fold could happen). The
|
|
54
|
+
// payload is { msgId, text, sessionId, backend } and polygram
|
|
55
|
+
// routes it to Telegram as a reply to the autosteered msgId. SDK
|
|
56
|
+
// backend never emits this — its PostToolBatch fold path
|
|
57
|
+
// guarantees one combined reply.
|
|
58
|
+
onExtraTurnReply: 'extra-turn-reply',
|
|
51
59
|
};
|
|
52
60
|
|
|
53
61
|
class ProcessManager {
|
package/lib/sdk/callbacks.js
CHANGED
|
@@ -153,6 +153,54 @@ function createSdkCallbacks({
|
|
|
153
153
|
}
|
|
154
154
|
},
|
|
155
155
|
|
|
156
|
+
// rc.7: tmux backend autosteer NEW-TURN extra reply. Fires when
|
|
157
|
+
// the TUI's queue dequeued an autosteered paste as a fresh user
|
|
158
|
+
// turn — typically when the primary turn was a short / cached
|
|
159
|
+
// reply that finished before the paste could fold in. The
|
|
160
|
+
// payload carries { msgId, text, sessionId, backend }; msgId is
|
|
161
|
+
// the Telegram message_id of the autosteered user message
|
|
162
|
+
// (so the reply lands as a Telegram reply to that message,
|
|
163
|
+
// matching how Ivan visually expects autosteer to behave).
|
|
164
|
+
//
|
|
165
|
+
// SDK backend NEVER emits this — its PostToolBatch fold path
|
|
166
|
+
// guarantees one combined reply via the primary pm.send().
|
|
167
|
+
// This is purely a tmux-backend bridge.
|
|
168
|
+
onExtraTurnReply: (sessionKey, payload /* , entry */) => {
|
|
169
|
+
try {
|
|
170
|
+
const text = payload?.text;
|
|
171
|
+
const msgId = payload?.msgId;
|
|
172
|
+
if (!text || msgId == null) return;
|
|
173
|
+
const chatId = getChatIdFromKey(sessionKey);
|
|
174
|
+
const threadIdRaw = getThreadIdFromKey(sessionKey);
|
|
175
|
+
const threadId = threadIdRaw ? parseInt(threadIdRaw, 10) : null;
|
|
176
|
+
if (!bot) {
|
|
177
|
+
logger.error?.(`[${botName}] extra-turn-reply: bot not ready, dropping ${text.length} chars`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const params = {
|
|
181
|
+
chat_id: chatId,
|
|
182
|
+
text,
|
|
183
|
+
reply_to_message_id: msgId,
|
|
184
|
+
...(Number.isInteger(threadId) && { message_thread_id: threadId }),
|
|
185
|
+
};
|
|
186
|
+
// Don't await — keep the pm event loop unblocked.
|
|
187
|
+
tg(bot, 'sendMessage', params,
|
|
188
|
+
{ source: 'extra-turn-reply', botName }).catch((err) => {
|
|
189
|
+
logger.error?.(`[${botName}] extra-turn-reply send failed: ${err.message}`);
|
|
190
|
+
});
|
|
191
|
+
logEvent('extra-turn-reply', {
|
|
192
|
+
chat_id: chatId,
|
|
193
|
+
session_key: sessionKey,
|
|
194
|
+
thread_id: threadIdRaw,
|
|
195
|
+
msg_id: msgId,
|
|
196
|
+
text_len: text.length,
|
|
197
|
+
backend: payload?.backend || 'tmux',
|
|
198
|
+
});
|
|
199
|
+
} catch (err) {
|
|
200
|
+
logger.error?.(`[${botName}] extra-turn-reply handler: ${err.message}`);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
|
|
156
204
|
// SDK auto-compaction observability. Fires when SDK emits
|
|
157
205
|
// SDKCompactBoundaryMessage. Surfaces a quiet system status note
|
|
158
206
|
// to the chat so the user knows the bot is busy reorganising
|
|
@@ -24,6 +24,16 @@
|
|
|
24
24
|
* - assistant with `content[].type === 'tool_use'` → emit 'tool-use' { name, input }
|
|
25
25
|
* - assistant with `message.stop_reason` → emit 'result' { subtype, text, ... }
|
|
26
26
|
* - last-prompt → emit 'last-prompt' (fallback complete signal)
|
|
27
|
+
* - user (top-level, content is string) → emit 'user-message' { text }
|
|
28
|
+
* The queue-becomes-new-turn signal: when the TUI dequeues a
|
|
29
|
+
* queued paste as a fresh user turn (because the prior turn
|
|
30
|
+
* ended before fold could happen). Note: when content is an
|
|
31
|
+
* ARRAY containing a `tool_result` block, that's API-shaped
|
|
32
|
+
* tool_result feedback (not a user prompt) — must NOT emit.
|
|
33
|
+
* - attachment with `attachment.type === 'queued_command'` → emit 'queue-folded' { prompt }
|
|
34
|
+
* The fold-confirmed signal: a paste IS being consumed inside
|
|
35
|
+
* the current turn. Polygram uses this to know it does NOT
|
|
36
|
+
* need to extract a second reply for the autosteered msg.
|
|
27
37
|
*
|
|
28
38
|
* Robust against malformed lines: returns null and skips.
|
|
29
39
|
*
|
|
@@ -147,6 +157,23 @@ function parseLine(line) {
|
|
|
147
157
|
}
|
|
148
158
|
} else if (obj.type === 'last-prompt') {
|
|
149
159
|
out.push({ type: 'last-prompt', text: obj.lastPrompt ?? '' });
|
|
160
|
+
} else if (obj.type === 'user' && obj.message) {
|
|
161
|
+
// Top-level user message — only emit when content is a non-empty
|
|
162
|
+
// string. Array content carries tool_result blocks (API-shaped
|
|
163
|
+
// tool feedback), which are NOT user prompts and must be skipped
|
|
164
|
+
// — they would otherwise trigger spurious "extra-turn" extraction
|
|
165
|
+
// in TmuxProcess.
|
|
166
|
+
const content = obj.message.content;
|
|
167
|
+
if (typeof content === 'string' && content.length > 0) {
|
|
168
|
+
out.push({ type: 'user-message', text: content });
|
|
169
|
+
}
|
|
170
|
+
} else if (obj.type === 'attachment' && obj.attachment) {
|
|
171
|
+
// queued_command attachment is the FOLD signal — the TUI consumed
|
|
172
|
+
// an autosteered paste inside the current in-flight turn.
|
|
173
|
+
const a = obj.attachment;
|
|
174
|
+
if (a.type === 'queued_command' && typeof a.prompt === 'string' && a.prompt.length > 0) {
|
|
175
|
+
out.push({ type: 'queue-folded', prompt: a.prompt });
|
|
176
|
+
}
|
|
150
177
|
}
|
|
151
178
|
|
|
152
179
|
return out;
|
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.7",
|
|
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": {
|