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
package/lib/db.js
CHANGED
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const Database = require('better-sqlite3');
|
|
10
|
+
const {
|
|
11
|
+
pruneCodexOperationalData,
|
|
12
|
+
} = require('./db/codex-retention');
|
|
13
|
+
const {
|
|
14
|
+
codexError,
|
|
15
|
+
listUnresolvedCodexAttempts,
|
|
16
|
+
requiredString: requiredCodexString,
|
|
17
|
+
reconcileCodexAttempt,
|
|
18
|
+
reconstructCodexRecovery,
|
|
19
|
+
} = require('./db/codex-reconciliation');
|
|
10
20
|
|
|
11
21
|
// 0.8.0 Phase 1: bumped from 9 → 10. Adds migration
|
|
12
22
|
// 010-tool-use-id.sql (pending_approvals.tool_use_id column for the
|
|
@@ -26,7 +36,7 @@ const Database = require('better-sqlite3');
|
|
|
26
36
|
//
|
|
27
37
|
// 0.15: bumped 13 → 14. Adds migration 014-secret-redactions.sql
|
|
28
38
|
// (secret_redactions audit table + messages.secret_scanned_at).
|
|
29
|
-
const SCHEMA_VERSION =
|
|
39
|
+
const SCHEMA_VERSION = 17;
|
|
30
40
|
|
|
31
41
|
// Sentinel `error` value for outbound rows whose API call may or may not
|
|
32
42
|
// have reached Telegram. markStalePending writes it; hasOutboundReplyTo
|
|
@@ -36,6 +46,78 @@ const SCHEMA_VERSION = 14;
|
|
|
36
46
|
// reply on boot).
|
|
37
47
|
const CRASHED_MID_SEND = 'crashed-mid-send';
|
|
38
48
|
|
|
49
|
+
const CLAUDE_INLINE_NAMESPACE = 'claude:inline';
|
|
50
|
+
const CLAUDE_CHANNELS_NAMESPACE = 'claude:channels';
|
|
51
|
+
const CODEX_APP_SERVER_NAMESPACE = 'codex:app-server';
|
|
52
|
+
const PROVIDER_NAMESPACES = new Set([
|
|
53
|
+
CLAUDE_INLINE_NAMESPACE,
|
|
54
|
+
CLAUDE_CHANNELS_NAMESPACE,
|
|
55
|
+
CODEX_APP_SERVER_NAMESPACE,
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
function claudeNamespaceForStoredBackend(backend) {
|
|
59
|
+
return backend === 'cli' || backend === 'channels'
|
|
60
|
+
? CLAUDE_CHANNELS_NAMESPACE
|
|
61
|
+
: CLAUDE_INLINE_NAMESPACE;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function requiredTimestamp(value, label) {
|
|
65
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
66
|
+
throw codexError(
|
|
67
|
+
`Codex ${label} must be a non-negative safe integer`,
|
|
68
|
+
'CODEX_PERSISTENCE_INPUT_INVALID',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function optionalCodexString(value, label) {
|
|
75
|
+
if (value == null) return null;
|
|
76
|
+
return requiredCodexString(String(value), label);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function optionalCodexRequestId(value) {
|
|
80
|
+
if (value == null) return null;
|
|
81
|
+
if (typeof value === 'string') {
|
|
82
|
+
return `string:${requiredCodexString(value, 'request ID')}`;
|
|
83
|
+
}
|
|
84
|
+
if (Number.isSafeInteger(value)) {
|
|
85
|
+
return `number:${value}`;
|
|
86
|
+
}
|
|
87
|
+
throw codexError(
|
|
88
|
+
'Codex request ID must be a bounded string or safe integer',
|
|
89
|
+
'CODEX_PERSISTENCE_INPUT_INVALID',
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function assertExactKeys(row, allowed, code, label) {
|
|
94
|
+
for (const key of Object.keys(row ?? {})) {
|
|
95
|
+
if (!allowed.has(key)) {
|
|
96
|
+
throw codexError(
|
|
97
|
+
`Codex ${label} rejected field ${key}`,
|
|
98
|
+
code,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function configChangeValue(value, { nullable, label }) {
|
|
105
|
+
if (value == null) {
|
|
106
|
+
if (nullable) return null;
|
|
107
|
+
const error = new TypeError(`config change ${label} is required`);
|
|
108
|
+
error.code = 'CONFIG_CHANGE_VALUE_INVALID';
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
if (typeof value === 'string') return value;
|
|
112
|
+
if (typeof value === 'boolean') return String(value);
|
|
113
|
+
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
|
|
114
|
+
const error = new TypeError(
|
|
115
|
+
`config change ${label} must be a string, boolean, or finite number`,
|
|
116
|
+
);
|
|
117
|
+
error.code = 'CONFIG_CHANGE_VALUE_INVALID';
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
|
|
39
121
|
function open(dbPath) {
|
|
40
122
|
const db = new Database(dbPath);
|
|
41
123
|
db.pragma('journal_mode = WAL');
|
|
@@ -147,6 +229,195 @@ function wrap(db) {
|
|
|
147
229
|
const clearSessionIdStmt = db.prepare(`DELETE FROM sessions WHERE session_key = ?`);
|
|
148
230
|
const setSessionBackendStmt = db.prepare(`UPDATE sessions SET pm_backend = ? WHERE session_key = ?`);
|
|
149
231
|
|
|
232
|
+
const upsertProviderSessionStmt = db.prepare(`
|
|
233
|
+
INSERT INTO agent_runtime_sessions (
|
|
234
|
+
session_key, namespace, provider, provider_session_id,
|
|
235
|
+
app_server_session_id, agent, cwd, model, effort, pm_backend,
|
|
236
|
+
created_ts, last_active_ts
|
|
237
|
+
) VALUES (
|
|
238
|
+
@session_key, @namespace, @provider, @provider_session_id,
|
|
239
|
+
@app_server_session_id, @agent, @cwd, @model, @effort, @pm_backend,
|
|
240
|
+
@ts, @ts
|
|
241
|
+
)
|
|
242
|
+
ON CONFLICT(session_key, namespace) DO UPDATE SET
|
|
243
|
+
provider = excluded.provider,
|
|
244
|
+
provider_session_id = excluded.provider_session_id,
|
|
245
|
+
app_server_session_id = excluded.app_server_session_id,
|
|
246
|
+
agent = excluded.agent,
|
|
247
|
+
cwd = excluded.cwd,
|
|
248
|
+
model = excluded.model,
|
|
249
|
+
effort = excluded.effort,
|
|
250
|
+
pm_backend = excluded.pm_backend,
|
|
251
|
+
last_active_ts = excluded.last_active_ts
|
|
252
|
+
`);
|
|
253
|
+
const getProviderSessionStmt = db.prepare(`
|
|
254
|
+
SELECT *
|
|
255
|
+
FROM agent_runtime_sessions
|
|
256
|
+
WHERE session_key = ? AND namespace = ?
|
|
257
|
+
`);
|
|
258
|
+
const clearProviderSessionStmt = db.prepare(`
|
|
259
|
+
DELETE FROM agent_runtime_sessions
|
|
260
|
+
WHERE session_key = ? AND namespace = ?
|
|
261
|
+
`);
|
|
262
|
+
const touchProviderSessionStmt = db.prepare(`
|
|
263
|
+
UPDATE agent_runtime_sessions
|
|
264
|
+
SET last_active_ts = ?
|
|
265
|
+
WHERE session_key = ? AND namespace = ?
|
|
266
|
+
`);
|
|
267
|
+
const setProviderBackendStmt = db.prepare(`
|
|
268
|
+
UPDATE agent_runtime_sessions
|
|
269
|
+
SET pm_backend = ?
|
|
270
|
+
WHERE session_key = ? AND namespace = ?
|
|
271
|
+
`);
|
|
272
|
+
|
|
273
|
+
function providerSessionParams(row) {
|
|
274
|
+
if (!row || !PROVIDER_NAMESPACES.has(row.namespace)) {
|
|
275
|
+
throw new TypeError('provider session namespace is invalid');
|
|
276
|
+
}
|
|
277
|
+
const provider = row.provider;
|
|
278
|
+
if (
|
|
279
|
+
(provider === 'claude' && !row.namespace.startsWith('claude:'))
|
|
280
|
+
|| (provider === 'codex' && row.namespace !== CODEX_APP_SERVER_NAMESPACE)
|
|
281
|
+
|| !['claude', 'codex'].includes(provider)
|
|
282
|
+
) {
|
|
283
|
+
throw new TypeError('provider session namespace/provider mismatch');
|
|
284
|
+
}
|
|
285
|
+
if (
|
|
286
|
+
typeof row.provider_session_id !== 'string'
|
|
287
|
+
|| row.provider_session_id.length === 0
|
|
288
|
+
) {
|
|
289
|
+
throw new TypeError('provider session ID must be a non-empty string');
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
session_key: String(row.session_key),
|
|
293
|
+
namespace: row.namespace,
|
|
294
|
+
provider,
|
|
295
|
+
provider_session_id: row.provider_session_id,
|
|
296
|
+
app_server_session_id: row.app_server_session_id || null,
|
|
297
|
+
agent: row.agent || null,
|
|
298
|
+
cwd: row.cwd || null,
|
|
299
|
+
model: row.model || null,
|
|
300
|
+
effort: row.effort || null,
|
|
301
|
+
pm_backend: row.pm_backend || null,
|
|
302
|
+
ts: row.ts || Date.now(),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function legacySessionParams(row) {
|
|
307
|
+
return {
|
|
308
|
+
session_key: row.session_key,
|
|
309
|
+
chat_id: String(row.chat_id),
|
|
310
|
+
thread_id: row.thread_id ? String(row.thread_id) : null,
|
|
311
|
+
claude_session_id: row.claude_session_id,
|
|
312
|
+
agent: row.agent || null,
|
|
313
|
+
cwd: row.cwd || null,
|
|
314
|
+
model: row.model || null,
|
|
315
|
+
effort: row.effort || null,
|
|
316
|
+
pm_backend: row.pm_backend || 'sdk',
|
|
317
|
+
ts: row.ts || Date.now(),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const upsertLegacyAndProviderSession = db.transaction((row) => {
|
|
322
|
+
const legacy = legacySessionParams(row);
|
|
323
|
+
const result = upsertSessionStmt.run(legacy);
|
|
324
|
+
upsertProviderSessionStmt.run(providerSessionParams({
|
|
325
|
+
...row,
|
|
326
|
+
session_key: legacy.session_key,
|
|
327
|
+
namespace: claudeNamespaceForStoredBackend(legacy.pm_backend),
|
|
328
|
+
provider: 'claude',
|
|
329
|
+
provider_session_id: legacy.claude_session_id,
|
|
330
|
+
pm_backend: legacy.pm_backend,
|
|
331
|
+
ts: legacy.ts,
|
|
332
|
+
}));
|
|
333
|
+
return result;
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const upsertProviderAndLegacySession = db.transaction((row) => {
|
|
337
|
+
const provider = providerSessionParams(row);
|
|
338
|
+
const result = upsertProviderSessionStmt.run(provider);
|
|
339
|
+
if (provider.provider === 'claude') {
|
|
340
|
+
upsertSessionStmt.run(legacySessionParams({
|
|
341
|
+
...row,
|
|
342
|
+
session_key: provider.session_key,
|
|
343
|
+
chat_id: row.chat_id ?? provider.session_key.split(':')[0],
|
|
344
|
+
claude_session_id: provider.provider_session_id,
|
|
345
|
+
pm_backend: provider.pm_backend
|
|
346
|
+
?? (
|
|
347
|
+
provider.namespace === CLAUDE_CHANNELS_NAMESPACE
|
|
348
|
+
? 'cli'
|
|
349
|
+
: 'sdk'
|
|
350
|
+
),
|
|
351
|
+
ts: provider.ts,
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
354
|
+
return result;
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const clearLegacySession = db.transaction((sessionKey) => {
|
|
358
|
+
const row = getSessionStmt.get(sessionKey);
|
|
359
|
+
const result = clearSessionIdStmt.run(sessionKey);
|
|
360
|
+
if (row) {
|
|
361
|
+
clearProviderSessionStmt.run(
|
|
362
|
+
sessionKey,
|
|
363
|
+
claudeNamespaceForStoredBackend(row.pm_backend),
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
return result;
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const clearOneProviderSession = db.transaction((sessionKey, namespace) => {
|
|
370
|
+
const row = getProviderSessionStmt.get(sessionKey, namespace);
|
|
371
|
+
const result = clearProviderSessionStmt.run(sessionKey, namespace);
|
|
372
|
+
if (row?.provider === 'claude') {
|
|
373
|
+
const legacy = getSessionStmt.get(sessionKey);
|
|
374
|
+
if (
|
|
375
|
+
legacy
|
|
376
|
+
&& claudeNamespaceForStoredBackend(legacy.pm_backend) === namespace
|
|
377
|
+
) {
|
|
378
|
+
clearSessionIdStmt.run(sessionKey);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return result;
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const readProviderSession = db.transaction((sessionKey, namespace) => {
|
|
385
|
+
if (
|
|
386
|
+
namespace === CLAUDE_INLINE_NAMESPACE
|
|
387
|
+
|| namespace === CLAUDE_CHANNELS_NAMESPACE
|
|
388
|
+
) {
|
|
389
|
+
const legacy = getSessionStmt.get(sessionKey);
|
|
390
|
+
if (
|
|
391
|
+
legacy
|
|
392
|
+
&& claudeNamespaceForStoredBackend(legacy.pm_backend) === namespace
|
|
393
|
+
) {
|
|
394
|
+
const namespaced = getProviderSessionStmt.get(sessionKey, namespace);
|
|
395
|
+
if (
|
|
396
|
+
!namespaced
|
|
397
|
+
|| legacy.last_active_ts > namespaced.last_active_ts
|
|
398
|
+
|| (
|
|
399
|
+
legacy.last_active_ts === namespaced.last_active_ts
|
|
400
|
+
&& legacy.claude_session_id !== namespaced.provider_session_id
|
|
401
|
+
)
|
|
402
|
+
) {
|
|
403
|
+
upsertProviderSessionStmt.run(providerSessionParams({
|
|
404
|
+
session_key: legacy.session_key,
|
|
405
|
+
namespace,
|
|
406
|
+
provider: 'claude',
|
|
407
|
+
provider_session_id: legacy.claude_session_id,
|
|
408
|
+
agent: legacy.agent,
|
|
409
|
+
cwd: legacy.cwd,
|
|
410
|
+
model: legacy.model,
|
|
411
|
+
effort: legacy.effort,
|
|
412
|
+
pm_backend: legacy.pm_backend,
|
|
413
|
+
ts: legacy.last_active_ts,
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return getProviderSessionStmt.get(sessionKey, namespace);
|
|
419
|
+
});
|
|
420
|
+
|
|
150
421
|
const getMessageStmt = db.prepare(`
|
|
151
422
|
SELECT * FROM messages WHERE chat_id = ? AND msg_id = ?
|
|
152
423
|
ORDER BY id DESC LIMIT 1
|
|
@@ -272,6 +543,3026 @@ function wrap(db) {
|
|
|
272
543
|
return { replayMarked: txn() };
|
|
273
544
|
}
|
|
274
545
|
|
|
546
|
+
function assertCodexGenerationIdentity(generationId, stableHostId, bootSessionId) {
|
|
547
|
+
const generation = db.prepare(`
|
|
548
|
+
SELECT * FROM codex_generations WHERE generation_id = ?
|
|
549
|
+
`).get(generationId);
|
|
550
|
+
if (!generation) {
|
|
551
|
+
throw codexError(
|
|
552
|
+
'Codex generation does not exist',
|
|
553
|
+
'CODEX_GENERATION_NOT_FOUND',
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
if (
|
|
557
|
+
generation.stable_host_id !== stableHostId
|
|
558
|
+
|| generation.boot_session_id !== bootSessionId
|
|
559
|
+
) {
|
|
560
|
+
throw codexError(
|
|
561
|
+
'Codex generation identity does not match the durable owner',
|
|
562
|
+
'CODEX_GENERATION_IDENTITY_MISMATCH',
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
return generation;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function createCodexGeneration(input) {
|
|
569
|
+
const generationId = requiredCodexString(
|
|
570
|
+
input?.generation_id,
|
|
571
|
+
'generation ID',
|
|
572
|
+
);
|
|
573
|
+
const sessionKey = requiredCodexString(input?.session_key, 'session key');
|
|
574
|
+
const stableHostId = requiredCodexString(
|
|
575
|
+
input?.stable_host_id,
|
|
576
|
+
'stable host identity',
|
|
577
|
+
);
|
|
578
|
+
const bootSessionId = requiredCodexString(
|
|
579
|
+
input?.boot_session_id,
|
|
580
|
+
'boot-session identity',
|
|
581
|
+
);
|
|
582
|
+
const ts = requiredTimestamp(input?.ts ?? Date.now(), 'generation timestamp');
|
|
583
|
+
const threadId = optionalCodexString(input?.thread_id, 'thread ID');
|
|
584
|
+
const appServerSessionId = optionalCodexString(
|
|
585
|
+
input?.app_server_session_id,
|
|
586
|
+
'app-server session ID',
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
return db.transaction(() => {
|
|
590
|
+
const identity = db.prepare(`
|
|
591
|
+
SELECT stable_host_id, last_boot_session_id
|
|
592
|
+
FROM codex_runtime_identity
|
|
593
|
+
WHERE singleton = 1
|
|
594
|
+
`).get();
|
|
595
|
+
if (!identity) {
|
|
596
|
+
const hasState = db.prepare(`
|
|
597
|
+
SELECT (
|
|
598
|
+
(SELECT COUNT(*) FROM codex_generations)
|
|
599
|
+
+ (SELECT COUNT(*) FROM codex_daemon_lease)
|
|
600
|
+
) AS count
|
|
601
|
+
`).get().count;
|
|
602
|
+
if (hasState) {
|
|
603
|
+
throw codexError(
|
|
604
|
+
'Codex persisted identity is missing',
|
|
605
|
+
'CODEX_PERSISTED_IDENTITY_MISSING',
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
db.prepare(`
|
|
609
|
+
INSERT INTO codex_runtime_identity (
|
|
610
|
+
singleton, stable_host_id, last_boot_session_id,
|
|
611
|
+
established_ts, updated_ts
|
|
612
|
+
) VALUES (1, ?, ?, ?, ?)
|
|
613
|
+
`).run(stableHostId, bootSessionId, ts, ts);
|
|
614
|
+
} else if (identity.stable_host_id !== stableHostId) {
|
|
615
|
+
throw codexError(
|
|
616
|
+
'Codex database belongs to another stable host',
|
|
617
|
+
'CODEX_STABLE_HOST_MISMATCH',
|
|
618
|
+
);
|
|
619
|
+
} else if (identity.last_boot_session_id !== bootSessionId) {
|
|
620
|
+
throw codexError(
|
|
621
|
+
'Codex boot identity must be reconstructed before generation creation',
|
|
622
|
+
'CODEX_BOOT_RECONSTRUCTION_REQUIRED',
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
return db.prepare(`
|
|
626
|
+
INSERT INTO codex_generations (
|
|
627
|
+
generation_id, session_key, thread_id, app_server_session_id,
|
|
628
|
+
stable_host_id, boot_session_id, state,
|
|
629
|
+
created_ts, updated_ts
|
|
630
|
+
) VALUES (?, ?, ?, ?, ?, ?, 'created', ?, ?)
|
|
631
|
+
`).run(
|
|
632
|
+
generationId,
|
|
633
|
+
sessionKey,
|
|
634
|
+
threadId,
|
|
635
|
+
appServerSessionId,
|
|
636
|
+
stableHostId,
|
|
637
|
+
bootSessionId,
|
|
638
|
+
ts,
|
|
639
|
+
ts,
|
|
640
|
+
);
|
|
641
|
+
})();
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const checkpointDetailKeys = [
|
|
645
|
+
'method',
|
|
646
|
+
'outcome',
|
|
647
|
+
'reason',
|
|
648
|
+
'errorCode',
|
|
649
|
+
'source',
|
|
650
|
+
'clientUserMessageId',
|
|
651
|
+
'terminalStatus',
|
|
652
|
+
'itemType',
|
|
653
|
+
'deltaBytes',
|
|
654
|
+
'resumed',
|
|
655
|
+
'model',
|
|
656
|
+
'effort',
|
|
657
|
+
'willRetry',
|
|
658
|
+
'deliveryState',
|
|
659
|
+
'cancellationCode',
|
|
660
|
+
'statusType',
|
|
661
|
+
];
|
|
662
|
+
const codexCheckpointKinds = new Set([
|
|
663
|
+
'request-prepared',
|
|
664
|
+
'request-write-attempted',
|
|
665
|
+
'request-response-observed',
|
|
666
|
+
'thread-initialized',
|
|
667
|
+
'thread-status-changed',
|
|
668
|
+
'turn-accepted',
|
|
669
|
+
'turn-steer-accepted',
|
|
670
|
+
'turn-error-observed',
|
|
671
|
+
'turn-started',
|
|
672
|
+
'item-delta-observed',
|
|
673
|
+
'item-started',
|
|
674
|
+
'item-completed',
|
|
675
|
+
'turn-terminal',
|
|
676
|
+
'active-start-cancelled',
|
|
677
|
+
'queued-send-cancelled',
|
|
678
|
+
'stop-background-cleanup-reused',
|
|
679
|
+
'stop-terminal-reconciled',
|
|
680
|
+
'stop-clean-accepted',
|
|
681
|
+
'stop-empty-registry-observed',
|
|
682
|
+
'background-terminal-reconciled',
|
|
683
|
+
'background-clean-accepted',
|
|
684
|
+
'background-empty-registry-observed',
|
|
685
|
+
'failed-ambiguous-entered',
|
|
686
|
+
'containment-entered',
|
|
687
|
+
'telegram-delivery-settled',
|
|
688
|
+
'telegram-delivery-failed',
|
|
689
|
+
]);
|
|
690
|
+
|
|
691
|
+
function checkpointDetail(input) {
|
|
692
|
+
const detail = {};
|
|
693
|
+
for (const key of checkpointDetailKeys) {
|
|
694
|
+
const value = input[key];
|
|
695
|
+
if (
|
|
696
|
+
value == null
|
|
697
|
+
|| typeof value === 'string'
|
|
698
|
+
|| typeof value === 'boolean'
|
|
699
|
+
|| Number.isSafeInteger(value)
|
|
700
|
+
) {
|
|
701
|
+
if (value != null) detail[key] = value;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return Object.keys(detail).length > 0 ? JSON.stringify(detail) : null;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function requireCodexAttempt(attemptId, generationId) {
|
|
708
|
+
const attempt = db.prepare(`
|
|
709
|
+
SELECT * FROM codex_turn_attempts WHERE attempt_id = ?
|
|
710
|
+
`).get(attemptId);
|
|
711
|
+
if (!attempt || attempt.generation_id !== generationId) {
|
|
712
|
+
throw codexError(
|
|
713
|
+
'Codex checkpoint attempt does not belong to the generation',
|
|
714
|
+
'CODEX_ATTEMPT_NOT_FOUND',
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
return attempt;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function markGenerationAttemptsAmbiguous(generationId, ts) {
|
|
721
|
+
const result = db.prepare(`
|
|
722
|
+
UPDATE codex_turn_attempts
|
|
723
|
+
SET recovery_state = 'ambiguous',
|
|
724
|
+
ambiguous_ts = COALESCE(ambiguous_ts, ?),
|
|
725
|
+
updated_ts = ?
|
|
726
|
+
WHERE generation_id = ?
|
|
727
|
+
AND delivery_state != 'prepared'
|
|
728
|
+
AND recovery_state NOT IN ('settled', 'cancelled')
|
|
729
|
+
`).run(ts, ts, generationId);
|
|
730
|
+
db.prepare(`
|
|
731
|
+
UPDATE codex_linked_inputs
|
|
732
|
+
SET state = 'ambiguous', settled_ts = ?
|
|
733
|
+
WHERE generation_id = ?
|
|
734
|
+
AND state = 'linked'
|
|
735
|
+
`).run(ts, generationId);
|
|
736
|
+
db.prepare(`
|
|
737
|
+
UPDATE codex_dispatch_reservations
|
|
738
|
+
SET state = 'ambiguous',
|
|
739
|
+
updated_ts = ?,
|
|
740
|
+
settled_ts = NULL
|
|
741
|
+
WHERE generation_id = ?
|
|
742
|
+
AND state IN ('reserved', 'steer-accepted', 'queue-authorized')
|
|
743
|
+
`).run(ts, generationId);
|
|
744
|
+
db.prepare(`
|
|
745
|
+
UPDATE messages
|
|
746
|
+
SET handler_status = 'codex-ambiguous'
|
|
747
|
+
WHERE direction = 'in'
|
|
748
|
+
AND handler_status IN (
|
|
749
|
+
'received', 'dispatched', 'processing', 'replay-pending'
|
|
750
|
+
)
|
|
751
|
+
AND EXISTS (
|
|
752
|
+
SELECT 1
|
|
753
|
+
FROM codex_dispatch_reservations reservation
|
|
754
|
+
WHERE reservation.generation_id = ?
|
|
755
|
+
AND reservation.state = 'ambiguous'
|
|
756
|
+
AND reservation.bot_name = messages.bot_name
|
|
757
|
+
AND reservation.telegram_chat_id = messages.chat_id
|
|
758
|
+
AND reservation.telegram_message_id = CAST(messages.msg_id AS TEXT)
|
|
759
|
+
)
|
|
760
|
+
`).run(generationId);
|
|
761
|
+
return result;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function assertCodexCheckpointOwner(generation, kind) {
|
|
765
|
+
if (['request-prepared', 'queued-send-cancelled'].includes(kind)) return;
|
|
766
|
+
const lease = db.prepare(`
|
|
767
|
+
SELECT generation_id, stable_host_id, boot_session_id, status
|
|
768
|
+
FROM codex_daemon_lease
|
|
769
|
+
WHERE singleton = 1
|
|
770
|
+
`).get();
|
|
771
|
+
if (
|
|
772
|
+
generation.state !== 'active'
|
|
773
|
+
|| lease?.status !== 'active'
|
|
774
|
+
|| lease.generation_id !== generation.generation_id
|
|
775
|
+
|| lease.stable_host_id !== generation.stable_host_id
|
|
776
|
+
|| lease.boot_session_id !== generation.boot_session_id
|
|
777
|
+
) {
|
|
778
|
+
throw codexError(
|
|
779
|
+
'Codex checkpoint generation does not own the active daemon lease',
|
|
780
|
+
'CODEX_CHECKPOINT_STALE_GENERATION',
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function assertCodexAttemptIdentity(attempt, fields) {
|
|
786
|
+
for (const [column, value] of Object.entries(fields)) {
|
|
787
|
+
if (value != null && attempt[column] != null && attempt[column] !== value) {
|
|
788
|
+
throw codexError(
|
|
789
|
+
`Codex attempt ${column} is immutable`,
|
|
790
|
+
'CODEX_ATTEMPT_IDENTITY_MISMATCH',
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const idempotentCheckpointKinds = new Set([
|
|
797
|
+
'request-prepared',
|
|
798
|
+
'request-write-attempted',
|
|
799
|
+
'request-response-observed',
|
|
800
|
+
'thread-initialized',
|
|
801
|
+
'turn-accepted',
|
|
802
|
+
'turn-steer-accepted',
|
|
803
|
+
'turn-started',
|
|
804
|
+
'item-started',
|
|
805
|
+
'item-completed',
|
|
806
|
+
'turn-terminal',
|
|
807
|
+
'active-start-cancelled',
|
|
808
|
+
'queued-send-cancelled',
|
|
809
|
+
'telegram-delivery-settled',
|
|
810
|
+
'telegram-delivery-failed',
|
|
811
|
+
]);
|
|
812
|
+
|
|
813
|
+
function hasExactCodexCheckpoint({
|
|
814
|
+
generationId,
|
|
815
|
+
attemptId,
|
|
816
|
+
kind,
|
|
817
|
+
threadId,
|
|
818
|
+
turnId,
|
|
819
|
+
requestId,
|
|
820
|
+
itemId,
|
|
821
|
+
detailJson,
|
|
822
|
+
}) {
|
|
823
|
+
if (!idempotentCheckpointKinds.has(kind)) return false;
|
|
824
|
+
return db.prepare(`
|
|
825
|
+
SELECT 1
|
|
826
|
+
FROM codex_attempt_checkpoints
|
|
827
|
+
WHERE generation_id = ?
|
|
828
|
+
AND attempt_id IS ?
|
|
829
|
+
AND kind = ?
|
|
830
|
+
AND thread_id IS ?
|
|
831
|
+
AND turn_id IS ?
|
|
832
|
+
AND request_id IS ?
|
|
833
|
+
AND item_id IS ?
|
|
834
|
+
AND detail_json IS ?
|
|
835
|
+
LIMIT 1
|
|
836
|
+
`).get(
|
|
837
|
+
generationId,
|
|
838
|
+
attemptId,
|
|
839
|
+
kind,
|
|
840
|
+
threadId,
|
|
841
|
+
turnId,
|
|
842
|
+
requestId,
|
|
843
|
+
itemId,
|
|
844
|
+
detailJson,
|
|
845
|
+
) != null;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function settleCodexInboundReservation(reservation, handlerStatus) {
|
|
849
|
+
const inbound = db.prepare(`
|
|
850
|
+
SELECT handler_status
|
|
851
|
+
FROM messages
|
|
852
|
+
WHERE direction = 'in'
|
|
853
|
+
AND bot_name = ?
|
|
854
|
+
AND chat_id = ?
|
|
855
|
+
AND CAST(msg_id AS TEXT) = ?
|
|
856
|
+
`).get(
|
|
857
|
+
reservation.bot_name,
|
|
858
|
+
reservation.telegram_chat_id,
|
|
859
|
+
reservation.telegram_message_id,
|
|
860
|
+
);
|
|
861
|
+
if (!inbound) {
|
|
862
|
+
throw codexError(
|
|
863
|
+
'Codex disposition has no exact inbound message',
|
|
864
|
+
'CODEX_LINKED_INPUT_MESSAGE_NOT_FOUND',
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
if (inbound.handler_status === handlerStatus) return;
|
|
868
|
+
if (![
|
|
869
|
+
'received',
|
|
870
|
+
'dispatched',
|
|
871
|
+
'processing',
|
|
872
|
+
'replay-pending',
|
|
873
|
+
].includes(inbound.handler_status)) {
|
|
874
|
+
throw codexError(
|
|
875
|
+
'Codex disposition conflicts with the inbound message state',
|
|
876
|
+
'CODEX_LINKED_INPUT_MESSAGE_NOT_FOUND',
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
db.prepare(`
|
|
880
|
+
UPDATE messages
|
|
881
|
+
SET handler_status = ?
|
|
882
|
+
WHERE direction = 'in'
|
|
883
|
+
AND bot_name = ?
|
|
884
|
+
AND chat_id = ?
|
|
885
|
+
AND CAST(msg_id AS TEXT) = ?
|
|
886
|
+
`).run(
|
|
887
|
+
handlerStatus,
|
|
888
|
+
reservation.bot_name,
|
|
889
|
+
reservation.telegram_chat_id,
|
|
890
|
+
reservation.telegram_message_id,
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function settleCodexPrimaryInbound(attempt, handlerStatus) {
|
|
895
|
+
if (attempt.telegram_source_message_id == null) return;
|
|
896
|
+
const selections = db.prepare(`
|
|
897
|
+
SELECT *
|
|
898
|
+
FROM inbound_runtime_selections
|
|
899
|
+
WHERE session_key = ?
|
|
900
|
+
AND telegram_message_id = ?
|
|
901
|
+
AND provider = 'codex'
|
|
902
|
+
ORDER BY bot_name, telegram_chat_id
|
|
903
|
+
`).all(
|
|
904
|
+
attempt.session_key,
|
|
905
|
+
attempt.telegram_source_message_id,
|
|
906
|
+
);
|
|
907
|
+
if (selections.length > 1) {
|
|
908
|
+
throw codexError(
|
|
909
|
+
'Codex attempt matches conflicting inbound runtime selections',
|
|
910
|
+
'INBOUND_RUNTIME_SELECTION_CONFLICT',
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
if (selections.length === 1) {
|
|
914
|
+
settleCodexInboundReservation(selections[0], handlerStatus);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function settleCodexAttemptConsumerOutcome({
|
|
919
|
+
attempt,
|
|
920
|
+
generationId,
|
|
921
|
+
linkedState,
|
|
922
|
+
attemptRecoveryState,
|
|
923
|
+
settleQueuedReservation,
|
|
924
|
+
ts,
|
|
925
|
+
}) {
|
|
926
|
+
db.prepare(`
|
|
927
|
+
UPDATE codex_turn_attempts
|
|
928
|
+
SET recovery_state = ?,
|
|
929
|
+
settled_ts = ?,
|
|
930
|
+
updated_ts = ?
|
|
931
|
+
WHERE attempt_id = ?
|
|
932
|
+
`).run(attemptRecoveryState, ts, ts, attempt.attempt_id);
|
|
933
|
+
|
|
934
|
+
const handlerStatus = linkedState === 'settled' ? 'replied' : 'failed';
|
|
935
|
+
settleCodexPrimaryInbound(attempt, handlerStatus);
|
|
936
|
+
|
|
937
|
+
const acceptedSteers = attempt.turn_id == null
|
|
938
|
+
? []
|
|
939
|
+
: db.prepare(`
|
|
940
|
+
SELECT *
|
|
941
|
+
FROM codex_turn_attempts
|
|
942
|
+
WHERE generation_id = ?
|
|
943
|
+
AND method = 'turn/steer'
|
|
944
|
+
AND turn_id = ?
|
|
945
|
+
AND delivery_state = 'response-observed'
|
|
946
|
+
AND response_outcome = 'result'
|
|
947
|
+
AND recovery_state = 'active'
|
|
948
|
+
ORDER BY created_ts, attempt_id
|
|
949
|
+
`).all(generationId, attempt.turn_id);
|
|
950
|
+
for (const steer of acceptedSteers) {
|
|
951
|
+
if (steer.telegram_source_message_id == null) continue;
|
|
952
|
+
const reservations = db.prepare(`
|
|
953
|
+
SELECT *
|
|
954
|
+
FROM codex_dispatch_reservations
|
|
955
|
+
WHERE generation_id = ?
|
|
956
|
+
AND session_key = ?
|
|
957
|
+
AND telegram_message_id = ?
|
|
958
|
+
AND state = 'reserved'
|
|
959
|
+
ORDER BY reservation_id
|
|
960
|
+
`).all(
|
|
961
|
+
generationId,
|
|
962
|
+
steer.session_key,
|
|
963
|
+
steer.telegram_source_message_id,
|
|
964
|
+
);
|
|
965
|
+
if (reservations.length > 1) {
|
|
966
|
+
throw codexError(
|
|
967
|
+
'Codex accepted steer matches conflicting reservations',
|
|
968
|
+
'CODEX_DISPATCH_RESERVATION_CONFLICT',
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
if (reservations.length === 0) continue;
|
|
972
|
+
const reservation = reservations[0];
|
|
973
|
+
settleCodexInboundReservation(reservation, handlerStatus);
|
|
974
|
+
db.prepare(`
|
|
975
|
+
INSERT INTO codex_linked_inputs (
|
|
976
|
+
linked_input_id, generation_id, attempt_id, target_attempt_id,
|
|
977
|
+
telegram_chat_id, telegram_message_id, state,
|
|
978
|
+
created_ts, settled_ts
|
|
979
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
980
|
+
`).run(
|
|
981
|
+
reservation.reservation_id,
|
|
982
|
+
generationId,
|
|
983
|
+
steer.attempt_id,
|
|
984
|
+
attempt.attempt_id,
|
|
985
|
+
reservation.telegram_chat_id,
|
|
986
|
+
reservation.telegram_message_id,
|
|
987
|
+
linkedState,
|
|
988
|
+
reservation.created_ts,
|
|
989
|
+
ts,
|
|
990
|
+
);
|
|
991
|
+
db.prepare(`
|
|
992
|
+
UPDATE codex_dispatch_reservations
|
|
993
|
+
SET state = ?,
|
|
994
|
+
steer_attempt_id = ?,
|
|
995
|
+
target_attempt_id = ?,
|
|
996
|
+
updated_ts = ?,
|
|
997
|
+
settled_ts = ?
|
|
998
|
+
WHERE reservation_id = ?
|
|
999
|
+
AND state = 'reserved'
|
|
1000
|
+
`).run(
|
|
1001
|
+
linkedState,
|
|
1002
|
+
steer.attempt_id,
|
|
1003
|
+
attempt.attempt_id,
|
|
1004
|
+
ts,
|
|
1005
|
+
ts,
|
|
1006
|
+
reservation.reservation_id,
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const linkedReservations = db.prepare(`
|
|
1011
|
+
SELECT reservation.*
|
|
1012
|
+
FROM codex_dispatch_reservations reservation
|
|
1013
|
+
JOIN codex_linked_inputs linked
|
|
1014
|
+
ON linked.linked_input_id = reservation.reservation_id
|
|
1015
|
+
WHERE linked.target_attempt_id = ?
|
|
1016
|
+
AND reservation.state = 'steer-accepted'
|
|
1017
|
+
`).all(attempt.attempt_id);
|
|
1018
|
+
for (const reservation of linkedReservations) {
|
|
1019
|
+
settleCodexInboundReservation(reservation, handlerStatus);
|
|
1020
|
+
}
|
|
1021
|
+
const linkedAttempts = db.prepare(`
|
|
1022
|
+
SELECT attempt_id
|
|
1023
|
+
FROM codex_linked_inputs
|
|
1024
|
+
WHERE target_attempt_id = ? AND state = 'linked'
|
|
1025
|
+
`).all(attempt.attempt_id);
|
|
1026
|
+
db.prepare(`
|
|
1027
|
+
UPDATE codex_linked_inputs
|
|
1028
|
+
SET state = ?, settled_ts = ?
|
|
1029
|
+
WHERE target_attempt_id = ? AND state = 'linked'
|
|
1030
|
+
`).run(linkedState, ts, attempt.attempt_id);
|
|
1031
|
+
db.prepare(`
|
|
1032
|
+
UPDATE codex_dispatch_reservations
|
|
1033
|
+
SET state = ?, updated_ts = ?, settled_ts = ?
|
|
1034
|
+
WHERE reservation_id IN (
|
|
1035
|
+
SELECT linked_input_id
|
|
1036
|
+
FROM codex_linked_inputs
|
|
1037
|
+
WHERE target_attempt_id = ?
|
|
1038
|
+
)
|
|
1039
|
+
AND state = 'steer-accepted'
|
|
1040
|
+
`).run(linkedState, ts, ts, attempt.attempt_id);
|
|
1041
|
+
for (const linked of linkedAttempts) {
|
|
1042
|
+
db.prepare(`
|
|
1043
|
+
UPDATE codex_turn_attempts
|
|
1044
|
+
SET recovery_state = 'settled',
|
|
1045
|
+
settled_ts = ?,
|
|
1046
|
+
updated_ts = ?
|
|
1047
|
+
WHERE attempt_id = ?
|
|
1048
|
+
AND recovery_state NOT IN ('settled', 'cancelled')
|
|
1049
|
+
`).run(ts, ts, linked.attempt_id);
|
|
1050
|
+
}
|
|
1051
|
+
if (attempt.turn_id != null) {
|
|
1052
|
+
db.prepare(`
|
|
1053
|
+
UPDATE codex_turn_attempts
|
|
1054
|
+
SET recovery_state = 'settled',
|
|
1055
|
+
settled_ts = ?,
|
|
1056
|
+
updated_ts = ?
|
|
1057
|
+
WHERE generation_id = ?
|
|
1058
|
+
AND method = 'turn/steer'
|
|
1059
|
+
AND turn_id = ?
|
|
1060
|
+
AND delivery_state = 'response-observed'
|
|
1061
|
+
AND response_outcome = 'result'
|
|
1062
|
+
AND recovery_state = 'active'
|
|
1063
|
+
`).run(ts, ts, generationId, attempt.turn_id);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
if (
|
|
1067
|
+
settleQueuedReservation
|
|
1068
|
+
&& attempt.telegram_source_message_id != null
|
|
1069
|
+
) {
|
|
1070
|
+
const queued = db.prepare(`
|
|
1071
|
+
SELECT *
|
|
1072
|
+
FROM codex_dispatch_reservations
|
|
1073
|
+
WHERE generation_id = ?
|
|
1074
|
+
AND session_key = ?
|
|
1075
|
+
AND telegram_message_id = ?
|
|
1076
|
+
AND state = 'queue-authorized'
|
|
1077
|
+
ORDER BY reservation_id
|
|
1078
|
+
`).all(
|
|
1079
|
+
generationId,
|
|
1080
|
+
attempt.session_key,
|
|
1081
|
+
attempt.telegram_source_message_id,
|
|
1082
|
+
);
|
|
1083
|
+
if (queued.length > 1) {
|
|
1084
|
+
throw codexError(
|
|
1085
|
+
'Codex terminal attempt matches conflicting queued reservations',
|
|
1086
|
+
'CODEX_QUEUE_SETTLEMENT_CONFLICT',
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
if (queued.length === 1) {
|
|
1090
|
+
settleCodexInboundReservation(queued[0], handlerStatus);
|
|
1091
|
+
db.prepare(`
|
|
1092
|
+
UPDATE codex_dispatch_reservations
|
|
1093
|
+
SET state = ?,
|
|
1094
|
+
target_attempt_id = ?,
|
|
1095
|
+
updated_ts = ?,
|
|
1096
|
+
settled_ts = ?
|
|
1097
|
+
WHERE reservation_id = ?
|
|
1098
|
+
AND state = 'queue-authorized'
|
|
1099
|
+
`).run(
|
|
1100
|
+
linkedState,
|
|
1101
|
+
attempt.attempt_id,
|
|
1102
|
+
ts,
|
|
1103
|
+
ts,
|
|
1104
|
+
queued[0].reservation_id,
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
db.prepare(`
|
|
1110
|
+
UPDATE codex_generations
|
|
1111
|
+
SET state = 'healthy-stopped',
|
|
1112
|
+
settled_ts = ?,
|
|
1113
|
+
updated_ts = ?
|
|
1114
|
+
WHERE generation_id = ?
|
|
1115
|
+
AND EXISTS (
|
|
1116
|
+
SELECT 1
|
|
1117
|
+
FROM codex_attempt_checkpoints
|
|
1118
|
+
WHERE generation_id = ?
|
|
1119
|
+
AND kind = 'stop-empty-registry-observed'
|
|
1120
|
+
)
|
|
1121
|
+
AND NOT EXISTS (
|
|
1122
|
+
SELECT 1
|
|
1123
|
+
FROM codex_turn_attempts
|
|
1124
|
+
WHERE generation_id = ?
|
|
1125
|
+
AND recovery_state NOT IN ('settled', 'cancelled')
|
|
1126
|
+
)
|
|
1127
|
+
`).run(ts, ts, generationId, generationId, generationId);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function recordCodexCheckpoint(input) {
|
|
1131
|
+
const kind = requiredCodexString(input?.kind, 'checkpoint kind');
|
|
1132
|
+
if (!codexCheckpointKinds.has(kind)) {
|
|
1133
|
+
throw codexError(
|
|
1134
|
+
'Codex checkpoint kind is not allowlisted',
|
|
1135
|
+
'CODEX_CHECKPOINT_KIND_REJECTED',
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
const generationId = requiredCodexString(
|
|
1139
|
+
input?.generationId ?? input?.generation_id,
|
|
1140
|
+
'generation ID',
|
|
1141
|
+
);
|
|
1142
|
+
const stableHostId = requiredCodexString(
|
|
1143
|
+
input?.stable_host_id ?? input?.hostIdentity,
|
|
1144
|
+
'stable host identity',
|
|
1145
|
+
);
|
|
1146
|
+
const bootSessionId = requiredCodexString(
|
|
1147
|
+
input?.boot_session_id ?? input?.bootSessionIdentity,
|
|
1148
|
+
'boot-session identity',
|
|
1149
|
+
);
|
|
1150
|
+
const attemptId = optionalCodexString(
|
|
1151
|
+
input?.attemptId ?? input?.attempt_id,
|
|
1152
|
+
'attempt ID',
|
|
1153
|
+
);
|
|
1154
|
+
const threadId = optionalCodexString(
|
|
1155
|
+
input?.threadId ?? input?.thread_id,
|
|
1156
|
+
'thread ID',
|
|
1157
|
+
);
|
|
1158
|
+
const turnId = optionalCodexString(
|
|
1159
|
+
input?.turnId ?? input?.turn_id,
|
|
1160
|
+
'turn ID',
|
|
1161
|
+
);
|
|
1162
|
+
const requestId = optionalCodexRequestId(
|
|
1163
|
+
input?.requestId ?? input?.request_id,
|
|
1164
|
+
);
|
|
1165
|
+
const itemId = optionalCodexString(
|
|
1166
|
+
input?.itemId ?? input?.item_id,
|
|
1167
|
+
'item ID',
|
|
1168
|
+
);
|
|
1169
|
+
const method = optionalCodexString(input?.method, 'request method');
|
|
1170
|
+
const sourceId = optionalCodexString(input?.source, 'source message ID');
|
|
1171
|
+
const clientUserMessageId = optionalCodexString(
|
|
1172
|
+
input?.clientUserMessageId ?? input?.client_user_message_id,
|
|
1173
|
+
'client user message ID',
|
|
1174
|
+
);
|
|
1175
|
+
const ts = requiredTimestamp(input?.ts ?? Date.now(), 'checkpoint timestamp');
|
|
1176
|
+
const detailJson = checkpointDetail(input);
|
|
1177
|
+
|
|
1178
|
+
return db.transaction(() => {
|
|
1179
|
+
const generation = assertCodexGenerationIdentity(
|
|
1180
|
+
generationId,
|
|
1181
|
+
stableHostId,
|
|
1182
|
+
bootSessionId,
|
|
1183
|
+
);
|
|
1184
|
+
let attempt = attemptId
|
|
1185
|
+
? db.prepare(`
|
|
1186
|
+
SELECT * FROM codex_turn_attempts WHERE attempt_id = ?
|
|
1187
|
+
`).get(attemptId)
|
|
1188
|
+
: null;
|
|
1189
|
+
if (attempt) {
|
|
1190
|
+
assertCodexAttemptIdentity(attempt, {
|
|
1191
|
+
method,
|
|
1192
|
+
thread_id: threadId,
|
|
1193
|
+
turn_id: turnId,
|
|
1194
|
+
request_id: requestId,
|
|
1195
|
+
telegram_source_message_id: sourceId,
|
|
1196
|
+
client_user_message_id: clientUserMessageId,
|
|
1197
|
+
});
|
|
1198
|
+
if (hasExactCodexCheckpoint({
|
|
1199
|
+
generationId,
|
|
1200
|
+
attemptId,
|
|
1201
|
+
kind,
|
|
1202
|
+
threadId,
|
|
1203
|
+
turnId,
|
|
1204
|
+
requestId,
|
|
1205
|
+
itemId,
|
|
1206
|
+
detailJson,
|
|
1207
|
+
})) {
|
|
1208
|
+
return { changes: 0, attemptId, kind };
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
assertCodexCheckpointOwner(generation, kind);
|
|
1212
|
+
|
|
1213
|
+
if (kind === 'request-prepared') {
|
|
1214
|
+
requiredCodexString(method, 'request method');
|
|
1215
|
+
if (!attemptId) {
|
|
1216
|
+
throw codexError(
|
|
1217
|
+
'Codex prepared checkpoint requires an attempt ID',
|
|
1218
|
+
'CODEX_CHECKPOINT_INPUT_INVALID',
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
if (attempt) {
|
|
1222
|
+
if (
|
|
1223
|
+
attempt.generation_id === generationId
|
|
1224
|
+
&& attempt.method === method
|
|
1225
|
+
&& attempt.delivery_state === 'prepared'
|
|
1226
|
+
) {
|
|
1227
|
+
return { changes: 0, attemptId, kind };
|
|
1228
|
+
}
|
|
1229
|
+
throw codexError(
|
|
1230
|
+
'Codex attempt ID was reused with different metadata',
|
|
1231
|
+
'CODEX_ATTEMPT_ID_REUSED',
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
db.prepare(`
|
|
1235
|
+
INSERT INTO codex_turn_attempts (
|
|
1236
|
+
attempt_id, generation_id, session_key, method,
|
|
1237
|
+
thread_id, turn_id, telegram_source_message_id,
|
|
1238
|
+
client_user_message_id, delivery_state, recovery_state,
|
|
1239
|
+
created_ts, updated_ts
|
|
1240
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'prepared', 'prepared', ?, ?)
|
|
1241
|
+
`).run(
|
|
1242
|
+
attemptId,
|
|
1243
|
+
generationId,
|
|
1244
|
+
generation.session_key,
|
|
1245
|
+
method,
|
|
1246
|
+
threadId,
|
|
1247
|
+
turnId,
|
|
1248
|
+
sourceId,
|
|
1249
|
+
clientUserMessageId,
|
|
1250
|
+
ts,
|
|
1251
|
+
ts,
|
|
1252
|
+
);
|
|
1253
|
+
attempt = requireCodexAttempt(attemptId, generationId);
|
|
1254
|
+
} else if (kind === 'queued-send-cancelled' && attemptId && !attempt) {
|
|
1255
|
+
db.prepare(`
|
|
1256
|
+
INSERT INTO codex_turn_attempts (
|
|
1257
|
+
attempt_id, generation_id, session_key, method,
|
|
1258
|
+
thread_id, turn_id, telegram_source_message_id,
|
|
1259
|
+
client_user_message_id, delivery_state, recovery_state,
|
|
1260
|
+
created_ts, updated_ts, settled_ts
|
|
1261
|
+
) VALUES (?, ?, ?, 'queued/send', ?, ?, ?, ?,
|
|
1262
|
+
'prepared', 'cancelled', ?, ?, ?)
|
|
1263
|
+
`).run(
|
|
1264
|
+
attemptId,
|
|
1265
|
+
generationId,
|
|
1266
|
+
generation.session_key,
|
|
1267
|
+
threadId,
|
|
1268
|
+
turnId,
|
|
1269
|
+
sourceId,
|
|
1270
|
+
clientUserMessageId,
|
|
1271
|
+
ts,
|
|
1272
|
+
ts,
|
|
1273
|
+
ts,
|
|
1274
|
+
);
|
|
1275
|
+
attempt = requireCodexAttempt(attemptId, generationId);
|
|
1276
|
+
} else if (attemptId) {
|
|
1277
|
+
attempt = requireCodexAttempt(attemptId, generationId);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
if (kind === 'request-write-attempted') {
|
|
1281
|
+
if (attempt.delivery_state !== 'prepared') {
|
|
1282
|
+
throw codexError(
|
|
1283
|
+
'Codex write-attempted checkpoint is out of order',
|
|
1284
|
+
'CODEX_CHECKPOINT_SEQUENCE_INVALID',
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
db.prepare(`
|
|
1288
|
+
UPDATE codex_turn_attempts
|
|
1289
|
+
SET request_id = ?,
|
|
1290
|
+
delivery_state = 'write-attempted',
|
|
1291
|
+
recovery_state = 'ambiguous',
|
|
1292
|
+
ambiguous_ts = COALESCE(ambiguous_ts, ?),
|
|
1293
|
+
updated_ts = ?
|
|
1294
|
+
WHERE attempt_id = ?
|
|
1295
|
+
`).run(requestId, ts, ts, attemptId);
|
|
1296
|
+
} else if (kind === 'request-response-observed') {
|
|
1297
|
+
if (attempt.delivery_state !== 'write-attempted') {
|
|
1298
|
+
throw codexError(
|
|
1299
|
+
'Codex response-observed checkpoint is out of order',
|
|
1300
|
+
'CODEX_CHECKPOINT_SEQUENCE_INVALID',
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
if (!['result', 'error'].includes(input?.outcome)) {
|
|
1304
|
+
throw codexError(
|
|
1305
|
+
'Codex response checkpoint outcome is invalid',
|
|
1306
|
+
'CODEX_CHECKPOINT_INPUT_INVALID',
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
db.prepare(`
|
|
1310
|
+
UPDATE codex_turn_attempts
|
|
1311
|
+
SET request_id = COALESCE(request_id, ?),
|
|
1312
|
+
delivery_state = 'response-observed',
|
|
1313
|
+
response_outcome = ?,
|
|
1314
|
+
updated_ts = ?
|
|
1315
|
+
WHERE attempt_id = ?
|
|
1316
|
+
`).run(requestId, input.outcome, ts, attemptId);
|
|
1317
|
+
} else if (
|
|
1318
|
+
['turn-accepted', 'turn-steer-accepted'].includes(kind)
|
|
1319
|
+
) {
|
|
1320
|
+
if (!attempt) {
|
|
1321
|
+
throw codexError(
|
|
1322
|
+
'Codex turn lifecycle checkpoint requires an attempt',
|
|
1323
|
+
'CODEX_CHECKPOINT_INPUT_INVALID',
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
if (
|
|
1327
|
+
attempt.delivery_state !== 'response-observed'
|
|
1328
|
+
|| attempt.response_outcome !== 'result'
|
|
1329
|
+
) {
|
|
1330
|
+
throw codexError(
|
|
1331
|
+
'Codex accepted checkpoint has no successful response predecessor',
|
|
1332
|
+
'CODEX_CHECKPOINT_SEQUENCE_INVALID',
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
db.prepare(`
|
|
1336
|
+
UPDATE codex_turn_attempts
|
|
1337
|
+
SET turn_id = COALESCE(?, turn_id),
|
|
1338
|
+
recovery_state = CASE
|
|
1339
|
+
WHEN terminal_status IS NULL THEN 'active'
|
|
1340
|
+
ELSE 'terminal-pending'
|
|
1341
|
+
END,
|
|
1342
|
+
updated_ts = ?
|
|
1343
|
+
WHERE attempt_id = ?
|
|
1344
|
+
`).run(turnId, ts, attemptId);
|
|
1345
|
+
db.prepare(`
|
|
1346
|
+
UPDATE codex_generations
|
|
1347
|
+
SET thread_id = COALESCE(?, thread_id),
|
|
1348
|
+
state = 'active',
|
|
1349
|
+
updated_ts = ?
|
|
1350
|
+
WHERE generation_id = ?
|
|
1351
|
+
`).run(threadId, ts, generationId);
|
|
1352
|
+
} else if (kind === 'turn-started') {
|
|
1353
|
+
if (
|
|
1354
|
+
!attempt
|
|
1355
|
+
|| attempt.terminal_status != null
|
|
1356
|
+
|| !(
|
|
1357
|
+
attempt.recovery_state === 'active'
|
|
1358
|
+
|| (
|
|
1359
|
+
attempt.recovery_state === 'ambiguous'
|
|
1360
|
+
&& ['write-attempted', 'response-observed'].includes(
|
|
1361
|
+
attempt.delivery_state,
|
|
1362
|
+
)
|
|
1363
|
+
)
|
|
1364
|
+
)
|
|
1365
|
+
) {
|
|
1366
|
+
throw codexError(
|
|
1367
|
+
'Codex turn-started checkpoint has no dispatched predecessor',
|
|
1368
|
+
'CODEX_CHECKPOINT_SEQUENCE_INVALID',
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
db.prepare(`
|
|
1372
|
+
UPDATE codex_turn_attempts
|
|
1373
|
+
SET turn_id = COALESCE(?, turn_id), updated_ts = ?
|
|
1374
|
+
WHERE attempt_id = ?
|
|
1375
|
+
`).run(turnId, ts, attemptId);
|
|
1376
|
+
} else if (
|
|
1377
|
+
['item-started', 'item-completed', 'item-delta-observed'].includes(kind)
|
|
1378
|
+
) {
|
|
1379
|
+
const notificationBeforeResponse = (
|
|
1380
|
+
attempt?.recovery_state === 'ambiguous'
|
|
1381
|
+
&& ['write-attempted', 'response-observed'].includes(
|
|
1382
|
+
attempt?.delivery_state,
|
|
1383
|
+
)
|
|
1384
|
+
);
|
|
1385
|
+
if (
|
|
1386
|
+
!attempt
|
|
1387
|
+
|| attempt.turn_id == null
|
|
1388
|
+
|| attempt.terminal_status != null
|
|
1389
|
+
|| !(attempt.recovery_state === 'active' || notificationBeforeResponse)
|
|
1390
|
+
) {
|
|
1391
|
+
throw codexError(
|
|
1392
|
+
'Codex item checkpoint requires a live dispatched turn',
|
|
1393
|
+
'CODEX_CHECKPOINT_SEQUENCE_INVALID',
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
db.prepare(`
|
|
1397
|
+
UPDATE codex_turn_attempts
|
|
1398
|
+
SET updated_ts = ?
|
|
1399
|
+
WHERE attempt_id = ?
|
|
1400
|
+
`).run(ts, attemptId);
|
|
1401
|
+
if (kind !== 'item-delta-observed' && itemId) {
|
|
1402
|
+
recordCodexItemEffect({
|
|
1403
|
+
generation_id: generationId,
|
|
1404
|
+
attempt_id: attemptId,
|
|
1405
|
+
item_id: itemId,
|
|
1406
|
+
item_type: input?.itemType ?? 'unknown',
|
|
1407
|
+
state: kind === 'item-started' ? 'started' : 'completed',
|
|
1408
|
+
ts,
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
} else if (kind === 'turn-terminal') {
|
|
1412
|
+
if (
|
|
1413
|
+
!attempt
|
|
1414
|
+
|| attempt.turn_id == null
|
|
1415
|
+
|| attempt.terminal_status != null
|
|
1416
|
+
|| !(
|
|
1417
|
+
attempt.recovery_state === 'active'
|
|
1418
|
+
|| (
|
|
1419
|
+
attempt.recovery_state === 'ambiguous'
|
|
1420
|
+
&& ['write-attempted', 'response-observed'].includes(
|
|
1421
|
+
attempt.delivery_state,
|
|
1422
|
+
)
|
|
1423
|
+
)
|
|
1424
|
+
)
|
|
1425
|
+
|| !['completed', 'interrupted', 'failed'].includes(
|
|
1426
|
+
input?.terminalStatus,
|
|
1427
|
+
)
|
|
1428
|
+
) {
|
|
1429
|
+
throw codexError(
|
|
1430
|
+
'Codex turn terminal checkpoint is invalid',
|
|
1431
|
+
'CODEX_CHECKPOINT_INPUT_INVALID',
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
db.prepare(`
|
|
1435
|
+
UPDATE codex_turn_attempts
|
|
1436
|
+
SET turn_id = COALESCE(?, turn_id),
|
|
1437
|
+
terminal_status = ?,
|
|
1438
|
+
recovery_state = CASE
|
|
1439
|
+
WHEN recovery_state = 'active' THEN 'terminal-pending'
|
|
1440
|
+
ELSE recovery_state
|
|
1441
|
+
END,
|
|
1442
|
+
updated_ts = ?
|
|
1443
|
+
WHERE attempt_id = ?
|
|
1444
|
+
`).run(turnId, input.terminalStatus, ts, attemptId);
|
|
1445
|
+
} else if (
|
|
1446
|
+
['stop-terminal-reconciled', 'background-terminal-reconciled']
|
|
1447
|
+
.includes(kind)
|
|
1448
|
+
) {
|
|
1449
|
+
db.prepare(`
|
|
1450
|
+
UPDATE codex_turn_attempts
|
|
1451
|
+
SET recovery_state = 'clean-pending', updated_ts = ?
|
|
1452
|
+
WHERE generation_id = ?
|
|
1453
|
+
AND method IN ('turn/start', 'turn/interrupt')
|
|
1454
|
+
AND recovery_state NOT IN ('settled', 'cancelled')
|
|
1455
|
+
AND (? IS NULL OR turn_id = ?)
|
|
1456
|
+
`).run(ts, generationId, turnId, turnId);
|
|
1457
|
+
} else if (
|
|
1458
|
+
['stop-clean-accepted', 'background-clean-accepted'].includes(kind)
|
|
1459
|
+
) {
|
|
1460
|
+
db.prepare(`
|
|
1461
|
+
UPDATE codex_turn_attempts
|
|
1462
|
+
SET recovery_state = 'empty-registry-pending', updated_ts = ?
|
|
1463
|
+
WHERE generation_id = ?
|
|
1464
|
+
AND method = 'thread/backgroundTerminals/clean'
|
|
1465
|
+
AND recovery_state NOT IN ('settled', 'cancelled')
|
|
1466
|
+
`).run(ts, generationId);
|
|
1467
|
+
} else if (
|
|
1468
|
+
['stop-empty-registry-observed', 'background-empty-registry-observed']
|
|
1469
|
+
.includes(kind)
|
|
1470
|
+
) {
|
|
1471
|
+
db.prepare(`
|
|
1472
|
+
UPDATE codex_turn_attempts
|
|
1473
|
+
SET recovery_state = 'settled',
|
|
1474
|
+
settled_ts = ?,
|
|
1475
|
+
updated_ts = ?
|
|
1476
|
+
WHERE generation_id = ?
|
|
1477
|
+
AND method IN (
|
|
1478
|
+
'turn/interrupt',
|
|
1479
|
+
'thread/backgroundTerminals/clean'
|
|
1480
|
+
)
|
|
1481
|
+
AND recovery_state NOT IN ('settled', 'cancelled')
|
|
1482
|
+
`).run(ts, ts, generationId);
|
|
1483
|
+
if (kind === 'stop-empty-registry-observed') {
|
|
1484
|
+
db.prepare(`
|
|
1485
|
+
UPDATE codex_generations
|
|
1486
|
+
SET state = 'healthy-stopped',
|
|
1487
|
+
settled_ts = ?,
|
|
1488
|
+
updated_ts = ?
|
|
1489
|
+
WHERE generation_id = ?
|
|
1490
|
+
AND NOT EXISTS (
|
|
1491
|
+
SELECT 1
|
|
1492
|
+
FROM codex_turn_attempts
|
|
1493
|
+
WHERE generation_id = ?
|
|
1494
|
+
AND recovery_state NOT IN ('settled', 'cancelled')
|
|
1495
|
+
)
|
|
1496
|
+
`).run(ts, ts, generationId, generationId);
|
|
1497
|
+
}
|
|
1498
|
+
} else if (
|
|
1499
|
+
['telegram-delivery-settled', 'telegram-delivery-failed']
|
|
1500
|
+
.includes(kind)
|
|
1501
|
+
) {
|
|
1502
|
+
if (!attempt || ![
|
|
1503
|
+
'terminal-pending',
|
|
1504
|
+
'clean-pending',
|
|
1505
|
+
'empty-registry-pending',
|
|
1506
|
+
].includes(
|
|
1507
|
+
attempt.recovery_state,
|
|
1508
|
+
)) {
|
|
1509
|
+
throw codexError(
|
|
1510
|
+
'Codex delivery settlement requires a terminal attempt',
|
|
1511
|
+
'CODEX_CHECKPOINT_SEQUENCE_INVALID',
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
const deliveryFailed = kind === 'telegram-delivery-failed';
|
|
1515
|
+
const linkedState = deliveryFailed
|
|
1516
|
+
? 'failed'
|
|
1517
|
+
: attempt.terminal_status === 'interrupted'
|
|
1518
|
+
? 'interrupted'
|
|
1519
|
+
: attempt.terminal_status === 'failed'
|
|
1520
|
+
? 'failed'
|
|
1521
|
+
: 'settled';
|
|
1522
|
+
settleCodexAttemptConsumerOutcome({
|
|
1523
|
+
attempt,
|
|
1524
|
+
generationId,
|
|
1525
|
+
linkedState,
|
|
1526
|
+
attemptRecoveryState: 'settled',
|
|
1527
|
+
settleQueuedReservation: true,
|
|
1528
|
+
ts,
|
|
1529
|
+
});
|
|
1530
|
+
} else if (
|
|
1531
|
+
['active-start-cancelled', 'queued-send-cancelled'].includes(kind)
|
|
1532
|
+
) {
|
|
1533
|
+
if (attempt) {
|
|
1534
|
+
db.prepare(`
|
|
1535
|
+
UPDATE codex_turn_attempts
|
|
1536
|
+
SET recovery_state = 'cancelled',
|
|
1537
|
+
settled_ts = ?,
|
|
1538
|
+
updated_ts = ?
|
|
1539
|
+
WHERE attempt_id = ?
|
|
1540
|
+
`).run(ts, ts, attemptId);
|
|
1541
|
+
}
|
|
1542
|
+
} else if (kind === 'failed-ambiguous-entered') {
|
|
1543
|
+
if (attempt) {
|
|
1544
|
+
db.prepare(`
|
|
1545
|
+
UPDATE codex_turn_attempts
|
|
1546
|
+
SET recovery_state = 'ambiguous',
|
|
1547
|
+
ambiguous_ts = COALESCE(ambiguous_ts, ?),
|
|
1548
|
+
updated_ts = ?
|
|
1549
|
+
WHERE attempt_id = ?
|
|
1550
|
+
`).run(ts, ts, attemptId);
|
|
1551
|
+
db.prepare(`
|
|
1552
|
+
UPDATE codex_linked_inputs
|
|
1553
|
+
SET state = 'ambiguous', settled_ts = ?
|
|
1554
|
+
WHERE (
|
|
1555
|
+
attempt_id = ?
|
|
1556
|
+
OR target_attempt_id = ?
|
|
1557
|
+
)
|
|
1558
|
+
AND state = 'linked'
|
|
1559
|
+
`).run(ts, attemptId, attemptId);
|
|
1560
|
+
db.prepare(`
|
|
1561
|
+
UPDATE codex_dispatch_reservations
|
|
1562
|
+
SET state = 'ambiguous',
|
|
1563
|
+
updated_ts = ?,
|
|
1564
|
+
settled_ts = NULL
|
|
1565
|
+
WHERE (
|
|
1566
|
+
steer_attempt_id = ?
|
|
1567
|
+
OR target_attempt_id = ?
|
|
1568
|
+
)
|
|
1569
|
+
AND state IN (
|
|
1570
|
+
'reserved', 'steer-accepted', 'queue-authorized'
|
|
1571
|
+
)
|
|
1572
|
+
`).run(ts, attemptId, attemptId);
|
|
1573
|
+
db.prepare(`
|
|
1574
|
+
UPDATE messages
|
|
1575
|
+
SET handler_status = 'codex-ambiguous'
|
|
1576
|
+
WHERE direction = 'in'
|
|
1577
|
+
AND handler_status IN (
|
|
1578
|
+
'received', 'dispatched', 'processing', 'replay-pending'
|
|
1579
|
+
)
|
|
1580
|
+
AND EXISTS (
|
|
1581
|
+
SELECT 1
|
|
1582
|
+
FROM codex_dispatch_reservations reservation
|
|
1583
|
+
WHERE (
|
|
1584
|
+
reservation.steer_attempt_id = ?
|
|
1585
|
+
OR reservation.target_attempt_id = ?
|
|
1586
|
+
)
|
|
1587
|
+
AND reservation.state = 'ambiguous'
|
|
1588
|
+
AND reservation.bot_name = messages.bot_name
|
|
1589
|
+
AND reservation.telegram_chat_id = messages.chat_id
|
|
1590
|
+
AND reservation.telegram_message_id
|
|
1591
|
+
= CAST(messages.msg_id AS TEXT)
|
|
1592
|
+
)
|
|
1593
|
+
`).run(attemptId, attemptId);
|
|
1594
|
+
} else {
|
|
1595
|
+
markGenerationAttemptsAmbiguous(generationId, ts);
|
|
1596
|
+
}
|
|
1597
|
+
} else if (kind === 'containment-entered') {
|
|
1598
|
+
const reason = optionalCodexString(
|
|
1599
|
+
input?.reason,
|
|
1600
|
+
'containment reason',
|
|
1601
|
+
);
|
|
1602
|
+
db.prepare(`
|
|
1603
|
+
UPDATE codex_generations
|
|
1604
|
+
SET state = 'containment-failed',
|
|
1605
|
+
containment_reason = ?,
|
|
1606
|
+
updated_ts = ?
|
|
1607
|
+
WHERE generation_id = ?
|
|
1608
|
+
`).run(reason, ts, generationId);
|
|
1609
|
+
markGenerationAttemptsAmbiguous(generationId, ts);
|
|
1610
|
+
const lease = db.prepare(`
|
|
1611
|
+
SELECT generation_id
|
|
1612
|
+
FROM codex_daemon_lease
|
|
1613
|
+
WHERE singleton = 1
|
|
1614
|
+
`).get();
|
|
1615
|
+
if (!lease || lease.generation_id !== generationId) {
|
|
1616
|
+
throw codexError(
|
|
1617
|
+
'Codex containment checkpoint requires the owned daemon lease',
|
|
1618
|
+
'CODEX_LEASE_MISMATCH',
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
db.prepare(`
|
|
1622
|
+
UPDATE codex_daemon_lease
|
|
1623
|
+
SET status = 'quarantined',
|
|
1624
|
+
quarantine_reason = ?,
|
|
1625
|
+
updated_ts = ?
|
|
1626
|
+
WHERE singleton = 1
|
|
1627
|
+
`).run(reason, ts);
|
|
1628
|
+
} else if (kind === 'thread-initialized') {
|
|
1629
|
+
const initializing = db.prepare(`
|
|
1630
|
+
SELECT attempt_id, delivery_state, response_outcome
|
|
1631
|
+
FROM codex_turn_attempts
|
|
1632
|
+
WHERE generation_id = ?
|
|
1633
|
+
AND method IN ('thread/start', 'thread/resume')
|
|
1634
|
+
AND recovery_state NOT IN ('settled', 'cancelled')
|
|
1635
|
+
ORDER BY created_ts DESC
|
|
1636
|
+
LIMIT 1
|
|
1637
|
+
`).get(generationId);
|
|
1638
|
+
if (
|
|
1639
|
+
!initializing
|
|
1640
|
+
|| initializing.delivery_state !== 'response-observed'
|
|
1641
|
+
|| initializing.response_outcome !== 'result'
|
|
1642
|
+
) {
|
|
1643
|
+
throw codexError(
|
|
1644
|
+
'Codex thread initialization has no successful response predecessor',
|
|
1645
|
+
'CODEX_CHECKPOINT_SEQUENCE_INVALID',
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
db.prepare(`
|
|
1649
|
+
UPDATE codex_generations
|
|
1650
|
+
SET thread_id = COALESCE(?, thread_id), updated_ts = ?
|
|
1651
|
+
WHERE generation_id = ?
|
|
1652
|
+
`).run(threadId, ts, generationId);
|
|
1653
|
+
db.prepare(`
|
|
1654
|
+
UPDATE codex_turn_attempts
|
|
1655
|
+
SET thread_id = COALESCE(?, thread_id),
|
|
1656
|
+
recovery_state = 'settled',
|
|
1657
|
+
settled_ts = ?,
|
|
1658
|
+
updated_ts = ?
|
|
1659
|
+
WHERE attempt_id = (
|
|
1660
|
+
SELECT attempt_id
|
|
1661
|
+
FROM codex_turn_attempts
|
|
1662
|
+
WHERE generation_id = ?
|
|
1663
|
+
AND method IN ('thread/start', 'thread/resume')
|
|
1664
|
+
AND recovery_state NOT IN ('settled', 'cancelled')
|
|
1665
|
+
ORDER BY created_ts DESC
|
|
1666
|
+
LIMIT 1
|
|
1667
|
+
)
|
|
1668
|
+
`).run(threadId, ts, ts, generationId);
|
|
1669
|
+
} else if (kind === 'thread-status-changed') {
|
|
1670
|
+
requiredCodexString(input?.statusType, 'thread status type');
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
db.prepare(`
|
|
1674
|
+
INSERT INTO codex_attempt_checkpoints (
|
|
1675
|
+
generation_id, attempt_id, kind, thread_id, turn_id,
|
|
1676
|
+
request_id, item_id, detail_json, ts
|
|
1677
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1678
|
+
`).run(
|
|
1679
|
+
generationId,
|
|
1680
|
+
attemptId,
|
|
1681
|
+
kind,
|
|
1682
|
+
threadId,
|
|
1683
|
+
turnId,
|
|
1684
|
+
requestId,
|
|
1685
|
+
itemId,
|
|
1686
|
+
detailJson,
|
|
1687
|
+
ts,
|
|
1688
|
+
);
|
|
1689
|
+
return { changes: 1, attemptId, kind };
|
|
1690
|
+
})();
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
function recordCodexDeliveryCheckpoint(input) {
|
|
1694
|
+
const allowed = new Set(['checkpoint', 'retireGeneration']);
|
|
1695
|
+
assertExactKeys(
|
|
1696
|
+
input,
|
|
1697
|
+
allowed,
|
|
1698
|
+
'CODEX_DELIVERY_PAYLOAD_REJECTED',
|
|
1699
|
+
'delivery checkpoint',
|
|
1700
|
+
);
|
|
1701
|
+
const checkpoint = input?.checkpoint;
|
|
1702
|
+
if (
|
|
1703
|
+
!checkpoint
|
|
1704
|
+
|| ![
|
|
1705
|
+
'telegram-delivery-settled',
|
|
1706
|
+
'telegram-delivery-failed',
|
|
1707
|
+
].includes(checkpoint.kind)
|
|
1708
|
+
|| typeof input.retireGeneration !== 'boolean'
|
|
1709
|
+
) {
|
|
1710
|
+
throw codexError(
|
|
1711
|
+
'Codex delivery checkpoint payload is invalid',
|
|
1712
|
+
'CODEX_DELIVERY_PAYLOAD_REJECTED',
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
return db.transaction(() => {
|
|
1716
|
+
const result = recordCodexCheckpoint(checkpoint);
|
|
1717
|
+
if (!input.retireGeneration) {
|
|
1718
|
+
return { ...result, retired: false };
|
|
1719
|
+
}
|
|
1720
|
+
const generationId = requiredCodexString(
|
|
1721
|
+
checkpoint.generationId ?? checkpoint.generation_id,
|
|
1722
|
+
'generation ID',
|
|
1723
|
+
);
|
|
1724
|
+
const ts = requiredTimestamp(
|
|
1725
|
+
checkpoint.ts ?? Date.now(),
|
|
1726
|
+
'delivery checkpoint timestamp',
|
|
1727
|
+
);
|
|
1728
|
+
const retirement = retireCodexGeneration(generationId, ts, {
|
|
1729
|
+
onlyIfHealthyStopped: true,
|
|
1730
|
+
});
|
|
1731
|
+
if (!retirement.retired) {
|
|
1732
|
+
throw codexError(
|
|
1733
|
+
'Codex delivery could not prove a healthy stopped generation',
|
|
1734
|
+
'CODEX_RETIREMENT_UNVERIFIED',
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
return { ...result, retired: true };
|
|
1738
|
+
})();
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
function readCodexDispatchReservation(reservationId) {
|
|
1742
|
+
return db.prepare(`
|
|
1743
|
+
SELECT *
|
|
1744
|
+
FROM codex_dispatch_reservations
|
|
1745
|
+
WHERE reservation_id = ?
|
|
1746
|
+
`).get(reservationId);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
function recordInboundRuntimeSelection(input) {
|
|
1750
|
+
const allowed = new Set([
|
|
1751
|
+
'session_key',
|
|
1752
|
+
'bot_name',
|
|
1753
|
+
'telegram_chat_id',
|
|
1754
|
+
'telegram_message_id',
|
|
1755
|
+
'provider',
|
|
1756
|
+
'ts',
|
|
1757
|
+
]);
|
|
1758
|
+
assertExactKeys(
|
|
1759
|
+
input,
|
|
1760
|
+
allowed,
|
|
1761
|
+
'INBOUND_RUNTIME_SELECTION_PAYLOAD_REJECTED',
|
|
1762
|
+
'inbound runtime selection',
|
|
1763
|
+
);
|
|
1764
|
+
const sessionKey = requiredCodexString(input?.session_key, 'session key');
|
|
1765
|
+
const botName = requiredCodexString(input?.bot_name, 'bot name');
|
|
1766
|
+
const telegramChatId = requiredCodexString(
|
|
1767
|
+
input?.telegram_chat_id,
|
|
1768
|
+
'Telegram chat ID',
|
|
1769
|
+
);
|
|
1770
|
+
const telegramMessageId = requiredCodexString(
|
|
1771
|
+
input?.telegram_message_id,
|
|
1772
|
+
'Telegram message ID',
|
|
1773
|
+
);
|
|
1774
|
+
const provider = input?.provider;
|
|
1775
|
+
if (!['claude', 'codex'].includes(provider)) {
|
|
1776
|
+
throw codexError(
|
|
1777
|
+
'Inbound runtime provider is invalid',
|
|
1778
|
+
'INBOUND_RUNTIME_SELECTION_INVALID',
|
|
1779
|
+
);
|
|
1780
|
+
}
|
|
1781
|
+
const ts = requiredTimestamp(
|
|
1782
|
+
input?.ts ?? Date.now(),
|
|
1783
|
+
'inbound runtime selection timestamp',
|
|
1784
|
+
);
|
|
1785
|
+
|
|
1786
|
+
return db.transaction(() => {
|
|
1787
|
+
const existing = db.prepare(`
|
|
1788
|
+
SELECT *
|
|
1789
|
+
FROM inbound_runtime_selections
|
|
1790
|
+
WHERE bot_name = ?
|
|
1791
|
+
AND telegram_chat_id = ?
|
|
1792
|
+
AND telegram_message_id = ?
|
|
1793
|
+
`).get(botName, telegramChatId, telegramMessageId);
|
|
1794
|
+
if (existing) {
|
|
1795
|
+
if (
|
|
1796
|
+
existing.session_key === sessionKey
|
|
1797
|
+
&& existing.provider === provider
|
|
1798
|
+
) {
|
|
1799
|
+
return { changes: 0, selection: existing };
|
|
1800
|
+
}
|
|
1801
|
+
throw codexError(
|
|
1802
|
+
'Inbound runtime selection conflicts with durable identity',
|
|
1803
|
+
'INBOUND_RUNTIME_SELECTION_CONFLICT',
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
const inbound = db.prepare(`
|
|
1807
|
+
SELECT direction, bot_name
|
|
1808
|
+
FROM messages
|
|
1809
|
+
WHERE chat_id = ?
|
|
1810
|
+
AND CAST(msg_id AS TEXT) = ?
|
|
1811
|
+
`).get(telegramChatId, telegramMessageId);
|
|
1812
|
+
if (inbound?.direction !== 'in' || inbound.bot_name !== botName) {
|
|
1813
|
+
throw codexError(
|
|
1814
|
+
'Inbound runtime selection has no exact inbound message',
|
|
1815
|
+
'INBOUND_RUNTIME_SELECTION_INBOUND_MISMATCH',
|
|
1816
|
+
);
|
|
1817
|
+
}
|
|
1818
|
+
db.prepare(`
|
|
1819
|
+
INSERT INTO inbound_runtime_selections (
|
|
1820
|
+
bot_name, telegram_chat_id, telegram_message_id,
|
|
1821
|
+
session_key, provider, selected_ts
|
|
1822
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
1823
|
+
`).run(
|
|
1824
|
+
botName,
|
|
1825
|
+
telegramChatId,
|
|
1826
|
+
telegramMessageId,
|
|
1827
|
+
sessionKey,
|
|
1828
|
+
provider,
|
|
1829
|
+
ts,
|
|
1830
|
+
);
|
|
1831
|
+
return {
|
|
1832
|
+
changes: 1,
|
|
1833
|
+
selection: db.prepare(`
|
|
1834
|
+
SELECT *
|
|
1835
|
+
FROM inbound_runtime_selections
|
|
1836
|
+
WHERE bot_name = ?
|
|
1837
|
+
AND telegram_chat_id = ?
|
|
1838
|
+
AND telegram_message_id = ?
|
|
1839
|
+
`).get(botName, telegramChatId, telegramMessageId),
|
|
1840
|
+
};
|
|
1841
|
+
})();
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
function publicCodexAttempt(attempt) {
|
|
1845
|
+
if (!attempt) return null;
|
|
1846
|
+
return {
|
|
1847
|
+
attemptId: attempt.attempt_id,
|
|
1848
|
+
generationId: attempt.generation_id,
|
|
1849
|
+
method: attempt.method,
|
|
1850
|
+
deliveryState: attempt.delivery_state,
|
|
1851
|
+
recoveryState: attempt.recovery_state,
|
|
1852
|
+
turnId: attempt.turn_id,
|
|
1853
|
+
terminalStatus: attempt.terminal_status,
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
function publicRuntimeSelection(selection) {
|
|
1858
|
+
return {
|
|
1859
|
+
provider: selection.provider,
|
|
1860
|
+
sessionKey: selection.session_key,
|
|
1861
|
+
selectedTs: selection.selected_ts,
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
function getReplayProviderRecovery(input) {
|
|
1866
|
+
const allowed = new Set([
|
|
1867
|
+
'sessionKey',
|
|
1868
|
+
'botName',
|
|
1869
|
+
'telegramChatId',
|
|
1870
|
+
'telegramMessageId',
|
|
1871
|
+
]);
|
|
1872
|
+
assertExactKeys(
|
|
1873
|
+
input,
|
|
1874
|
+
allowed,
|
|
1875
|
+
'REPLAY_PROVIDER_RECOVERY_PAYLOAD_REJECTED',
|
|
1876
|
+
'replay provider recovery',
|
|
1877
|
+
);
|
|
1878
|
+
const sessionKey = requiredCodexString(input?.sessionKey, 'session key');
|
|
1879
|
+
const botName = requiredCodexString(input?.botName, 'bot name');
|
|
1880
|
+
const telegramChatId = requiredCodexString(
|
|
1881
|
+
input?.telegramChatId,
|
|
1882
|
+
'Telegram chat ID',
|
|
1883
|
+
);
|
|
1884
|
+
const telegramMessageId = requiredCodexString(
|
|
1885
|
+
input?.telegramMessageId,
|
|
1886
|
+
'Telegram message ID',
|
|
1887
|
+
);
|
|
1888
|
+
const unknown = (reason) => ({ provider: 'unknown', reason });
|
|
1889
|
+
const selection = db.prepare(`
|
|
1890
|
+
SELECT *
|
|
1891
|
+
FROM inbound_runtime_selections
|
|
1892
|
+
WHERE bot_name = ?
|
|
1893
|
+
AND telegram_chat_id = ?
|
|
1894
|
+
AND telegram_message_id = ?
|
|
1895
|
+
`).get(botName, telegramChatId, telegramMessageId);
|
|
1896
|
+
if (!selection) return unknown('selection-missing');
|
|
1897
|
+
if (selection.session_key !== sessionKey) {
|
|
1898
|
+
return unknown('selection-conflict');
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
const reservations = db.prepare(`
|
|
1902
|
+
SELECT *
|
|
1903
|
+
FROM codex_dispatch_reservations
|
|
1904
|
+
WHERE bot_name = ?
|
|
1905
|
+
AND telegram_chat_id = ?
|
|
1906
|
+
AND telegram_message_id = ?
|
|
1907
|
+
`).all(botName, telegramChatId, telegramMessageId);
|
|
1908
|
+
const attempts = db.prepare(`
|
|
1909
|
+
SELECT attempt.*
|
|
1910
|
+
FROM codex_turn_attempts attempt
|
|
1911
|
+
JOIN codex_generations generation
|
|
1912
|
+
ON generation.generation_id = attempt.generation_id
|
|
1913
|
+
WHERE generation.session_key = ?
|
|
1914
|
+
AND attempt.telegram_source_message_id = ?
|
|
1915
|
+
ORDER BY attempt.created_ts, attempt.attempt_id
|
|
1916
|
+
`).all(sessionKey, telegramMessageId);
|
|
1917
|
+
const linkedInputs = db.prepare(`
|
|
1918
|
+
SELECT linked.*
|
|
1919
|
+
FROM codex_linked_inputs linked
|
|
1920
|
+
JOIN codex_generations generation
|
|
1921
|
+
ON generation.generation_id = linked.generation_id
|
|
1922
|
+
WHERE generation.session_key = ?
|
|
1923
|
+
AND linked.telegram_chat_id = ?
|
|
1924
|
+
AND linked.telegram_message_id = ?
|
|
1925
|
+
ORDER BY linked.created_ts, linked.linked_input_id
|
|
1926
|
+
`).all(sessionKey, telegramChatId, telegramMessageId);
|
|
1927
|
+
const hasCodexEvidence = (
|
|
1928
|
+
reservations.length > 0
|
|
1929
|
+
|| attempts.length > 0
|
|
1930
|
+
|| linkedInputs.length > 0
|
|
1931
|
+
);
|
|
1932
|
+
if (selection.provider === 'claude') {
|
|
1933
|
+
return hasCodexEvidence
|
|
1934
|
+
? unknown('provider-evidence-conflict')
|
|
1935
|
+
: {
|
|
1936
|
+
provider: 'claude',
|
|
1937
|
+
kind: 'selected',
|
|
1938
|
+
selection: publicRuntimeSelection(selection),
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
if (
|
|
1942
|
+
reservations.length > 1
|
|
1943
|
+
|| attempts.length > 1
|
|
1944
|
+
|| linkedInputs.length > 1
|
|
1945
|
+
) {
|
|
1946
|
+
return unknown('codex-evidence-conflict');
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
const reservation = reservations[0] ?? null;
|
|
1950
|
+
const attempt = attempts[0] ?? null;
|
|
1951
|
+
const linkedInput = linkedInputs[0] ?? null;
|
|
1952
|
+
if (
|
|
1953
|
+
reservation
|
|
1954
|
+
&& (
|
|
1955
|
+
reservation.session_key !== sessionKey
|
|
1956
|
+
|| (
|
|
1957
|
+
attempt
|
|
1958
|
+
&& reservation.generation_id !== attempt.generation_id
|
|
1959
|
+
)
|
|
1960
|
+
)
|
|
1961
|
+
) {
|
|
1962
|
+
return unknown('codex-evidence-conflict');
|
|
1963
|
+
}
|
|
1964
|
+
if (
|
|
1965
|
+
attempt
|
|
1966
|
+
&& !['turn/start', 'turn/steer'].includes(attempt.method)
|
|
1967
|
+
) {
|
|
1968
|
+
return unknown('codex-evidence-conflict');
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
let kind = 'selection-only';
|
|
1972
|
+
let targetAttempt = null;
|
|
1973
|
+
if (linkedInput) {
|
|
1974
|
+
if (
|
|
1975
|
+
!attempt
|
|
1976
|
+
|| linkedInput.attempt_id !== attempt.attempt_id
|
|
1977
|
+
|| linkedInput.generation_id !== attempt.generation_id
|
|
1978
|
+
|| (
|
|
1979
|
+
reservation
|
|
1980
|
+
&& (
|
|
1981
|
+
reservation.reservation_id !== linkedInput.linked_input_id
|
|
1982
|
+
|| reservation.steer_attempt_id !== attempt.attempt_id
|
|
1983
|
+
|| reservation.target_attempt_id !== linkedInput.target_attempt_id
|
|
1984
|
+
)
|
|
1985
|
+
)
|
|
1986
|
+
) {
|
|
1987
|
+
return unknown('codex-evidence-conflict');
|
|
1988
|
+
}
|
|
1989
|
+
targetAttempt = db.prepare(`
|
|
1990
|
+
SELECT attempt.*
|
|
1991
|
+
FROM codex_turn_attempts attempt
|
|
1992
|
+
JOIN codex_generations generation
|
|
1993
|
+
ON generation.generation_id = attempt.generation_id
|
|
1994
|
+
WHERE attempt.attempt_id = ?
|
|
1995
|
+
AND attempt.generation_id = ?
|
|
1996
|
+
AND generation.session_key = ?
|
|
1997
|
+
`).get(
|
|
1998
|
+
linkedInput.target_attempt_id,
|
|
1999
|
+
linkedInput.generation_id,
|
|
2000
|
+
sessionKey,
|
|
2001
|
+
);
|
|
2002
|
+
if (
|
|
2003
|
+
!targetAttempt
|
|
2004
|
+
|| targetAttempt.method !== 'turn/start'
|
|
2005
|
+
|| targetAttempt.turn_id !== attempt.turn_id
|
|
2006
|
+
) {
|
|
2007
|
+
return unknown('codex-evidence-conflict');
|
|
2008
|
+
}
|
|
2009
|
+
kind = 'linked-input';
|
|
2010
|
+
} else if (reservation) {
|
|
2011
|
+
if (reservation.steer_attempt_id != null) {
|
|
2012
|
+
return unknown('codex-evidence-conflict');
|
|
2013
|
+
}
|
|
2014
|
+
if (
|
|
2015
|
+
reservation.target_attempt_id != null
|
|
2016
|
+
&& (
|
|
2017
|
+
!attempt
|
|
2018
|
+
|| reservation.target_attempt_id !== attempt.attempt_id
|
|
2019
|
+
|| attempt.method !== 'turn/start'
|
|
2020
|
+
)
|
|
2021
|
+
) {
|
|
2022
|
+
return unknown('codex-evidence-conflict');
|
|
2023
|
+
}
|
|
2024
|
+
if (reservation.target_attempt_id != null) {
|
|
2025
|
+
targetAttempt = attempt;
|
|
2026
|
+
}
|
|
2027
|
+
if (attempt && attempt.method !== 'turn/start') {
|
|
2028
|
+
return unknown('codex-evidence-conflict');
|
|
2029
|
+
}
|
|
2030
|
+
kind = 'dispatch-reservation';
|
|
2031
|
+
} else if (attempt) {
|
|
2032
|
+
if (attempt.method !== 'turn/start') {
|
|
2033
|
+
return unknown('codex-evidence-conflict');
|
|
2034
|
+
}
|
|
2035
|
+
kind = 'primary-turn';
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
return {
|
|
2039
|
+
provider: 'codex',
|
|
2040
|
+
kind,
|
|
2041
|
+
selection: publicRuntimeSelection(selection),
|
|
2042
|
+
reservation: reservation
|
|
2043
|
+
? {
|
|
2044
|
+
reservationId: reservation.reservation_id,
|
|
2045
|
+
generationId: reservation.generation_id,
|
|
2046
|
+
state: reservation.state,
|
|
2047
|
+
targetAttemptId: reservation.target_attempt_id,
|
|
2048
|
+
}
|
|
2049
|
+
: null,
|
|
2050
|
+
attempt: publicCodexAttempt(attempt),
|
|
2051
|
+
linkedInput: linkedInput
|
|
2052
|
+
? {
|
|
2053
|
+
linkedInputId: linkedInput.linked_input_id,
|
|
2054
|
+
state: linkedInput.state,
|
|
2055
|
+
attemptId: linkedInput.attempt_id,
|
|
2056
|
+
targetAttemptId: linkedInput.target_attempt_id,
|
|
2057
|
+
}
|
|
2058
|
+
: null,
|
|
2059
|
+
targetAttempt: publicCodexAttempt(targetAttempt),
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
function assertCodexDispatchReservationIdentity(
|
|
2064
|
+
reservation,
|
|
2065
|
+
generationId,
|
|
2066
|
+
) {
|
|
2067
|
+
if (!reservation) {
|
|
2068
|
+
throw codexError(
|
|
2069
|
+
'Codex dispatch reservation does not exist',
|
|
2070
|
+
'CODEX_DISPATCH_RESERVATION_NOT_FOUND',
|
|
2071
|
+
);
|
|
2072
|
+
}
|
|
2073
|
+
if (reservation.generation_id !== generationId) {
|
|
2074
|
+
throw codexError(
|
|
2075
|
+
'Codex dispatch reservation belongs to another generation',
|
|
2076
|
+
'CODEX_DISPATCH_RESERVATION_CONFLICT',
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
return reservation;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
function claimCodexDispatchReservation(input) {
|
|
2083
|
+
const allowed = new Set([
|
|
2084
|
+
'reservation_id',
|
|
2085
|
+
'generation_id',
|
|
2086
|
+
'session_key',
|
|
2087
|
+
'bot_name',
|
|
2088
|
+
'telegram_chat_id',
|
|
2089
|
+
'telegram_message_id',
|
|
2090
|
+
'stable_host_id',
|
|
2091
|
+
'boot_session_id',
|
|
2092
|
+
'ts',
|
|
2093
|
+
]);
|
|
2094
|
+
assertExactKeys(
|
|
2095
|
+
input,
|
|
2096
|
+
allowed,
|
|
2097
|
+
'CODEX_DISPATCH_RESERVATION_PAYLOAD_REJECTED',
|
|
2098
|
+
'dispatch reservation',
|
|
2099
|
+
);
|
|
2100
|
+
const reservationId = requiredCodexString(
|
|
2101
|
+
input?.reservation_id,
|
|
2102
|
+
'dispatch reservation ID',
|
|
2103
|
+
);
|
|
2104
|
+
const generationId = requiredCodexString(
|
|
2105
|
+
input?.generation_id,
|
|
2106
|
+
'generation ID',
|
|
2107
|
+
);
|
|
2108
|
+
const sessionKey = requiredCodexString(
|
|
2109
|
+
input?.session_key,
|
|
2110
|
+
'session key',
|
|
2111
|
+
);
|
|
2112
|
+
const botName = requiredCodexString(input?.bot_name, 'bot name');
|
|
2113
|
+
const telegramChatId = requiredCodexString(
|
|
2114
|
+
input?.telegram_chat_id,
|
|
2115
|
+
'Telegram chat ID',
|
|
2116
|
+
);
|
|
2117
|
+
const telegramMessageId = requiredCodexString(
|
|
2118
|
+
input?.telegram_message_id,
|
|
2119
|
+
'Telegram message ID',
|
|
2120
|
+
);
|
|
2121
|
+
const stableHostId = requiredCodexString(
|
|
2122
|
+
input?.stable_host_id,
|
|
2123
|
+
'stable host identity',
|
|
2124
|
+
);
|
|
2125
|
+
const bootSessionId = requiredCodexString(
|
|
2126
|
+
input?.boot_session_id,
|
|
2127
|
+
'boot-session identity',
|
|
2128
|
+
);
|
|
2129
|
+
const ts = requiredTimestamp(
|
|
2130
|
+
input?.ts ?? Date.now(),
|
|
2131
|
+
'dispatch reservation timestamp',
|
|
2132
|
+
);
|
|
2133
|
+
|
|
2134
|
+
return db.transaction(() => {
|
|
2135
|
+
const generation = assertCodexGenerationIdentity(
|
|
2136
|
+
generationId,
|
|
2137
|
+
stableHostId,
|
|
2138
|
+
bootSessionId,
|
|
2139
|
+
);
|
|
2140
|
+
if (generation.session_key !== sessionKey) {
|
|
2141
|
+
throw codexError(
|
|
2142
|
+
'Codex dispatch session does not own the generation',
|
|
2143
|
+
'CODEX_DISPATCH_SESSION_MISMATCH',
|
|
2144
|
+
);
|
|
2145
|
+
}
|
|
2146
|
+
const existingRows = db.prepare(`
|
|
2147
|
+
SELECT *
|
|
2148
|
+
FROM codex_dispatch_reservations
|
|
2149
|
+
WHERE reservation_id = ?
|
|
2150
|
+
OR (
|
|
2151
|
+
bot_name = ?
|
|
2152
|
+
AND
|
|
2153
|
+
telegram_chat_id = ?
|
|
2154
|
+
AND telegram_message_id = ?
|
|
2155
|
+
)
|
|
2156
|
+
ORDER BY reservation_id
|
|
2157
|
+
`).all(reservationId, botName, telegramChatId, telegramMessageId);
|
|
2158
|
+
if (existingRows.length > 0) {
|
|
2159
|
+
const existing = existingRows[0];
|
|
2160
|
+
if (
|
|
2161
|
+
existingRows.length === 1
|
|
2162
|
+
&& existing.reservation_id === reservationId
|
|
2163
|
+
&& existing.generation_id === generationId
|
|
2164
|
+
&& existing.session_key === sessionKey
|
|
2165
|
+
&& existing.bot_name === botName
|
|
2166
|
+
&& existing.telegram_chat_id === telegramChatId
|
|
2167
|
+
&& existing.telegram_message_id === telegramMessageId
|
|
2168
|
+
) {
|
|
2169
|
+
const handlerStatus = existing.state === 'settled'
|
|
2170
|
+
? 'replied'
|
|
2171
|
+
: ['failed', 'interrupted', 'cancelled'].includes(existing.state)
|
|
2172
|
+
? 'failed'
|
|
2173
|
+
: existing.state === 'ambiguous'
|
|
2174
|
+
? 'codex-ambiguous'
|
|
2175
|
+
: 'dispatched';
|
|
2176
|
+
const inbound = db.prepare(`
|
|
2177
|
+
UPDATE messages
|
|
2178
|
+
SET handler_status = ?
|
|
2179
|
+
WHERE direction = 'in'
|
|
2180
|
+
AND bot_name = ?
|
|
2181
|
+
AND chat_id = ?
|
|
2182
|
+
AND CAST(msg_id AS TEXT) = ?
|
|
2183
|
+
`).run(
|
|
2184
|
+
handlerStatus,
|
|
2185
|
+
existing.bot_name,
|
|
2186
|
+
existing.telegram_chat_id,
|
|
2187
|
+
existing.telegram_message_id,
|
|
2188
|
+
);
|
|
2189
|
+
if (inbound.changes !== 1) {
|
|
2190
|
+
throw codexError(
|
|
2191
|
+
'Codex duplicate reservation has no exact inbound message',
|
|
2192
|
+
'CODEX_DISPATCH_INBOUND_MISMATCH',
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
return { claimed: false, reservation: existing };
|
|
2196
|
+
}
|
|
2197
|
+
throw codexError(
|
|
2198
|
+
'Codex dispatch reservation identity was reused',
|
|
2199
|
+
'CODEX_DISPATCH_RESERVATION_CONFLICT',
|
|
2200
|
+
);
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
assertCodexCheckpointOwner(generation, 'request-write-attempted');
|
|
2204
|
+
const inbound = db.prepare(`
|
|
2205
|
+
SELECT direction, bot_name
|
|
2206
|
+
FROM messages
|
|
2207
|
+
WHERE chat_id = ?
|
|
2208
|
+
AND CAST(msg_id AS TEXT) = ?
|
|
2209
|
+
`).get(telegramChatId, telegramMessageId);
|
|
2210
|
+
if (
|
|
2211
|
+
inbound?.direction !== 'in'
|
|
2212
|
+
|| inbound.bot_name !== botName
|
|
2213
|
+
) {
|
|
2214
|
+
throw codexError(
|
|
2215
|
+
'Codex dispatch reservation has no exact inbound message',
|
|
2216
|
+
'CODEX_DISPATCH_INBOUND_MISMATCH',
|
|
2217
|
+
);
|
|
2218
|
+
}
|
|
2219
|
+
db.prepare(`
|
|
2220
|
+
INSERT INTO codex_dispatch_reservations (
|
|
2221
|
+
reservation_id, generation_id, session_key,
|
|
2222
|
+
bot_name, telegram_chat_id, telegram_message_id, state,
|
|
2223
|
+
created_ts, updated_ts
|
|
2224
|
+
) VALUES (?, ?, ?, ?, ?, ?, 'reserved', ?, ?)
|
|
2225
|
+
`).run(
|
|
2226
|
+
reservationId,
|
|
2227
|
+
generationId,
|
|
2228
|
+
sessionKey,
|
|
2229
|
+
botName,
|
|
2230
|
+
telegramChatId,
|
|
2231
|
+
telegramMessageId,
|
|
2232
|
+
ts,
|
|
2233
|
+
ts,
|
|
2234
|
+
);
|
|
2235
|
+
return {
|
|
2236
|
+
claimed: true,
|
|
2237
|
+
reservation: readCodexDispatchReservation(reservationId),
|
|
2238
|
+
};
|
|
2239
|
+
})();
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
function hasCodexTurnCheckpoint(generationId, attemptId, kind, turnId) {
|
|
2243
|
+
return db.prepare(`
|
|
2244
|
+
SELECT 1
|
|
2245
|
+
FROM codex_attempt_checkpoints
|
|
2246
|
+
WHERE generation_id = ?
|
|
2247
|
+
AND attempt_id = ?
|
|
2248
|
+
AND kind = ?
|
|
2249
|
+
AND turn_id = ?
|
|
2250
|
+
LIMIT 1
|
|
2251
|
+
`).get(generationId, attemptId, kind, turnId) != null;
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
function finalizeCodexAcceptedSteer(input) {
|
|
2255
|
+
const allowed = new Set([
|
|
2256
|
+
'reservation_id',
|
|
2257
|
+
'generation_id',
|
|
2258
|
+
'steer_attempt_id',
|
|
2259
|
+
'target_attempt_id',
|
|
2260
|
+
'stable_host_id',
|
|
2261
|
+
'boot_session_id',
|
|
2262
|
+
'ts',
|
|
2263
|
+
]);
|
|
2264
|
+
assertExactKeys(
|
|
2265
|
+
input,
|
|
2266
|
+
allowed,
|
|
2267
|
+
'CODEX_DISPATCH_RESERVATION_PAYLOAD_REJECTED',
|
|
2268
|
+
'accepted steer',
|
|
2269
|
+
);
|
|
2270
|
+
const reservationId = requiredCodexString(
|
|
2271
|
+
input?.reservation_id,
|
|
2272
|
+
'dispatch reservation ID',
|
|
2273
|
+
);
|
|
2274
|
+
const generationId = requiredCodexString(
|
|
2275
|
+
input?.generation_id,
|
|
2276
|
+
'generation ID',
|
|
2277
|
+
);
|
|
2278
|
+
const steerAttemptId = requiredCodexString(
|
|
2279
|
+
input?.steer_attempt_id,
|
|
2280
|
+
'steer attempt ID',
|
|
2281
|
+
);
|
|
2282
|
+
const targetAttemptId = requiredCodexString(
|
|
2283
|
+
input?.target_attempt_id,
|
|
2284
|
+
'target attempt ID',
|
|
2285
|
+
);
|
|
2286
|
+
const stableHostId = requiredCodexString(
|
|
2287
|
+
input?.stable_host_id,
|
|
2288
|
+
'stable host identity',
|
|
2289
|
+
);
|
|
2290
|
+
const bootSessionId = requiredCodexString(
|
|
2291
|
+
input?.boot_session_id,
|
|
2292
|
+
'boot-session identity',
|
|
2293
|
+
);
|
|
2294
|
+
const ts = requiredTimestamp(
|
|
2295
|
+
input?.ts ?? Date.now(),
|
|
2296
|
+
'accepted steer timestamp',
|
|
2297
|
+
);
|
|
2298
|
+
|
|
2299
|
+
return db.transaction(() => {
|
|
2300
|
+
const generation = assertCodexGenerationIdentity(
|
|
2301
|
+
generationId,
|
|
2302
|
+
stableHostId,
|
|
2303
|
+
bootSessionId,
|
|
2304
|
+
);
|
|
2305
|
+
let reservation = assertCodexDispatchReservationIdentity(
|
|
2306
|
+
readCodexDispatchReservation(reservationId),
|
|
2307
|
+
generationId,
|
|
2308
|
+
);
|
|
2309
|
+
if ([
|
|
2310
|
+
'steer-accepted',
|
|
2311
|
+
'settled',
|
|
2312
|
+
'failed',
|
|
2313
|
+
'interrupted',
|
|
2314
|
+
].includes(reservation.state)) {
|
|
2315
|
+
const linked = db.prepare(`
|
|
2316
|
+
SELECT generation_id, attempt_id, target_attempt_id, state,
|
|
2317
|
+
telegram_chat_id, telegram_message_id
|
|
2318
|
+
FROM codex_linked_inputs
|
|
2319
|
+
WHERE linked_input_id = ?
|
|
2320
|
+
`).get(reservationId);
|
|
2321
|
+
if (
|
|
2322
|
+
reservation.steer_attempt_id === steerAttemptId
|
|
2323
|
+
&& reservation.target_attempt_id === targetAttemptId
|
|
2324
|
+
&& linked?.generation_id === generationId
|
|
2325
|
+
&& linked.attempt_id === steerAttemptId
|
|
2326
|
+
&& linked.target_attempt_id === targetAttemptId
|
|
2327
|
+
&& reservation.bot_name != null
|
|
2328
|
+
&& linked.telegram_chat_id === reservation.telegram_chat_id
|
|
2329
|
+
&& linked.telegram_message_id === reservation.telegram_message_id
|
|
2330
|
+
&& (
|
|
2331
|
+
(
|
|
2332
|
+
reservation.state === 'steer-accepted'
|
|
2333
|
+
&& linked.state === 'linked'
|
|
2334
|
+
)
|
|
2335
|
+
|| linked.state === reservation.state
|
|
2336
|
+
)
|
|
2337
|
+
) {
|
|
2338
|
+
return { changes: 0, reservation };
|
|
2339
|
+
}
|
|
2340
|
+
throw codexError(
|
|
2341
|
+
'Codex accepted steer reservation metadata conflicts',
|
|
2342
|
+
'CODEX_DISPATCH_RESERVATION_CONFLICT',
|
|
2343
|
+
);
|
|
2344
|
+
}
|
|
2345
|
+
if (reservation.state !== 'reserved') {
|
|
2346
|
+
throw codexError(
|
|
2347
|
+
'Codex dispatch reservation cannot accept a steer in this state',
|
|
2348
|
+
'CODEX_DISPATCH_TRANSITION_INVALID',
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
2351
|
+
assertCodexCheckpointOwner(generation, 'turn-steer-accepted');
|
|
2352
|
+
|
|
2353
|
+
const steer = requireCodexAttempt(steerAttemptId, generationId);
|
|
2354
|
+
const target = requireCodexAttempt(targetAttemptId, generationId);
|
|
2355
|
+
const exactTurn = (
|
|
2356
|
+
steer.turn_id != null
|
|
2357
|
+
&& target.turn_id === steer.turn_id
|
|
2358
|
+
&& steer.thread_id != null
|
|
2359
|
+
&& target.thread_id === steer.thread_id
|
|
2360
|
+
);
|
|
2361
|
+
const steerDurable = (
|
|
2362
|
+
steer.method === 'turn/steer'
|
|
2363
|
+
&& steer.delivery_state === 'response-observed'
|
|
2364
|
+
&& steer.response_outcome === 'result'
|
|
2365
|
+
&& steer.recovery_state === 'active'
|
|
2366
|
+
&& hasCodexTurnCheckpoint(
|
|
2367
|
+
generationId,
|
|
2368
|
+
steerAttemptId,
|
|
2369
|
+
'turn-steer-accepted',
|
|
2370
|
+
steer.turn_id,
|
|
2371
|
+
)
|
|
2372
|
+
);
|
|
2373
|
+
const targetDurable = (
|
|
2374
|
+
target.method === 'turn/start'
|
|
2375
|
+
&& target.delivery_state === 'response-observed'
|
|
2376
|
+
&& target.response_outcome === 'result'
|
|
2377
|
+
&& [
|
|
2378
|
+
'active',
|
|
2379
|
+
'terminal-pending',
|
|
2380
|
+
'clean-pending',
|
|
2381
|
+
'empty-registry-pending',
|
|
2382
|
+
'settled',
|
|
2383
|
+
].includes(target.recovery_state)
|
|
2384
|
+
&& hasCodexTurnCheckpoint(
|
|
2385
|
+
generationId,
|
|
2386
|
+
targetAttemptId,
|
|
2387
|
+
'turn-accepted',
|
|
2388
|
+
target.turn_id,
|
|
2389
|
+
)
|
|
2390
|
+
);
|
|
2391
|
+
if (!exactTurn || !steerDurable || !targetDurable) {
|
|
2392
|
+
throw codexError(
|
|
2393
|
+
'Codex accepted steer is not durably bound to its target turn',
|
|
2394
|
+
'CODEX_DISPATCH_STEER_NOT_DURABLE',
|
|
2395
|
+
);
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
const targetAlreadyDelivered = (
|
|
2399
|
+
target.recovery_state === 'settled'
|
|
2400
|
+
&& ['completed', 'interrupted', 'failed'].includes(
|
|
2401
|
+
target.terminal_status,
|
|
2402
|
+
)
|
|
2403
|
+
&& hasCodexTurnCheckpoint(
|
|
2404
|
+
generationId,
|
|
2405
|
+
targetAttemptId,
|
|
2406
|
+
'telegram-delivery-settled',
|
|
2407
|
+
target.turn_id,
|
|
2408
|
+
)
|
|
2409
|
+
);
|
|
2410
|
+
if (target.recovery_state === 'settled' && !targetAlreadyDelivered) {
|
|
2411
|
+
throw codexError(
|
|
2412
|
+
'Codex settled target lacks exact Telegram delivery proof',
|
|
2413
|
+
'CODEX_DISPATCH_STEER_NOT_DURABLE',
|
|
2414
|
+
);
|
|
2415
|
+
}
|
|
2416
|
+
const linkedState = targetAlreadyDelivered
|
|
2417
|
+
? target.terminal_status === 'completed'
|
|
2418
|
+
? 'settled'
|
|
2419
|
+
: target.terminal_status
|
|
2420
|
+
: 'linked';
|
|
2421
|
+
const linkedSettledTs = targetAlreadyDelivered
|
|
2422
|
+
? target.settled_ts ?? ts
|
|
2423
|
+
: null;
|
|
2424
|
+
db.prepare(`
|
|
2425
|
+
INSERT INTO codex_linked_inputs (
|
|
2426
|
+
linked_input_id, generation_id, attempt_id, target_attempt_id,
|
|
2427
|
+
telegram_chat_id, telegram_message_id, state, created_ts, settled_ts
|
|
2428
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2429
|
+
`).run(
|
|
2430
|
+
reservationId,
|
|
2431
|
+
generationId,
|
|
2432
|
+
steerAttemptId,
|
|
2433
|
+
targetAttemptId,
|
|
2434
|
+
reservation.telegram_chat_id,
|
|
2435
|
+
reservation.telegram_message_id,
|
|
2436
|
+
linkedState,
|
|
2437
|
+
ts,
|
|
2438
|
+
linkedSettledTs,
|
|
2439
|
+
);
|
|
2440
|
+
if (targetAlreadyDelivered) {
|
|
2441
|
+
const handlerStatus = linkedState === 'settled' ? 'replied' : 'failed';
|
|
2442
|
+
const inbound = db.prepare(`
|
|
2443
|
+
UPDATE messages
|
|
2444
|
+
SET handler_status = ?
|
|
2445
|
+
WHERE direction = 'in'
|
|
2446
|
+
AND bot_name = ?
|
|
2447
|
+
AND chat_id = ?
|
|
2448
|
+
AND CAST(msg_id AS TEXT) = ?
|
|
2449
|
+
AND handler_status IN (
|
|
2450
|
+
'received', 'dispatched', 'processing', 'replay-pending'
|
|
2451
|
+
)
|
|
2452
|
+
`).run(
|
|
2453
|
+
handlerStatus,
|
|
2454
|
+
reservation.bot_name,
|
|
2455
|
+
reservation.telegram_chat_id,
|
|
2456
|
+
reservation.telegram_message_id,
|
|
2457
|
+
);
|
|
2458
|
+
if (inbound.changes !== 1) {
|
|
2459
|
+
throw codexError(
|
|
2460
|
+
'Codex late accepted steer has no live exact inbound message',
|
|
2461
|
+
'CODEX_LINKED_INPUT_MESSAGE_NOT_FOUND',
|
|
2462
|
+
);
|
|
2463
|
+
}
|
|
2464
|
+
db.prepare(`
|
|
2465
|
+
UPDATE codex_turn_attempts
|
|
2466
|
+
SET recovery_state = 'settled',
|
|
2467
|
+
settled_ts = ?,
|
|
2468
|
+
updated_ts = ?
|
|
2469
|
+
WHERE attempt_id = ?
|
|
2470
|
+
AND recovery_state = 'active'
|
|
2471
|
+
`).run(linkedSettledTs, ts, steerAttemptId);
|
|
2472
|
+
}
|
|
2473
|
+
db.prepare(`
|
|
2474
|
+
UPDATE codex_dispatch_reservations
|
|
2475
|
+
SET state = ?,
|
|
2476
|
+
steer_attempt_id = ?,
|
|
2477
|
+
target_attempt_id = ?,
|
|
2478
|
+
updated_ts = ?,
|
|
2479
|
+
settled_ts = ?
|
|
2480
|
+
WHERE reservation_id = ?
|
|
2481
|
+
AND state = 'reserved'
|
|
2482
|
+
`).run(
|
|
2483
|
+
targetAlreadyDelivered ? linkedState : 'steer-accepted',
|
|
2484
|
+
steerAttemptId,
|
|
2485
|
+
targetAttemptId,
|
|
2486
|
+
ts,
|
|
2487
|
+
linkedSettledTs,
|
|
2488
|
+
reservationId,
|
|
2489
|
+
);
|
|
2490
|
+
reservation = readCodexDispatchReservation(reservationId);
|
|
2491
|
+
return { changes: 1, reservation };
|
|
2492
|
+
})();
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
function markCodexDispatchDisposition(input) {
|
|
2496
|
+
const allowed = new Set([
|
|
2497
|
+
'reservation_id',
|
|
2498
|
+
'generation_id',
|
|
2499
|
+
'disposition',
|
|
2500
|
+
'stable_host_id',
|
|
2501
|
+
'boot_session_id',
|
|
2502
|
+
'ts',
|
|
2503
|
+
]);
|
|
2504
|
+
assertExactKeys(
|
|
2505
|
+
input,
|
|
2506
|
+
allowed,
|
|
2507
|
+
'CODEX_DISPATCH_RESERVATION_PAYLOAD_REJECTED',
|
|
2508
|
+
'dispatch disposition',
|
|
2509
|
+
);
|
|
2510
|
+
const reservationId = requiredCodexString(
|
|
2511
|
+
input?.reservation_id,
|
|
2512
|
+
'dispatch reservation ID',
|
|
2513
|
+
);
|
|
2514
|
+
const generationId = requiredCodexString(
|
|
2515
|
+
input?.generation_id,
|
|
2516
|
+
'generation ID',
|
|
2517
|
+
);
|
|
2518
|
+
const disposition = input?.disposition;
|
|
2519
|
+
if (!['queue-authorized', 'ambiguous', 'cancelled'].includes(disposition)) {
|
|
2520
|
+
throw codexError(
|
|
2521
|
+
'Codex dispatch disposition is invalid',
|
|
2522
|
+
'CODEX_DISPATCH_DISPOSITION_INVALID',
|
|
2523
|
+
);
|
|
2524
|
+
}
|
|
2525
|
+
const stableHostId = requiredCodexString(
|
|
2526
|
+
input?.stable_host_id,
|
|
2527
|
+
'stable host identity',
|
|
2528
|
+
);
|
|
2529
|
+
const bootSessionId = requiredCodexString(
|
|
2530
|
+
input?.boot_session_id,
|
|
2531
|
+
'boot-session identity',
|
|
2532
|
+
);
|
|
2533
|
+
const ts = requiredTimestamp(
|
|
2534
|
+
input?.ts ?? Date.now(),
|
|
2535
|
+
'dispatch disposition timestamp',
|
|
2536
|
+
);
|
|
2537
|
+
|
|
2538
|
+
return db.transaction(() => {
|
|
2539
|
+
const generation = assertCodexGenerationIdentity(
|
|
2540
|
+
generationId,
|
|
2541
|
+
stableHostId,
|
|
2542
|
+
bootSessionId,
|
|
2543
|
+
);
|
|
2544
|
+
let reservation = assertCodexDispatchReservationIdentity(
|
|
2545
|
+
readCodexDispatchReservation(reservationId),
|
|
2546
|
+
generationId,
|
|
2547
|
+
);
|
|
2548
|
+
if (reservation.state === disposition) {
|
|
2549
|
+
return { changes: 0, reservation };
|
|
2550
|
+
}
|
|
2551
|
+
const legal = (
|
|
2552
|
+
reservation.state === 'reserved'
|
|
2553
|
+
|| (
|
|
2554
|
+
reservation.state === 'queue-authorized'
|
|
2555
|
+
&& ['ambiguous', 'cancelled'].includes(disposition)
|
|
2556
|
+
)
|
|
2557
|
+
);
|
|
2558
|
+
if (!legal) {
|
|
2559
|
+
throw codexError(
|
|
2560
|
+
'Codex dispatch disposition transition is invalid',
|
|
2561
|
+
'CODEX_DISPATCH_TRANSITION_INVALID',
|
|
2562
|
+
);
|
|
2563
|
+
}
|
|
2564
|
+
assertCodexCheckpointOwner(generation, 'request-write-attempted');
|
|
2565
|
+
db.prepare(`
|
|
2566
|
+
UPDATE codex_dispatch_reservations
|
|
2567
|
+
SET state = ?,
|
|
2568
|
+
updated_ts = ?,
|
|
2569
|
+
settled_ts = CASE WHEN ? = 'cancelled' THEN ? ELSE NULL END
|
|
2570
|
+
WHERE reservation_id = ?
|
|
2571
|
+
`).run(disposition, ts, disposition, ts, reservationId);
|
|
2572
|
+
reservation = readCodexDispatchReservation(reservationId);
|
|
2573
|
+
if (['ambiguous', 'cancelled'].includes(disposition)) {
|
|
2574
|
+
const handlerStatus = disposition === 'ambiguous'
|
|
2575
|
+
? 'codex-ambiguous'
|
|
2576
|
+
: 'failed';
|
|
2577
|
+
const inbound = db.prepare(`
|
|
2578
|
+
UPDATE messages
|
|
2579
|
+
SET handler_status = ?
|
|
2580
|
+
WHERE direction = 'in'
|
|
2581
|
+
AND bot_name = ?
|
|
2582
|
+
AND chat_id = ?
|
|
2583
|
+
AND CAST(msg_id AS TEXT) = ?
|
|
2584
|
+
AND handler_status IN (
|
|
2585
|
+
'received', 'dispatched', 'processing', 'replay-pending'
|
|
2586
|
+
)
|
|
2587
|
+
`).run(
|
|
2588
|
+
handlerStatus,
|
|
2589
|
+
reservation.bot_name,
|
|
2590
|
+
reservation.telegram_chat_id,
|
|
2591
|
+
reservation.telegram_message_id,
|
|
2592
|
+
);
|
|
2593
|
+
if (inbound.changes !== 1) {
|
|
2594
|
+
throw codexError(
|
|
2595
|
+
'Codex dispatch disposition has no live exact inbound message',
|
|
2596
|
+
'CODEX_DISPATCH_INBOUND_MISMATCH',
|
|
2597
|
+
);
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
return { changes: 1, reservation };
|
|
2601
|
+
})();
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
function settleCodexQueuedDispatch(input) {
|
|
2605
|
+
const allowed = new Set([
|
|
2606
|
+
'attempt_id',
|
|
2607
|
+
'generation_id',
|
|
2608
|
+
'session_key',
|
|
2609
|
+
'bot_name',
|
|
2610
|
+
'telegram_chat_id',
|
|
2611
|
+
'telegram_message_id',
|
|
2612
|
+
'stable_host_id',
|
|
2613
|
+
'boot_session_id',
|
|
2614
|
+
'ts',
|
|
2615
|
+
]);
|
|
2616
|
+
assertExactKeys(
|
|
2617
|
+
input,
|
|
2618
|
+
allowed,
|
|
2619
|
+
'CODEX_QUEUE_SETTLEMENT_PAYLOAD_REJECTED',
|
|
2620
|
+
'queued dispatch settlement',
|
|
2621
|
+
);
|
|
2622
|
+
const attemptId = requiredCodexString(
|
|
2623
|
+
input?.attempt_id,
|
|
2624
|
+
'queued turn attempt ID',
|
|
2625
|
+
);
|
|
2626
|
+
const generationId = requiredCodexString(
|
|
2627
|
+
input?.generation_id,
|
|
2628
|
+
'generation ID',
|
|
2629
|
+
);
|
|
2630
|
+
const sessionKey = requiredCodexString(input?.session_key, 'session key');
|
|
2631
|
+
const botName = requiredCodexString(input?.bot_name, 'bot name');
|
|
2632
|
+
const telegramChatId = requiredCodexString(
|
|
2633
|
+
input?.telegram_chat_id,
|
|
2634
|
+
'Telegram chat ID',
|
|
2635
|
+
);
|
|
2636
|
+
const telegramMessageId = requiredCodexString(
|
|
2637
|
+
input?.telegram_message_id,
|
|
2638
|
+
'Telegram message ID',
|
|
2639
|
+
);
|
|
2640
|
+
const stableHostId = requiredCodexString(
|
|
2641
|
+
input?.stable_host_id,
|
|
2642
|
+
'stable host identity',
|
|
2643
|
+
);
|
|
2644
|
+
const bootSessionId = requiredCodexString(
|
|
2645
|
+
input?.boot_session_id,
|
|
2646
|
+
'boot-session identity',
|
|
2647
|
+
);
|
|
2648
|
+
const ts = requiredTimestamp(
|
|
2649
|
+
input?.ts ?? Date.now(),
|
|
2650
|
+
'queued dispatch settlement timestamp',
|
|
2651
|
+
);
|
|
2652
|
+
|
|
2653
|
+
return db.transaction(() => {
|
|
2654
|
+
const generation = assertCodexGenerationIdentity(
|
|
2655
|
+
generationId,
|
|
2656
|
+
stableHostId,
|
|
2657
|
+
bootSessionId,
|
|
2658
|
+
);
|
|
2659
|
+
if (generation.session_key !== sessionKey) {
|
|
2660
|
+
throw codexError(
|
|
2661
|
+
'Codex queued turn session does not own its generation',
|
|
2662
|
+
'CODEX_QUEUE_SETTLEMENT_CONFLICT',
|
|
2663
|
+
);
|
|
2664
|
+
}
|
|
2665
|
+
const attempt = requireCodexAttempt(attemptId, generationId);
|
|
2666
|
+
const durableCompleted = (
|
|
2667
|
+
attempt.session_key === sessionKey
|
|
2668
|
+
&& attempt.method === 'turn/start'
|
|
2669
|
+
&& attempt.telegram_source_message_id === telegramMessageId
|
|
2670
|
+
&& attempt.delivery_state === 'response-observed'
|
|
2671
|
+
&& attempt.response_outcome === 'result'
|
|
2672
|
+
&& attempt.recovery_state === 'settled'
|
|
2673
|
+
&& attempt.terminal_status === 'completed'
|
|
2674
|
+
&& attempt.turn_id != null
|
|
2675
|
+
&& hasCodexTurnCheckpoint(
|
|
2676
|
+
generationId,
|
|
2677
|
+
attemptId,
|
|
2678
|
+
'turn-accepted',
|
|
2679
|
+
attempt.turn_id,
|
|
2680
|
+
)
|
|
2681
|
+
&& hasCodexTurnCheckpoint(
|
|
2682
|
+
generationId,
|
|
2683
|
+
attemptId,
|
|
2684
|
+
'turn-terminal',
|
|
2685
|
+
attempt.turn_id,
|
|
2686
|
+
)
|
|
2687
|
+
&& hasCodexTurnCheckpoint(
|
|
2688
|
+
generationId,
|
|
2689
|
+
attemptId,
|
|
2690
|
+
'telegram-delivery-settled',
|
|
2691
|
+
attempt.turn_id,
|
|
2692
|
+
)
|
|
2693
|
+
);
|
|
2694
|
+
if (!durableCompleted) {
|
|
2695
|
+
throw codexError(
|
|
2696
|
+
'Codex queued turn lacks exact completed delivery proof',
|
|
2697
|
+
'CODEX_QUEUE_SETTLEMENT_NOT_DURABLE',
|
|
2698
|
+
);
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
const candidates = db.prepare(`
|
|
2702
|
+
SELECT *
|
|
2703
|
+
FROM codex_dispatch_reservations
|
|
2704
|
+
WHERE (
|
|
2705
|
+
bot_name = ?
|
|
2706
|
+
AND telegram_chat_id = ?
|
|
2707
|
+
AND telegram_message_id = ?
|
|
2708
|
+
)
|
|
2709
|
+
OR (
|
|
2710
|
+
generation_id = ?
|
|
2711
|
+
AND session_key = ?
|
|
2712
|
+
AND telegram_message_id = ?
|
|
2713
|
+
)
|
|
2714
|
+
ORDER BY reservation_id
|
|
2715
|
+
`).all(
|
|
2716
|
+
botName,
|
|
2717
|
+
telegramChatId,
|
|
2718
|
+
telegramMessageId,
|
|
2719
|
+
generationId,
|
|
2720
|
+
sessionKey,
|
|
2721
|
+
telegramMessageId,
|
|
2722
|
+
);
|
|
2723
|
+
if (candidates.length === 0) {
|
|
2724
|
+
return {
|
|
2725
|
+
changes: 0,
|
|
2726
|
+
outcome: 'no-reservation',
|
|
2727
|
+
reservation: null,
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
if (candidates.length !== 1) {
|
|
2731
|
+
throw codexError(
|
|
2732
|
+
'Codex queued turn matches conflicting dispatch reservations',
|
|
2733
|
+
'CODEX_QUEUE_SETTLEMENT_CONFLICT',
|
|
2734
|
+
);
|
|
2735
|
+
}
|
|
2736
|
+
let reservation = candidates[0];
|
|
2737
|
+
const exactReservation = (
|
|
2738
|
+
reservation.generation_id === generationId
|
|
2739
|
+
&& reservation.session_key === sessionKey
|
|
2740
|
+
&& reservation.bot_name === botName
|
|
2741
|
+
&& reservation.telegram_chat_id === telegramChatId
|
|
2742
|
+
&& reservation.telegram_message_id === telegramMessageId
|
|
2743
|
+
);
|
|
2744
|
+
if (!exactReservation) {
|
|
2745
|
+
throw codexError(
|
|
2746
|
+
'Codex queued turn reservation identity conflicts',
|
|
2747
|
+
'CODEX_QUEUE_SETTLEMENT_CONFLICT',
|
|
2748
|
+
);
|
|
2749
|
+
}
|
|
2750
|
+
if (
|
|
2751
|
+
reservation.state === 'settled'
|
|
2752
|
+
&& reservation.target_attempt_id === attemptId
|
|
2753
|
+
&& reservation.steer_attempt_id == null
|
|
2754
|
+
) {
|
|
2755
|
+
return { changes: 0, outcome: 'settled', reservation };
|
|
2756
|
+
}
|
|
2757
|
+
if (
|
|
2758
|
+
reservation.state !== 'queue-authorized'
|
|
2759
|
+
|| reservation.steer_attempt_id != null
|
|
2760
|
+
|| reservation.target_attempt_id != null
|
|
2761
|
+
) {
|
|
2762
|
+
throw codexError(
|
|
2763
|
+
'Codex dispatch reservation cannot settle this queued turn',
|
|
2764
|
+
'CODEX_QUEUE_SETTLEMENT_CONFLICT',
|
|
2765
|
+
);
|
|
2766
|
+
}
|
|
2767
|
+
db.prepare(`
|
|
2768
|
+
UPDATE codex_dispatch_reservations
|
|
2769
|
+
SET state = 'settled',
|
|
2770
|
+
target_attempt_id = ?,
|
|
2771
|
+
updated_ts = ?,
|
|
2772
|
+
settled_ts = ?
|
|
2773
|
+
WHERE reservation_id = ?
|
|
2774
|
+
AND state = 'queue-authorized'
|
|
2775
|
+
`).run(
|
|
2776
|
+
attemptId,
|
|
2777
|
+
ts,
|
|
2778
|
+
attempt.settled_ts ?? ts,
|
|
2779
|
+
reservation.reservation_id,
|
|
2780
|
+
);
|
|
2781
|
+
reservation = readCodexDispatchReservation(reservation.reservation_id);
|
|
2782
|
+
return { changes: 1, outcome: 'settled', reservation };
|
|
2783
|
+
})();
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
function settleCodexStoppedGeneration(input) {
|
|
2787
|
+
const allowed = new Set([
|
|
2788
|
+
'generation_id',
|
|
2789
|
+
'stable_host_id',
|
|
2790
|
+
'boot_session_id',
|
|
2791
|
+
'ts',
|
|
2792
|
+
]);
|
|
2793
|
+
assertExactKeys(
|
|
2794
|
+
input,
|
|
2795
|
+
allowed,
|
|
2796
|
+
'CODEX_RETIREMENT_PAYLOAD_REJECTED',
|
|
2797
|
+
'stopped generation settlement',
|
|
2798
|
+
);
|
|
2799
|
+
const generationId = requiredCodexString(
|
|
2800
|
+
input?.generation_id,
|
|
2801
|
+
'generation ID',
|
|
2802
|
+
);
|
|
2803
|
+
const stableHostId = requiredCodexString(
|
|
2804
|
+
input?.stable_host_id,
|
|
2805
|
+
'stable host identity',
|
|
2806
|
+
);
|
|
2807
|
+
const bootSessionId = requiredCodexString(
|
|
2808
|
+
input?.boot_session_id,
|
|
2809
|
+
'boot-session identity',
|
|
2810
|
+
);
|
|
2811
|
+
const ts = requiredTimestamp(
|
|
2812
|
+
input?.ts ?? Date.now(),
|
|
2813
|
+
'stopped generation settlement timestamp',
|
|
2814
|
+
);
|
|
2815
|
+
|
|
2816
|
+
return db.transaction(() => {
|
|
2817
|
+
const generation = assertCodexGenerationIdentity(
|
|
2818
|
+
generationId,
|
|
2819
|
+
stableHostId,
|
|
2820
|
+
bootSessionId,
|
|
2821
|
+
);
|
|
2822
|
+
const lease = db.prepare(`
|
|
2823
|
+
SELECT generation_id, stable_host_id, boot_session_id, status
|
|
2824
|
+
FROM codex_daemon_lease
|
|
2825
|
+
WHERE singleton = 1
|
|
2826
|
+
`).get();
|
|
2827
|
+
if (
|
|
2828
|
+
!['active', 'healthy-stopped'].includes(generation.state)
|
|
2829
|
+
|| lease?.status !== 'active'
|
|
2830
|
+
|| lease.generation_id !== generationId
|
|
2831
|
+
|| lease.stable_host_id !== stableHostId
|
|
2832
|
+
|| lease.boot_session_id !== bootSessionId
|
|
2833
|
+
) {
|
|
2834
|
+
throw codexError(
|
|
2835
|
+
'Codex stopped generation does not own the active daemon lease',
|
|
2836
|
+
'CODEX_CHECKPOINT_STALE_GENERATION',
|
|
2837
|
+
);
|
|
2838
|
+
}
|
|
2839
|
+
const stop = db.prepare(`
|
|
2840
|
+
SELECT turn_id
|
|
2841
|
+
FROM codex_attempt_checkpoints
|
|
2842
|
+
WHERE generation_id = ?
|
|
2843
|
+
AND kind = 'stop-empty-registry-observed'
|
|
2844
|
+
ORDER BY id DESC
|
|
2845
|
+
LIMIT 1
|
|
2846
|
+
`).get(generationId);
|
|
2847
|
+
if (!stop) {
|
|
2848
|
+
throw codexError(
|
|
2849
|
+
'Codex stopped generation lacks exact empty-registry proof',
|
|
2850
|
+
'CODEX_RETIREMENT_UNVERIFIED',
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2853
|
+
const unresolved = db.prepare(`
|
|
2854
|
+
SELECT *
|
|
2855
|
+
FROM codex_turn_attempts
|
|
2856
|
+
WHERE generation_id = ?
|
|
2857
|
+
AND delivery_state != 'prepared'
|
|
2858
|
+
AND recovery_state NOT IN ('settled', 'cancelled')
|
|
2859
|
+
ORDER BY created_ts, attempt_id
|
|
2860
|
+
`).all(generationId);
|
|
2861
|
+
if (unresolved.length === 0) {
|
|
2862
|
+
retireCodexGeneration(generationId, ts);
|
|
2863
|
+
return {
|
|
2864
|
+
changes: 0,
|
|
2865
|
+
disposition: 'already-disposed',
|
|
2866
|
+
attemptId: null,
|
|
2867
|
+
retired: true,
|
|
2868
|
+
};
|
|
2869
|
+
}
|
|
2870
|
+
const interrupted = unresolved.filter((attempt) => (
|
|
2871
|
+
attempt.method === 'turn/start'
|
|
2872
|
+
&& attempt.turn_id === stop.turn_id
|
|
2873
|
+
&& attempt.terminal_status === 'interrupted'
|
|
2874
|
+
&& hasCodexTurnCheckpoint(
|
|
2875
|
+
generationId,
|
|
2876
|
+
attempt.attempt_id,
|
|
2877
|
+
'turn-terminal',
|
|
2878
|
+
attempt.turn_id,
|
|
2879
|
+
)
|
|
2880
|
+
));
|
|
2881
|
+
if (interrupted.length === 0) {
|
|
2882
|
+
return {
|
|
2883
|
+
changes: 0,
|
|
2884
|
+
disposition: 'pending-delivery',
|
|
2885
|
+
attemptId: null,
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2888
|
+
if (interrupted.length !== 1) {
|
|
2889
|
+
throw codexError(
|
|
2890
|
+
'Codex stopped generation has conflicting interrupted targets',
|
|
2891
|
+
'CODEX_RETIREMENT_UNVERIFIED',
|
|
2892
|
+
);
|
|
2893
|
+
}
|
|
2894
|
+
const attempt = interrupted[0];
|
|
2895
|
+
settleCodexAttemptConsumerOutcome({
|
|
2896
|
+
attempt,
|
|
2897
|
+
generationId,
|
|
2898
|
+
linkedState: 'interrupted',
|
|
2899
|
+
attemptRecoveryState: 'cancelled',
|
|
2900
|
+
settleQueuedReservation: true,
|
|
2901
|
+
ts,
|
|
2902
|
+
});
|
|
2903
|
+
retireCodexGeneration(generationId, ts);
|
|
2904
|
+
return {
|
|
2905
|
+
changes: 1,
|
|
2906
|
+
disposition: 'stop-cancelled',
|
|
2907
|
+
attemptId: attempt.attempt_id,
|
|
2908
|
+
retired: true,
|
|
2909
|
+
};
|
|
2910
|
+
})();
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
function linkCodexSteeringInput(input) {
|
|
2914
|
+
const allowed = new Set([
|
|
2915
|
+
'linked_input_id',
|
|
2916
|
+
'generation_id',
|
|
2917
|
+
'attempt_id',
|
|
2918
|
+
'target_attempt_id',
|
|
2919
|
+
'telegram_chat_id',
|
|
2920
|
+
'telegram_message_id',
|
|
2921
|
+
'ts',
|
|
2922
|
+
]);
|
|
2923
|
+
assertExactKeys(
|
|
2924
|
+
input,
|
|
2925
|
+
allowed,
|
|
2926
|
+
'CODEX_LINKED_INPUT_PAYLOAD_REJECTED',
|
|
2927
|
+
'linked input',
|
|
2928
|
+
);
|
|
2929
|
+
const ts = requiredTimestamp(input?.ts ?? Date.now(), 'linked input timestamp');
|
|
2930
|
+
const linkedInputId = requiredCodexString(
|
|
2931
|
+
input?.linked_input_id,
|
|
2932
|
+
'linked input ID',
|
|
2933
|
+
);
|
|
2934
|
+
const generationId = requiredCodexString(
|
|
2935
|
+
input?.generation_id,
|
|
2936
|
+
'generation ID',
|
|
2937
|
+
);
|
|
2938
|
+
const attemptId = requiredCodexString(
|
|
2939
|
+
input?.attempt_id,
|
|
2940
|
+
'steer attempt ID',
|
|
2941
|
+
);
|
|
2942
|
+
const targetAttemptId = requiredCodexString(
|
|
2943
|
+
input?.target_attempt_id,
|
|
2944
|
+
'target attempt ID',
|
|
2945
|
+
);
|
|
2946
|
+
const telegramChatId = requiredCodexString(
|
|
2947
|
+
input?.telegram_chat_id,
|
|
2948
|
+
'Telegram chat ID',
|
|
2949
|
+
);
|
|
2950
|
+
const telegramMessageId = requiredCodexString(
|
|
2951
|
+
input?.telegram_message_id,
|
|
2952
|
+
'Telegram message ID',
|
|
2953
|
+
);
|
|
2954
|
+
return db.transaction(() => {
|
|
2955
|
+
const attempt = requireCodexAttempt(
|
|
2956
|
+
attemptId,
|
|
2957
|
+
generationId,
|
|
2958
|
+
);
|
|
2959
|
+
const target = requireCodexAttempt(
|
|
2960
|
+
targetAttemptId,
|
|
2961
|
+
generationId,
|
|
2962
|
+
);
|
|
2963
|
+
if (attempt.method !== 'turn/steer' || target.method !== 'turn/start') {
|
|
2964
|
+
throw codexError(
|
|
2965
|
+
'Codex linked input must connect steer work to a turn',
|
|
2966
|
+
'CODEX_LINKED_INPUT_INVALID',
|
|
2967
|
+
);
|
|
2968
|
+
}
|
|
2969
|
+
const existing = db.prepare(`
|
|
2970
|
+
SELECT generation_id, attempt_id, target_attempt_id,
|
|
2971
|
+
telegram_chat_id, telegram_message_id
|
|
2972
|
+
FROM codex_linked_inputs
|
|
2973
|
+
WHERE linked_input_id = ?
|
|
2974
|
+
`).get(linkedInputId);
|
|
2975
|
+
if (existing) {
|
|
2976
|
+
if (
|
|
2977
|
+
existing.generation_id === generationId
|
|
2978
|
+
&& existing.attempt_id === attemptId
|
|
2979
|
+
&& existing.target_attempt_id === targetAttemptId
|
|
2980
|
+
&& existing.telegram_chat_id === telegramChatId
|
|
2981
|
+
&& existing.telegram_message_id === telegramMessageId
|
|
2982
|
+
) {
|
|
2983
|
+
return { changes: 0 };
|
|
2984
|
+
}
|
|
2985
|
+
throw codexError(
|
|
2986
|
+
'Codex linked input ID was reused with different metadata',
|
|
2987
|
+
'CODEX_LINKED_INPUT_ID_REUSED',
|
|
2988
|
+
);
|
|
2989
|
+
}
|
|
2990
|
+
return db.prepare(`
|
|
2991
|
+
INSERT INTO codex_linked_inputs (
|
|
2992
|
+
linked_input_id, generation_id, attempt_id, target_attempt_id,
|
|
2993
|
+
telegram_chat_id, telegram_message_id, state, created_ts
|
|
2994
|
+
) VALUES (?, ?, ?, ?, ?, ?, 'linked', ?)
|
|
2995
|
+
`).run(
|
|
2996
|
+
linkedInputId,
|
|
2997
|
+
generationId,
|
|
2998
|
+
attemptId,
|
|
2999
|
+
targetAttemptId,
|
|
3000
|
+
telegramChatId,
|
|
3001
|
+
telegramMessageId,
|
|
3002
|
+
ts,
|
|
3003
|
+
);
|
|
3004
|
+
})();
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
function claimCodexRetryReservation(input) {
|
|
3008
|
+
const originalAttemptId = requiredCodexString(
|
|
3009
|
+
input?.original_attempt_id,
|
|
3010
|
+
'original attempt ID',
|
|
3011
|
+
);
|
|
3012
|
+
const retryAttemptId = requiredCodexString(
|
|
3013
|
+
input?.retry_attempt_id,
|
|
3014
|
+
'retry attempt ID',
|
|
3015
|
+
);
|
|
3016
|
+
const generationId = requiredCodexString(
|
|
3017
|
+
input?.generation_id,
|
|
3018
|
+
'generation ID',
|
|
3019
|
+
);
|
|
3020
|
+
const stableHostId = requiredCodexString(
|
|
3021
|
+
input?.stable_host_id,
|
|
3022
|
+
'stable host identity',
|
|
3023
|
+
);
|
|
3024
|
+
const bootSessionId = requiredCodexString(
|
|
3025
|
+
input?.boot_session_id,
|
|
3026
|
+
'boot-session identity',
|
|
3027
|
+
);
|
|
3028
|
+
const method = requiredCodexString(input?.method, 'retry method');
|
|
3029
|
+
const threadId = optionalCodexString(input?.thread_id, 'thread ID');
|
|
3030
|
+
const sourceMessageId = optionalCodexString(
|
|
3031
|
+
input?.telegram_source_message_id,
|
|
3032
|
+
'source message ID',
|
|
3033
|
+
);
|
|
3034
|
+
const clientUserMessageId = optionalCodexString(
|
|
3035
|
+
input?.client_user_message_id,
|
|
3036
|
+
'client user message ID',
|
|
3037
|
+
);
|
|
3038
|
+
const ts = requiredTimestamp(input?.ts ?? Date.now(), 'retry claim timestamp');
|
|
3039
|
+
return db.transaction(() => {
|
|
3040
|
+
const generation = assertCodexGenerationIdentity(
|
|
3041
|
+
generationId,
|
|
3042
|
+
stableHostId,
|
|
3043
|
+
bootSessionId,
|
|
3044
|
+
);
|
|
3045
|
+
assertCodexCheckpointOwner(generation, 'request-write-attempted');
|
|
3046
|
+
const reservation = db.prepare(`
|
|
3047
|
+
SELECT reservation.*,
|
|
3048
|
+
original.generation_id AS original_generation_id,
|
|
3049
|
+
original.session_key AS original_session_key,
|
|
3050
|
+
original_generation.stable_host_id AS original_stable_host_id,
|
|
3051
|
+
original_generation.boot_session_id AS original_boot_session_id,
|
|
3052
|
+
original_generation.state AS original_generation_state
|
|
3053
|
+
FROM codex_retry_reservations reservation
|
|
3054
|
+
JOIN codex_turn_attempts original
|
|
3055
|
+
ON original.attempt_id = reservation.original_attempt_id
|
|
3056
|
+
JOIN codex_generations original_generation
|
|
3057
|
+
ON original_generation.generation_id = original.generation_id
|
|
3058
|
+
WHERE reservation.original_attempt_id = ?
|
|
3059
|
+
`).get(originalAttemptId);
|
|
3060
|
+
if (
|
|
3061
|
+
!reservation
|
|
3062
|
+
|| reservation.retry_attempt_id !== retryAttemptId
|
|
3063
|
+
) {
|
|
3064
|
+
throw codexError(
|
|
3065
|
+
'Codex retry reservation does not match the claimed attempt',
|
|
3066
|
+
'CODEX_RETRY_RESERVATION_MISMATCH',
|
|
3067
|
+
);
|
|
3068
|
+
}
|
|
3069
|
+
if (reservation.original_generation_id === generationId) {
|
|
3070
|
+
throw codexError(
|
|
3071
|
+
'Codex retry requires a new generation after reboot release',
|
|
3072
|
+
'CODEX_RETRY_NEW_GENERATION_REQUIRED',
|
|
3073
|
+
);
|
|
3074
|
+
}
|
|
3075
|
+
if (
|
|
3076
|
+
reservation.original_session_key !== generation.session_key
|
|
3077
|
+
|| reservation.original_stable_host_id !== generation.stable_host_id
|
|
3078
|
+
) {
|
|
3079
|
+
throw codexError(
|
|
3080
|
+
'Codex retry generation does not match the original session',
|
|
3081
|
+
'CODEX_RETRY_RESERVATION_MISMATCH',
|
|
3082
|
+
);
|
|
3083
|
+
}
|
|
3084
|
+
const released = db.prepare(`
|
|
3085
|
+
SELECT 1
|
|
3086
|
+
FROM codex_reboot_releases
|
|
3087
|
+
WHERE stable_host_id = ?
|
|
3088
|
+
AND incident_boot_session_id = ?
|
|
3089
|
+
AND released_boot_session_id = ?
|
|
3090
|
+
`).get(
|
|
3091
|
+
reservation.original_stable_host_id,
|
|
3092
|
+
reservation.original_boot_session_id,
|
|
3093
|
+
generation.boot_session_id,
|
|
3094
|
+
);
|
|
3095
|
+
if (!released) {
|
|
3096
|
+
throw codexError(
|
|
3097
|
+
'Codex retry requires a durable reboot release',
|
|
3098
|
+
'CODEX_RETRY_REBOOT_RELEASE_REQUIRED',
|
|
3099
|
+
);
|
|
3100
|
+
}
|
|
3101
|
+
const existing = db.prepare(`
|
|
3102
|
+
SELECT * FROM codex_turn_attempts WHERE attempt_id = ?
|
|
3103
|
+
`).get(retryAttemptId);
|
|
3104
|
+
if (
|
|
3105
|
+
['claimed', 'dispatched', 'retired'].includes(reservation.state)
|
|
3106
|
+
&& existing
|
|
3107
|
+
) {
|
|
3108
|
+
assertCodexAttemptIdentity(existing, {
|
|
3109
|
+
generation_id: generationId,
|
|
3110
|
+
session_key: generation.session_key,
|
|
3111
|
+
method,
|
|
3112
|
+
thread_id: threadId,
|
|
3113
|
+
telegram_source_message_id: sourceMessageId,
|
|
3114
|
+
client_user_message_id: clientUserMessageId,
|
|
3115
|
+
});
|
|
3116
|
+
return { changes: 0, retryAttemptId };
|
|
3117
|
+
}
|
|
3118
|
+
if (reservation.state !== 'reserved' || existing) {
|
|
3119
|
+
throw codexError(
|
|
3120
|
+
'Codex retry reservation has already been consumed',
|
|
3121
|
+
'CODEX_RETRY_ALREADY_CLAIMED',
|
|
3122
|
+
);
|
|
3123
|
+
}
|
|
3124
|
+
db.prepare(`
|
|
3125
|
+
INSERT INTO codex_turn_attempts (
|
|
3126
|
+
attempt_id, generation_id, session_key, method,
|
|
3127
|
+
thread_id, telegram_source_message_id, client_user_message_id,
|
|
3128
|
+
delivery_state, recovery_state, created_ts, updated_ts
|
|
3129
|
+
)
|
|
3130
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'prepared', 'prepared', ?, ?)
|
|
3131
|
+
`).run(
|
|
3132
|
+
retryAttemptId,
|
|
3133
|
+
generationId,
|
|
3134
|
+
generation.session_key,
|
|
3135
|
+
method,
|
|
3136
|
+
threadId,
|
|
3137
|
+
sourceMessageId,
|
|
3138
|
+
clientUserMessageId,
|
|
3139
|
+
ts,
|
|
3140
|
+
ts,
|
|
3141
|
+
);
|
|
3142
|
+
db.prepare(`
|
|
3143
|
+
INSERT INTO codex_attempt_checkpoints (
|
|
3144
|
+
generation_id, attempt_id, kind, thread_id,
|
|
3145
|
+
detail_json, ts
|
|
3146
|
+
) VALUES (?, ?, 'request-prepared', ?, ?, ?)
|
|
3147
|
+
`).run(
|
|
3148
|
+
generationId,
|
|
3149
|
+
retryAttemptId,
|
|
3150
|
+
threadId,
|
|
3151
|
+
checkpointDetail({
|
|
3152
|
+
method,
|
|
3153
|
+
source: sourceMessageId,
|
|
3154
|
+
clientUserMessageId,
|
|
3155
|
+
}),
|
|
3156
|
+
ts,
|
|
3157
|
+
);
|
|
3158
|
+
db.prepare(`
|
|
3159
|
+
UPDATE codex_retry_reservations
|
|
3160
|
+
SET state = 'claimed', claimed_ts = ?
|
|
3161
|
+
WHERE original_attempt_id = ?
|
|
3162
|
+
`).run(ts, originalAttemptId);
|
|
3163
|
+
return { changes: 1, retryAttemptId };
|
|
3164
|
+
})();
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
function markCodexRetryDispatched(input) {
|
|
3168
|
+
const originalAttemptId = requiredCodexString(
|
|
3169
|
+
input?.original_attempt_id,
|
|
3170
|
+
'original attempt ID',
|
|
3171
|
+
);
|
|
3172
|
+
const retryAttemptId = requiredCodexString(
|
|
3173
|
+
input?.retry_attempt_id,
|
|
3174
|
+
'retry attempt ID',
|
|
3175
|
+
);
|
|
3176
|
+
const ts = requiredTimestamp(
|
|
3177
|
+
input?.ts ?? Date.now(),
|
|
3178
|
+
'retry dispatch timestamp',
|
|
3179
|
+
);
|
|
3180
|
+
return db.transaction(() => {
|
|
3181
|
+
const reservation = db.prepare(`
|
|
3182
|
+
SELECT * FROM codex_retry_reservations
|
|
3183
|
+
WHERE original_attempt_id = ?
|
|
3184
|
+
`).get(originalAttemptId);
|
|
3185
|
+
const retry = db.prepare(`
|
|
3186
|
+
SELECT * FROM codex_turn_attempts WHERE attempt_id = ?
|
|
3187
|
+
`).get(retryAttemptId);
|
|
3188
|
+
if (
|
|
3189
|
+
!reservation
|
|
3190
|
+
|| reservation.retry_attempt_id !== retryAttemptId
|
|
3191
|
+
|| !retry
|
|
3192
|
+
) {
|
|
3193
|
+
throw codexError(
|
|
3194
|
+
'Codex retry dispatch has no materialized reservation',
|
|
3195
|
+
'CODEX_RETRY_RESERVATION_MISMATCH',
|
|
3196
|
+
);
|
|
3197
|
+
}
|
|
3198
|
+
if (reservation.state === 'dispatched') {
|
|
3199
|
+
return { changes: 0, retryAttemptId };
|
|
3200
|
+
}
|
|
3201
|
+
if (
|
|
3202
|
+
reservation.state !== 'claimed'
|
|
3203
|
+
|| retry.delivery_state === 'prepared'
|
|
3204
|
+
) {
|
|
3205
|
+
throw codexError(
|
|
3206
|
+
'Codex retry was not durably write-attempted',
|
|
3207
|
+
'CODEX_RETRY_NOT_WRITE_ATTEMPTED',
|
|
3208
|
+
);
|
|
3209
|
+
}
|
|
3210
|
+
const result = db.prepare(`
|
|
3211
|
+
UPDATE codex_retry_reservations
|
|
3212
|
+
SET state = 'dispatched', dispatched_ts = ?
|
|
3213
|
+
WHERE original_attempt_id = ?
|
|
3214
|
+
`).run(ts, originalAttemptId);
|
|
3215
|
+
return { changes: result.changes, retryAttemptId };
|
|
3216
|
+
})();
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
function retireCodexRetryReservation(input) {
|
|
3220
|
+
const originalAttemptId = requiredCodexString(
|
|
3221
|
+
input?.original_attempt_id,
|
|
3222
|
+
'original attempt ID',
|
|
3223
|
+
);
|
|
3224
|
+
const retryAttemptId = requiredCodexString(
|
|
3225
|
+
input?.retry_attempt_id,
|
|
3226
|
+
'retry attempt ID',
|
|
3227
|
+
);
|
|
3228
|
+
const ts = requiredTimestamp(
|
|
3229
|
+
input?.ts ?? Date.now(),
|
|
3230
|
+
'retry retirement timestamp',
|
|
3231
|
+
);
|
|
3232
|
+
return db.transaction(() => {
|
|
3233
|
+
const reservation = db.prepare(`
|
|
3234
|
+
SELECT * FROM codex_retry_reservations
|
|
3235
|
+
WHERE original_attempt_id = ?
|
|
3236
|
+
`).get(originalAttemptId);
|
|
3237
|
+
const retry = db.prepare(`
|
|
3238
|
+
SELECT recovery_state
|
|
3239
|
+
FROM codex_turn_attempts
|
|
3240
|
+
WHERE attempt_id = ?
|
|
3241
|
+
`).get(retryAttemptId);
|
|
3242
|
+
if (
|
|
3243
|
+
!reservation
|
|
3244
|
+
|| reservation.retry_attempt_id !== retryAttemptId
|
|
3245
|
+
|| !retry
|
|
3246
|
+
) {
|
|
3247
|
+
throw codexError(
|
|
3248
|
+
'Codex retry retirement has no materialized reservation',
|
|
3249
|
+
'CODEX_RETRY_RESERVATION_MISMATCH',
|
|
3250
|
+
);
|
|
3251
|
+
}
|
|
3252
|
+
if (reservation.state === 'retired') {
|
|
3253
|
+
return { changes: 0, retryAttemptId };
|
|
3254
|
+
}
|
|
3255
|
+
if (!['settled', 'cancelled'].includes(retry.recovery_state)) {
|
|
3256
|
+
throw codexError(
|
|
3257
|
+
'Codex retry attempt is not durably settled',
|
|
3258
|
+
'CODEX_RETRY_NOT_SETTLED',
|
|
3259
|
+
);
|
|
3260
|
+
}
|
|
3261
|
+
const result = db.prepare(`
|
|
3262
|
+
UPDATE codex_retry_reservations
|
|
3263
|
+
SET state = 'retired', retired_ts = ?
|
|
3264
|
+
WHERE original_attempt_id = ?
|
|
3265
|
+
`).run(ts, originalAttemptId);
|
|
3266
|
+
return { changes: result.changes, retryAttemptId };
|
|
3267
|
+
})();
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
function recordCodexItemEffect(input) {
|
|
3271
|
+
const allowed = new Set([
|
|
3272
|
+
'generation_id',
|
|
3273
|
+
'attempt_id',
|
|
3274
|
+
'item_id',
|
|
3275
|
+
'item_type',
|
|
3276
|
+
'state',
|
|
3277
|
+
'ts',
|
|
3278
|
+
]);
|
|
3279
|
+
assertExactKeys(
|
|
3280
|
+
input,
|
|
3281
|
+
allowed,
|
|
3282
|
+
'CODEX_EFFECT_PAYLOAD_REJECTED',
|
|
3283
|
+
'item effect',
|
|
3284
|
+
);
|
|
3285
|
+
const generationId = requiredCodexString(
|
|
3286
|
+
input?.generation_id,
|
|
3287
|
+
'generation ID',
|
|
3288
|
+
);
|
|
3289
|
+
const attemptId = requiredCodexString(input?.attempt_id, 'attempt ID');
|
|
3290
|
+
const itemId = requiredCodexString(input?.item_id, 'item ID');
|
|
3291
|
+
const itemType = requiredCodexString(input?.item_type, 'item type');
|
|
3292
|
+
const state = input?.state;
|
|
3293
|
+
if (!['started', 'completed', 'failed'].includes(state)) {
|
|
3294
|
+
throw codexError(
|
|
3295
|
+
'Codex item effect state is invalid',
|
|
3296
|
+
'CODEX_EFFECT_PAYLOAD_REJECTED',
|
|
3297
|
+
);
|
|
3298
|
+
}
|
|
3299
|
+
const ts = requiredTimestamp(input?.ts ?? Date.now(), 'item effect timestamp');
|
|
3300
|
+
return db.transaction(() => {
|
|
3301
|
+
const attempt = requireCodexAttempt(attemptId, generationId);
|
|
3302
|
+
const generation = db.prepare(`
|
|
3303
|
+
SELECT * FROM codex_generations WHERE generation_id = ?
|
|
3304
|
+
`).get(generationId);
|
|
3305
|
+
assertCodexCheckpointOwner(generation, 'item-started');
|
|
3306
|
+
const existing = db.prepare(`
|
|
3307
|
+
SELECT generation_id, item_type, state
|
|
3308
|
+
FROM codex_item_effects
|
|
3309
|
+
WHERE attempt_id = ? AND item_id = ?
|
|
3310
|
+
`).get(attemptId, itemId);
|
|
3311
|
+
if (existing) {
|
|
3312
|
+
if (
|
|
3313
|
+
existing.generation_id !== generationId
|
|
3314
|
+
|| existing.item_type !== itemType
|
|
3315
|
+
) {
|
|
3316
|
+
throw codexError(
|
|
3317
|
+
'Codex item effect identity is immutable',
|
|
3318
|
+
'CODEX_EFFECT_IDENTITY_MISMATCH',
|
|
3319
|
+
);
|
|
3320
|
+
}
|
|
3321
|
+
if (existing.state === state) return { changes: 0 };
|
|
3322
|
+
}
|
|
3323
|
+
const earlyNotification = (
|
|
3324
|
+
attempt.recovery_state === 'ambiguous'
|
|
3325
|
+
&& ['write-attempted', 'response-observed'].includes(
|
|
3326
|
+
attempt.delivery_state,
|
|
3327
|
+
)
|
|
3328
|
+
);
|
|
3329
|
+
if (
|
|
3330
|
+
attempt.turn_id == null
|
|
3331
|
+
|| attempt.terminal_status != null
|
|
3332
|
+
|| !(attempt.recovery_state === 'active' || earlyNotification)
|
|
3333
|
+
) {
|
|
3334
|
+
throw codexError(
|
|
3335
|
+
'Codex item effect requires a live dispatched turn',
|
|
3336
|
+
'CODEX_EFFECT_SEQUENCE_INVALID',
|
|
3337
|
+
);
|
|
3338
|
+
}
|
|
3339
|
+
if (existing) {
|
|
3340
|
+
if (existing.state !== 'started') {
|
|
3341
|
+
throw codexError(
|
|
3342
|
+
'Codex terminal item effect cannot regress or change',
|
|
3343
|
+
'CODEX_EFFECT_STATE_CONFLICT',
|
|
3344
|
+
);
|
|
3345
|
+
}
|
|
3346
|
+
return db.prepare(`
|
|
3347
|
+
UPDATE codex_item_effects
|
|
3348
|
+
SET state = ?, updated_ts = ?
|
|
3349
|
+
WHERE attempt_id = ? AND item_id = ?
|
|
3350
|
+
`).run(state, ts, attemptId, itemId);
|
|
3351
|
+
}
|
|
3352
|
+
return db.prepare(`
|
|
3353
|
+
INSERT INTO codex_item_effects (
|
|
3354
|
+
generation_id, attempt_id, item_id, item_type,
|
|
3355
|
+
state, created_ts, updated_ts
|
|
3356
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
3357
|
+
`).run(
|
|
3358
|
+
generationId,
|
|
3359
|
+
attemptId,
|
|
3360
|
+
itemId,
|
|
3361
|
+
itemType,
|
|
3362
|
+
state,
|
|
3363
|
+
ts,
|
|
3364
|
+
ts,
|
|
3365
|
+
);
|
|
3366
|
+
})();
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
function acquireCodexLease(input) {
|
|
3370
|
+
const generationId = requiredCodexString(
|
|
3371
|
+
input?.generation_id,
|
|
3372
|
+
'generation ID',
|
|
3373
|
+
);
|
|
3374
|
+
const stableHostId = requiredCodexString(
|
|
3375
|
+
input?.stable_host_id,
|
|
3376
|
+
'stable host identity',
|
|
3377
|
+
);
|
|
3378
|
+
const bootSessionId = requiredCodexString(
|
|
3379
|
+
input?.boot_session_id,
|
|
3380
|
+
'boot-session identity',
|
|
3381
|
+
);
|
|
3382
|
+
const ts = requiredTimestamp(input?.ts ?? Date.now(), 'lease timestamp');
|
|
3383
|
+
return db.transaction(() => {
|
|
3384
|
+
assertCodexGenerationIdentity(generationId, stableHostId, bootSessionId);
|
|
3385
|
+
const lease = db.prepare(`
|
|
3386
|
+
SELECT * FROM codex_daemon_lease WHERE singleton = 1
|
|
3387
|
+
`).get();
|
|
3388
|
+
if (
|
|
3389
|
+
lease?.status === 'quarantined'
|
|
3390
|
+
|| (
|
|
3391
|
+
lease?.status === 'active'
|
|
3392
|
+
&& lease.generation_id !== generationId
|
|
3393
|
+
)
|
|
3394
|
+
) {
|
|
3395
|
+
throw codexError(
|
|
3396
|
+
'Codex daemon generation lease is unavailable',
|
|
3397
|
+
lease.status === 'quarantined'
|
|
3398
|
+
? 'CODEX_CONTAINMENT_QUARANTINED'
|
|
3399
|
+
: 'CODEX_DAEMON_GENERATION_BUSY',
|
|
3400
|
+
);
|
|
3401
|
+
}
|
|
3402
|
+
db.prepare(`
|
|
3403
|
+
INSERT INTO codex_daemon_lease (
|
|
3404
|
+
singleton, generation_id, stable_host_id, boot_session_id,
|
|
3405
|
+
status, quarantine_reason, acquired_ts, updated_ts, released_ts
|
|
3406
|
+
) VALUES (1, ?, ?, ?, 'active', NULL, ?, ?, NULL)
|
|
3407
|
+
ON CONFLICT(singleton) DO UPDATE SET
|
|
3408
|
+
generation_id = excluded.generation_id,
|
|
3409
|
+
stable_host_id = excluded.stable_host_id,
|
|
3410
|
+
boot_session_id = excluded.boot_session_id,
|
|
3411
|
+
status = 'active',
|
|
3412
|
+
quarantine_reason = NULL,
|
|
3413
|
+
acquired_ts = excluded.acquired_ts,
|
|
3414
|
+
updated_ts = excluded.updated_ts,
|
|
3415
|
+
released_ts = NULL
|
|
3416
|
+
`).run(generationId, stableHostId, bootSessionId, ts, ts);
|
|
3417
|
+
db.prepare(`
|
|
3418
|
+
UPDATE codex_generations
|
|
3419
|
+
SET state = 'active', updated_ts = ?
|
|
3420
|
+
WHERE generation_id = ?
|
|
3421
|
+
`).run(ts, generationId);
|
|
3422
|
+
return db.prepare(`
|
|
3423
|
+
SELECT * FROM codex_daemon_lease WHERE singleton = 1
|
|
3424
|
+
`).get();
|
|
3425
|
+
})();
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
function markCodexContainment(input) {
|
|
3429
|
+
const generationId = requiredCodexString(
|
|
3430
|
+
input?.generation_id,
|
|
3431
|
+
'generation ID',
|
|
3432
|
+
);
|
|
3433
|
+
const stableHostId = requiredCodexString(
|
|
3434
|
+
input?.stable_host_id,
|
|
3435
|
+
'stable host identity',
|
|
3436
|
+
);
|
|
3437
|
+
const bootSessionId = requiredCodexString(
|
|
3438
|
+
input?.boot_session_id,
|
|
3439
|
+
'boot-session identity',
|
|
3440
|
+
);
|
|
3441
|
+
const reason = requiredCodexString(input?.reason, 'containment reason');
|
|
3442
|
+
const ts = requiredTimestamp(
|
|
3443
|
+
input?.ts ?? Date.now(),
|
|
3444
|
+
'containment timestamp',
|
|
3445
|
+
);
|
|
3446
|
+
return db.transaction(() => {
|
|
3447
|
+
assertCodexGenerationIdentity(generationId, stableHostId, bootSessionId);
|
|
3448
|
+
const lease = db.prepare(`
|
|
3449
|
+
SELECT * FROM codex_daemon_lease WHERE singleton = 1
|
|
3450
|
+
`).get();
|
|
3451
|
+
if (!lease || lease.generation_id !== generationId) {
|
|
3452
|
+
throw codexError(
|
|
3453
|
+
'Codex containment requires the owned daemon lease',
|
|
3454
|
+
'CODEX_LEASE_MISMATCH',
|
|
3455
|
+
);
|
|
3456
|
+
}
|
|
3457
|
+
db.prepare(`
|
|
3458
|
+
UPDATE codex_generations
|
|
3459
|
+
SET state = 'containment-failed',
|
|
3460
|
+
containment_reason = ?,
|
|
3461
|
+
updated_ts = ?
|
|
3462
|
+
WHERE generation_id = ?
|
|
3463
|
+
`).run(reason, ts, generationId);
|
|
3464
|
+
markGenerationAttemptsAmbiguous(generationId, ts);
|
|
3465
|
+
return db.prepare(`
|
|
3466
|
+
UPDATE codex_daemon_lease
|
|
3467
|
+
SET status = 'quarantined',
|
|
3468
|
+
quarantine_reason = ?,
|
|
3469
|
+
updated_ts = ?
|
|
3470
|
+
WHERE singleton = 1
|
|
3471
|
+
`).run(reason, ts);
|
|
3472
|
+
})();
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
function retireCodexGeneration(
|
|
3476
|
+
generationId,
|
|
3477
|
+
ts,
|
|
3478
|
+
{ onlyIfHealthyStopped = false } = {},
|
|
3479
|
+
) {
|
|
3480
|
+
const generation = db.prepare(`
|
|
3481
|
+
SELECT * FROM codex_generations WHERE generation_id = ?
|
|
3482
|
+
`).get(generationId);
|
|
3483
|
+
if (!generation) {
|
|
3484
|
+
throw codexError(
|
|
3485
|
+
'Codex generation does not exist',
|
|
3486
|
+
'CODEX_GENERATION_NOT_FOUND',
|
|
3487
|
+
);
|
|
3488
|
+
}
|
|
3489
|
+
if (generation.state === 'retired') {
|
|
3490
|
+
return { changes: 0, retired: true };
|
|
3491
|
+
}
|
|
3492
|
+
if (onlyIfHealthyStopped && generation.state !== 'healthy-stopped') {
|
|
3493
|
+
return { changes: 0, retired: false };
|
|
3494
|
+
}
|
|
3495
|
+
const unresolved = db.prepare(`
|
|
3496
|
+
SELECT 1
|
|
3497
|
+
FROM codex_turn_attempts
|
|
3498
|
+
WHERE generation_id = ?
|
|
3499
|
+
AND delivery_state != 'prepared'
|
|
3500
|
+
AND recovery_state NOT IN ('settled', 'cancelled')
|
|
3501
|
+
LIMIT 1
|
|
3502
|
+
`).get(generationId);
|
|
3503
|
+
if (unresolved) {
|
|
3504
|
+
throw codexError(
|
|
3505
|
+
'Codex generation still owns unresolved delivery work',
|
|
3506
|
+
'CODEX_RETIREMENT_UNVERIFIED',
|
|
3507
|
+
);
|
|
3508
|
+
}
|
|
3509
|
+
const lease = db.prepare(`
|
|
3510
|
+
SELECT * FROM codex_daemon_lease WHERE singleton = 1
|
|
3511
|
+
`).get();
|
|
3512
|
+
if (
|
|
3513
|
+
lease?.status !== 'active'
|
|
3514
|
+
|| lease.generation_id !== generationId
|
|
3515
|
+
|| lease.stable_host_id !== generation.stable_host_id
|
|
3516
|
+
|| lease.boot_session_id !== generation.boot_session_id
|
|
3517
|
+
) {
|
|
3518
|
+
throw codexError(
|
|
3519
|
+
'Codex retirement requires the exact active daemon lease',
|
|
3520
|
+
'CODEX_CHECKPOINT_STALE_GENERATION',
|
|
3521
|
+
);
|
|
3522
|
+
}
|
|
3523
|
+
const result = db.prepare(`
|
|
3524
|
+
UPDATE codex_generations
|
|
3525
|
+
SET state = 'retired', settled_ts = ?, updated_ts = ?
|
|
3526
|
+
WHERE generation_id = ?
|
|
3527
|
+
AND state != 'retired'
|
|
3528
|
+
`).run(ts, ts, generationId);
|
|
3529
|
+
const released = db.prepare(`
|
|
3530
|
+
UPDATE codex_daemon_lease
|
|
3531
|
+
SET generation_id = NULL,
|
|
3532
|
+
status = 'clear',
|
|
3533
|
+
quarantine_reason = NULL,
|
|
3534
|
+
updated_ts = ?,
|
|
3535
|
+
released_ts = ?
|
|
3536
|
+
WHERE singleton = 1
|
|
3537
|
+
AND generation_id = ?
|
|
3538
|
+
AND stable_host_id = ?
|
|
3539
|
+
AND boot_session_id = ?
|
|
3540
|
+
AND status = 'active'
|
|
3541
|
+
`).run(
|
|
3542
|
+
ts,
|
|
3543
|
+
ts,
|
|
3544
|
+
generationId,
|
|
3545
|
+
generation.stable_host_id,
|
|
3546
|
+
generation.boot_session_id,
|
|
3547
|
+
);
|
|
3548
|
+
if (result.changes !== 1 || released.changes !== 1) {
|
|
3549
|
+
throw codexError(
|
|
3550
|
+
'Codex retirement lost the exact generation lease',
|
|
3551
|
+
'CODEX_CHECKPOINT_STALE_GENERATION',
|
|
3552
|
+
);
|
|
3553
|
+
}
|
|
3554
|
+
return { changes: 1, retired: true };
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
function markCodexGenerationRetired(input) {
|
|
3558
|
+
const generationId = requiredCodexString(
|
|
3559
|
+
input?.generation_id,
|
|
3560
|
+
'generation ID',
|
|
3561
|
+
);
|
|
3562
|
+
const ts = requiredTimestamp(input?.ts ?? Date.now(), 'retirement timestamp');
|
|
3563
|
+
return db.transaction(() => retireCodexGeneration(generationId, ts))();
|
|
3564
|
+
}
|
|
3565
|
+
|
|
275
3566
|
return {
|
|
276
3567
|
raw: db,
|
|
277
3568
|
|
|
@@ -323,38 +3614,117 @@ function wrap(db) {
|
|
|
323
3614
|
},
|
|
324
3615
|
|
|
325
3616
|
upsertSession(row) {
|
|
326
|
-
return
|
|
327
|
-
session_key: row.session_key,
|
|
328
|
-
chat_id: String(row.chat_id),
|
|
329
|
-
thread_id: row.thread_id ? String(row.thread_id) : null,
|
|
330
|
-
claude_session_id: row.claude_session_id,
|
|
331
|
-
agent: row.agent || null,
|
|
332
|
-
cwd: row.cwd || null,
|
|
333
|
-
model: row.model || null,
|
|
334
|
-
effort: row.effort || null,
|
|
335
|
-
// 0.10.0: pm_backend defaults to 'sdk' if caller doesn't set it.
|
|
336
|
-
pm_backend: row.pm_backend || 'sdk',
|
|
337
|
-
ts: row.ts || Date.now(),
|
|
338
|
-
});
|
|
3617
|
+
return upsertLegacyAndProviderSession(row);
|
|
339
3618
|
},
|
|
340
3619
|
|
|
341
3620
|
getSession(sessionKey) {
|
|
342
3621
|
return getSessionStmt.get(sessionKey);
|
|
343
3622
|
},
|
|
344
3623
|
|
|
3624
|
+
upsertProviderSession(row) {
|
|
3625
|
+
return upsertProviderAndLegacySession(row);
|
|
3626
|
+
},
|
|
3627
|
+
|
|
3628
|
+
getProviderSession(sessionKey, namespace) {
|
|
3629
|
+
if (!PROVIDER_NAMESPACES.has(namespace)) {
|
|
3630
|
+
throw new TypeError('provider session namespace is invalid');
|
|
3631
|
+
}
|
|
3632
|
+
return readProviderSession(String(sessionKey), namespace);
|
|
3633
|
+
},
|
|
3634
|
+
|
|
3635
|
+
clearProviderSession(sessionKey, namespace) {
|
|
3636
|
+
if (!PROVIDER_NAMESPACES.has(namespace)) {
|
|
3637
|
+
throw new TypeError('provider session namespace is invalid');
|
|
3638
|
+
}
|
|
3639
|
+
return clearOneProviderSession(String(sessionKey), namespace);
|
|
3640
|
+
},
|
|
3641
|
+
|
|
345
3642
|
touchSession(sessionKey, ts = Date.now()) {
|
|
346
|
-
|
|
3643
|
+
const row = getSessionStmt.get(sessionKey);
|
|
3644
|
+
const result = touchSessionStmt.run(ts, sessionKey);
|
|
3645
|
+
if (row) {
|
|
3646
|
+
touchProviderSessionStmt.run(
|
|
3647
|
+
ts,
|
|
3648
|
+
sessionKey,
|
|
3649
|
+
claudeNamespaceForStoredBackend(row.pm_backend),
|
|
3650
|
+
);
|
|
3651
|
+
}
|
|
3652
|
+
return result;
|
|
347
3653
|
},
|
|
348
3654
|
|
|
349
3655
|
clearSessionId(sessionKey) {
|
|
350
|
-
return
|
|
3656
|
+
return clearLegacySession(sessionKey);
|
|
351
3657
|
},
|
|
352
3658
|
|
|
353
3659
|
// 0.10.0: backend reassignment without resetting other session fields.
|
|
354
3660
|
// Used when ProcessManager spawns a Process with a different backend
|
|
355
3661
|
// than the persisted row says (drift event fires too).
|
|
356
3662
|
setSessionBackend(sessionKey, backend) {
|
|
357
|
-
|
|
3663
|
+
const row = getSessionStmt.get(sessionKey);
|
|
3664
|
+
const result = setSessionBackendStmt.run(backend, sessionKey);
|
|
3665
|
+
if (row) {
|
|
3666
|
+
const beforeNamespace = claudeNamespaceForStoredBackend(row.pm_backend);
|
|
3667
|
+
const afterNamespace = claudeNamespaceForStoredBackend(backend);
|
|
3668
|
+
if (beforeNamespace === afterNamespace) {
|
|
3669
|
+
setProviderBackendStmt.run(backend, sessionKey, beforeNamespace);
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
return result;
|
|
3673
|
+
},
|
|
3674
|
+
|
|
3675
|
+
createCodexGeneration,
|
|
3676
|
+
recordCodexCheckpoint,
|
|
3677
|
+
recordCodexDeliveryCheckpoint,
|
|
3678
|
+
getCodexAttempt(attemptId) {
|
|
3679
|
+
return db.prepare(`
|
|
3680
|
+
SELECT * FROM codex_turn_attempts WHERE attempt_id = ?
|
|
3681
|
+
`).get(attemptId);
|
|
3682
|
+
},
|
|
3683
|
+
listCodexAttemptCheckpoints(attemptId) {
|
|
3684
|
+
return db.prepare(`
|
|
3685
|
+
SELECT *
|
|
3686
|
+
FROM codex_attempt_checkpoints
|
|
3687
|
+
WHERE attempt_id = ?
|
|
3688
|
+
ORDER BY id
|
|
3689
|
+
`).all(attemptId);
|
|
3690
|
+
},
|
|
3691
|
+
claimCodexDispatchReservation,
|
|
3692
|
+
recordInboundRuntimeSelection,
|
|
3693
|
+
getReplayProviderRecovery,
|
|
3694
|
+
getCodexDispatchReservation(reservationId) {
|
|
3695
|
+
return readCodexDispatchReservation(requiredCodexString(
|
|
3696
|
+
reservationId,
|
|
3697
|
+
'dispatch reservation ID',
|
|
3698
|
+
));
|
|
3699
|
+
},
|
|
3700
|
+
finalizeCodexAcceptedSteer,
|
|
3701
|
+
markCodexDispatchDisposition,
|
|
3702
|
+
settleCodexQueuedDispatch,
|
|
3703
|
+
settleCodexStoppedGeneration,
|
|
3704
|
+
linkCodexSteeringInput,
|
|
3705
|
+
claimCodexRetryReservation,
|
|
3706
|
+
markCodexRetryDispatched,
|
|
3707
|
+
retireCodexRetryReservation,
|
|
3708
|
+
recordCodexItemEffect,
|
|
3709
|
+
acquireCodexLease,
|
|
3710
|
+
getCodexLease() {
|
|
3711
|
+
return db.prepare(`
|
|
3712
|
+
SELECT * FROM codex_daemon_lease WHERE singleton = 1
|
|
3713
|
+
`).get();
|
|
3714
|
+
},
|
|
3715
|
+
markCodexContainment,
|
|
3716
|
+
markCodexGenerationRetired,
|
|
3717
|
+
reconstructCodexRecovery(input) {
|
|
3718
|
+
return reconstructCodexRecovery(db, input);
|
|
3719
|
+
},
|
|
3720
|
+
listUnresolvedCodexAttempts(input) {
|
|
3721
|
+
return listUnresolvedCodexAttempts(db, input);
|
|
3722
|
+
},
|
|
3723
|
+
reconcileCodexAttempt(input) {
|
|
3724
|
+
return reconcileCodexAttempt(db, input);
|
|
3725
|
+
},
|
|
3726
|
+
pruneCodexOperationalData(options) {
|
|
3727
|
+
return pruneCodexOperationalData(db, options);
|
|
358
3728
|
},
|
|
359
3729
|
|
|
360
3730
|
getMessage(chatId, msgId) {
|
|
@@ -526,13 +3896,42 @@ function wrap(db) {
|
|
|
526
3896
|
return deleteChatToolDecisionStmt.run(String(bot_name), String(chat_id), id);
|
|
527
3897
|
},
|
|
528
3898
|
|
|
3899
|
+
logConfigChanges(rows) {
|
|
3900
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
3901
|
+
throw new TypeError('config change rows are required');
|
|
3902
|
+
}
|
|
3903
|
+
return db.transaction(() => rows.map((row) => logConfigChangeStmt.run({
|
|
3904
|
+
chat_id: String(row.chat_id),
|
|
3905
|
+
thread_id: row.thread_id ? String(row.thread_id) : null,
|
|
3906
|
+
field: row.field,
|
|
3907
|
+
old_value: configChangeValue(row.old_value, {
|
|
3908
|
+
nullable: true,
|
|
3909
|
+
label: 'old_value',
|
|
3910
|
+
}),
|
|
3911
|
+
new_value: configChangeValue(row.new_value, {
|
|
3912
|
+
nullable: false,
|
|
3913
|
+
label: 'new_value',
|
|
3914
|
+
}),
|
|
3915
|
+
user_id: row.user_id || null,
|
|
3916
|
+
user: row.user || null,
|
|
3917
|
+
source: row.source || 'command',
|
|
3918
|
+
ts: row.ts || Date.now(),
|
|
3919
|
+
})))();
|
|
3920
|
+
},
|
|
3921
|
+
|
|
529
3922
|
logConfigChange(row) {
|
|
530
3923
|
return logConfigChangeStmt.run({
|
|
531
3924
|
chat_id: String(row.chat_id),
|
|
532
3925
|
thread_id: row.thread_id ? String(row.thread_id) : null,
|
|
533
3926
|
field: row.field,
|
|
534
|
-
old_value: row.old_value
|
|
535
|
-
|
|
3927
|
+
old_value: configChangeValue(row.old_value, {
|
|
3928
|
+
nullable: true,
|
|
3929
|
+
label: 'old_value',
|
|
3930
|
+
}),
|
|
3931
|
+
new_value: configChangeValue(row.new_value, {
|
|
3932
|
+
nullable: false,
|
|
3933
|
+
label: 'new_value',
|
|
3934
|
+
}),
|
|
536
3935
|
user_id: row.user_id || null,
|
|
537
3936
|
user: row.user || null,
|
|
538
3937
|
source: row.source || 'command',
|