polygram 0.8.0-rc.55 → 0.8.0-rc.57

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.55",
4
+ "version": "0.8.0-rc.57",
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",
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Pure formatters for /context command output and the 85%-full hint.
2
+ * Pure formatters for /context command output and the context-full hint.
3
3
  *
4
4
  * Lifted from polygram.js so the formatting can be unit-tested without
5
5
  * spinning up the full handleMessage stack. Both functions are pure —
@@ -12,11 +12,22 @@
12
12
  * 0-1 ratio and multiplied by 100, which displayed "7700% full" and
13
13
  * skipped the 85% hint threshold. The formatters below assume the
14
14
  * 0-100 scale; do not multiply or divide.
15
+ *
16
+ * rc.56 threshold change: default lowered from 85 → 70.
17
+ * Background: at 85% the SDK has typically already auto-compacted
18
+ * mid-turn, so polygram's post-turn check sees a low percentage
19
+ * and the hint never fires. Production data showed 0 user-visible
20
+ * hint triggers across 15 auto-compactions in May 2026, all of
21
+ * which fired at pre_tokens 167-262k (≈85% of Sonnet's 200k
22
+ * window). Lowering to 70% means polygram warns ~30k tokens
23
+ * before the SDK auto-compacts, giving the user 1-3 turns of
24
+ * headroom to choose /new vs /compact vs continue. Configurable
25
+ * per-bot or per-chat via `contextHintThreshold` (number, 0-100).
15
26
  */
16
27
 
17
28
  'use strict';
18
29
 
19
- const HINT_THRESHOLD_PCT = 85;
30
+ const HINT_THRESHOLD_PCT = 70;
20
31
 
21
32
  /**
22
33
  * Format a getContextUsage() result into a multi-line chat reply.
@@ -55,14 +66,20 @@ function formatContextReply(usage) {
55
66
  }
56
67
 
57
68
  /**
58
- * Decide whether to send the 85% hint and return the hint text if so.
69
+ * Decide whether to send the context-full hint and return the hint
70
+ * text if so.
59
71
  *
60
72
  * @param {object} usage — same shape as formatContextReply input.
61
- * @returns {string|null} the hint text to send, or null when below threshold.
73
+ * @param {object} [opts]
74
+ * @param {number} [opts.threshold] — override the default percent
75
+ * threshold (rc.56). Caller resolves per-chat / per-bot config
76
+ * and passes it in. Defaults to HINT_THRESHOLD_PCT (70).
77
+ * @returns {string|null} the hint text to send, or null when below
78
+ * threshold.
62
79
  */
63
- function maybeContextFullHint(usage) {
80
+ function maybeContextFullHint(usage, { threshold = HINT_THRESHOLD_PCT } = {}) {
64
81
  const pct = usage?.percentage ?? 0;
65
- if (pct < HINT_THRESHOLD_PCT) return null;
82
+ if (pct < threshold) return null;
66
83
  return [
67
84
  `📚 Context window ${pct.toFixed(0)}% full. Three options:`,
68
85
  '',
@@ -0,0 +1,53 @@
1
+ /**
2
+ * rc.57: pure resolver for the boot-replay window in milliseconds.
3
+ *
4
+ * Lifted out of polygram.js's main() so the derivation rule can be
5
+ * unit-tested without spinning up the daemon.
6
+ *
7
+ * Precedence:
8
+ * 1. config.bot.replayWindowMs — explicit operator override (any
9
+ * positive integer in ms).
10
+ * 2. Auto-derive from max(maxTurn) × 1.2 across all configured chats
11
+ * (and defaults.maxTurn). Reasoning: if a chat allows turns up to
12
+ * maxTurn seconds, an interrupted turn could be that old when
13
+ * polygram restarts; replay window should outlast it. ×1.2 adds
14
+ * buffer.
15
+ * 3. If no maxTurn is configured anywhere, return undefined (db.js
16
+ * uses its 3-min default).
17
+ *
18
+ * Floor at 3 min (legacy default — never tighter than what we shipped
19
+ * before). Cap at 2h (sanity bound — replaying anything older is
20
+ * almost certainly stale work the user already moved on from).
21
+ *
22
+ * Discovery: msg 151 in Shumabit@UMI thread :24 (chat -1003369922517)
23
+ * was sent 2026-05-05 01:55:14, polygram restarted for rc.56 at
24
+ * 02:17 (22 min later). Pre-rc.57 the 3-min default discarded msg 151
25
+ * as too old; the agent's 7-hour Xero-template-build task was
26
+ * abandoned silently. Shumabit@UMI has maxTurn=3600 (60 min); 1.2×
27
+ * = 72 min replay window now keeps long turns alive across deploys.
28
+ */
29
+
30
+ 'use strict';
31
+
32
+ const FLOOR_MS = 3 * 60 * 1000; // 3 min
33
+ const CAP_MS = 2 * 60 * 60 * 1000; // 2 h
34
+ const BUFFER = 1.2; // ×
35
+
36
+ function resolveReplayWindowMs(config) {
37
+ const explicit = Number(config?.bot?.replayWindowMs);
38
+ if (Number.isInteger(explicit) && explicit > 0) return explicit;
39
+ const chatMaxes = Object.values(config?.chats || {})
40
+ .map((c) => Number(c?.maxTurn) || 0);
41
+ const defaultMax = Number(config?.defaults?.maxTurn) || 0;
42
+ const maxTurnSec = Math.max(0, ...chatMaxes, defaultMax);
43
+ if (maxTurnSec === 0) return undefined;
44
+ const derivedMs = Math.round(maxTurnSec * BUFFER * 1000);
45
+ return Math.max(FLOOR_MS, Math.min(CAP_MS, derivedMs));
46
+ }
47
+
48
+ module.exports = {
49
+ resolveReplayWindowMs,
50
+ FLOOR_MS,
51
+ CAP_MS,
52
+ BUFFER,
53
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.8.0-rc.55",
3
+ "version": "0.8.0-rc.57",
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
@@ -66,6 +66,7 @@ const { createReactionManager, classifyToolName } = require('./lib/status-reacti
66
66
  const { createMediaGroupBuffer } = require('./lib/media-group-buffer');
67
67
  const { classify: classifyError, isTransientHttpError } = require('./lib/error-classify');
68
68
  const { createAutoResumeTracker, isAutoResumable } = require('./lib/auto-resume');
69
+ const { resolveReplayWindowMs } = require('./lib/replay-window');
69
70
  const {
70
71
  createStore: createApprovalsStore,
71
72
  matchesAnyPattern: matchesApprovalPattern,
@@ -2859,14 +2860,22 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
2859
2860
  // result, the followup messages (if any) get their own SDK
2860
2861
  // pause to absorb at, no special handling needed.
2861
2862
 
2862
- // 0.8.0 Phase 2 step 4: 85%-context-full live hint. After a
2863
+ // 0.8.0 Phase 2 step 4: context-full live hint. After a
2863
2864
  // successful turn, peek at SDK's getContextUsage(); if past
2864
- // 85%, post a quiet hint so the user knows /new will help.
2865
- // SDK pm only — CLI pm has no equivalent (no Query object,
2866
- // no getContextUsage). OPT-IN per-chat or per-bot
2867
- // (rc.12+) — most chats don't want the noise. Per-chat takes
2865
+ // the threshold, post a quiet hint so the user knows /new
2866
+ // will help. SDK pm only — CLI pm has no equivalent (no
2867
+ // Query object, no getContextUsage). OPT-IN per-chat or
2868
+ // per-bot — most chats don't want the noise. Per-chat takes
2868
2869
  // precedence over per-bot so admins (Ivan DM) can opt in
2869
2870
  // without forcing it on every other chat.
2871
+ //
2872
+ // rc.56: threshold default lowered to 70% (was 85%) because
2873
+ // the SDK auto-compacts mid-turn at ~85% — by the time
2874
+ // polygram queries getContextUsage post-turn, the percentage
2875
+ // has already dropped and the hint never fires. 70% gives
2876
+ // the user 1-3 turns of headroom before SDK compaction.
2877
+ // Configurable via `contextHintThreshold` (number, 0-100)
2878
+ // per-chat or per-bot. Same precedence rule as contextHint.
2870
2879
  const chatCtxHint = chatConfig.contextHint != null
2871
2880
  ? chatConfig.contextHint
2872
2881
  : config.bot?.contextHint;
@@ -2874,8 +2883,13 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
2874
2883
  const entry = pm.get(sessionKey);
2875
2884
  const q = entry?.query;
2876
2885
  if (q && typeof q.getContextUsage === 'function') {
2886
+ const threshold = chatConfig.contextHintThreshold != null
2887
+ ? chatConfig.contextHintThreshold
2888
+ : (config.bot?.contextHintThreshold != null
2889
+ ? config.bot.contextHintThreshold
2890
+ : undefined);
2877
2891
  q.getContextUsage().then((usage) => {
2878
- const text = maybeContextFullHint(usage);
2892
+ const text = maybeContextFullHint(usage, threshold != null ? { threshold } : undefined);
2879
2893
  if (!text) return;
2880
2894
  return tg(bot, 'sendMessage', {
2881
2895
  chat_id: chatId,
@@ -3641,7 +3655,13 @@ async function pollBot(bot) {
3641
3655
  const chatId = m.chat.id.toString();
3642
3656
  const chatConfig = config.chats[chatId];
3643
3657
  const threadId = m.message_thread_id?.toString();
3644
- const topicName = threadId && chatConfig?.topics?.[threadId] ? chatConfig.topics[threadId] : threadId;
3658
+ // rc.57: use getTopicName() helper which handles BOTH legacy string
3659
+ // form and rc.48 object form. Pre-rc.57 the direct lookup
3660
+ // `chatConfig.topics[threadId]` template-literal'd into "[object Object]"
3661
+ // because rc.48 topics are objects like {name:"Music",agent:"...",...}.
3662
+ const topicName = threadId
3663
+ ? (chatConfig ? getTopicName(chatConfig, threadId) : threadId)
3664
+ : null;
3645
3665
  const chatLabel = chatConfig?.name || chatId;
3646
3666
  const label = topicName ? `${chatLabel}/${topicName}` : chatLabel;
3647
3667
  console.log(`[${BOT_NAME}] ← ${label}: ${(m.text || m.caption || '(media)').slice(0, 60)}`);
@@ -4153,18 +4173,25 @@ async function main() {
4153
4173
  // Boot replay: re-dispatch any inbound turns that were interrupted by
4154
4174
  // the previous polygram's shutdown or crash. These are rows marked
4155
4175
  // 'dispatched', 'processing', or 'replay-pending' (set by the SIGTERM
4156
- // handler) — all within the last `replayWindowMs` (default 3 min) so
4157
- // we don't resurrect ancient work. Override via
4158
- // `config.bot.replayWindowMs` for ops tuning. Dedupe against
4159
- // already-sent outbound replies in case the previous instance DID
4160
- // answer before dying.
4176
+ // handler) — all within the last `replayWindowMs` so we don't
4177
+ // resurrect ancient work. Dedupe against already-sent outbound
4178
+ // replies in case the previous instance DID answer before dying.
4179
+ //
4180
+ // rc.57: auto-derive replayWindowMs from max(maxTurn) * 1.2 when not
4181
+ // explicitly set. Pre-rc.57 the default was 3 min — but chats with
4182
+ // long agent tasks (Shumabit@UMI maxTurn=3600 = 60 min) would have
4183
+ // their interrupted turns silently dropped because the turn was
4184
+ // typically older than 3 min when polygram restarted. Discovery
4185
+ // context: msg 151 in Shumabit@UMI thread :24 on 2026-05-05 was
4186
+ // sent at 01:55:14, polygram restarted for rc.56 at 02:17 (22 min
4187
+ // later). msg 151 was 'replay-pending' but boot-replay's 3-min
4188
+ // window discarded it; the agent's 7-hour Xero task was abandoned.
4189
+ // Auto-derive: 1.2 × max(chatConfig.maxTurn) across all chats,
4190
+ // floored at 3 min (legacy default), capped at 2 hours (sanity).
4161
4191
  try {
4162
4192
  const chatIds = Object.keys(config.chats);
4163
4193
  if (chatIds.length > 0) {
4164
- const replayWindowMs = (() => {
4165
- const v = Number(config.bot?.replayWindowMs);
4166
- return (Number.isInteger(v) && v > 0) ? v : undefined; // undefined → use db.js default
4167
- })();
4194
+ const replayWindowMs = resolveReplayWindowMs(config);
4168
4195
  const candidates = db.getReplayCandidates({ chatIds, ...(replayWindowMs && { olderThanMs: replayWindowMs }) });
4169
4196
  let replayed = 0;
4170
4197
  let skipped = 0;