polygram 0.4.9 → 0.4.11

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.4.9",
4
+ "version": "0.4.11",
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",
@@ -54,11 +54,12 @@ class ProcessManager {
54
54
  db = null,
55
55
  logger = console,
56
56
  killTimeoutMs = DEFAULT_KILL_TIMEOUT_MS,
57
- onInit = null, // (sessionKey, event, entry) → void
58
- onResult = null, // (sessionKey, event, entry, pending) → void
59
- onClose = null, // (sessionKey, code, entry) → void
60
- onStreamChunk = null,// (sessionKey, partialText, entry) → void — routes to pendingQueue[0]
61
- onToolUse = null, // (sessionKey, toolName, entry) → void — routes to pendingQueue[0]
57
+ onInit = null, // (sessionKey, event, entry) → void
58
+ onResult = null, // (sessionKey, event, entry, pending) → void
59
+ onClose = null, // (sessionKey, code, entry) → void
60
+ onStreamChunk = null, // (sessionKey, partialText, entry) → void — routes to pendingQueue[0]
61
+ onToolUse = null, // (sessionKey, toolName, entry) → void — routes to pendingQueue[0]
62
+ onRespawn = null, // (sessionKey, reason, entry) → void — fires after graceful drain-and-kill
62
63
  } = {}) {
63
64
  if (!spawnFn) throw new Error('spawnFn required');
64
65
  this.cap = cap;
@@ -71,6 +72,7 @@ class ProcessManager {
71
72
  this.onClose = onClose;
72
73
  this.onStreamChunk = onStreamChunk;
73
74
  this.onToolUse = onToolUse;
75
+ this.onRespawn = onRespawn;
74
76
  this.procs = new Map();
75
77
  }
76
78
 
@@ -137,13 +139,22 @@ class ProcessManager {
137
139
  queued: entry.pendingQueue.length,
138
140
  });
139
141
  if (entry.pendingQueue.length === 0) {
140
- // Fire-and-forgetcaller doesn't need to await the kill.
141
- this.kill(sessionKey).catch(() => {});
142
+ // Queue empty kill immediately, fire onRespawn after close.
143
+ this._killAndNotifyRespawn(sessionKey, reason).catch(() => {});
142
144
  return { killed: true, queued: 0 };
143
145
  }
144
146
  return { killed: false, queued: entry.pendingQueue.length };
145
147
  }
146
148
 
149
+ async _killAndNotifyRespawn(sessionKey, reason) {
150
+ const entry = this.procs.get(sessionKey);
151
+ await this.kill(sessionKey);
152
+ if (this.onRespawn && entry) {
153
+ try { this.onRespawn(sessionKey, reason, entry); }
154
+ catch (err) { this.logger.error(`[pm] onRespawn: ${err.message}`); }
155
+ }
156
+ }
157
+
147
158
  async kill(sessionKey) {
148
159
  const entry = this.procs.get(sessionKey);
149
160
  if (!entry) return;
@@ -257,7 +268,8 @@ class ProcessManager {
257
268
  } else {
258
269
  entry.inFlight = false;
259
270
  // Graceful drain-and-respawn: if caller asked for a respawn
260
- // (e.g. /model change) and we just emptied the queue, kill now.
271
+ // (e.g. /model change) and we just emptied the queue, kill now
272
+ // and fire onRespawn so the caller can post confirmation.
261
273
  if (entry.needsRespawn) {
262
274
  const reason = entry.needsRespawn;
263
275
  entry.needsRespawn = null;
@@ -266,7 +278,7 @@ class ProcessManager {
266
278
  chat_id: entry.chatId,
267
279
  reason,
268
280
  });
269
- this.kill(sessionKey).catch(() => {});
281
+ this._killAndNotifyRespawn(sessionKey, reason).catch(() => {});
270
282
  }
271
283
  }
272
284
  }
@@ -398,6 +410,11 @@ class ProcessManager {
398
410
  maxTurnMs,
399
411
  );
400
412
  pending.maxTimer = maxTimer;
413
+ // Give callers a hook so they can transition user-visible state
414
+ // (e.g. status reaction "👀 queued" → "🤔 thinking") the moment
415
+ // Claude actually starts this pending, not the moment it arrived.
416
+ try { context?.onActivate?.(); }
417
+ catch (err) { this.logger.error(`[${entry.label}] onActivate: ${err.message}`); }
401
418
  };
402
419
 
403
420
  pending.resetIdleTimer = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
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
@@ -1246,7 +1246,11 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
1246
1246
  },
1247
1247
  logError: (m) => console.error(`[${label}] ${m}`),
1248
1248
  });
1249
- reactor.setState('THINKING');
1249
+ // Start at QUEUED (👀) so user sees their message was received but
1250
+ // not yet being worked on. pm calls context.onActivate when this
1251
+ // pending becomes the queue head (Claude is actually starting it),
1252
+ // at which point we flip to THINKING (🤔).
1253
+ reactor.setState('QUEUED');
1250
1254
 
1251
1255
  try {
1252
1256
  // Pass streamer + reactor as per-turn context. pm's callbacks pick
@@ -1254,6 +1258,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
1254
1258
  // get routed to their own streamer/reactor.
1255
1259
  const result = await sendToProcess(sessionKey, prompt, {
1256
1260
  streamer, reactor, sourceMsgId: msg.message_id,
1261
+ onActivate: () => reactor.setState('THINKING'),
1257
1262
  });
1258
1263
  const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
1259
1264
 
@@ -1904,6 +1909,25 @@ async function main() {
1904
1909
  const r = head?.context?.reactor;
1905
1910
  if (r) r.setState(classifyToolName(toolName));
1906
1911
  },
1912
+ // Fires after a graceful /model or /effort drain has actually
1913
+ // swapped to the new settings. Post a confirmation back to the
1914
+ // chat so the user knows the switch happened.
1915
+ onRespawn: (sessionKey, reason, entry) => {
1916
+ const chatId = entry.chatId;
1917
+ if (!chatId) return;
1918
+ const chatConfig = config.chats[chatId];
1919
+ if (!chatConfig) return;
1920
+ const text = reason === 'model-change'
1921
+ ? `✓ Using ${chatConfig.model} now.`
1922
+ : reason === 'effort-change'
1923
+ ? `✓ Effort is ${chatConfig.effort} now.`
1924
+ : `✓ Ready.`;
1925
+ const threadId = entry.threadId || undefined;
1926
+ tg(bot, 'sendMessage', {
1927
+ chat_id: chatId, text,
1928
+ ...(threadId && { message_thread_id: threadId }),
1929
+ }, { source: 'respawn-confirm', botName: BOT_NAME }).catch(() => {});
1930
+ },
1907
1931
  });
1908
1932
 
1909
1933
  console.log(`polygram (LRU cap=${cap}, SQLite source of truth)`);