polygram 0.37.1 → 0.37.2

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.37.1",
4
+ "version": "0.37.2",
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",
package/lib/db.js CHANGED
@@ -553,6 +553,13 @@ function wrap(db) {
553
553
  AND bot_name = ?
554
554
  AND ts > ?
555
555
  `);
556
+ const completeAcceptedClaudeAutosteerStmt = db.prepare(`
557
+ UPDATE messages SET handler_status = 'replied'
558
+ WHERE chat_id = ?
559
+ AND msg_id = ?
560
+ AND direction = 'in'
561
+ AND handler_status IN ('dispatched', 'replay-attempted')
562
+ `);
556
563
  const clearCleanShutdownMarkerStmt = db.prepare(`
557
564
  UPDATE polling_state SET clean_shutdown_at = NULL
558
565
  WHERE bot_name = ?
@@ -4174,6 +4181,27 @@ function wrap(db) {
4174
4181
  AND handler_status NOT IN ('codex-ambiguous', 'failed')
4175
4182
  `).run(status, chat_id, msg_id);
4176
4183
  },
4184
+ completeAcceptedClaudeAutosteer({ chat_id, msg_id }) {
4185
+ let result;
4186
+ try {
4187
+ result = completeAcceptedClaudeAutosteerStmt.run(chat_id, msg_id);
4188
+ } catch (cause) {
4189
+ const error = new Error(
4190
+ 'accepted Claude follow-up handler status could not be persisted',
4191
+ { cause },
4192
+ );
4193
+ error.code = 'CLAUDE_AUTOSTEER_STATUS_PERSIST_FAILED';
4194
+ throw error;
4195
+ }
4196
+ if (result.changes !== 1) {
4197
+ const error = new Error(
4198
+ 'accepted Claude follow-up did not own one live inbound row',
4199
+ );
4200
+ error.code = 'CLAUDE_AUTOSTEER_STATUS_CONFLICT';
4201
+ throw error;
4202
+ }
4203
+ return result;
4204
+ },
4177
4205
 
4178
4206
  // 0.9.0: True when a specific inbound msg is still being processed
4179
4207
  // by the SDK turn loop (handler_status in dispatched/processing).
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ const PRIMARY_BLOCKING_CODEX_DECISIONS = new Set([
4
+ 'duplicate',
5
+ 'ambiguous',
6
+ 'unavailable',
7
+ ]);
8
+
9
+ /**
10
+ * Complete ownership of a follow-up that the Claude injection path accepted.
11
+ *
12
+ * Codex acceptance is deliberately a no-op here: its linked target turn and
13
+ * Telegram delivery checkpoint own the eventual terminal status.
14
+ */
15
+ function settleAcceptedAutosteerOwnership({
16
+ selectedProvider,
17
+ steered,
18
+ db,
19
+ chatId,
20
+ msgId,
21
+ sessionKey,
22
+ logLabel = sessionKey,
23
+ logEvent = () => {},
24
+ logger = console,
25
+ } = {}) {
26
+ if (selectedProvider !== 'claude' || steered?.autosteered !== true) {
27
+ return steered;
28
+ }
29
+
30
+ try {
31
+ db.completeAcceptedClaudeAutosteer({
32
+ chat_id: chatId,
33
+ msg_id: msgId,
34
+ });
35
+ return steered;
36
+ } catch (error) {
37
+ const errorCode = error?.code || error?.name || 'unknown';
38
+ logger.error(
39
+ `[${logLabel}] accepted Claude follow-up persistence failed: ${errorCode}`,
40
+ );
41
+ logEvent('autosteer-handler-status-failed', {
42
+ chat_id: chatId,
43
+ msg_id: msgId,
44
+ session_key: sessionKey,
45
+ code: errorCode,
46
+ });
47
+ return {
48
+ ...steered,
49
+ autosteered: false,
50
+ outcome: 'accepted-persistence-ambiguous',
51
+ errorCode,
52
+ };
53
+ }
54
+ }
55
+
56
+ function shouldDispatchPrimaryAfterAutosteer({
57
+ steered,
58
+ codexDispatchDecision = null,
59
+ } = {}) {
60
+ return (
61
+ steered?.autosteered !== true
62
+ && steered?.outcome !== 'accepted-persistence-ambiguous'
63
+ && !PRIMARY_BLOCKING_CODEX_DECISIONS.has(codexDispatchDecision)
64
+ );
65
+ }
66
+
67
+ module.exports = {
68
+ settleAcceptedAutosteerOwnership,
69
+ shouldDispatchPrimaryAfterAutosteer,
70
+ };
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ function requireRestartRequestId(value) {
4
+ if (
5
+ typeof value !== 'string'
6
+ || value.length === 0
7
+ || Buffer.byteLength(value, 'utf8') > 128
8
+ || /[\x00-\x1f\x7f]/.test(value)
9
+ ) {
10
+ const error = new Error('deploy restart request ID is invalid');
11
+ error.code = 'INVALID_DEPLOY_RESTART_REQUEST_ID';
12
+ throw error;
13
+ }
14
+ return value;
15
+ }
16
+
17
+ module.exports = { requireRestartRequestId };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.37.1",
3
+ "version": "0.37.2",
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
@@ -31,6 +31,9 @@ const {
31
31
  const { buildPrompt, resolvePromptBackend } = require('./lib/prompt');
32
32
  const { countInFlight } = require('./lib/queue-utils');
33
33
  const { createIpcHandlers } = require('./lib/ipc/handlers');
34
+ const {
35
+ requireRestartRequestId,
36
+ } = require('./lib/ipc/restart-request-id');
34
37
  const { classifyOrphanSweep } = require('./lib/ops/tmux-preflight');
35
38
  const {
36
39
  parseSystemdInvocationId,
@@ -98,6 +101,10 @@ const { createDownloadAttachments } = require('./lib/handlers/download');
98
101
  const { createHandleConfigCallback } = require('./lib/handlers/config-callback');
99
102
  const { createHandleAbort } = require('./lib/handlers/abort');
100
103
  const { createAutosteerHandlers } = require('./lib/handlers/autosteer');
104
+ const {
105
+ settleAcceptedAutosteerOwnership,
106
+ shouldDispatchPrimaryAfterAutosteer,
107
+ } = require('./lib/handlers/claude-autosteer-ownership');
101
108
  const { createEditCorrectionInjector } = require('./lib/handlers/edit-correction');
102
109
  const { createEditRedelivery } = require('./lib/handlers/edit-redelivery');
103
110
  const { createGateInbound, ADMIN_CMD_RE, PAIR_CLAIM_RE } = require('./lib/handlers/gate-inbound');
@@ -2024,6 +2031,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
2024
2031
  let sendPromise = null;
2025
2032
  let codexDispatch = null;
2026
2033
  let codexDispatchDecision = null;
2034
+ let acceptedAutosteerProvider = null;
2027
2035
  try {
2028
2036
  const current = pm.get(sessionKey);
2029
2037
  const liveCodexGeneration = Boolean(
@@ -2070,6 +2078,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
2070
2078
  msg,
2071
2079
  prompt,
2072
2080
  });
2081
+ acceptedAutosteerProvider = 'codex';
2073
2082
  if (steered.outcome === 'accepted') {
2074
2083
  try {
2075
2084
  codexRuntimeController.finalizeAcceptedSteer({
@@ -2196,12 +2205,23 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
2196
2205
  msg,
2197
2206
  prompt,
2198
2207
  });
2208
+ acceptedAutosteerProvider = 'claude';
2199
2209
  }
2200
- if (
2201
- !steered.autosteered
2202
- && !['duplicate', 'ambiguous', 'unavailable']
2203
- .includes(codexDispatchDecision)
2204
- ) {
2210
+ steered = settleAcceptedAutosteerOwnership({
2211
+ selectedProvider: acceptedAutosteerProvider,
2212
+ steered,
2213
+ db,
2214
+ chatId,
2215
+ msgId: msg.message_id,
2216
+ sessionKey,
2217
+ logLabel: label,
2218
+ logEvent,
2219
+ logger: console,
2220
+ });
2221
+ if (shouldDispatchPrimaryAfterAutosteer({
2222
+ steered,
2223
+ codexDispatchDecision,
2224
+ })) {
2205
2225
  if (codexDispatchDecision === 'queue') {
2206
2226
  reactor.setState('THINKING');
2207
2227
  }
@@ -2279,6 +2299,31 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
2279
2299
  }
2280
2300
  return;
2281
2301
  }
2302
+ if (steered.outcome === 'accepted-persistence-ambiguous') {
2303
+ stopTyping();
2304
+ await finishStreamer();
2305
+ await reactor.clear().catch(() => {});
2306
+ reactor.stop();
2307
+ await tg(bot, 'sendMessage', {
2308
+ chat_id: chatId,
2309
+ text: 'This follow-up may have been incorporated. Please wait for the current turn to finish before retrying it.',
2310
+ ...replyOpts(threadId),
2311
+ }, {
2312
+ source: 'claude-autosteer-notice',
2313
+ botName: BOT_NAME,
2314
+ }).catch((error) => {
2315
+ console.error(
2316
+ `[${label}] failed to send Claude autosteer ambiguity notice: ${error.message}`,
2317
+ );
2318
+ logEvent('autosteer-ambiguity-notice-failed', {
2319
+ chat_id: chatId,
2320
+ msg_id: msg.message_id,
2321
+ session_key: sessionKey,
2322
+ code: error?.code || error?.name || 'unknown',
2323
+ });
2324
+ });
2325
+ return;
2326
+ }
2282
2327
  if (['ambiguous', 'unavailable'].includes(codexDispatchDecision)) {
2283
2328
  stopTyping();
2284
2329
  await finishStreamer();
@@ -4419,7 +4464,11 @@ async function main() {
4419
4464
  // replay picks it up. Prevents "Sorry, I couldn't process that message"
4420
4465
  // from showing on every restart.
4421
4466
  const SHUTDOWN_DRAIN_MS = 30_000;
4422
- const shutdown = async ({ continuationAuthorized = false, trigger = 'signal' } = {}) => {
4467
+ const shutdown = async ({
4468
+ continuationAuthorized = false,
4469
+ trigger = 'signal',
4470
+ restartRequestId = null,
4471
+ } = {}) => {
4423
4472
  if (isShuttingDown) return;
4424
4473
  const shutdownObservation = cgroupOomObserver.sample();
4425
4474
  // Sample in-flight work at SIGNAL time, alongside the OOM observation and
@@ -4611,6 +4660,12 @@ async function main() {
4611
4660
  elapsed_ms: drainElapsed,
4612
4661
  clean: res.clean,
4613
4662
  shutdown_reason: res.shutdownReason,
4663
+ restart_trigger: trigger,
4664
+ continuation_authorized: continuationAuthorized === true,
4665
+ resume_intents_recorded: res.intentsRecorded ?? 0,
4666
+ restart_request_id: trigger === 'deploy-ipc'
4667
+ ? restartRequestId
4668
+ : null,
4614
4669
  ...(res.oomKillDelta != null ? { oom_kill_delta: res.oomKillDelta } : {}),
4615
4670
  }, invocationId));
4616
4671
  console.log(`[shutdown] ${res.clean ? 'clean' : 'crash-like'} shutdown recorded (${res.shutdownReason}); ${inFlightAtSignal} in-flight at signal, drained ${drainElapsed}ms, ${remaining} still in-flight, ${res.replayMarked} marked replay-pending`);
@@ -4684,16 +4739,18 @@ async function main() {
4684
4739
  botName: BOT_NAME,
4685
4740
  getInFlightHandlers: () => inFlightHandlers,
4686
4741
  handleSendOverIpc: (req) => handleSendOverIpc(req),
4687
- requestDeployRestart: () => {
4742
+ requestDeployRestart: (req) => {
4688
4743
  if (isShuttingDown) {
4689
4744
  const error = new Error('shutdown already in progress');
4690
4745
  error.code = 'SHUTDOWN_IN_PROGRESS';
4691
4746
  throw error;
4692
4747
  }
4748
+ const restartRequestId = requireRestartRequestId(req.id);
4693
4749
  const oldPid = process.pid;
4694
4750
  shutdown({
4695
4751
  continuationAuthorized: true,
4696
4752
  trigger: 'deploy-ipc',
4753
+ restartRequestId,
4697
4754
  }).catch((error) => {
4698
4755
  console.error(`[shutdown] deploy restart failed: ${error.message}`);
4699
4756
  });
@@ -1,10 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Quick IPC round-trip probe.
4
- * Usage: node scripts/ipc-smoke.js <bot-name> [busy|restart]
4
+ * Usage: node scripts/ipc-smoke.js <bot-name> [busy|restart] [restart-request-id]
5
5
  */
6
6
 
7
+ const crypto = require('node:crypto');
7
8
  const { call, readSecret, socketPathFor } = require('../lib/ipc/client');
9
+ const {
10
+ requireRestartRequestId,
11
+ } = require('../lib/ipc/restart-request-id');
12
+
13
+ function restartRequestId(value) {
14
+ return requireRestartRequestId(value || crypto.randomUUID());
15
+ }
8
16
 
9
17
  (async () => {
10
18
  const bot = process.argv[2] || 'shumabit';
@@ -33,15 +41,18 @@ const { call, readSecret, socketPathFor } = require('../lib/ipc/client');
33
41
  }
34
42
 
35
43
  if (command === 'restart') {
44
+ const requestId = restartRequestId(process.argv[4]);
36
45
  const result = await call({
37
46
  path,
38
47
  op: 'deploy_restart',
48
+ id: requestId,
39
49
  secret: readSecret(bot),
40
50
  callTimeoutMs: 60_000,
41
51
  });
42
52
  if (
43
53
  result.ok !== true
44
54
  || result.accepted !== true
55
+ || result.id !== requestId
45
56
  || !Number.isSafeInteger(result.old_pid)
46
57
  || result.old_pid <= 1
47
58
  ) {
@@ -51,6 +62,7 @@ const { call, readSecret, socketPathFor } = require('../lib/ipc/client');
51
62
  bot,
52
63
  accepted: true,
53
64
  old_pid: result.old_pid,
65
+ restart_request_id: requestId,
54
66
  }));
55
67
  return;
56
68
  }