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.
@@ -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.13",
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.13",
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,