polygram 0.10.0-rc.7 → 0.10.0-rc.9
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.9",
|
|
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",
|
|
@@ -696,6 +696,17 @@ class TmuxProcess extends Process {
|
|
|
696
696
|
const { msgId } = this._pendingAutosteers[idx];
|
|
697
697
|
this._pendingAutosteers.splice(idx, 1);
|
|
698
698
|
this._extraTurnState = { msgId, text: '' };
|
|
699
|
+
// rc.9: signal that turn 2 just started for the autosteered
|
|
700
|
+
// msg. Polygram subscribes to re-engage typing indicator + ✍
|
|
701
|
+
// reaction on msgId during the gap (clearAutosteeredReactions
|
|
702
|
+
// had fired when primary turn 1 succeeded under SDK-style
|
|
703
|
+
// fold assumption — for tmux NEW-TURN that clear was
|
|
704
|
+
// premature; this event lets polygram restore the visual).
|
|
705
|
+
this.emit('extra-turn-started', {
|
|
706
|
+
msgId,
|
|
707
|
+
sessionId: this.claudeSessionId,
|
|
708
|
+
backend: 'tmux',
|
|
709
|
+
});
|
|
699
710
|
}
|
|
700
711
|
} else if (ev.type === 'last-prompt') {
|
|
701
712
|
// Fallback complete signal. If 'result' didn't fire (rare; some
|
package/lib/process-manager.js
CHANGED
|
@@ -56,6 +56,14 @@ const CALLBACK_TO_EVENT = {
|
|
|
56
56
|
// backend never emits this — its PostToolBatch fold path
|
|
57
57
|
// guarantees one combined reply.
|
|
58
58
|
onExtraTurnReply: 'extra-turn-reply',
|
|
59
|
+
// rc.9: pair-of with onExtraTurnReply. Fires the MOMENT TmuxProcess
|
|
60
|
+
// sees the dequeued user-message in JSONL, which is the start of
|
|
61
|
+
// turn 2 — gives polygram a hook to re-engage typing indicator and
|
|
62
|
+
// re-apply the ✍ reaction on the autosteered msg (otherwise the
|
|
63
|
+
// user sees a silent gap from when clearAutosteeredReactions fired
|
|
64
|
+
// at primary turn 1 success until the extra-turn reply lands).
|
|
65
|
+
// Payload: { msgId, sessionId, backend }.
|
|
66
|
+
onExtraTurnStarted: 'extra-turn-started',
|
|
59
67
|
};
|
|
60
68
|
|
|
61
69
|
class ProcessManager {
|
package/lib/sdk/callbacks.js
CHANGED
|
@@ -37,6 +37,64 @@ function createSdkCallbacks({
|
|
|
37
37
|
getThreadIdFromKey,
|
|
38
38
|
logger = console,
|
|
39
39
|
} = {}) {
|
|
40
|
+
// rc.9: typing-indicator state for autosteer NEW-TURN extraction.
|
|
41
|
+
// Keyed by sessionKey. extra-turn-started installs a 4-second
|
|
42
|
+
// sendChatAction loop + tracks the autosteered msgId; extra-turn-
|
|
43
|
+
// reply tears it down and clears ✍. SDK backend never installs
|
|
44
|
+
// entries (it doesn't emit either event). Per-session, not per-msg,
|
|
45
|
+
// because the TUI's queue is FIFO and we only watch one extra turn
|
|
46
|
+
// at a time per session.
|
|
47
|
+
const extraTurnTracker = new Map(); // sessionKey → { msgId, intervalHandle, chatId }
|
|
48
|
+
|
|
49
|
+
function startExtraTurnVisuals(sessionKey, msgId) {
|
|
50
|
+
if (!bot) return;
|
|
51
|
+
const chatId = getChatIdFromKey(sessionKey);
|
|
52
|
+
// Re-apply ✍ on the autosteered msg — clearAutosteeredReactions
|
|
53
|
+
// fired when primary turn 1 succeeded, so the reaction is gone.
|
|
54
|
+
// Best-effort; failures don't block.
|
|
55
|
+
tg(bot, 'setMessageReaction', {
|
|
56
|
+
chat_id: chatId,
|
|
57
|
+
message_id: msgId,
|
|
58
|
+
reaction: [{ type: 'emoji', emoji: '✍' }],
|
|
59
|
+
}, { source: 'extra-turn-started', botName }).catch((err) => {
|
|
60
|
+
logger.error?.(`[${botName}] extra-turn ✍ re-apply failed: ${err.message}`);
|
|
61
|
+
});
|
|
62
|
+
// Typing indicator loop — Telegram's typing action expires after
|
|
63
|
+
// ~5s of inactivity, so we re-emit every 4s. Stops on extra-turn-
|
|
64
|
+
// reply (or session close — see kill cleanup at the bottom of
|
|
65
|
+
// this comment chain if needed).
|
|
66
|
+
const tick = () => {
|
|
67
|
+
tg(bot, 'sendChatAction', {
|
|
68
|
+
chat_id: chatId,
|
|
69
|
+
action: 'typing',
|
|
70
|
+
}, { source: 'extra-turn-typing', botName }).catch(() => {});
|
|
71
|
+
};
|
|
72
|
+
tick();
|
|
73
|
+
const handle = setInterval(tick, 4_000);
|
|
74
|
+
const prev = extraTurnTracker.get(sessionKey);
|
|
75
|
+
if (prev?.intervalHandle) clearInterval(prev.intervalHandle);
|
|
76
|
+
extraTurnTracker.set(sessionKey, { msgId, intervalHandle: handle, chatId });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function stopExtraTurnVisuals(sessionKey, msgId) {
|
|
80
|
+
const entry = extraTurnTracker.get(sessionKey);
|
|
81
|
+
if (!entry) return;
|
|
82
|
+
if (entry.intervalHandle) clearInterval(entry.intervalHandle);
|
|
83
|
+
extraTurnTracker.delete(sessionKey);
|
|
84
|
+
// Clear ✍ on the autosteered msg — the reply itself is now the
|
|
85
|
+
// "answered" signal. Use the tracker's chatId so we don't depend
|
|
86
|
+
// on the caller passing it.
|
|
87
|
+
if (bot && entry.chatId != null) {
|
|
88
|
+
tg(bot, 'setMessageReaction', {
|
|
89
|
+
chat_id: entry.chatId,
|
|
90
|
+
message_id: msgId ?? entry.msgId,
|
|
91
|
+
reaction: [],
|
|
92
|
+
}, { source: 'extra-turn-reply-clear', botName }).catch((err) => {
|
|
93
|
+
logger.error?.(`[${botName}] extra-turn ✍ clear failed: ${err.message}`);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
40
98
|
return {
|
|
41
99
|
onInit: (sessionKey, event, entry) => {
|
|
42
100
|
dbWrite(() => db.upsertSession({
|
|
@@ -54,6 +112,11 @@ function createSdkCallbacks({
|
|
|
54
112
|
onClose: (sessionKey, code, entry) => {
|
|
55
113
|
logger.log?.(`[${entry.label}] Process exited (code ${code})`);
|
|
56
114
|
logEvent('process-close', { chat_id: entry.chatId, session_key: sessionKey, code });
|
|
115
|
+
// rc.9: if a session closes mid-extra-turn (turn 2 crashed,
|
|
116
|
+
// user /stop, daemon kill), tear down typing-indicator + ✍
|
|
117
|
+
// visuals so we don't leak the interval and aren't stuck
|
|
118
|
+
// showing "writing…" on a dead session.
|
|
119
|
+
stopExtraTurnVisuals(sessionKey, null);
|
|
57
120
|
},
|
|
58
121
|
|
|
59
122
|
onStreamChunk: (sessionKey, partial, entry) => {
|
|
@@ -153,6 +216,30 @@ function createSdkCallbacks({
|
|
|
153
216
|
}
|
|
154
217
|
},
|
|
155
218
|
|
|
219
|
+
// rc.9: pair-of with onExtraTurnReply. Fires the moment
|
|
220
|
+
// TmuxProcess sees the dequeued user-message in JSONL → turn 2
|
|
221
|
+
// is starting. Re-engages typing indicator + ✍ on the
|
|
222
|
+
// autosteered msg so the user has a visible "still working on
|
|
223
|
+
// this" signal during the gap between primary turn 1 ending and
|
|
224
|
+
// the extra reply landing. Without this, Ivan saw a few seconds
|
|
225
|
+
// of nothing (✍ cleared by clearAutosteeredReactions, no
|
|
226
|
+
// typing).
|
|
227
|
+
onExtraTurnStarted: (sessionKey, payload /* , entry */) => {
|
|
228
|
+
try {
|
|
229
|
+
const msgId = payload?.msgId;
|
|
230
|
+
if (msgId == null) return;
|
|
231
|
+
startExtraTurnVisuals(sessionKey, msgId);
|
|
232
|
+
logEvent('extra-turn-started', {
|
|
233
|
+
chat_id: getChatIdFromKey(sessionKey),
|
|
234
|
+
session_key: sessionKey,
|
|
235
|
+
msg_id: msgId,
|
|
236
|
+
backend: payload?.backend || 'tmux',
|
|
237
|
+
});
|
|
238
|
+
} catch (err) {
|
|
239
|
+
logger.error?.(`[${botName}] extra-turn-started handler: ${err.message}`);
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
|
|
156
243
|
// rc.7: tmux backend autosteer NEW-TURN extra reply. Fires when
|
|
157
244
|
// the TUI's queue dequeued an autosteered paste as a fresh user
|
|
158
245
|
// turn — typically when the primary turn was a short / cached
|
|
@@ -169,6 +256,10 @@ function createSdkCallbacks({
|
|
|
169
256
|
try {
|
|
170
257
|
const text = payload?.text;
|
|
171
258
|
const msgId = payload?.msgId;
|
|
259
|
+
// rc.9: ALWAYS tear down extra-turn visuals first, even if
|
|
260
|
+
// text/msgId are missing — otherwise the typing-indicator
|
|
261
|
+
// loop would run forever for that session.
|
|
262
|
+
stopExtraTurnVisuals(sessionKey, msgId);
|
|
172
263
|
if (!text || msgId == null) return;
|
|
173
264
|
const chatId = getChatIdFromKey(sessionKey);
|
|
174
265
|
const threadIdRaw = getThreadIdFromKey(sessionKey);
|
|
@@ -109,12 +109,24 @@ function parseLine(line) {
|
|
|
109
109
|
if (obj.type === 'assistant' && obj.message) {
|
|
110
110
|
const content = obj.message.content;
|
|
111
111
|
if (Array.isArray(content)) {
|
|
112
|
+
// Cross-backend parity (rc.8): SDK's extractAssistantText
|
|
113
|
+
// (lib/process/sdk-process.js:62-73) joins all text blocks of
|
|
114
|
+
// one assistant message into a single string and applies a
|
|
115
|
+
// trailing-colon → ellipsis transform: "Listing deps:" →
|
|
116
|
+
// "Listing deps…". This keeps streamed-but-not-final text from
|
|
117
|
+
// reading as half-formed during the pause while a tool runs.
|
|
118
|
+
// The CLI/tmux backend previously emitted one assistant-chunk
|
|
119
|
+
// per text block with no normalisation — Telegram bubbles
|
|
120
|
+
// would briefly show "files are in place:" while the agent
|
|
121
|
+
// ran follow-up tools. Now we mirror SDK byte-for-byte.
|
|
122
|
+
const textParts = [];
|
|
123
|
+
const toolUses = [];
|
|
112
124
|
for (const block of content) {
|
|
113
125
|
if (!block || typeof block !== 'object') continue;
|
|
114
126
|
if (block.type === 'text' && typeof block.text === 'string' && block.text.length > 0) {
|
|
115
|
-
|
|
127
|
+
textParts.push(block.text);
|
|
116
128
|
} else if (block.type === 'tool_use' && block.name) {
|
|
117
|
-
|
|
129
|
+
toolUses.push({
|
|
118
130
|
type: 'tool-use',
|
|
119
131
|
name: block.name,
|
|
120
132
|
input: block.input ?? null,
|
|
@@ -122,6 +134,16 @@ function parseLine(line) {
|
|
|
122
134
|
});
|
|
123
135
|
}
|
|
124
136
|
}
|
|
137
|
+
// Emit text FIRST then tool-uses — matches the pre-rc.8 order
|
|
138
|
+
// (when the per-block iteration interleaved them in source
|
|
139
|
+
// order, but text-then-tool is the dominant real-world shape).
|
|
140
|
+
if (textParts.length > 0) {
|
|
141
|
+
const joined = textParts.join('\n\n').trim().replace(/([^:]):\s*$/, '$1…');
|
|
142
|
+
if (joined.length > 0) {
|
|
143
|
+
out.push({ type: 'assistant-chunk', text: joined });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const t of toolUses) out.push(t);
|
|
125
147
|
}
|
|
126
148
|
// Token-usage telemetry. Every assistant message carries the
|
|
127
149
|
// cumulative usage snapshot — input_tokens + cache_creation +
|
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.9",
|
|
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": {
|