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.
- package/.claude-plugin/plugin.json +1 -1
- package/lib/db.js +28 -0
- package/lib/handlers/claude-autosteer-ownership.js +70 -0
- package/lib/ipc/restart-request-id.js +17 -0
- package/package.json +1 -1
- package/polygram.js +64 -7
- package/scripts/ipc-smoke.js +13 -1
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
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 ({
|
|
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
|
});
|
package/scripts/ipc-smoke.js
CHANGED
|
@@ -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
|
}
|