polygram 0.10.0-rc.38 → 0.10.0-rc.39

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.38",
4
+ "version": "0.10.0-rc.39",
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",
@@ -0,0 +1,82 @@
1
+ /**
2
+ * sanitize-reply — outbound assistant-text sanitizer for claude-CLI
3
+ * canned-string leakage.
4
+ *
5
+ * The model occasionally emits CLI-context boilerplate strings
6
+ * verbatim as Telegram replies — typically when its reasoning
7
+ * decides "no response needed." `POLYGRAM_DISPLAY_HINT` (rc.37
8
+ * hardening) explicitly forbids them, but the hint mitigation
9
+ * proved partial: the model still leaked `No response requested.`
10
+ * on a substantive user question (shumorobot Music, 2026-05-22
11
+ * 14:14). Likely CLI-internal, not prompt-driven.
12
+ *
13
+ * This sanitizer is the polygram-side safety net. Runs AFTER
14
+ * `parseResponse` — sees the parsed text the streamer/deliver
15
+ * path will send. On a verbatim match against a narrow allowlist
16
+ * of known canned strings, replaces with an honest brief message
17
+ * the user can act on (rephrase / retry).
18
+ *
19
+ * Narrow allowlist on purpose:
20
+ * - Exact full-text match (not substring) — paranoia against
21
+ * accidentally rewriting legitimate replies that mention these
22
+ * phrases (e.g. an explanation of the issue itself).
23
+ * - Does NOT include `No response generated. Please try again.`
24
+ * because that's polygram's own R10 empty-turn fallback, which
25
+ * is intentional output.
26
+ * - Does NOT include `Stopped.` because that's polygram's `/stop`
27
+ * confirmation.
28
+ *
29
+ * If new canned strings are observed in production, add them to
30
+ * CANNED_STRINGS with a comment naming the production trace.
31
+ */
32
+
33
+ 'use strict';
34
+
35
+ // Exact-match (trimmed) canned strings to intercept. Keep this list
36
+ // short and explicit — every entry is a known production leak.
37
+ const CANNED_STRINGS = new Set([
38
+ // shumorobot 2026-05-22 (Music topic, 13:17 and 14:14, both on
39
+ // rc.36/37). Model emitted this verbatim on the first occurrence
40
+ // after an ambiguous ack ("okay"); on the second, after a real
41
+ // substantive question. Prompt-side mitigation (rc.37) didn't
42
+ // catch the second case — confirming this is CLI-internal.
43
+ 'No response requested.',
44
+ // Listed in the rc.37 display hint as an adjacent variant. Treated
45
+ // the same way if it ever appears.
46
+ 'No response needed.',
47
+ ]);
48
+
49
+ // Replacement text — italic, brief, honest, actionable. Avoids
50
+ // pretending the bot did useful work; tells the user explicitly that
51
+ // the model didn't generate a real reply.
52
+ const SANITIZED_REPLACEMENT =
53
+ '_(the model returned no actual reply — try rephrasing or asking again)_';
54
+
55
+ /**
56
+ * Inspect an outbound assistant text. If the FULL TRIMMED text
57
+ * matches a known CLI-context canned string, return the honest
58
+ * replacement and a `replaced` flag so the caller can log the
59
+ * substitution. Otherwise return the original text unchanged.
60
+ *
61
+ * @param {string} text — the assistant text about to be sent.
62
+ * @returns {{ text: string, replaced: boolean, original?: string }}
63
+ */
64
+ function sanitizeAssistantReply(text) {
65
+ if (typeof text !== 'string') return { text, replaced: false };
66
+ const trimmed = text.trim();
67
+ if (!trimmed) return { text, replaced: false };
68
+ if (CANNED_STRINGS.has(trimmed)) {
69
+ return {
70
+ text: SANITIZED_REPLACEMENT,
71
+ replaced: true,
72
+ original: trimmed,
73
+ };
74
+ }
75
+ return { text, replaced: false };
76
+ }
77
+
78
+ module.exports = {
79
+ CANNED_STRINGS,
80
+ SANITIZED_REPLACEMENT,
81
+ sanitizeAssistantReply,
82
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.10.0-rc.38",
3
+ "version": "0.10.0-rc.39",
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
@@ -80,6 +80,7 @@ const { transcribe: transcribeVoice, isVoiceAttachment } = require('./lib/telegr
80
80
  const { createStreamer } = require('./lib/telegram/streamer');
81
81
  const { chunkMarkdownText } = require('./lib/telegram/chunk');
82
82
  const { deliverReplies } = require('./lib/telegram/deliver');
83
+ const { sanitizeAssistantReply } = require('./lib/telegram/sanitize-reply');
83
84
  const { announce, shouldAnnounce } = require('./lib/announces');
84
85
  const { isAbortRequest } = require('./lib/abort-detector');
85
86
  const { startTyping } = require('./lib/telegram/typing');
@@ -1308,6 +1309,25 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
1308
1309
  }
1309
1310
 
1310
1311
  const parsed = parseResponse(result.text);
1312
+ // rc.39: intercept CLI-context canned-string leaks (`No response
1313
+ // requested.` etc.) before they reach the streamer/deliver path.
1314
+ // Replaces with an honest brief message; logs the substitution
1315
+ // for forensic post-hoc analysis of how often the leak fires.
1316
+ // See lib/telegram/sanitize-reply.js for the (narrow) allowlist
1317
+ // and rationale — the rc.37 prompt-side hint mitigation proved
1318
+ // insufficient, so this is the polygram-layer safety net.
1319
+ if (parsed.text) {
1320
+ const sanitized = sanitizeAssistantReply(parsed.text);
1321
+ if (sanitized.replaced) {
1322
+ logEvent('canned-reply-suppressed', {
1323
+ chat_id: chatId,
1324
+ msg_id: msg.message_id,
1325
+ original: sanitized.original,
1326
+ backend: result?.backend || null,
1327
+ });
1328
+ parsed.text = sanitized.text;
1329
+ }
1330
+ }
1311
1331
  const outMeta = { ...outMetaBase, sessionId: result.sessionId, costUsd: result.cost };
1312
1332
 
1313
1333
  // 0.8.0-rc.39: send any inline stickers Claude embedded with