polygram 0.5.7 → 0.5.9

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.7",
3
+ "version": "0.5.9",
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
@@ -811,7 +811,18 @@ function buildConfigKeyboard(chatConfig, show = 'all') {
811
811
  // Card text shown above the inline keyboard. Includes plain-language
812
812
  // guidance on when to pick which model / effort, since most users
813
813
  // (especially in shared groups) don't know which option to tap.
814
- const MODEL_VERSIONS_DESC = { opus: 'claude-opus-4-6', sonnet: 'claude-sonnet-4-6', haiku: 'claude-haiku-4-5' };
814
+ //
815
+ // Model versions: polygram passes the alias ("opus", "sonnet", "haiku")
816
+ // to claude, which resolves it to the latest snapshot. We mirror those
817
+ // snapshots here for display only, and verify them on each release with
818
+ // `claude --model <alias>` (probing the system:init event's `model`
819
+ // field). Bumping is a manual step — when claude releases a new
820
+ // snapshot the alias maps to, update this map and ship.
821
+ const MODEL_VERSIONS_DESC = {
822
+ opus: 'claude-opus-4-7',
823
+ sonnet: 'claude-sonnet-4-6',
824
+ haiku: 'claude-haiku-4-5',
825
+ };
815
826
 
816
827
  function formatConfigInfoText(chatConfig, show, sessionKey) {
817
828
  const alive = pm.has(sessionKey) && !pm.get(sessionKey).closed;
@@ -1196,7 +1207,9 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
1196
1207
  ...(tid && { message_thread_id: tid }),
1197
1208
  });
1198
1209
 
1199
- const MODEL_VERSIONS = { opus: 'claude-opus-4-6', sonnet: 'claude-sonnet-4-6', haiku: 'claude-haiku-4-5' };
1210
+ // Single source of truth at module scope (MODEL_VERSIONS_DESC) see the
1211
+ // comment there for the bump procedure.
1212
+ const MODEL_VERSIONS = MODEL_VERSIONS_DESC;
1200
1213
 
1201
1214
  const botAllowsCommands = !!config.bot?.allowConfigCommands;
1202
1215
  const cmdUser = msg.from?.first_name || msg.from?.username || null;
@@ -2161,8 +2174,13 @@ async function main() {
2161
2174
  },
2162
2175
  // Fires after a graceful /model or /effort drain has actually
2163
2176
  // swapped to the new settings. Post a confirmation back to the
2164
- // chat so the user knows the switch happened.
2165
- onRespawn: (sessionKey, reason, entry) => {
2177
+ // chat ONLY when wasDrained=true — the user actively waited for an
2178
+ // in-flight turn to finish before the switch took effect, so the
2179
+ // explicit "switched" message is meaningful. When the kill was
2180
+ // immediate (queue empty), the inline-card update + button toast
2181
+ // already convey "done", and a separate message is just noise.
2182
+ onRespawn: (sessionKey, reason, entry, wasDrained) => {
2183
+ if (!wasDrained) return;
2166
2184
  const chatId = entry.chatId;
2167
2185
  if (!chatId) return;
2168
2186
  const chatConfig = config.chats[chatId];