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.
- package/.claude-plugin/plugin.json +1 -1
- package/lib/context-format.js +23 -6
- package/lib/replay-window.js +53 -0
- package/package.json +1 -1
- package/polygram.js +43 -16
|
@@ -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.
|
|
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",
|
package/lib/context-format.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure formatters for /context command output and the
|
|
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 =
|
|
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
|
|
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
|
-
* @
|
|
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 <
|
|
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.
|
|
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:
|
|
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
|
-
//
|
|
2865
|
-
// SDK pm only — CLI pm has no equivalent (no
|
|
2866
|
-
// no getContextUsage). OPT-IN per-chat or
|
|
2867
|
-
//
|
|
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
|
-
|
|
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`
|
|
4157
|
-
//
|
|
4158
|
-
//
|
|
4159
|
-
//
|
|
4160
|
-
//
|
|
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;
|