polygram 0.8.0-rc.43 → 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.43",
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,6 +55,21 @@ function createStreamer({
55
55
  schedule = setTimeout,
56
56
  cancel = clearTimeout,
57
57
  logger = console,
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.
72
+ preserveIntermediateBubbles = true,
58
73
  } = {}) {
59
74
  throttleMs = Math.max(THROTTLE_FLOOR_MS, throttleMs);
60
75
  let state = 'idle'; // 'idle' | 'live' | 'finalized'
@@ -67,9 +82,22 @@ function createStreamer({
67
82
  // 0.7.2: msg_ids of bubbles that have been superseded by
68
83
  // forceNewMessage(). The caller (polygram.js handleMessage at
69
84
  // end-of-turn) reads getArchived() and issues deleteMessage on
70
- // each — matches OpenClaw's archivedAnswerPreviews cleanup so
71
- // the user sees only the final answer's bubble, not every
72
- // "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).
73
101
  const archived = [];
74
102
 
75
103
  // LIVE-EDIT truncation only — used during streaming when latestText
@@ -158,15 +186,26 @@ function createStreamer({
158
186
  // emits a new top-level assistant message mid-turn (post tool-result):
159
187
  // we want it in its own bubble below the previous one, not appended
160
188
  // via editMessageText to the original.
189
+ //
190
+ // rc.44: by default, the previous bubble is PRESERVED (not archived
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`.
197
+ //
198
+ // When preserving, we still cancel the pending throttled edit (it
199
+ // wouldn't fire after we transition to a new bubble anyway) but
200
+ // there may be a recently-flushed edit in flight whose result we
201
+ // don't await — the bubble will display whatever its last
202
+ // successful edit landed, which is typically very close to the
203
+ // segment's final text (throttle is 250ms; segments take seconds).
161
204
  function forceNewMessage() {
162
205
  if (pendingEdit) { cancel(pendingEdit); pendingEdit = null; }
163
- // Don't await flushPromise the caller has decided to start a new
164
- // message; whatever the old bubble shows is "done".
165
- // 0.7.2: track the previous bubble's msgId for end-of-turn cleanup.
166
- // Without this, every intermediate "thinking out loud" assistant
167
- // message in a tool-heavy turn leaves a permanent bubble in the
168
- // chat — the user wants only the final answer's bubble visible.
169
- if (msgId != null) archived.push(msgId);
206
+ if (msgId != null && !preserveIntermediateBubbles) {
207
+ archived.push(msgId);
208
+ }
170
209
  msgId = null;
171
210
  currentText = '';
172
211
  latestText = '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.8.0-rc.43",
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,16 +2303,37 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
2303
2303
  },
2304
2304
  minChars: botCfg.streamMinChars,
2305
2305
  throttleMs: botCfg.streamThrottleMs,
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).
2315
+ preserveIntermediateBubbles: chatConfig.preserveIntermediateBubbles != null
2316
+ ? chatConfig.preserveIntermediateBubbles
2317
+ : (botCfg.preserveIntermediateBubbles != null
2318
+ ? botCfg.preserveIntermediateBubbles
2319
+ : true),
2306
2320
  logger: { error: (m) => console.error(`[${label}] ${m}`) },
2307
2321
  });
2308
2322
  // streamer is registered with this turn via pm.send's context (below)
2309
2323
 
2310
2324
  // 0.7.2: clean up bubbles superseded by forceNewMessage() — the
2311
- // intermediate "thinking out loud" assistant messages that fired in
2312
- // a tool-heavy turn. Without this, every tool-result cycle leaves a
2313
- // permanent bubble in the chat (see the screenshot from the post-
2314
- // 0.7.1 deploy where six bubbles appeared for one logical turn).
2315
- // 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.
2316
2337
  // Call AFTER finalize/discard decisions so we never delete the
2317
2338
  // bubble that's the final reply.
2318
2339
  async function cleanupArchivedBubbles() {