polygram 0.8.0-rc.44 → 0.8.0-rc.45

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.44",
4
+ "version": "0.8.0-rc.45",
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",
@@ -458,22 +458,51 @@ class ProcessManagerSdk {
458
458
  }
459
459
  }
460
460
 
461
- // forceNewMessage trigger fires BEFORE the new bubble's first
462
- // chunk: detect message-id transition with non-empty prior
463
- // streamText, then advance lastAssistantMessageId, THEN emit
464
- // onStreamChunk for the new content.
461
+ // rc.45: multi-segment same-bubble streaming.
462
+ //
463
+ // Pre-rc.45: every message-id transition fired
464
+ // onAssistantMessageStart (= forceNewMessage on the streamer),
465
+ // producing a fresh bubble per SDK assistant message even
466
+ // though the user only sent one input. Tool-heavy turns
467
+ // showed 2-6 bubbles per logical user-input cycle.
468
+ //
469
+ // rc.45: only fire onAssistantMessageStart when the user
470
+ // STEERED (injectUserMessage set pendingSteerCausesNewBubble).
471
+ // Otherwise, accumulate the prior segment's text into
472
+ // priorMessagesText and append the new segment to it — same
473
+ // bubble grows naturally. Same-message-id events (cumulative
474
+ // streaming within a single SDKAssistantMessage) still
475
+ // REPLACE the segment text; the carry-over only kicks in on
476
+ // message-id TRANSITIONS.
465
477
  if (added) {
466
478
  const isNewMessage = head.lastAssistantMessageId != null
467
479
  && messageId != null
468
480
  && head.lastAssistantMessageId !== messageId
469
481
  && head.streamText
470
482
  && head.streamText.length > 0;
471
- if (isNewMessage && this.onAssistantMessageStart) {
472
- try { await this.onAssistantMessageStart(entry.sessionKey, entry); }
473
- catch (err) { this.logger.error?.(`[${entry.label}] onAssistantMessageStart: ${err.message}`); }
483
+ if (isNewMessage) {
484
+ if (head.pendingSteerCausesNewBubble) {
485
+ // Steered: fire onAssistantMessageStart so the streamer
486
+ // forceNewMessage's. Reset the prior carry-over so the
487
+ // new bubble starts clean.
488
+ if (this.onAssistantMessageStart) {
489
+ try { await this.onAssistantMessageStart(entry.sessionKey, entry); }
490
+ catch (err) { this.logger.error?.(`[${entry.label}] onAssistantMessageStart: ${err.message}`); }
491
+ }
492
+ head.priorMessagesText = '';
493
+ head.pendingSteerCausesNewBubble = false;
494
+ } else {
495
+ // No steer: roll the just-finished segment's full text
496
+ // into priorMessagesText so the new segment appends to it.
497
+ head.priorMessagesText = head.streamText;
498
+ }
474
499
  }
475
500
  if (messageId != null) head.lastAssistantMessageId = messageId;
476
- head.streamText = added;
501
+ // Compose visible bubble text: carry-over (prior segments in
502
+ // this bubble) + the current segment's cumulative text.
503
+ head.streamText = head.priorMessagesText
504
+ ? head.priorMessagesText + '\n\n' + added
505
+ : added;
477
506
  if (this.onStreamChunk) {
478
507
  try { this.onStreamChunk(entry.sessionKey, head.streamText, entry); }
479
508
  catch (err) { this.logger.error?.(`[${entry.label}] onStreamChunk: ${err.message}`); }
@@ -615,6 +644,17 @@ class ProcessManagerSdk {
615
644
  transientRetries: 0,
616
645
  firstAssistantSeen: false,
617
646
  thinkingFired: false, // rc.29: extended-thinking → reactor THINKING
647
+ // rc.45: multi-segment same-bubble streaming. priorMessagesText
648
+ // accumulates the full text of completed assistant-message
649
+ // segments in the SAME bubble. On message-id transition WITHOUT
650
+ // a steer, the just-finished segment rolls into priorMessagesText
651
+ // and the new segment's text appends to it (one bubble grows).
652
+ // On message-id transition WITH a steer, priorMessagesText
653
+ // resets and a new bubble starts. pendingSteerCausesNewBubble is
654
+ // set by injectUserMessage; consumed + cleared on the next
655
+ // message-id transition.
656
+ priorMessagesText: '',
657
+ pendingSteerCausesNewBubble: false,
618
658
  };
619
659
 
620
660
  pending.fireFirstStream = () => {
@@ -881,6 +921,15 @@ class ProcessManagerSdk {
881
921
  if (priority !== undefined) msg.priority = priority;
882
922
  if (shouldQuery !== undefined) msg.shouldQuery = shouldQuery;
883
923
  entry.inputController.push(msg);
924
+ // rc.45: signal the streamer to start a new bubble at the next
925
+ // assistant-message-id transition. Without this flag,
926
+ // _handleEvent would APPEND the post-steer assistant text into
927
+ // the same bubble as the pre-steer text, hiding the user's
928
+ // intervention. Only set when there's a head pending — if the
929
+ // session is idle, the next pm.send will start a fresh bubble
930
+ // anyway.
931
+ const head = entry.pendingQueue?.[0];
932
+ if (head) head.pendingSteerCausesNewBubble = true;
884
933
  this._logEvent('inject-user-message', {
885
934
  session_key: sessionKey,
886
935
  chat_id: entry.chatId,
@@ -55,13 +55,20 @@ function createStreamer({
55
55
  schedule = setTimeout,
56
56
  cancel = clearTimeout,
57
57
  logger = console,
58
- // rc.44: by default, KEEP intermediate "thinking out loud" bubbles
59
- // when forceNewMessage transitions to a fresh bubble for a new
60
- // top-level assistant message. Pre-rc.44 those were pushed onto
61
- // archived[] and deleted at turn-end, leaving only the final
62
- // answer visible but users wanted the full reasoning trail
63
- // visible, especially in DM debugging contexts. Set to false to
64
- // restore the 0.7.2 "delete intermediate" behaviour.
58
+ // rc.44: by default, KEEP intermediate text bubbles when
59
+ // forceNewMessage transitions to a fresh bubble for a new
60
+ // top-level assistant message. These are NOT "thinking" tokens
61
+ // (those are filtered out by extractAssistantText
62
+ // b.type === 'text' only). They're regular text segments the
63
+ // model emitted as part of the reply (e.g. "Let me check that..."
64
+ // tool runs → "Found it. Here's the answer..."). Pre-0.7.2
65
+ // these were preserved (the original 0.7.0 multi-bubble design);
66
+ // 0.7.2 added archive-and-delete-at-turn-end as OpenClaw-parity
67
+ // cleanup. rc.44 reverts to the 0.7.0 preserve-all default
68
+ // because the intermediate text is substantive reply content,
69
+ // not noise. Set to false to restore the 0.7.2 deletion behaviour
70
+ // (only final bubble visible) for partner-facing chats that
71
+ // prefer terse output.
65
72
  preserveIntermediateBubbles = true,
66
73
  } = {}) {
67
74
  throttleMs = Math.max(THROTTLE_FLOOR_MS, throttleMs);
@@ -75,9 +82,22 @@ function createStreamer({
75
82
  // 0.7.2: msg_ids of bubbles that have been superseded by
76
83
  // forceNewMessage(). The caller (polygram.js handleMessage at
77
84
  // end-of-turn) reads getArchived() and issues deleteMessage on
78
- // each — matches OpenClaw's archivedAnswerPreviews cleanup so
79
- // the user sees only the final answer's bubble, not every
80
- // "thinking out loud" intermediate from a tool-heavy turn.
85
+ // each.
86
+ //
87
+ // History note (rc.44 correction): the 0.7.2 commit claimed this
88
+ // was "OpenClaw-parity / archivedAnswerPreviews cleanup" — that
89
+ // was wrong. The OFFICIAL OpenClaw + pi-telegram model is
90
+ // single-bubble-per-turn edited in place via sendMessageDraft (or
91
+ // sendMessage + editMessageText fallback); intermediate text
92
+ // segments don't exist there because the streamer concatenates
93
+ // everything into the same bubble. Polygram's multi-bubble shape
94
+ // is a 0.7.0 polygram-specific decision (one bubble per top-level
95
+ // assistant-message id, motivated by the SDK's segmentation), and
96
+ // the 0.7.2 archive-and-delete was a polygram-specific terseness
97
+ // cleanup, not OpenClaw porting. rc.44 made preserve-all the
98
+ // default again — archived[] only fills when
99
+ // preserveIntermediateBubbles=false (opt-out for partner-facing
100
+ // chats that prefer only-final-answer-visible output).
81
101
  const archived = [];
82
102
 
83
103
  // LIVE-EDIT truncation only — used during streaming when latestText
@@ -168,12 +188,12 @@ function createStreamer({
168
188
  // via editMessageText to the original.
169
189
  //
170
190
  // rc.44: by default, the previous bubble is PRESERVED (not archived
171
- // for end-of-turn deletion). Users wanted the full reasoning trail
172
- // visible "thinking out loud" + tool-use intermediates document
173
- // what the agent actually did, which is valuable transparency. The
174
- // 0.7.2 deletion behaviour assumed users wanted only the final
175
- // answer; for chat-style DM debugging that's wrong. Opt back into
176
- // the old behaviour with `preserveIntermediateBubbles: false`.
191
+ // for end-of-turn deletion). Intermediate text segments are
192
+ // substantive reply content the user typed up not "thinking"
193
+ // tokens (those are filtered upstream). Pre-0.7.2 polygram kept
194
+ // them all; 0.7.2 added deletion for OpenClaw-parity terseness.
195
+ // rc.44 reverts to the 0.7.0 preserve-all default. Opt back into
196
+ // the 0.7.2 behaviour with `preserveIntermediateBubbles: false`.
177
197
  //
178
198
  // When preserving, we still cancel the pending throttled edit (it
179
199
  // wouldn't fire after we transition to a new bubble anyway) but
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.8.0-rc.44",
3
+ "version": "0.8.0-rc.45",
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
@@ -2303,10 +2303,15 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
2303
2303
  },
2304
2304
  minChars: botCfg.streamMinChars,
2305
2305
  throttleMs: botCfg.streamThrottleMs,
2306
- // rc.44: preserve intermediate "thinking out loud" bubbles by
2307
- // default. Per-chat / per-bot opt-out via
2308
- // `preserveIntermediateBubbles: false` for chats where the
2309
- // partner-facing UX wants only the final answer (e.g. UMI Group).
2306
+ // rc.44: preserve intermediate bubbles by default. These are
2307
+ // regular text segments the model emits across an agentic
2308
+ // multi-step turn ("Let me check..." → tool runs → "Found it.
2309
+ // Here's the answer..."). Pre-0.7.2 polygram preserved them;
2310
+ // 0.7.2 added archive-and-delete-at-turn-end for terseness.
2311
+ // rc.44 reverts to preserve-all (the 0.7.0 default). Per-chat /
2312
+ // per-bot opt-out via `preserveIntermediateBubbles: false` for
2313
+ // chats where the partner-facing UX wants only the final answer
2314
+ // (e.g. UMI Group).
2310
2315
  preserveIntermediateBubbles: chatConfig.preserveIntermediateBubbles != null
2311
2316
  ? chatConfig.preserveIntermediateBubbles
2312
2317
  : (botCfg.preserveIntermediateBubbles != null
@@ -2317,11 +2322,18 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
2317
2322
  // streamer is registered with this turn via pm.send's context (below)
2318
2323
 
2319
2324
  // 0.7.2: clean up bubbles superseded by forceNewMessage() — the
2320
- // intermediate "thinking out loud" assistant messages that fired in
2321
- // a tool-heavy turn. Without this, every tool-result cycle leaves a
2322
- // permanent bubble in the chat (see the screenshot from the post-
2323
- // 0.7.1 deploy where six bubbles appeared for one logical turn).
2324
- // Matches OpenClaw's archivedAnswerPreviews end-of-turn cleanup.
2325
+ // intermediate text segments that fired across a tool-heavy turn.
2326
+ // Pre-0.7.2 (since 0.7.0 multi-bubble landed) those bubbles were
2327
+ // kept; 0.7.2 added cleanup motivated by a post-0.7.1 deploy
2328
+ // screenshot of six bubbles per logical turn terseness goal,
2329
+ // NOT OpenClaw porting. (Earlier comments mis-cited OpenClaw
2330
+ // parity; the official OpenClaw + pi-telegram model is
2331
+ // single-bubble-per-turn edited in place. Polygram's
2332
+ // multi-bubble shape is its own decision.)
2333
+ // rc.44 made preservation the default again — getArchived()
2334
+ // returns [] unless the chat opted out via
2335
+ // `preserveIntermediateBubbles: false`. This function still runs
2336
+ // unconditionally because the opt-out path needs it to fire.
2325
2337
  // Call AFTER finalize/discard decisions so we never delete the
2326
2338
  // bubble that's the final reply.
2327
2339
  async function cleanupArchivedBubbles() {