polygram 0.22.0 → 0.23.0
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/README.md +5 -3
- package/config.example.json +23 -1
- package/lib/codex/binary.js +287 -0
- package/lib/codex/delivery-finalizer.js +97 -0
- package/lib/codex/diagnostics.js +483 -0
- package/lib/codex/event-normalizer.js +50 -0
- package/lib/codex/host-identity.js +103 -0
- package/lib/codex/model-catalog.js +359 -0
- package/lib/codex/retention-scheduler.js +59 -0
- package/lib/codex/runtime-controller.js +1292 -0
- package/lib/codex/runtime-profile.js +888 -0
- package/lib/codex/spawn-context.js +87 -0
- package/lib/db/auto-resume.js +77 -9
- package/lib/db/codex-reconciliation.js +490 -0
- package/lib/db/codex-retention.js +139 -0
- package/lib/db/sessions.js +102 -1
- package/lib/db.js +3418 -19
- package/lib/error/classify.js +140 -0
- package/lib/handlers/abort.js +143 -5
- package/lib/handlers/autosteer.js +256 -1
- package/lib/handlers/codex-reconciliation.js +230 -0
- package/lib/handlers/config-callback.js +324 -89
- package/lib/handlers/config-ui.js +401 -39
- package/lib/handlers/dispatcher.js +71 -8
- package/lib/handlers/edit-correction.js +11 -1
- package/lib/handlers/edit-redelivery.js +230 -4
- package/lib/handlers/gate-inbound.js +7 -0
- package/lib/handlers/replay-disposition.js +133 -11
- package/lib/handlers/slash-commands.js +443 -37
- package/lib/ipc/client.js +22 -12
- package/lib/ipc/runtime-directory.js +208 -0
- package/lib/ipc/server.js +18 -12
- package/lib/model-costs.js +4 -3
- package/lib/pm-interface.js +33 -0
- package/lib/prompt.js +15 -9
- package/lib/rewind/rewind.js +21 -1
- package/lib/runtime-config.js +605 -0
- package/lib/sdk/build-options.js +4 -1
- package/lib/sdk/callbacks.js +46 -3
- package/migrations/015-agent-runtime-sessions.sql +50 -0
- package/migrations/016-agent-turn-attempts.sql +211 -0
- package/migrations/017-config-runtime-changes.sql +46 -0
- package/ops/README.md +57 -0
- package/package.json +2 -2
- package/polygram.js +697 -48
- package/scripts/doctor.js +37 -8
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "polygram",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.23.0",
|
|
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/README.md
CHANGED
|
@@ -45,8 +45,9 @@ ergonomics while running on top of `claude` CLI.
|
|
|
45
45
|
- **Write-before-send atomicity.** Outbound messages hit the DB as
|
|
46
46
|
`pending` before the Telegram call, flip to `sent` or `failed` after.
|
|
47
47
|
Boot sweep resolves stale `pending` rows from the last crash.
|
|
48
|
-
- **Unix-socket IPC per bot.** Cron jobs and
|
|
49
|
-
|
|
48
|
+
- **Unix-socket IPC per bot.** Cron jobs and external scripts talk to the
|
|
49
|
+
bot process over `<data-dir>/.ipc/polygram-<bot>.sock` inside an
|
|
50
|
+
owner-only runtime directory. The bot
|
|
50
51
|
is the only writer to its own DB.
|
|
51
52
|
- **Inline-keyboard approvals.** Destructive tool calls gate on operator
|
|
52
53
|
click via Claude Code's `PreToolUse` hook; 5-minute auto-deny.
|
|
@@ -139,9 +140,10 @@ polygram --bot partner-bot # another bot, another process
|
|
|
139
140
|
- `config.json` → `$PWD/config.json` (override with `--config` or `POLYGRAM_CONFIG`)
|
|
140
141
|
- `<bot>.db` → `$PWD/<bot>.db` (override with `--db` or `POLYGRAM_DB`)
|
|
141
142
|
- `inbox/` → `$PWD/inbox/` (override with `POLYGRAM_INBOX`)
|
|
143
|
+
- `.ipc/` → `$PWD/.ipc/` (override with canonical absolute `POLYGRAM_IPC_DIR`)
|
|
142
144
|
|
|
143
145
|
Migrations are bundled in the package and apply automatically. The daemon
|
|
144
|
-
opens a Unix socket at
|
|
146
|
+
opens a Unix socket at `$PWD/.ipc/polygram-<bot>.sock`.
|
|
145
147
|
|
|
146
148
|
For production, LaunchAgent plists are in `ops/`. See `ops/README.md`.
|
|
147
149
|
|
package/config.example.json
CHANGED
|
@@ -63,6 +63,17 @@
|
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
65
|
|
|
66
|
+
"codex": {
|
|
67
|
+
"_comment": "Optional native macOS Codex app-server beta. Codex stays disabled unless a chat/topic explicitly sets pm:'codex' and exact startup preflight succeeds. Linux and Windows are rejected in this milestone. `home` must be a dedicated non-temporary 0700 CODEX_HOME already authenticated with ChatGPT; polygram never copies or reads credential contents. `binary` must be the canonical immutable path to the reviewed pinned Codex CLI, not a PATH symlink. Every directory containing daemon credentials or control data must be listed in daemonSecretRoots and must not overlap a Codex workspace or CODEX_HOME. Command network and model-native web search are disabled; product MCP tools and interactive approvals are unavailable. Keep long-running commands in the foreground. Detached/background servers are unsupported and may survive hard runtime loss.",
|
|
68
|
+
"home": "/Users/you/.codex-polygram",
|
|
69
|
+
"binary": "/Users/you/.local/share/codex/versions/0.145.0/codex",
|
|
70
|
+
"daemonSecretRoots": [
|
|
71
|
+
"/Users/you/.polygram",
|
|
72
|
+
"/Users/you/.claude",
|
|
73
|
+
"/Users/you/.codex"
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
|
|
66
77
|
"chats": {
|
|
67
78
|
"123456789": {
|
|
68
79
|
"name": "Admin DM",
|
|
@@ -77,6 +88,17 @@
|
|
|
77
88
|
"compactionWarnings": true
|
|
78
89
|
},
|
|
79
90
|
|
|
91
|
+
"987654321": {
|
|
92
|
+
"name": "Codex beta DM",
|
|
93
|
+
"bot": "admin-bot",
|
|
94
|
+
"_comment_pm": "Codex uses the pinned app-server SDK surface directly (no tmux). Native macOS beta only: command network and model-native web search are disabled; product MCP tools and interactive approvals are unavailable; long-running commands must stay in the foreground; detached/background servers are unsupported and may survive hard runtime loss. The model and effort are accepted only when returned by this deployment's authenticated ChatGPT-backed model catalog.",
|
|
95
|
+
"pm": "codex",
|
|
96
|
+
"codexModel": "gpt-5.6-sol",
|
|
97
|
+
"codexEffort": "xhigh",
|
|
98
|
+
"cwd": "/Users/you/codex-workspace",
|
|
99
|
+
"timeout": 600
|
|
100
|
+
},
|
|
101
|
+
|
|
80
102
|
"-1000000000001": {
|
|
81
103
|
"name": "Ops Group",
|
|
82
104
|
"bot": "admin-bot",
|
|
@@ -104,7 +126,7 @@
|
|
|
104
126
|
"isolateTopics": true,
|
|
105
127
|
"_comment_topics": "rc.48: each topic entry is EITHER a string (legacy: just a label) OR an object with optional fields {name, agent, cwd, model, effort, permissionMode, isolateUserConfig}. Object form lets a topic override chat-level config. Per-topic permissionMode overrides chat-level — typical use: scope one topic to permissionMode:'default' (so settings.json gates apply) while the rest of the chat stays on bypassPermissions. Object form requires isolateTopics: true (each topic gets its own SDK Query); polygram emits a startup warning otherwise.",
|
|
106
128
|
"_comment_isolateUserConfig": "0.10.0, tmux backend only: isolateUserConfig:true spawns the topic's claude TUI cut off from the user-level ~/.claude config — passes --strict-mcp-config (zero MCP servers load) and --setting-sources project,local (drops ~/.claude/settings.json; the spawn cwd's own .claude/settings.json still loads). Use it when a topic's agent would otherwise inherit slow user-global MCP servers whose cold-start (tens of seconds) wedges the TUI before it can accept a prompt. Settable at chat OR topic level (topic wins). Default false.",
|
|
107
|
-
"_comment_pm": "
|
|
129
|
+
"_comment_pm": "'pm' selects the session runtime. Canonical values: 'sdk' (default Claude API SDK), 'cli' (subscription-priced Claude CLI in tmux + Channels bridge), and 'codex' (subscription-backed pinned Codex app-server, no tmux). Settable at bot, chat, or topic scope (topic > chat > bot). Claude aliases 'channels' and 'tmux' still resolve to 'cli'. Codex uses separate codexModel/codexEffort fields and never consumes Claude model/effort values.",
|
|
108
130
|
"topics": {
|
|
109
131
|
"100": "Customer A",
|
|
110
132
|
"200": {
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFile } = require('node:child_process');
|
|
4
|
+
const { createHash } = require('node:crypto');
|
|
5
|
+
const {
|
|
6
|
+
createReadStream,
|
|
7
|
+
lstatSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
statSync,
|
|
10
|
+
} = require('node:fs');
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
const { promisify } = require('node:util');
|
|
13
|
+
|
|
14
|
+
const CODEX_CLI_PINNED_VERSION = 'codex-cli 0.145.0';
|
|
15
|
+
const CODEX_BINARY_SHA256 =
|
|
16
|
+
'1da3f4e0e96028b8a771814293c3033dafd1971f943f6c7e79b0897fe705f590';
|
|
17
|
+
const CONTROLLED_PATH = '/usr/bin:/bin';
|
|
18
|
+
const SHA256_RE = /^[a-f0-9]{64}$/;
|
|
19
|
+
const VERSION_PROBE_TIMEOUT_MS = 10_000;
|
|
20
|
+
const VERSION_PROBE_MAX_BYTES = 4 * 1024;
|
|
21
|
+
|
|
22
|
+
class CodexBinaryError extends Error {
|
|
23
|
+
constructor(message, code, action, options) {
|
|
24
|
+
super(message, options);
|
|
25
|
+
this.name = 'CodexBinaryError';
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.action = action;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function deepFreeze(value) {
|
|
32
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
36
|
+
return Object.freeze(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function binaryAction(version = CODEX_CLI_PINNED_VERSION) {
|
|
40
|
+
return (
|
|
41
|
+
`Install ${version} as an immutable versioned executable and set `
|
|
42
|
+
+ 'POLYGRAM_CODEX_BIN to its canonical absolute path.'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function fail(message, code, version, cause) {
|
|
47
|
+
return new CodexBinaryError(
|
|
48
|
+
message,
|
|
49
|
+
code,
|
|
50
|
+
binaryAction(version),
|
|
51
|
+
cause ? { cause } : undefined,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function fingerprint(binary) {
|
|
56
|
+
const stat = statSync(binary, { bigint: true });
|
|
57
|
+
if (!stat.isFile()) {
|
|
58
|
+
throw fail(
|
|
59
|
+
'Pinned Codex binary is not a regular file.',
|
|
60
|
+
'CODEX_BINARY_MISMATCH',
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
dev: stat.dev.toString(),
|
|
65
|
+
ino: stat.ino.toString(),
|
|
66
|
+
size: stat.size.toString(),
|
|
67
|
+
mtimeNs: stat.mtimeNs.toString(),
|
|
68
|
+
ctimeNs: stat.ctimeNs.toString(),
|
|
69
|
+
mode: Number(stat.mode),
|
|
70
|
+
uid: Number(stat.uid),
|
|
71
|
+
nlink: Number(stat.nlink),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function sameFingerprint(left, right) {
|
|
76
|
+
return Object.keys(left).every((key) => left[key] === right[key]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function hashFile(binary) {
|
|
80
|
+
const hash = createHash('sha256');
|
|
81
|
+
await new Promise((resolve, reject) => {
|
|
82
|
+
const stream = createReadStream(binary);
|
|
83
|
+
stream.on('data', (chunk) => hash.update(chunk));
|
|
84
|
+
stream.once('error', reject);
|
|
85
|
+
stream.once('end', resolve);
|
|
86
|
+
});
|
|
87
|
+
return hash.digest('hex');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function validatePin(cliVersion, binarySha256) {
|
|
91
|
+
if (
|
|
92
|
+
typeof cliVersion !== 'string'
|
|
93
|
+
|| cliVersion.length === 0
|
|
94
|
+
|| !SHA256_RE.test(binarySha256)
|
|
95
|
+
) {
|
|
96
|
+
throw new TypeError(
|
|
97
|
+
'Pinned Codex resolver requires an exact version and lowercase SHA-256.',
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function createPinnedCodexBinaryResolver({
|
|
103
|
+
cliVersion,
|
|
104
|
+
binarySha256,
|
|
105
|
+
execFileFn = promisify(execFile),
|
|
106
|
+
platform = process.platform,
|
|
107
|
+
getuid = process.getuid?.bind(process),
|
|
108
|
+
} = {}) {
|
|
109
|
+
validatePin(cliVersion, binarySha256);
|
|
110
|
+
if (typeof execFileFn !== 'function') {
|
|
111
|
+
throw new TypeError('Pinned Codex resolver requires execFileFn.');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return async function resolve({
|
|
115
|
+
binaryPath,
|
|
116
|
+
env = process.env,
|
|
117
|
+
} = {}) {
|
|
118
|
+
if (platform !== 'darwin') {
|
|
119
|
+
throw new CodexBinaryError(
|
|
120
|
+
'The native Codex beta currently supports only macOS hosts.',
|
|
121
|
+
'CODEX_BINARY_PLATFORM_UNSUPPORTED',
|
|
122
|
+
'Use macOS; Linux and Windows support are outside this rollout.',
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const configured = binaryPath ?? env?.POLYGRAM_CODEX_BIN;
|
|
127
|
+
if (typeof configured !== 'string' || configured.length === 0) {
|
|
128
|
+
throw new CodexBinaryError(
|
|
129
|
+
'Pinned Codex binary is not configured.',
|
|
130
|
+
'CODEX_BINARY_NOT_CONFIGURED',
|
|
131
|
+
binaryAction(cliVersion),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (!path.isAbsolute(configured)) {
|
|
135
|
+
throw fail(
|
|
136
|
+
'Pinned Codex binary must be an absolute path; PATH lookup is disabled.',
|
|
137
|
+
'CODEX_BINARY_NOT_ABSOLUTE',
|
|
138
|
+
cliVersion,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let canonical;
|
|
143
|
+
try {
|
|
144
|
+
canonical = realpathSync(configured);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') {
|
|
147
|
+
throw fail(
|
|
148
|
+
'Pinned Codex binary is missing.',
|
|
149
|
+
'CODEX_BINARY_MISSING',
|
|
150
|
+
cliVersion,
|
|
151
|
+
error,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
throw fail(
|
|
155
|
+
'Pinned Codex binary could not be resolved safely.',
|
|
156
|
+
'CODEX_BINARY_MISMATCH',
|
|
157
|
+
cliVersion,
|
|
158
|
+
error,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (canonical !== configured) {
|
|
162
|
+
throw fail(
|
|
163
|
+
'Pinned Codex binary path must be canonical and must not be a symlink.',
|
|
164
|
+
'CODEX_BINARY_MISMATCH',
|
|
165
|
+
cliVersion,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const allowedOwners = new Set([0]);
|
|
171
|
+
if (typeof getuid === 'function') allowedOwners.add(getuid());
|
|
172
|
+
const root = path.parse(canonical).root;
|
|
173
|
+
let component = root;
|
|
174
|
+
for (const part of canonical.slice(root.length).split('/').filter(Boolean)) {
|
|
175
|
+
component = path.join(component, part);
|
|
176
|
+
const stat = lstatSync(component);
|
|
177
|
+
if (
|
|
178
|
+
stat.isSymbolicLink()
|
|
179
|
+
|| !allowedOwners.has(stat.uid)
|
|
180
|
+
|| (stat.mode & 0o022) !== 0
|
|
181
|
+
) {
|
|
182
|
+
throw fail(
|
|
183
|
+
'Pinned Codex binary path ownership or mode is unsafe.',
|
|
184
|
+
'CODEX_BINARY_MISMATCH',
|
|
185
|
+
cliVersion,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const before = fingerprint(canonical);
|
|
191
|
+
if (
|
|
192
|
+
(before.mode & 0o022) !== 0
|
|
193
|
+
|| (before.mode & 0o111) === 0
|
|
194
|
+
|| !allowedOwners.has(before.uid)
|
|
195
|
+
|| before.nlink !== 1
|
|
196
|
+
) {
|
|
197
|
+
throw fail(
|
|
198
|
+
'Pinned Codex binary ownership or mode is unsafe.',
|
|
199
|
+
'CODEX_BINARY_MISMATCH',
|
|
200
|
+
cliVersion,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const actualSha256 = await hashFile(canonical);
|
|
205
|
+
if (actualSha256 !== binarySha256) {
|
|
206
|
+
throw fail(
|
|
207
|
+
'Pinned Codex binary checksum does not match the reviewed runtime.',
|
|
208
|
+
'CODEX_BINARY_MISMATCH',
|
|
209
|
+
cliVersion,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let stdout;
|
|
214
|
+
try {
|
|
215
|
+
({ stdout } = await execFileFn(canonical, ['--version'], {
|
|
216
|
+
encoding: 'utf8',
|
|
217
|
+
timeout: VERSION_PROBE_TIMEOUT_MS,
|
|
218
|
+
maxBuffer: VERSION_PROBE_MAX_BYTES,
|
|
219
|
+
env: { PATH: CONTROLLED_PATH },
|
|
220
|
+
}));
|
|
221
|
+
} catch (error) {
|
|
222
|
+
throw fail(
|
|
223
|
+
'Pinned Codex binary version probe failed.',
|
|
224
|
+
'CODEX_BINARY_MISMATCH',
|
|
225
|
+
cliVersion,
|
|
226
|
+
error,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
if (typeof stdout !== 'string' || stdout.trim() !== cliVersion) {
|
|
230
|
+
throw fail(
|
|
231
|
+
'Pinned Codex binary version does not match the reviewed runtime.',
|
|
232
|
+
'CODEX_BINARY_MISMATCH',
|
|
233
|
+
cliVersion,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (
|
|
238
|
+
realpathSync(canonical) !== canonical
|
|
239
|
+
|| !sameFingerprint(before, fingerprint(canonical))
|
|
240
|
+
) {
|
|
241
|
+
throw fail(
|
|
242
|
+
'Pinned Codex binary changed during verification.',
|
|
243
|
+
'CODEX_BINARY_MISMATCH',
|
|
244
|
+
cliVersion,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return deepFreeze({
|
|
249
|
+
path: canonical,
|
|
250
|
+
version: cliVersion,
|
|
251
|
+
sha256: actualSha256,
|
|
252
|
+
fingerprint: before,
|
|
253
|
+
});
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (error instanceof CodexBinaryError) throw error;
|
|
256
|
+
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') {
|
|
257
|
+
throw fail(
|
|
258
|
+
'Pinned Codex binary disappeared during verification.',
|
|
259
|
+
'CODEX_BINARY_MISSING',
|
|
260
|
+
cliVersion,
|
|
261
|
+
error,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
throw fail(
|
|
265
|
+
'Pinned Codex binary failed ownership and integrity verification.',
|
|
266
|
+
'CODEX_BINARY_MISMATCH',
|
|
267
|
+
cliVersion,
|
|
268
|
+
error,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Application code uses this fixed resolver. The factory exists so a pin
|
|
275
|
+
// upgrade can verify its candidate fixture before changing the production pin.
|
|
276
|
+
const resolvePinnedCodexBinary = createPinnedCodexBinaryResolver({
|
|
277
|
+
cliVersion: CODEX_CLI_PINNED_VERSION,
|
|
278
|
+
binarySha256: CODEX_BINARY_SHA256,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
module.exports = {
|
|
282
|
+
CODEX_BINARY_SHA256,
|
|
283
|
+
CODEX_CLI_PINNED_VERSION,
|
|
284
|
+
CodexBinaryError,
|
|
285
|
+
createPinnedCodexBinaryResolver,
|
|
286
|
+
resolvePinnedCodexBinary,
|
|
287
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function claimsCodexRuntime(result) {
|
|
4
|
+
return result?.runtime === 'codex' || result?.backend === 'codex';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function missingControllerError() {
|
|
8
|
+
const error = new Error(
|
|
9
|
+
'A Codex result cannot be finalized without its runtime controller',
|
|
10
|
+
);
|
|
11
|
+
error.code = 'CODEX_DELIVERY_CONTROLLER_MISSING';
|
|
12
|
+
return error;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function finalizeTelegramDelivery({
|
|
16
|
+
controller,
|
|
17
|
+
sessionKey,
|
|
18
|
+
result,
|
|
19
|
+
deliveryComplete = true,
|
|
20
|
+
queuedDispatch = null,
|
|
21
|
+
markHandlerStatus,
|
|
22
|
+
} = {}) {
|
|
23
|
+
if (typeof markHandlerStatus !== 'function') {
|
|
24
|
+
throw new TypeError('markHandlerStatus function required');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!claimsCodexRuntime(result)) {
|
|
28
|
+
await markHandlerStatus('replied');
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
runtime: 'claude',
|
|
31
|
+
disposition: 'delivered',
|
|
32
|
+
handlerStatus: 'replied',
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (!controller) throw missingControllerError();
|
|
36
|
+
|
|
37
|
+
const disposition = (
|
|
38
|
+
result?.error == null
|
|
39
|
+
&& deliveryComplete === true
|
|
40
|
+
)
|
|
41
|
+
? 'delivered'
|
|
42
|
+
: 'failed';
|
|
43
|
+
await controller.settleTelegramDelivery(
|
|
44
|
+
sessionKey,
|
|
45
|
+
result,
|
|
46
|
+
{ disposition },
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (disposition === 'delivered' && queuedDispatch?.reservationId) {
|
|
50
|
+
controller.settleQueuedDispatch({
|
|
51
|
+
sessionKey,
|
|
52
|
+
generationId: result.generationId,
|
|
53
|
+
reservationId: queuedDispatch.reservationId,
|
|
54
|
+
attemptId: result.attemptId,
|
|
55
|
+
botName: queuedDispatch.botName,
|
|
56
|
+
telegramChatId: queuedDispatch.telegramChatId,
|
|
57
|
+
telegramMessageId: queuedDispatch.telegramMessageId,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const handlerStatus = disposition === 'delivered' ? 'replied' : 'failed';
|
|
62
|
+
await markHandlerStatus(handlerStatus);
|
|
63
|
+
return Object.freeze({
|
|
64
|
+
runtime: 'codex',
|
|
65
|
+
disposition,
|
|
66
|
+
handlerStatus,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function createTelegramDeliveryFinalizer(options = {}) {
|
|
71
|
+
let outcome = null;
|
|
72
|
+
|
|
73
|
+
async function finalize(deliveryComplete = true) {
|
|
74
|
+
if (outcome) return outcome;
|
|
75
|
+
outcome = await finalizeTelegramDelivery({
|
|
76
|
+
...options,
|
|
77
|
+
deliveryComplete,
|
|
78
|
+
});
|
|
79
|
+
return outcome;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function failIfPending() {
|
|
83
|
+
if (outcome || !claimsCodexRuntime(options.result)) return outcome;
|
|
84
|
+
return finalize(false);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
finalize,
|
|
89
|
+
failIfPending,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
claimsCodexRuntime,
|
|
95
|
+
createTelegramDeliveryFinalizer,
|
|
96
|
+
finalizeTelegramDelivery,
|
|
97
|
+
};
|