polygram 0.10.0-rc.2 → 0.10.0-rc.20

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.10.0-rc.2",
4
+ "version": "0.10.0-rc.20",
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",
@@ -46,12 +46,22 @@
46
46
  * logged to opts.logger?.error — they never block clearing of
47
47
  * subsequent refs.
48
48
  * @param {{ error?: (msg: string) => void }} [opts.logger]
49
+ * @param {number} [opts.minIntervalMs=250]
50
+ * minimum gap (ms) between successive applyClear calls inside a
51
+ * single clear() loop. Telegram's setMessageReaction rate limit
52
+ * is ~5/sec/chat; 250ms (4/sec) stays under that. Pass 0 to
53
+ * disable pacing in tests / contexts where the underlying applyClear
54
+ * doesn't talk to a rate-limited API. Only the GAP between calls
55
+ * is paced — the first call fires immediately, single-ref clears
56
+ * incur no delay. L7 fix 2026-05-16: was unpaced, exceeded the
57
+ * Telegram cap under N≥6 autosteers per turn.
49
58
  * @returns {AutosteeredRefs}
50
59
  */
51
- function createAutosteeredRefs({ applyClear, logger = console } = {}) {
60
+ function createAutosteeredRefs({ applyClear, logger = console, minIntervalMs = 250 } = {}) {
52
61
  if (typeof applyClear !== 'function') {
53
62
  throw new TypeError('applyClear function required');
54
63
  }
64
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
55
65
  /** @type {Map<string, MsgRef[]>} */
56
66
  const refs = new Map();
57
67
 
@@ -79,7 +89,15 @@ function createAutosteeredRefs({ applyClear, logger = console } = {}) {
79
89
  if (!list || list.length === 0) return 0;
80
90
  refs.delete(sessionKey);
81
91
  let cleared = 0;
82
- for (const ref of list) {
92
+ // L7: pace inter-call gaps to stay under Telegram's
93
+ // setMessageReaction rate limit (~5/sec/chat). The first call
94
+ // fires immediately — pacing applies only to the gap BEFORE the
95
+ // 2nd+ call. minIntervalMs=0 disables pacing entirely.
96
+ for (let i = 0; i < list.length; i += 1) {
97
+ const ref = list[i];
98
+ if (i > 0 && minIntervalMs > 0) {
99
+ await sleep(minIntervalMs);
100
+ }
83
101
  try {
84
102
  await applyClear(ref);
85
103
  cleared += 1;
@@ -0,0 +1,78 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+
7
+ /**
8
+ * Resolve + verify the pinned claude CLI binary for the tmux backend.
9
+ *
10
+ * Why this exists: the tmux backend reads claude CLI INTERNAL
11
+ * artefacts (JSONL events, queue-operation semantics, TUI banner
12
+ * ASCII, READY hint strings, stop_reason values) — none a stable
13
+ * public contract. polygram pins ONE version
14
+ * (CLAUDE_CLI_PINNED_VERSION in lib/process/tmux-process.js) and
15
+ * must spawn THAT binary, never whatever `claude` on $PATH happens
16
+ * to resolve to.
17
+ *
18
+ * Before this module the tmux runner spawned the bare string
19
+ * `claude`, resolved through $PATH. The claude CLI installs each
20
+ * version as a standalone binary at
21
+ * ~/.local/share/claude/versions/<version>
22
+ * and points ~/.local/bin/claude (a symlink) at the active one.
23
+ * Its auto-updater re-points that symlink whenever a new version
24
+ * lands — so a $PATH spawn silently drifts (shumorobot 2026-05-16:
25
+ * CLI auto-updated 2.1.142 → 2.1.143 between deploys).
26
+ *
27
+ * Spawning the ABSOLUTE versioned path is immune to that: the
28
+ * updater only ADDS new version files, it never overwrites an
29
+ * existing one. `versions/2.1.142` stays byte-identical forever.
30
+ */
31
+
32
+ /**
33
+ * Absolute path to the pinned claude binary.
34
+ *
35
+ * Resolution order:
36
+ * 1. POLYGRAM_CLAUDE_BIN env — explicit override (non-standard
37
+ * installs, CI, hosts where the layout differs).
38
+ * 2. ~/.local/share/claude/versions/<version> — the standard
39
+ * claude-CLI install location.
40
+ *
41
+ * The returned path is NOT guaranteed to exist — callers verify
42
+ * via verifyPinnedClaudeBin().
43
+ *
44
+ * @param {string} version — pinned version, e.g. '2.1.142'
45
+ * @returns {string} absolute path
46
+ */
47
+ function resolvePinnedClaudeBin(version) {
48
+ const override = process.env.POLYGRAM_CLAUDE_BIN;
49
+ if (override) return override;
50
+ return path.join(os.homedir(), '.local', 'share', 'claude', 'versions', version);
51
+ }
52
+
53
+ /**
54
+ * Verify the pinned binary exists and is executable.
55
+ *
56
+ * @param {string} version — pinned version, e.g. '2.1.142'
57
+ * @returns {{ ok: boolean, path: string, reason?: string }}
58
+ * ok=true → path is a spawnable binary.
59
+ * ok=false → reason carries an operator-actionable message.
60
+ */
61
+ function verifyPinnedClaudeBin(version) {
62
+ const binPath = resolvePinnedClaudeBin(version);
63
+ try {
64
+ fs.accessSync(binPath, fs.constants.X_OK);
65
+ return { ok: true, path: binPath };
66
+ } catch (err) {
67
+ const code = err && err.code ? err.code : (err && err.message) || 'unknown';
68
+ return {
69
+ ok: false,
70
+ path: binPath,
71
+ reason: `pinned claude CLI v${version} not found or not executable at `
72
+ + `${binPath} (${code}). Install it with \`claude install ${version}\` `
73
+ + 'or set POLYGRAM_CLAUDE_BIN to the correct binary path.',
74
+ };
75
+ }
76
+ }
77
+
78
+ module.exports = { resolvePinnedClaudeBin, verifyPinnedClaudeBin };
@@ -69,9 +69,15 @@ function createAutosteerHandlers({
69
69
  if (!entry?.inFlight) return { autosteered: false };
70
70
 
71
71
  const priority = priorityFor(chatConfig, config);
72
+ // rc.7: pass the autosteered msg_id through to the backend so the
73
+ // tmux backend can route an extra-turn reply back to Telegram if
74
+ // the TUI dequeues the paste as a fresh user turn (NEW-TURN path).
75
+ // SDK backend ignores msgId — its PostToolBatch fold path
76
+ // guarantees one combined reply via the primary pm.send.
72
77
  const ok = pm.injectUserMessage(sessionKey, {
73
78
  content: prompt,
74
79
  priority,
80
+ msgId: msg.message_id,
75
81
  });
76
82
  if (!ok) return { autosteered: false };
77
83