polygram 0.8.0-rc.63 β†’ 0.8.0-rc.65

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.63",
4
+ "version": "0.8.0-rc.65",
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 plus history (transcript queries) and polygram-send (out-of-turn IPC sends with file-upload validation) skills.",
6
6
  "keywords": [
7
7
  "telegram",
package/lib/db.js CHANGED
@@ -360,7 +360,8 @@ function wrap(db) {
360
360
  json_extract(detail_json, '$.session_key') AS session_key,
361
361
  json_extract(detail_json, '$.user') AS user,
362
362
  json_extract(detail_json, '$.user_id') AS user_id,
363
- json_extract(detail_json, '$.text_len') AS text_len
363
+ json_extract(detail_json, '$.text_len') AS text_len,
364
+ json_extract(detail_json, '$.text') AS text
364
365
  FROM events
365
366
  WHERE kind = 'compact-command'
366
367
  AND ts > ?
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.8.0-rc.63",
3
+ "version": "0.8.0-rc.65",
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
@@ -2172,11 +2172,37 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
2172
2172
  await sendReply('πŸ—œοΈ /compact requires the SDK pm. This chat is on the CLI pm path.');
2173
2173
  return;
2174
2174
  }
2175
- if (!pm.has(sessionKey)) {
2176
- await sendReply('πŸ—œοΈ No active session β€” /compact only works once a turn has started.');
2177
- return;
2175
+ // rc.64: if the in-memory session was evicted (LRU cap pressure)
2176
+ // but there's a saved Claude session_id in DB, auto-spawn the
2177
+ // Query with --resume so /compact has something to work with.
2178
+ // Pre-rc.64 we returned "πŸ—œοΈ No active session" β€” confusing
2179
+ // because the user just had a conversation 5 minutes ago, the
2180
+ // session went idle, LRU evicted it, and "No active session"
2181
+ // reads as "I never knew you" instead of "I unloaded your
2182
+ // session, hold on."
2183
+ let entry = pm.get(sessionKey);
2184
+ if (!entry) {
2185
+ const savedSessionId = getClaudeSessionId(db, sessionKey);
2186
+ if (!savedSessionId) {
2187
+ await sendReply('πŸ—œοΈ No conversation to compact yet. Send a message first, then /compact.');
2188
+ return;
2189
+ }
2190
+ try {
2191
+ entry = await getOrSpawnForChat(sessionKey);
2192
+ } catch (err) {
2193
+ console.error(`[${label}] /compact spawn-resume: ${err.message}`);
2194
+ await sendReply(`πŸ—œοΈ Couldn't load session for compaction: ${err.message}`);
2195
+ return;
2196
+ }
2197
+ if (!entry) {
2198
+ await sendReply('πŸ—œοΈ Session not loadable (config missing).');
2199
+ return;
2200
+ }
2201
+ logEvent('compact-spawn-resumed', {
2202
+ chat_id: chatId, thread_id: threadIdStr, session_key: sessionKey,
2203
+ resumed_session_id: savedSessionId,
2204
+ });
2178
2205
  }
2179
- const entry = pm.get(sessionKey);
2180
2206
  if (!entry?.inputController?.push) {
2181
2207
  await sendReply('πŸ—œοΈ Session not ready for /compact (no input controller).');
2182
2208
  return;
@@ -2194,6 +2220,12 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
2194
2220
  logEvent('compact-command', {
2195
2221
  chat_id: chatId, thread_id: threadIdStr, session_key: sessionKey,
2196
2222
  text_len: text.length,
2223
+ // rc.65: store the full /compact text so boot-time orphan
2224
+ // recovery can silently re-push it after a deploy
2225
+ // interrupted compaction. Agent-supplied hints (e.g.
2226
+ // "/compact keep the Q3 commission decisions") need to be
2227
+ // preserved verbatim for the retry to summarize correctly.
2228
+ text,
2197
2229
  user: cmdUser, user_id: cmdUserId,
2198
2230
  });
2199
2231
  const hasHint = text.length > '/compact'.length + 1;
@@ -4394,28 +4426,77 @@ async function main() {
4394
4426
  console.error(`[replay] boot replay failed: ${err.message}`);
4395
4427
  }
4396
4428
 
4397
- // rc.61: surface compact-command events that were never paired with
4398
- // a compact-boundary (i.e. interrupted by deploy / crash). The SDK
4399
- // accepts /compact via stream input but compaction itself runs at a
4400
- // turn boundary; if polygram restarts before the SDK gets a chance
4401
- // to actually compact, the work is lost silently. Pre-rc.61 the
4402
- // user only saw "πŸ—œοΈ Compacting with your hint…" then nothing β€”
4403
- // and on the next over-threshold turn, the contextHint reminded
4404
- // them context was still full. Now: detect orphaned compact-command
4405
- // events on boot, post a "compaction was interrupted" message to
4406
- // the chat so the user knows to re-run.
4429
+ // rc.61 + rc.65: handle compact-command events that were never
4430
+ // paired with a compact-boundary (deploy / crash interrupted them
4431
+ // before the SDK processed). rc.65 changes behaviour from
4432
+ // "post a 'please retry' message and ask the user" to
4433
+ // "silently retry by re-pushing the same /compact text to a
4434
+ // freshly-spawned (resumed) Query." Same pattern as boot-replay
4435
+ // for inbound messages β€” recovery should be invisible.
4436
+ //
4437
+ // Requirements for a silent retry:
4438
+ // 1. The compact-command event was logged with full `text`
4439
+ // (rc.65+ does this; pre-rc.65 events have only text_len β€”
4440
+ // we fall back to the old "please retry" message for those).
4441
+ // 2. The chat is still configured (config.chats[chat_id] exists).
4442
+ // 3. The session has a saved claude_session_id we can resume.
4407
4443
  //
4408
- // Scan window matches replayWindowMs β€” same logic: anything older
4409
- // than the bot's expected long-turn ceiling is stale and not worth
4410
- // surfacing.
4444
+ // Dedupe: if multiple orphans for the same session_key (e.g. user
4445
+ // ran /compact twice in quick succession before a deploy), retry
4446
+ // only the MOST RECENT β€” older ones are obsolete.
4447
+ //
4448
+ // Scan window matches replayWindowMs β€” anything older than the
4449
+ // bot's expected long-turn ceiling is stale.
4411
4450
  try {
4412
- const orphans = db.findOrphanedCompactCommands({
4451
+ const orphansAll = db.findOrphanedCompactCommands({
4413
4452
  olderThanMs: resolveReplayWindowMs(config) ?? 30 * 60 * 1000,
4414
4453
  });
4415
- for (const o of orphans) {
4454
+ // Dedupe per-session_key, keep the most recent (highest ts).
4455
+ const orphansLatest = new Map();
4456
+ for (const o of orphansAll) {
4457
+ orphansLatest.set(o.session_key, o);
4458
+ }
4459
+ let replayed = 0;
4460
+ let surfacedFallback = 0;
4461
+ for (const o of orphansLatest.values()) {
4416
4462
  const chatCfg = config.chats[o.chat_id];
4417
- if (!chatCfg) continue; // chat not owned by this bot
4463
+ if (!chatCfg) continue;
4418
4464
  const threadId = o.thread_id ? Number(o.thread_id) : null;
4465
+ const savedSessionId = getClaudeSessionId(db, o.session_key);
4466
+
4467
+ // Silent retry path: only when we have BOTH the original text
4468
+ // (rc.65+) AND a session_id to resume into.
4469
+ if (o.text && savedSessionId) {
4470
+ try {
4471
+ const entry = await pm.getOrSpawn(o.session_key, buildSpawnContext(o.session_key));
4472
+ if (!entry?.inputController?.push) {
4473
+ throw new Error('input controller not ready post-spawn');
4474
+ }
4475
+ entry.inputController.push({
4476
+ type: 'user',
4477
+ message: { role: 'user', content: o.text },
4478
+ parent_tool_use_id: null,
4479
+ });
4480
+ logEvent('compact-replay', {
4481
+ chat_id: o.chat_id,
4482
+ thread_id: o.thread_id,
4483
+ session_key: o.session_key,
4484
+ original_ts: o.ts,
4485
+ text_len: o.text.length,
4486
+ user: o.user,
4487
+ user_id: o.user_id,
4488
+ });
4489
+ replayed += 1;
4490
+ continue;
4491
+ } catch (err) {
4492
+ console.error(`[compact-replay] ${o.session_key}: ${err.message} β€” falling back to surface`);
4493
+ // fall through to surface fallback below
4494
+ }
4495
+ }
4496
+
4497
+ // Fallback: surface the legacy "please retry" message. Only
4498
+ // happens for pre-rc.65 events (no `text` field) or when
4499
+ // the silent-retry spawn failed.
4419
4500
  try {
4420
4501
  await tg(bot, 'sendMessage', {
4421
4502
  chat_id: o.chat_id,
@@ -4429,16 +4510,18 @@ async function main() {
4429
4510
  original_ts: o.ts,
4430
4511
  user: o.user,
4431
4512
  user_id: o.user_id,
4513
+ reason: o.text ? 'spawn-failed' : 'pre-rc65-event-no-text',
4432
4514
  });
4515
+ surfacedFallback += 1;
4433
4516
  } catch (err) {
4434
4517
  console.error(`[compact-orphan-surface] ${o.session_key}: ${err.message}`);
4435
4518
  }
4436
4519
  }
4437
- if (orphans.length > 0) {
4438
- console.log(`[compact-orphan] surfaced ${orphans.length} interrupted /compact events`);
4520
+ if (replayed + surfacedFallback > 0) {
4521
+ console.log(`[compact-orphan] silent-replayed=${replayed}, surfaced-fallback=${surfacedFallback}`);
4439
4522
  }
4440
4523
  } catch (err) {
4441
- console.error(`[compact-orphan-surface] failed: ${err.message}`);
4524
+ console.error(`[compact-orphan-handler] failed: ${err.message}`);
4442
4525
  }
4443
4526
 
4444
4527
  console.log(`[${BOT_NAME}] Starting...`);