polygram 0.8.0-rc.13 → 0.8.0-rc.14
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/package.json +1 -1
- package/polygram.js +103 -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.14",
|
|
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/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.14",
|
|
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
|
@@ -707,6 +707,83 @@ let pm = null; // ProcessManager, created in main()
|
|
|
707
707
|
// the assistant was mid-tool-use).
|
|
708
708
|
const autosteerBuffer = createAutosteerBuffer();
|
|
709
709
|
|
|
710
|
+
// 0.8.0-rc.14: track msg_ids that received the AUTOSTEERED ✍ ack, per
|
|
711
|
+
// session, so we can clear those reactions when the in-flight turn
|
|
712
|
+
// finishes. Pre-rc.14 the ✍ persisted forever because each autosteer
|
|
713
|
+
// invocation runs in its OWN handleMessage scope (own reactor), and
|
|
714
|
+
// the TRIGGER message's reactor.clear() at turn-end couldn't reach
|
|
715
|
+
// across to other messages. Without this map, users see ✍ stuck on
|
|
716
|
+
// every follow-up and don't know whether the bot incorporated them.
|
|
717
|
+
const autosteeredMsgRefs = new Map(); // sessionKey → [{chatId, msgId}]
|
|
718
|
+
|
|
719
|
+
async function clearAutosteeredReactions(sessionKey) {
|
|
720
|
+
const list = autosteeredMsgRefs.get(sessionKey);
|
|
721
|
+
if (!list || list.length === 0) return;
|
|
722
|
+
autosteeredMsgRefs.delete(sessionKey);
|
|
723
|
+
if (!bot) return;
|
|
724
|
+
for (const { chatId: cid, msgId } of list) {
|
|
725
|
+
try {
|
|
726
|
+
await tg(bot, 'setMessageReaction', {
|
|
727
|
+
chat_id: cid, message_id: msgId, reaction: [],
|
|
728
|
+
}, { source: 'autosteer-clear', botName: BOT_NAME });
|
|
729
|
+
} catch (err) {
|
|
730
|
+
// Ack-clear failures are silent — the ✍ stays on screen
|
|
731
|
+
// but doesn't block the in-flight turn's reply UX.
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// 0.8.0-rc.14: tool-less-turn drain. PostToolBatch hook only fires
|
|
737
|
+
// on tool boundaries — when a Query produces a turn that uses ZERO
|
|
738
|
+
// tools (just a text answer), the autosteerBuffer never gets
|
|
739
|
+
// drained and any user follow-ups buffered during that turn
|
|
740
|
+
// disappear silently into the next tool-using turn (or never, if
|
|
741
|
+
// the chat is purely conversational).
|
|
742
|
+
//
|
|
743
|
+
// Workaround: at every success exit in handleMessage, check if
|
|
744
|
+
// the buffer still has items and dispatch them as a synthetic
|
|
745
|
+
// next turn via pm.send. The bot replies to the drained content
|
|
746
|
+
// in a fresh turn — UX-wise the user sees TWO replies (one to
|
|
747
|
+
// the trigger message, one to "B + C") which is the same as if
|
|
748
|
+
// they'd sent the messages without autosteer. Better than losing.
|
|
749
|
+
async function drainStaleAutosteerBuffer(sessionKey, chatId, threadId) {
|
|
750
|
+
const stale = autosteerBuffer.drain(sessionKey);
|
|
751
|
+
if (stale.length === 0) return;
|
|
752
|
+
const followUpPrompt = stale.join('\n\n');
|
|
753
|
+
logEvent('autosteer-stale-drain', {
|
|
754
|
+
chat_id: chatId,
|
|
755
|
+
session_key: sessionKey,
|
|
756
|
+
message_count: stale.length,
|
|
757
|
+
text_len: followUpPrompt.length,
|
|
758
|
+
});
|
|
759
|
+
// Dispatch as a fresh pm.send via setImmediate so we don't
|
|
760
|
+
// block the current handleMessage's success-path return. No
|
|
761
|
+
// streamer / reactor — the synthetic turn gets a plain bubble
|
|
762
|
+
// reply (no streaming preview, no progress reactions). User
|
|
763
|
+
// already saw their ✍ ack on the original follow-up; this
|
|
764
|
+
// turn's existence is the substantive response.
|
|
765
|
+
setImmediate(async () => {
|
|
766
|
+
try {
|
|
767
|
+
const chatConfig = config.chats[chatId];
|
|
768
|
+
if (!chatConfig) return;
|
|
769
|
+
const result = await sendToProcess(sessionKey, followUpPrompt, {
|
|
770
|
+
streamer: null, reactor: null, sourceMsgId: null,
|
|
771
|
+
});
|
|
772
|
+
if (result?.text && bot) {
|
|
773
|
+
await tg(bot, 'sendMessage', {
|
|
774
|
+
chat_id: chatId,
|
|
775
|
+
text: result.text,
|
|
776
|
+
...(threadId ? { message_thread_id: threadId } : {}),
|
|
777
|
+
}, { source: 'autosteer-stale-reply', botName: BOT_NAME }).catch((err) => {
|
|
778
|
+
console.error(`[${BOT_NAME}] autosteer-stale-reply send: ${err.message}`);
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
} catch (err) {
|
|
782
|
+
console.error(`[${BOT_NAME}] autosteer-stale-drain dispatch: ${err.message}`);
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
710
787
|
function spawnClaude(sessionKey, ctx) {
|
|
711
788
|
const { chatConfig, existingSessionId, label, chatId } = ctx;
|
|
712
789
|
// 0.7.3: Claude Code's Chrome-extension integration (browser
|
|
@@ -2464,6 +2541,11 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
2464
2541
|
if (entry?.inFlight) {
|
|
2465
2542
|
const ok = autosteerBuffer.append(sessionKey, prompt);
|
|
2466
2543
|
if (ok) {
|
|
2544
|
+
// Track this msg_id so the in-flight turn's success / abort
|
|
2545
|
+
// / error path can clear the ✍ reaction at turn-end.
|
|
2546
|
+
const refs = autosteeredMsgRefs.get(sessionKey) || [];
|
|
2547
|
+
refs.push({ chatId, msgId: msg.message_id });
|
|
2548
|
+
autosteeredMsgRefs.set(sessionKey, refs);
|
|
2467
2549
|
logEvent('autosteer', {
|
|
2468
2550
|
chat_id: chatId, msg_id: msg.message_id,
|
|
2469
2551
|
text_len: prompt?.length ?? 0,
|
|
@@ -2567,6 +2649,16 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
2567
2649
|
// every answered message is chat noise (plus triggers reaction
|
|
2568
2650
|
// notifications for other group members).
|
|
2569
2651
|
reactor.clear().catch(() => {});
|
|
2652
|
+
// 0.8.0-rc.14: also clear ✍ reactions on every follow-up
|
|
2653
|
+
// message that was autosteered into THIS turn — they live in
|
|
2654
|
+
// separate handleMessage scopes whose reactors are already GC'd.
|
|
2655
|
+
clearAutosteeredReactions(sessionKey).catch(() => {});
|
|
2656
|
+
// rc.14: tool-less-turn drain. PostToolBatch hook fires only
|
|
2657
|
+
// on tool boundaries; if this turn produced ZERO tools, the
|
|
2658
|
+
// hook never fired and the autosteer buffer still has the
|
|
2659
|
+
// user's follow-ups. Dispatch them as a synthetic next turn
|
|
2660
|
+
// so the bot at least addresses them (better than losing).
|
|
2661
|
+
drainStaleAutosteerBuffer(sessionKey, chatId, threadId).catch(() => {});
|
|
2570
2662
|
|
|
2571
2663
|
// 0.8.0 Phase 2 step 4: 85%-context-full live hint. After a
|
|
2572
2664
|
// successful turn, peek at SDK's getContextUsage(); if past
|
|
@@ -2623,6 +2715,11 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
2623
2715
|
&& (result.metrics?.numAssistantMessages ?? 0) > 0;
|
|
2624
2716
|
if (toolOnlyTurn) {
|
|
2625
2717
|
await reactor.clear().catch(() => {});
|
|
2718
|
+
clearAutosteeredReactions(sessionKey).catch(() => {});
|
|
2719
|
+
// Tool-only turns DID fire PostToolBatch — buffer was drained
|
|
2720
|
+
// — but autosteers received AFTER the last tool-result still
|
|
2721
|
+
// wouldn't be merged. Defensive drain here too.
|
|
2722
|
+
drainStaleAutosteerBuffer(sessionKey, chatId, threadId).catch(() => {});
|
|
2626
2723
|
logEvent('tool-only-completion', {
|
|
2627
2724
|
chat_id: chatId, msg_id: msg.message_id, bot: BOT_NAME,
|
|
2628
2725
|
num_tool_uses: result.metrics?.numToolUses,
|
|
@@ -2793,6 +2890,9 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
2793
2890
|
// the visible reaction. We DON'T set 🤯/😨 (those are for
|
|
2794
2891
|
// unexpected errors); the user just wants their stop honored.
|
|
2795
2892
|
await reactor.clear().catch(() => {});
|
|
2893
|
+
// rc.14: clear ✍ on autosteered followups too (per-msg
|
|
2894
|
+
// reactors are already GC'd in their own handleMessage scopes).
|
|
2895
|
+
await clearAutosteeredReactions(sessionKey).catch(() => {});
|
|
2796
2896
|
} else {
|
|
2797
2897
|
await streamer.finalize('', { errorSuffix: 'stream interrupted' }).catch(() => {});
|
|
2798
2898
|
if (/wall-clock ceiling|idle with no Claude activity/i.test(err?.message || '')) {
|
|
@@ -3007,6 +3107,9 @@ function createBot(token) {
|
|
|
3007
3107
|
// (stale steer leak across abort boundary, which is what the
|
|
3008
3108
|
// user just asked us not to do).
|
|
3009
3109
|
autosteerBuffer.clear(sessionKey);
|
|
3110
|
+
// rc.14: also clear ✍ reactions on already-autosteered
|
|
3111
|
+
// messages from this aborted turn — they're now dead context.
|
|
3112
|
+
clearAutosteeredReactions(sessionKey).catch(() => {});
|
|
3010
3113
|
logEvent('abort-requested', {
|
|
3011
3114
|
chat_id: chatId, user_id: msg.from?.id || null,
|
|
3012
3115
|
had_active: hadActive,
|