polygram 0.7.8 → 0.8.0-rc.1

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.
@@ -276,7 +276,31 @@ function chunkMarkdownText(text, limit) {
276
276
  remaining = next;
277
277
  }
278
278
  if (remaining.length) chunks.push(remaining);
279
- return chunks;
279
+ // 0.7.x defensive post-pass: enforce limit on every chunk. The
280
+ // fence-splitting "force the break" path (line ~250) can produce
281
+ // a chunk whose length = breakIdx + closeLine.length + 1, which
282
+ // may overflow when there's no safe break. Same for the
283
+ // hard-cut path when no progress is possible. Production saw
284
+ // chunks of 4097-4500 chars hitting Telegram's 400 "message is
285
+ // too long". Splitting again byte-wise here is uglier than
286
+ // markdown-aware, but it's better than the user getting a
287
+ // failed-out row + missing reply.
288
+ const safe = [];
289
+ for (const c of chunks) {
290
+ if (c.length <= limit) { safe.push(c); continue; }
291
+ let rest = c;
292
+ while (rest.length > limit) {
293
+ // Prefer a newline cut within the last 200 chars of the
294
+ // limit (cheap heuristic — much better than mid-word).
295
+ const window = rest.slice(0, limit);
296
+ const lastNl = window.lastIndexOf('\n', limit - 1);
297
+ const cutAt = lastNl > limit - 200 ? lastNl + 1 : limit;
298
+ safe.push(rest.slice(0, cutAt));
299
+ rest = rest.slice(cutAt);
300
+ }
301
+ if (rest.length) safe.push(rest);
302
+ }
303
+ return safe;
280
304
  }
281
305
 
282
306
  module.exports = {
@@ -0,0 +1,62 @@
1
+ -- 0.8.0 Phase 1 step 1: SDK migration prep.
2
+ --
3
+ -- Two changes (atomic in one migration to keep schema-version
4
+ -- bookkeeping clean):
5
+ --
6
+ -- (1) Add `tool_use_id` column to pending_approvals — under SDK,
7
+ -- canUseTool's `opts.toolUseID` is a stable per-call identifier
8
+ -- that the SDK guarantees across retries of the same tool call
9
+ -- within a turn. It's the better dedup key than the current
10
+ -- (bot_name, turn_id, tool_input_digest) tuple — avoids the
11
+ -- digest collision risk if Claude reorders JSON keys between
12
+ -- retries.
13
+ --
14
+ -- Existing rows get NULL. Boot-time sweep marks legacy 'pending'
15
+ -- rows as 'expired' so the new dedup query (matching on
16
+ -- non-NULL tool_use_id) doesn't accidentally match them. See
17
+ -- v4 plan §6.5.4 + seam G.
18
+ --
19
+ -- (2) Add chat_tool_decisions table — cross-turn "always allow /
20
+ -- always deny" persistence for the new 4-button approval UI
21
+ -- (Allow / Deny / Always allow / Always deny). canUseTool
22
+ -- consults this table FIRST (step 3 of §4.2) before posting a
23
+ -- card; "always" buttons write here. Per-bot scoping prevents
24
+ -- cross-bot leakage (ship-breaker H7 mitigation).
25
+ --
26
+ -- match_type semantics (v4 plan §6.5.4):
27
+ -- 'exact' — full canonical-JSON-stringify of `input` matches
28
+ -- input_pattern exactly. Default for "always allow
29
+ -- this exact command".
30
+ -- 'prefix' — canonical-JSON `input` starts with input_pattern.
31
+ -- Default for "always allow this command name with
32
+ -- any args".
33
+ -- 'regex' — full match against canonical input. Power user
34
+ -- only; not exposed in 0.8.0 UI.
35
+ --
36
+ -- Canonical JSON: keys sorted alphabetically, no whitespace.
37
+ -- Prevents Claude reordering keys from breaking dedup
38
+ -- (ship-breaker M8 mitigation).
39
+
40
+ ALTER TABLE pending_approvals ADD COLUMN tool_use_id TEXT;
41
+
42
+ CREATE INDEX IF NOT EXISTS idx_pending_approvals_tool_use_id
43
+ ON pending_approvals(bot_name, tool_use_id)
44
+ WHERE tool_use_id IS NOT NULL;
45
+
46
+ CREATE TABLE IF NOT EXISTS chat_tool_decisions (
47
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
48
+ bot_name TEXT NOT NULL,
49
+ chat_id TEXT NOT NULL,
50
+ tool_name TEXT NOT NULL,
51
+ match_type TEXT NOT NULL
52
+ CHECK (match_type IN ('exact', 'prefix', 'regex')),
53
+ input_pattern TEXT NOT NULL,
54
+ decision TEXT NOT NULL
55
+ CHECK (decision IN ('allow', 'deny')),
56
+ issued_ts INTEGER NOT NULL,
57
+ issued_by_user_id TEXT,
58
+ expires_ts INTEGER
59
+ );
60
+
61
+ CREATE INDEX IF NOT EXISTS idx_chat_tool_decisions_lookup
62
+ ON chat_tool_decisions(bot_name, chat_id, tool_name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.7.8",
3
+ "version": "0.8.0-rc.1",
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": {
@@ -62,6 +62,7 @@
62
62
  "homepage": "https://github.com/shumkov/polygram#readme",
63
63
  "type": "commonjs",
64
64
  "dependencies": {
65
+ "@anthropic-ai/claude-agent-sdk": "0.2.123",
65
66
  "better-sqlite3": "^12.9.0",
66
67
  "grammy": "^1.42.0",
67
68
  "markdown-it": "^14.1.1",