polygram 0.5.8 → 0.5.10

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.
@@ -168,6 +168,12 @@ class ProcessManager {
168
168
  * If the queue is empty, kill now; otherwise mark the entry so it kills
169
169
  * itself when the last pending resolves. Next send() respawns fresh
170
170
  * with whatever config spawnFn reads at that moment.
171
+ *
172
+ * onRespawn fires with `wasDrained=true` ONLY when we waited for an
173
+ * in-flight turn to finish before swapping. The immediate-kill case
174
+ * (queue empty at request time) calls onRespawn with `wasDrained=false`
175
+ * so callers can decide whether to post a user-visible confirmation
176
+ * (which is redundant noise when the user wasn't waiting on a turn).
171
177
  */
172
178
  requestRespawn(sessionKey, reason = 'config-change') {
173
179
  const entry = this.procs.get(sessionKey);
@@ -181,17 +187,17 @@ class ProcessManager {
181
187
  });
182
188
  if (entry.pendingQueue.length === 0) {
183
189
  // Queue empty — kill immediately, fire onRespawn after close.
184
- this._killAndNotifyRespawn(sessionKey, reason).catch(() => {});
190
+ this._killAndNotifyRespawn(sessionKey, reason, false).catch(() => {});
185
191
  return { killed: true, queued: 0 };
186
192
  }
187
193
  return { killed: false, queued: entry.pendingQueue.length };
188
194
  }
189
195
 
190
- async _killAndNotifyRespawn(sessionKey, reason) {
196
+ async _killAndNotifyRespawn(sessionKey, reason, wasDrained) {
191
197
  const entry = this.procs.get(sessionKey);
192
198
  await this.kill(sessionKey);
193
199
  if (this.onRespawn && entry) {
194
- try { this.onRespawn(sessionKey, reason, entry); }
200
+ try { this.onRespawn(sessionKey, reason, entry, wasDrained); }
195
201
  catch (err) { this.logger.error(`[pm] onRespawn: ${err.message}`); }
196
202
  }
197
203
  }
@@ -321,7 +327,10 @@ class ProcessManager {
321
327
  chat_id: entry.chatId,
322
328
  reason,
323
329
  });
324
- this._killAndNotifyRespawn(sessionKey, reason).catch(() => {});
330
+ // wasDrained=true: this path runs after the queue emptied
331
+ // naturally (an in-flight turn finished), so the user was
332
+ // waiting and the confirmation message is meaningful.
333
+ this._killAndNotifyRespawn(sessionKey, reason, true).catch(() => {});
325
334
  }
326
335
  }
327
336
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.5.8",
3
+ "version": "0.5.10",
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
@@ -1626,9 +1626,17 @@ function shouldHandle(msg, chatConfig, botUsername) {
1626
1626
  const text = msg.text || msg.caption || '';
1627
1627
  const isReplyToBot = msg.reply_to_message?.from?.username === botUsername;
1628
1628
  const hasMention = text.includes(`@${botUsername}`);
1629
- // Paired users bypass requireMention they've been explicitly trusted
1630
- // in this chat by an operator, no need for a mention every time.
1631
- const paired = pairings && msg.from?.id
1629
+ // A reply targeting some other user (not the bot) is a strong signal
1630
+ // "this message is for that person, not me". Paired users normally
1631
+ // bypass requireMention, but not in this case — without the guard a
1632
+ // paired user saying "Gotcha!" to a teammate gets processed by the
1633
+ // bot just because the user is paired, which is what bit us in
1634
+ // UMI Group on 0.5.9 (bot leaked reasoning as a reply to "Gotcha!").
1635
+ const repliesToOtherUser = !!msg.reply_to_message
1636
+ && msg.reply_to_message.from?.username !== botUsername;
1637
+ // Paired users bypass requireMention — operator-trusted, no @ needed
1638
+ // every time. Skipped when they're replying to a non-bot user (above).
1639
+ const paired = !repliesToOtherUser && pairings && msg.from?.id
1632
1640
  ? pairings.hasLivePairing({ bot_name: BOT_NAME, user_id: msg.from.id, chat_id: chatId })
1633
1641
  : false;
1634
1642
  if (!isReplyToBot && !hasMention && !paired) return false;
@@ -2174,8 +2182,13 @@ async function main() {
2174
2182
  },
2175
2183
  // Fires after a graceful /model or /effort drain has actually
2176
2184
  // swapped to the new settings. Post a confirmation back to the
2177
- // chat so the user knows the switch happened.
2178
- onRespawn: (sessionKey, reason, entry) => {
2185
+ // chat ONLY when wasDrained=true — the user actively waited for an
2186
+ // in-flight turn to finish before the switch took effect, so the
2187
+ // explicit "switched" message is meaningful. When the kill was
2188
+ // immediate (queue empty), the inline-card update + button toast
2189
+ // already convey "done", and a separate message is just noise.
2190
+ onRespawn: (sessionKey, reason, entry, wasDrained) => {
2191
+ if (!wasDrained) return;
2179
2192
  const chatId = entry.chatId;
2180
2193
  if (!chatId) return;
2181
2194
  const chatConfig = config.chats[chatId];