polygram 0.38.3 → 0.38.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/lib/codex/clean-replay-dispatch.js +51 -0
- package/lib/codex/dispatch-reservation-id.js +20 -0
- package/lib/codex/runtime-controller.js +152 -18
- package/lib/codex/spawn-context.js +1 -0
- package/lib/db/auto-resume.js +161 -4
- package/lib/db/event-detail-schema.js +9 -0
- package/lib/db.js +642 -25
- package/lib/handlers/dispatcher.js +42 -11
- package/lib/handlers/redeliver.js +38 -7
- package/lib/handlers/replay-disposition.js +16 -13
- package/lib/ops/clean-codex-boot-replay.js +56 -0
- package/lib/ops/clean-replay-session-coordinator.js +106 -0
- package/lib/ops/clean-resume.js +4 -0
- package/lib/sdk/callbacks.js +3 -13
- package/migrations/021-codex-replay-evidence-indexes.sql +19 -0
- package/package.json +2 -2
- package/polygram.js +192 -24
package/lib/db.js
CHANGED
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const crypto = require('crypto');
|
|
10
|
+
const { isDeepStrictEqual } = require('node:util');
|
|
10
11
|
const Database = require('better-sqlite3');
|
|
12
|
+
const {
|
|
13
|
+
classifyCodexRecoveryEvidence,
|
|
14
|
+
} = require('./db/auto-resume');
|
|
11
15
|
const {
|
|
12
16
|
pruneCodexOperationalData,
|
|
13
17
|
} = require('./db/codex-retention');
|
|
@@ -26,6 +30,9 @@ const {
|
|
|
26
30
|
const {
|
|
27
31
|
sanitizeCodexFaultProvenance,
|
|
28
32
|
} = require('./codex/fault-provenance');
|
|
33
|
+
const {
|
|
34
|
+
codexDispatchReservationId,
|
|
35
|
+
} = require('./codex/dispatch-reservation-id');
|
|
29
36
|
const {
|
|
30
37
|
sanitizeForDurableWrite,
|
|
31
38
|
sanitizeDurableStructured,
|
|
@@ -87,7 +94,7 @@ const durableJson = (value) => {
|
|
|
87
94
|
//
|
|
88
95
|
// 0.15: bumped 13 → 14. Adds migration 014-secret-redactions.sql
|
|
89
96
|
// (secret_redactions audit table + messages.secret_scanned_at).
|
|
90
|
-
const SCHEMA_VERSION =
|
|
97
|
+
const SCHEMA_VERSION = 21;
|
|
91
98
|
|
|
92
99
|
// Sentinel `error` value for outbound rows whose API call may or may not
|
|
93
100
|
// have reached Telegram. markStalePending writes it; hasOutboundReplyTo
|
|
@@ -384,6 +391,25 @@ function wrap(db) {
|
|
|
384
391
|
};
|
|
385
392
|
}
|
|
386
393
|
|
|
394
|
+
const codexProviderIdentityFields = [
|
|
395
|
+
'session_key',
|
|
396
|
+
'namespace',
|
|
397
|
+
'provider',
|
|
398
|
+
'provider_session_id',
|
|
399
|
+
'agent',
|
|
400
|
+
'cwd',
|
|
401
|
+
'model',
|
|
402
|
+
'effort',
|
|
403
|
+
'pm_backend',
|
|
404
|
+
'spawn_profile_id',
|
|
405
|
+
];
|
|
406
|
+
|
|
407
|
+
function hasExactCodexProviderIdentity(actual, expected) {
|
|
408
|
+
return actual != null && codexProviderIdentityFields.every(
|
|
409
|
+
(field) => actual[field] === expected[field],
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
387
413
|
function legacySessionParams(row) {
|
|
388
414
|
return {
|
|
389
415
|
session_key: row.session_key,
|
|
@@ -1020,7 +1046,7 @@ function wrap(db) {
|
|
|
1020
1046
|
}
|
|
1021
1047
|
|
|
1022
1048
|
function assertCodexCheckpointOwner(generation, kind) {
|
|
1023
|
-
if (
|
|
1049
|
+
if (kind === 'request-prepared') return;
|
|
1024
1050
|
const lease = db.prepare(`
|
|
1025
1051
|
SELECT generation_id, stable_host_id, boot_session_id, status
|
|
1026
1052
|
FROM codex_daemon_lease
|
|
@@ -1385,6 +1411,62 @@ function wrap(db) {
|
|
|
1385
1411
|
`).run(ts, ts, generationId, generationId, generationId);
|
|
1386
1412
|
}
|
|
1387
1413
|
|
|
1414
|
+
function insertCodexPreparedCheckpoint({
|
|
1415
|
+
generation,
|
|
1416
|
+
generationId,
|
|
1417
|
+
attemptId,
|
|
1418
|
+
method,
|
|
1419
|
+
threadId,
|
|
1420
|
+
turnId,
|
|
1421
|
+
requestId,
|
|
1422
|
+
itemId,
|
|
1423
|
+
sourceId,
|
|
1424
|
+
clientUserMessageId,
|
|
1425
|
+
detailJson,
|
|
1426
|
+
ts,
|
|
1427
|
+
}) {
|
|
1428
|
+
db.prepare(`
|
|
1429
|
+
INSERT INTO codex_turn_attempts (
|
|
1430
|
+
attempt_id, generation_id, session_key, method,
|
|
1431
|
+
thread_id, turn_id, telegram_source_message_id,
|
|
1432
|
+
client_user_message_id, delivery_state, recovery_state,
|
|
1433
|
+
created_ts, updated_ts
|
|
1434
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'prepared', 'prepared', ?, ?)
|
|
1435
|
+
`).run(
|
|
1436
|
+
attemptId,
|
|
1437
|
+
generationId,
|
|
1438
|
+
generation.session_key,
|
|
1439
|
+
method,
|
|
1440
|
+
threadId,
|
|
1441
|
+
turnId,
|
|
1442
|
+
sourceId,
|
|
1443
|
+
clientUserMessageId,
|
|
1444
|
+
ts,
|
|
1445
|
+
ts,
|
|
1446
|
+
);
|
|
1447
|
+
requireCodexAttempt(attemptId, generationId);
|
|
1448
|
+
db.prepare(`
|
|
1449
|
+
INSERT INTO codex_attempt_checkpoints (
|
|
1450
|
+
generation_id, attempt_id, kind, thread_id, turn_id,
|
|
1451
|
+
request_id, item_id, detail_json, ts
|
|
1452
|
+
) VALUES (?, ?, 'request-prepared', ?, ?, ?, ?, ?, ?)
|
|
1453
|
+
`).run(
|
|
1454
|
+
generationId,
|
|
1455
|
+
attemptId,
|
|
1456
|
+
threadId,
|
|
1457
|
+
turnId,
|
|
1458
|
+
requestId,
|
|
1459
|
+
itemId,
|
|
1460
|
+
detailJson,
|
|
1461
|
+
ts,
|
|
1462
|
+
);
|
|
1463
|
+
return {
|
|
1464
|
+
changes: 1,
|
|
1465
|
+
attemptId,
|
|
1466
|
+
kind: 'request-prepared',
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1388
1470
|
function recordCodexCheckpoint(input) {
|
|
1389
1471
|
const kind = requiredCodexString(input?.kind, 'checkpoint kind');
|
|
1390
1472
|
if (!codexCheckpointKinds.has(kind)) {
|
|
@@ -1499,27 +1581,33 @@ function wrap(db) {
|
|
|
1499
1581
|
'CODEX_ATTEMPT_ID_REUSED',
|
|
1500
1582
|
);
|
|
1501
1583
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
attempt_id, generation_id, session_key, method,
|
|
1505
|
-
thread_id, turn_id, telegram_source_message_id,
|
|
1506
|
-
client_user_message_id, delivery_state, recovery_state,
|
|
1507
|
-
created_ts, updated_ts
|
|
1508
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'prepared', 'prepared', ?, ?)
|
|
1509
|
-
`).run(
|
|
1510
|
-
attemptId,
|
|
1584
|
+
return insertCodexPreparedCheckpoint({
|
|
1585
|
+
generation,
|
|
1511
1586
|
generationId,
|
|
1512
|
-
|
|
1587
|
+
attemptId,
|
|
1513
1588
|
method,
|
|
1514
1589
|
threadId,
|
|
1515
1590
|
turnId,
|
|
1591
|
+
requestId,
|
|
1592
|
+
itemId,
|
|
1516
1593
|
sourceId,
|
|
1517
1594
|
clientUserMessageId,
|
|
1595
|
+
detailJson,
|
|
1518
1596
|
ts,
|
|
1519
|
-
|
|
1520
|
-
);
|
|
1521
|
-
attempt = requireCodexAttempt(attemptId, generationId);
|
|
1597
|
+
});
|
|
1522
1598
|
} else if (kind === 'queued-send-cancelled' && attemptId && !attempt) {
|
|
1599
|
+
if (
|
|
1600
|
+
method !== 'queued/send'
|
|
1601
|
+
|| !threadId
|
|
1602
|
+
|| turnId != null
|
|
1603
|
+
|| !sourceId
|
|
1604
|
+
|| !clientUserMessageId
|
|
1605
|
+
) {
|
|
1606
|
+
throw codexError(
|
|
1607
|
+
'Codex queued cancellation requires exact pre-write identity',
|
|
1608
|
+
'CODEX_CHECKPOINT_INPUT_INVALID',
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1523
1611
|
db.prepare(`
|
|
1524
1612
|
INSERT INTO codex_turn_attempts (
|
|
1525
1613
|
attempt_id, generation_id, session_key, method,
|
|
@@ -1966,6 +2054,217 @@ function wrap(db) {
|
|
|
1966
2054
|
})();
|
|
1967
2055
|
}
|
|
1968
2056
|
|
|
2057
|
+
function recordCodexTurnStartPrepared({
|
|
2058
|
+
checkpoint,
|
|
2059
|
+
provider,
|
|
2060
|
+
expectedProviderGenerationId = null,
|
|
2061
|
+
} = {}) {
|
|
2062
|
+
if (
|
|
2063
|
+
checkpoint?.kind !== 'request-prepared'
|
|
2064
|
+
|| checkpoint?.method !== 'turn/start'
|
|
2065
|
+
) {
|
|
2066
|
+
throw codexError(
|
|
2067
|
+
'Codex turn preparation requires a turn/start prepared checkpoint',
|
|
2068
|
+
'CODEX_CHECKPOINT_INPUT_INVALID',
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
const generationId = requiredCodexString(
|
|
2072
|
+
checkpoint.generationId ?? checkpoint.generation_id,
|
|
2073
|
+
'generation ID',
|
|
2074
|
+
);
|
|
2075
|
+
const stableHostId = requiredCodexString(
|
|
2076
|
+
checkpoint.stable_host_id ?? checkpoint.hostIdentity,
|
|
2077
|
+
'stable host identity',
|
|
2078
|
+
);
|
|
2079
|
+
const bootSessionId = requiredCodexString(
|
|
2080
|
+
checkpoint.boot_session_id ?? checkpoint.bootSessionIdentity,
|
|
2081
|
+
'boot-session identity',
|
|
2082
|
+
);
|
|
2083
|
+
const attemptId = requiredCodexString(
|
|
2084
|
+
checkpoint.attemptId ?? checkpoint.attempt_id,
|
|
2085
|
+
'attempt ID',
|
|
2086
|
+
);
|
|
2087
|
+
const threadId = requiredCodexString(
|
|
2088
|
+
checkpoint.threadId ?? checkpoint.thread_id,
|
|
2089
|
+
'thread ID',
|
|
2090
|
+
);
|
|
2091
|
+
const sourceId = optionalCodexString(
|
|
2092
|
+
checkpoint.source,
|
|
2093
|
+
'source message ID',
|
|
2094
|
+
);
|
|
2095
|
+
const clientUserMessageId = requiredCodexString(
|
|
2096
|
+
checkpoint.clientUserMessageId ?? checkpoint.client_user_message_id,
|
|
2097
|
+
'client user message ID',
|
|
2098
|
+
);
|
|
2099
|
+
const turnId = optionalCodexString(
|
|
2100
|
+
checkpoint.turnId ?? checkpoint.turn_id,
|
|
2101
|
+
'turn ID',
|
|
2102
|
+
);
|
|
2103
|
+
const requestId = optionalCodexRequestId(
|
|
2104
|
+
checkpoint.requestId ?? checkpoint.request_id,
|
|
2105
|
+
);
|
|
2106
|
+
const itemId = optionalCodexString(
|
|
2107
|
+
checkpoint.itemId ?? checkpoint.item_id,
|
|
2108
|
+
'item ID',
|
|
2109
|
+
);
|
|
2110
|
+
const ts = requiredTimestamp(
|
|
2111
|
+
checkpoint.ts ?? Date.now(),
|
|
2112
|
+
'checkpoint timestamp',
|
|
2113
|
+
);
|
|
2114
|
+
const detailJson = checkpointDetail(checkpoint);
|
|
2115
|
+
const expectedLineage = expectedProviderGenerationId == null
|
|
2116
|
+
? null
|
|
2117
|
+
: requiredCodexString(
|
|
2118
|
+
expectedProviderGenerationId,
|
|
2119
|
+
'provider-session lineage',
|
|
2120
|
+
);
|
|
2121
|
+
const providerSession = providerSessionParams({
|
|
2122
|
+
...provider,
|
|
2123
|
+
session_key: requiredCodexString(provider?.session_key, 'session key'),
|
|
2124
|
+
cwd: requiredCodexString(provider?.cwd, 'provider cwd'),
|
|
2125
|
+
model: requiredCodexString(provider?.model, 'provider model'),
|
|
2126
|
+
effort: requiredCodexString(provider?.effort, 'provider effort'),
|
|
2127
|
+
spawn_profile_id: requiredCodexString(
|
|
2128
|
+
provider?.spawn_profile_id,
|
|
2129
|
+
'provider spawn profile ID',
|
|
2130
|
+
),
|
|
2131
|
+
});
|
|
2132
|
+
if (
|
|
2133
|
+
providerSession.namespace !== CODEX_APP_SERVER_NAMESPACE
|
|
2134
|
+
|| providerSession.provider !== 'codex'
|
|
2135
|
+
|| providerSession.pm_backend !== 'codex'
|
|
2136
|
+
|| providerSession.provider_session_id !== threadId
|
|
2137
|
+
|| providerSession.agent !== null
|
|
2138
|
+
) {
|
|
2139
|
+
throw codexError(
|
|
2140
|
+
'Codex turn preparation provider identity is inconsistent',
|
|
2141
|
+
'CODEX_CHECKPOINT_INPUT_INVALID',
|
|
2142
|
+
);
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
return db.transaction(() => {
|
|
2146
|
+
const generation = assertCodexGenerationIdentity(
|
|
2147
|
+
generationId,
|
|
2148
|
+
stableHostId,
|
|
2149
|
+
bootSessionId,
|
|
2150
|
+
);
|
|
2151
|
+
assertCodexCheckpointOwner(generation, 'turn-start-prepared');
|
|
2152
|
+
if (
|
|
2153
|
+
generation.session_key !== providerSession.session_key
|
|
2154
|
+
|| generation.thread_id !== threadId
|
|
2155
|
+
|| !db.prepare(`
|
|
2156
|
+
SELECT 1
|
|
2157
|
+
FROM codex_attempt_checkpoints
|
|
2158
|
+
WHERE generation_id = ?
|
|
2159
|
+
AND kind = 'thread-initialized'
|
|
2160
|
+
AND thread_id = ?
|
|
2161
|
+
LIMIT 1
|
|
2162
|
+
`).get(generationId, threadId)
|
|
2163
|
+
) {
|
|
2164
|
+
throw codexError(
|
|
2165
|
+
'Codex turn preparation does not match the initialized generation',
|
|
2166
|
+
'CODEX_CHECKPOINT_STALE_GENERATION',
|
|
2167
|
+
);
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
const currentProvider = getProviderSessionStmt.get(
|
|
2171
|
+
providerSession.session_key,
|
|
2172
|
+
CODEX_APP_SERVER_NAMESPACE,
|
|
2173
|
+
);
|
|
2174
|
+
const attempt = db.prepare(`
|
|
2175
|
+
SELECT * FROM codex_turn_attempts WHERE attempt_id = ?
|
|
2176
|
+
`).get(attemptId);
|
|
2177
|
+
if (attempt) {
|
|
2178
|
+
assertCodexAttemptIdentity(attempt, {
|
|
2179
|
+
method: 'turn/start',
|
|
2180
|
+
thread_id: threadId,
|
|
2181
|
+
telegram_source_message_id: sourceId,
|
|
2182
|
+
client_user_message_id: clientUserMessageId,
|
|
2183
|
+
});
|
|
2184
|
+
const exactCheckpoint = hasExactCodexCheckpoint({
|
|
2185
|
+
generationId,
|
|
2186
|
+
attemptId,
|
|
2187
|
+
kind: 'request-prepared',
|
|
2188
|
+
threadId,
|
|
2189
|
+
turnId: null,
|
|
2190
|
+
requestId: null,
|
|
2191
|
+
itemId: null,
|
|
2192
|
+
detailJson,
|
|
2193
|
+
});
|
|
2194
|
+
if (!exactCheckpoint) {
|
|
2195
|
+
throw codexError(
|
|
2196
|
+
'Codex attempt ID was reused with different metadata',
|
|
2197
|
+
'CODEX_ATTEMPT_ID_REUSED',
|
|
2198
|
+
);
|
|
2199
|
+
}
|
|
2200
|
+
if (
|
|
2201
|
+
!hasExactCodexProviderIdentity(currentProvider, providerSession)
|
|
2202
|
+
|| (
|
|
2203
|
+
expectedLineage !== null
|
|
2204
|
+
&& currentProvider.generation_id !== expectedLineage
|
|
2205
|
+
)
|
|
2206
|
+
) {
|
|
2207
|
+
throw codexError(
|
|
2208
|
+
'Codex prepared turn replay no longer matches provider identity',
|
|
2209
|
+
expectedLineage === null
|
|
2210
|
+
? 'CODEX_TURN_PREPARATION_STALE'
|
|
2211
|
+
: 'CODEX_PROVIDER_LINEAGE_MISMATCH',
|
|
2212
|
+
);
|
|
2213
|
+
}
|
|
2214
|
+
return {
|
|
2215
|
+
changes: 0,
|
|
2216
|
+
attemptId,
|
|
2217
|
+
kind: 'request-prepared',
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
if (
|
|
2222
|
+
expectedLineage !== null
|
|
2223
|
+
&& (
|
|
2224
|
+
!hasExactCodexProviderIdentity(currentProvider, providerSession)
|
|
2225
|
+
|| currentProvider.generation_id !== expectedLineage
|
|
2226
|
+
)
|
|
2227
|
+
) {
|
|
2228
|
+
throw codexError(
|
|
2229
|
+
'Codex strict resume provider lineage changed before turn preparation',
|
|
2230
|
+
'CODEX_PROVIDER_LINEAGE_MISMATCH',
|
|
2231
|
+
);
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
upsertProviderSessionStmt.run(providerSession);
|
|
2235
|
+
const checkpointResult = insertCodexPreparedCheckpoint({
|
|
2236
|
+
generation,
|
|
2237
|
+
generationId,
|
|
2238
|
+
attemptId,
|
|
2239
|
+
method: 'turn/start',
|
|
2240
|
+
threadId,
|
|
2241
|
+
turnId,
|
|
2242
|
+
requestId,
|
|
2243
|
+
itemId,
|
|
2244
|
+
sourceId,
|
|
2245
|
+
clientUserMessageId,
|
|
2246
|
+
detailJson,
|
|
2247
|
+
ts,
|
|
2248
|
+
});
|
|
2249
|
+
if (expectedLineage !== null) {
|
|
2250
|
+
const committedProvider = getProviderSessionStmt.get(
|
|
2251
|
+
providerSession.session_key,
|
|
2252
|
+
CODEX_APP_SERVER_NAMESPACE,
|
|
2253
|
+
);
|
|
2254
|
+
if (
|
|
2255
|
+
!hasExactCodexProviderIdentity(committedProvider, providerSession)
|
|
2256
|
+
|| committedProvider.generation_id !== expectedLineage
|
|
2257
|
+
) {
|
|
2258
|
+
throw codexError(
|
|
2259
|
+
'Codex strict resume provider lineage rotated during turn preparation',
|
|
2260
|
+
'CODEX_PROVIDER_LINEAGE_MISMATCH',
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
return checkpointResult;
|
|
2265
|
+
})();
|
|
2266
|
+
}
|
|
2267
|
+
|
|
1969
2268
|
function isCleanRetirementDelivery(attempt) {
|
|
1970
2269
|
if (
|
|
1971
2270
|
attempt?.method !== 'turn/start'
|
|
@@ -2292,6 +2591,77 @@ function wrap(db) {
|
|
|
2292
2591
|
};
|
|
2293
2592
|
}
|
|
2294
2593
|
|
|
2594
|
+
function cleanRestartCancellationProof(attempt) {
|
|
2595
|
+
if (
|
|
2596
|
+
!attempt
|
|
2597
|
+
|| attempt.delivery_state !== 'prepared'
|
|
2598
|
+
|| attempt.recovery_state !== 'cancelled'
|
|
2599
|
+
|| attempt.turn_id != null
|
|
2600
|
+
|| attempt.terminal_status != null
|
|
2601
|
+
|| !['turn/start', 'queued/send'].includes(attempt.method)
|
|
2602
|
+
|| typeof attempt.thread_id !== 'string'
|
|
2603
|
+
|| attempt.thread_id.length === 0
|
|
2604
|
+
|| typeof attempt.telegram_source_message_id !== 'string'
|
|
2605
|
+
|| attempt.telegram_source_message_id.length === 0
|
|
2606
|
+
|| typeof attempt.client_user_message_id !== 'string'
|
|
2607
|
+
|| attempt.client_user_message_id.length === 0
|
|
2608
|
+
) {
|
|
2609
|
+
return null;
|
|
2610
|
+
}
|
|
2611
|
+
const expectedKind = attempt.method === 'turn/start'
|
|
2612
|
+
? 'active-start-cancelled'
|
|
2613
|
+
: 'queued-send-cancelled';
|
|
2614
|
+
const checkpoints = db.prepare(`
|
|
2615
|
+
SELECT kind, thread_id, turn_id, detail_json
|
|
2616
|
+
FROM codex_attempt_checkpoints
|
|
2617
|
+
WHERE generation_id = ?
|
|
2618
|
+
AND attempt_id = ?
|
|
2619
|
+
ORDER BY id
|
|
2620
|
+
LIMIT 3
|
|
2621
|
+
`).all(attempt.generation_id, attempt.attempt_id);
|
|
2622
|
+
const allowedKinds = attempt.method === 'turn/start'
|
|
2623
|
+
? new Set(['request-prepared', expectedKind])
|
|
2624
|
+
: new Set([expectedKind]);
|
|
2625
|
+
if (
|
|
2626
|
+
checkpoints.some((checkpoint) => !allowedKinds.has(checkpoint.kind))
|
|
2627
|
+
|| checkpoints.filter((checkpoint) => checkpoint.kind === expectedKind)
|
|
2628
|
+
.length !== 1
|
|
2629
|
+
|| (
|
|
2630
|
+
attempt.method === 'turn/start'
|
|
2631
|
+
&& checkpoints.filter((checkpoint) => checkpoint.kind === 'request-prepared')
|
|
2632
|
+
.length !== 1
|
|
2633
|
+
)
|
|
2634
|
+
) {
|
|
2635
|
+
return null;
|
|
2636
|
+
}
|
|
2637
|
+
const cancellation = checkpoints.find(
|
|
2638
|
+
(checkpoint) => checkpoint.kind === expectedKind,
|
|
2639
|
+
);
|
|
2640
|
+
let detail;
|
|
2641
|
+
try {
|
|
2642
|
+
detail = JSON.parse(cancellation.detail_json);
|
|
2643
|
+
} catch {
|
|
2644
|
+
return null;
|
|
2645
|
+
}
|
|
2646
|
+
if (
|
|
2647
|
+
detail == null
|
|
2648
|
+
|| Array.isArray(detail)
|
|
2649
|
+
|| typeof detail !== 'object'
|
|
2650
|
+
|| detail.reason !== 'clean-restart'
|
|
2651
|
+
|| cancellation.thread_id !== attempt.thread_id
|
|
2652
|
+
|| cancellation.turn_id != null
|
|
2653
|
+
|| detail.method !== attempt.method
|
|
2654
|
+
|| detail.source !== attempt.telegram_source_message_id
|
|
2655
|
+
|| detail.clientUserMessageId !== attempt.client_user_message_id
|
|
2656
|
+
) {
|
|
2657
|
+
return null;
|
|
2658
|
+
}
|
|
2659
|
+
return {
|
|
2660
|
+
kind: expectedKind,
|
|
2661
|
+
reason: 'clean-restart',
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2295
2665
|
function getReplayProviderRecovery(input) {
|
|
2296
2666
|
const allowed = new Set([
|
|
2297
2667
|
'sessionKey',
|
|
@@ -2338,11 +2708,10 @@ function wrap(db) {
|
|
|
2338
2708
|
const attempts = db.prepare(`
|
|
2339
2709
|
SELECT attempt.*
|
|
2340
2710
|
FROM codex_turn_attempts attempt
|
|
2341
|
-
|
|
2342
|
-
ON generation.generation_id = attempt.generation_id
|
|
2343
|
-
WHERE generation.session_key = ?
|
|
2711
|
+
WHERE attempt.session_key = ?
|
|
2344
2712
|
AND attempt.telegram_source_message_id = ?
|
|
2345
2713
|
ORDER BY attempt.created_ts, attempt.attempt_id
|
|
2714
|
+
LIMIT 2
|
|
2346
2715
|
`).all(sessionKey, telegramMessageId);
|
|
2347
2716
|
const linkedInputs = db.prepare(`
|
|
2348
2717
|
SELECT linked.*
|
|
@@ -2353,6 +2722,7 @@ function wrap(db) {
|
|
|
2353
2722
|
AND linked.telegram_chat_id = ?
|
|
2354
2723
|
AND linked.telegram_message_id = ?
|
|
2355
2724
|
ORDER BY linked.created_ts, linked.linked_input_id
|
|
2725
|
+
LIMIT 2
|
|
2356
2726
|
`).all(sessionKey, telegramChatId, telegramMessageId);
|
|
2357
2727
|
const hasCodexEvidence = (
|
|
2358
2728
|
reservations.length > 0
|
|
@@ -2393,7 +2763,14 @@ function wrap(db) {
|
|
|
2393
2763
|
}
|
|
2394
2764
|
if (
|
|
2395
2765
|
attempt
|
|
2396
|
-
&& !['turn/start', 'turn/steer'].includes(attempt.method)
|
|
2766
|
+
&& !['turn/start', 'turn/steer', 'queued/send'].includes(attempt.method)
|
|
2767
|
+
) {
|
|
2768
|
+
return unknown('codex-evidence-conflict');
|
|
2769
|
+
}
|
|
2770
|
+
if (
|
|
2771
|
+
reservation
|
|
2772
|
+
&& ['reserved', 'queue-authorized'].includes(reservation.state)
|
|
2773
|
+
&& reservation.settled_ts != null
|
|
2397
2774
|
) {
|
|
2398
2775
|
return unknown('codex-evidence-conflict');
|
|
2399
2776
|
}
|
|
@@ -2454,17 +2831,26 @@ function wrap(db) {
|
|
|
2454
2831
|
if (reservation.target_attempt_id != null) {
|
|
2455
2832
|
targetAttempt = attempt;
|
|
2456
2833
|
}
|
|
2457
|
-
if (attempt
|
|
2458
|
-
|
|
2834
|
+
if (attempt?.method === 'queued/send') {
|
|
2835
|
+
kind = 'queued-send';
|
|
2836
|
+
} else {
|
|
2837
|
+
if (attempt && attempt.method !== 'turn/start') {
|
|
2838
|
+
return unknown('codex-evidence-conflict');
|
|
2839
|
+
}
|
|
2840
|
+
kind = 'dispatch-reservation';
|
|
2459
2841
|
}
|
|
2460
|
-
kind = 'dispatch-reservation';
|
|
2461
2842
|
} else if (attempt) {
|
|
2462
|
-
if (attempt.method
|
|
2843
|
+
if (attempt.method === 'queued/send') {
|
|
2844
|
+
kind = 'queued-send';
|
|
2845
|
+
} else if (attempt.method !== 'turn/start') {
|
|
2463
2846
|
return unknown('codex-evidence-conflict');
|
|
2847
|
+
} else {
|
|
2848
|
+
kind = 'primary-turn';
|
|
2464
2849
|
}
|
|
2465
|
-
kind = 'primary-turn';
|
|
2466
2850
|
}
|
|
2467
2851
|
|
|
2852
|
+
const cancellationProof = cleanRestartCancellationProof(attempt);
|
|
2853
|
+
|
|
2468
2854
|
return {
|
|
2469
2855
|
provider: 'codex',
|
|
2470
2856
|
kind,
|
|
@@ -2474,6 +2860,7 @@ function wrap(db) {
|
|
|
2474
2860
|
reservationId: reservation.reservation_id,
|
|
2475
2861
|
generationId: reservation.generation_id,
|
|
2476
2862
|
state: reservation.state,
|
|
2863
|
+
steerAttemptId: reservation.steer_attempt_id,
|
|
2477
2864
|
targetAttemptId: reservation.target_attempt_id,
|
|
2478
2865
|
}
|
|
2479
2866
|
: null,
|
|
@@ -2487,9 +2874,237 @@ function wrap(db) {
|
|
|
2487
2874
|
}
|
|
2488
2875
|
: null,
|
|
2489
2876
|
targetAttempt: publicCodexAttempt(targetAttempt),
|
|
2877
|
+
...(cancellationProof ? { cancellationProof } : {}),
|
|
2490
2878
|
};
|
|
2491
2879
|
}
|
|
2492
2880
|
|
|
2881
|
+
function prepareCodexCleanReplay(input = {}) {
|
|
2882
|
+
assertExactKeys(
|
|
2883
|
+
input,
|
|
2884
|
+
new Set([
|
|
2885
|
+
'source',
|
|
2886
|
+
'sessionKey',
|
|
2887
|
+
'currentGeneration',
|
|
2888
|
+
'expectedEvidence',
|
|
2889
|
+
'owner',
|
|
2890
|
+
'ts',
|
|
2891
|
+
]),
|
|
2892
|
+
'CODEX_CLEAN_REPLAY_PAYLOAD_REJECTED',
|
|
2893
|
+
'clean replay',
|
|
2894
|
+
);
|
|
2895
|
+
assertExactKeys(
|
|
2896
|
+
input.source,
|
|
2897
|
+
new Set(['botName', 'telegramChatId', 'telegramMessageId']),
|
|
2898
|
+
'CODEX_CLEAN_REPLAY_PAYLOAD_REJECTED',
|
|
2899
|
+
'clean replay source',
|
|
2900
|
+
);
|
|
2901
|
+
assertExactKeys(
|
|
2902
|
+
input.currentGeneration,
|
|
2903
|
+
new Set(['generationId']),
|
|
2904
|
+
'CODEX_CLEAN_REPLAY_PAYLOAD_REJECTED',
|
|
2905
|
+
'clean replay generation',
|
|
2906
|
+
);
|
|
2907
|
+
assertExactKeys(
|
|
2908
|
+
input.owner,
|
|
2909
|
+
new Set(['stableHostId', 'bootSessionId']),
|
|
2910
|
+
'CODEX_CLEAN_REPLAY_PAYLOAD_REJECTED',
|
|
2911
|
+
'clean replay owner',
|
|
2912
|
+
);
|
|
2913
|
+
const sessionKey = requiredCodexString(
|
|
2914
|
+
input.sessionKey,
|
|
2915
|
+
'clean replay session key',
|
|
2916
|
+
);
|
|
2917
|
+
const botName = requiredCodexString(
|
|
2918
|
+
input.source?.botName,
|
|
2919
|
+
'clean replay bot name',
|
|
2920
|
+
);
|
|
2921
|
+
const telegramChatId = requiredCodexString(
|
|
2922
|
+
input.source?.telegramChatId,
|
|
2923
|
+
'clean replay Telegram chat ID',
|
|
2924
|
+
);
|
|
2925
|
+
const telegramMessageId = requiredCodexString(
|
|
2926
|
+
input.source?.telegramMessageId,
|
|
2927
|
+
'clean replay Telegram message ID',
|
|
2928
|
+
);
|
|
2929
|
+
const generationId = requiredCodexString(
|
|
2930
|
+
input.currentGeneration?.generationId,
|
|
2931
|
+
'clean replay generation ID',
|
|
2932
|
+
);
|
|
2933
|
+
const stableHostId = requiredCodexString(
|
|
2934
|
+
input.owner?.stableHostId,
|
|
2935
|
+
'clean replay stable host identity',
|
|
2936
|
+
);
|
|
2937
|
+
const bootSessionId = requiredCodexString(
|
|
2938
|
+
input.owner?.bootSessionId,
|
|
2939
|
+
'clean replay boot-session identity',
|
|
2940
|
+
);
|
|
2941
|
+
const ts = requiredTimestamp(
|
|
2942
|
+
input.ts ?? Date.now(),
|
|
2943
|
+
'clean replay timestamp',
|
|
2944
|
+
);
|
|
2945
|
+
const expectedEvidence = input.expectedEvidence;
|
|
2946
|
+
if (expectedEvidence == null || typeof expectedEvidence !== 'object') {
|
|
2947
|
+
throw codexError(
|
|
2948
|
+
'Codex clean replay expected evidence is required',
|
|
2949
|
+
'CODEX_CLEAN_REPLAY_EVIDENCE_INVALID',
|
|
2950
|
+
);
|
|
2951
|
+
}
|
|
2952
|
+
const reservationId = codexDispatchReservationId({
|
|
2953
|
+
botName,
|
|
2954
|
+
telegramChatId,
|
|
2955
|
+
telegramMessageId,
|
|
2956
|
+
});
|
|
2957
|
+
|
|
2958
|
+
return db.transaction(() => {
|
|
2959
|
+
const actualEvidence = getReplayProviderRecovery({
|
|
2960
|
+
sessionKey,
|
|
2961
|
+
botName,
|
|
2962
|
+
telegramChatId,
|
|
2963
|
+
telegramMessageId,
|
|
2964
|
+
});
|
|
2965
|
+
const disposition = classifyCodexRecoveryEvidence(actualEvidence);
|
|
2966
|
+
if (
|
|
2967
|
+
!isDeepStrictEqual(actualEvidence, expectedEvidence)
|
|
2968
|
+
|| disposition.action !== 'recover'
|
|
2969
|
+
|| disposition.cleanRestartSafe !== true
|
|
2970
|
+
) {
|
|
2971
|
+
throw codexError(
|
|
2972
|
+
'Codex clean replay evidence changed or is not safe',
|
|
2973
|
+
'CODEX_CLEAN_REPLAY_EVIDENCE_CHANGED',
|
|
2974
|
+
);
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
const generation = db.prepare(`
|
|
2978
|
+
SELECT *
|
|
2979
|
+
FROM codex_generations
|
|
2980
|
+
WHERE generation_id = ?
|
|
2981
|
+
`).get(generationId);
|
|
2982
|
+
const lease = db.prepare(`
|
|
2983
|
+
SELECT * FROM codex_daemon_lease WHERE singleton = 1
|
|
2984
|
+
`).get();
|
|
2985
|
+
if (
|
|
2986
|
+
!generation
|
|
2987
|
+
|| generation.session_key !== sessionKey
|
|
2988
|
+
|| generation.stable_host_id !== stableHostId
|
|
2989
|
+
|| generation.boot_session_id !== bootSessionId
|
|
2990
|
+
|| generation.state !== 'active'
|
|
2991
|
+
|| lease?.generation_id !== generationId
|
|
2992
|
+
|| lease.stable_host_id !== stableHostId
|
|
2993
|
+
|| lease.boot_session_id !== bootSessionId
|
|
2994
|
+
|| lease.status !== 'active'
|
|
2995
|
+
) {
|
|
2996
|
+
throw codexError(
|
|
2997
|
+
'Codex clean replay generation does not own the active lease',
|
|
2998
|
+
'CODEX_CLEAN_REPLAY_GENERATION_MISMATCH',
|
|
2999
|
+
);
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
const inboundRows = db.prepare(`
|
|
3003
|
+
SELECT id, handler_status
|
|
3004
|
+
FROM messages
|
|
3005
|
+
WHERE direction = 'in'
|
|
3006
|
+
AND bot_name = ?
|
|
3007
|
+
AND chat_id = ?
|
|
3008
|
+
AND CAST(msg_id AS TEXT) = ?
|
|
3009
|
+
`).all(botName, telegramChatId, telegramMessageId);
|
|
3010
|
+
if (
|
|
3011
|
+
inboundRows.length !== 1
|
|
3012
|
+
|| inboundRows[0].handler_status !== 'replay-pending'
|
|
3013
|
+
) {
|
|
3014
|
+
throw codexError(
|
|
3015
|
+
'Codex clean replay no longer owns one replay-pending input',
|
|
3016
|
+
'CODEX_CLEAN_REPLAY_INBOUND_MISMATCH',
|
|
3017
|
+
);
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
const reservationRows = db.prepare(`
|
|
3021
|
+
SELECT *
|
|
3022
|
+
FROM codex_dispatch_reservations
|
|
3023
|
+
WHERE reservation_id = ?
|
|
3024
|
+
OR (
|
|
3025
|
+
bot_name = ?
|
|
3026
|
+
AND telegram_chat_id = ?
|
|
3027
|
+
AND telegram_message_id = ?
|
|
3028
|
+
)
|
|
3029
|
+
ORDER BY reservation_id
|
|
3030
|
+
`).all(
|
|
3031
|
+
reservationId,
|
|
3032
|
+
botName,
|
|
3033
|
+
telegramChatId,
|
|
3034
|
+
telegramMessageId,
|
|
3035
|
+
);
|
|
3036
|
+
if (reservationRows.length === 0) {
|
|
3037
|
+
db.prepare(`
|
|
3038
|
+
INSERT INTO codex_dispatch_reservations (
|
|
3039
|
+
reservation_id, generation_id, session_key,
|
|
3040
|
+
bot_name, telegram_chat_id, telegram_message_id, state,
|
|
3041
|
+
steer_attempt_id, target_attempt_id,
|
|
3042
|
+
created_ts, updated_ts, settled_ts
|
|
3043
|
+
) VALUES (?, ?, ?, ?, ?, ?, 'reserved', NULL, NULL, ?, ?, NULL)
|
|
3044
|
+
`).run(
|
|
3045
|
+
reservationId,
|
|
3046
|
+
generationId,
|
|
3047
|
+
sessionKey,
|
|
3048
|
+
botName,
|
|
3049
|
+
telegramChatId,
|
|
3050
|
+
telegramMessageId,
|
|
3051
|
+
ts,
|
|
3052
|
+
ts,
|
|
3053
|
+
);
|
|
3054
|
+
} else {
|
|
3055
|
+
const reservation = reservationRows[0];
|
|
3056
|
+
if (
|
|
3057
|
+
reservationRows.length !== 1
|
|
3058
|
+
|| reservation.reservation_id !== reservationId
|
|
3059
|
+
|| reservation.session_key !== sessionKey
|
|
3060
|
+
|| reservation.bot_name !== botName
|
|
3061
|
+
|| reservation.telegram_chat_id !== telegramChatId
|
|
3062
|
+
|| reservation.telegram_message_id !== telegramMessageId
|
|
3063
|
+
) {
|
|
3064
|
+
throw codexError(
|
|
3065
|
+
'Codex clean replay reservation identity conflicts',
|
|
3066
|
+
'CODEX_CLEAN_REPLAY_RESERVATION_CONFLICT',
|
|
3067
|
+
);
|
|
3068
|
+
}
|
|
3069
|
+
const rebound = db.prepare(`
|
|
3070
|
+
UPDATE codex_dispatch_reservations
|
|
3071
|
+
SET generation_id = ?,
|
|
3072
|
+
state = 'reserved',
|
|
3073
|
+
steer_attempt_id = NULL,
|
|
3074
|
+
target_attempt_id = NULL,
|
|
3075
|
+
updated_ts = ?,
|
|
3076
|
+
settled_ts = NULL
|
|
3077
|
+
WHERE reservation_id = ?
|
|
3078
|
+
`).run(generationId, ts, reservationId);
|
|
3079
|
+
if (rebound.changes !== 1) {
|
|
3080
|
+
throw codexError(
|
|
3081
|
+
'Codex clean replay reservation could not be rebound',
|
|
3082
|
+
'CODEX_CLEAN_REPLAY_RESERVATION_CONFLICT',
|
|
3083
|
+
);
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
const consumed = db.prepare(`
|
|
3088
|
+
UPDATE messages
|
|
3089
|
+
SET handler_status = 'replay-attempted'
|
|
3090
|
+
WHERE id = ?
|
|
3091
|
+
AND handler_status = 'replay-pending'
|
|
3092
|
+
`).run(inboundRows[0].id);
|
|
3093
|
+
if (consumed.changes !== 1) {
|
|
3094
|
+
throw codexError(
|
|
3095
|
+
'Codex clean replay input could not be consumed',
|
|
3096
|
+
'CODEX_CLEAN_REPLAY_INBOUND_MISMATCH',
|
|
3097
|
+
);
|
|
3098
|
+
}
|
|
3099
|
+
const reservation = readCodexDispatchReservation(reservationId);
|
|
3100
|
+
return {
|
|
3101
|
+
reservationId,
|
|
3102
|
+
generationId,
|
|
3103
|
+
reservation,
|
|
3104
|
+
};
|
|
3105
|
+
})();
|
|
3106
|
+
}
|
|
3107
|
+
|
|
2493
3108
|
function assertCodexDispatchReservationIdentity(
|
|
2494
3109
|
reservation,
|
|
2495
3110
|
generationId,
|
|
@@ -4117,6 +4732,7 @@ function wrap(db) {
|
|
|
4117
4732
|
createCodexGeneration,
|
|
4118
4733
|
prepareCodexCleanRetirement,
|
|
4119
4734
|
recordCodexCheckpoint,
|
|
4735
|
+
recordCodexTurnStartPrepared,
|
|
4120
4736
|
recordCodexDeliveryCheckpoint,
|
|
4121
4737
|
getCodexAttempt(attemptId) {
|
|
4122
4738
|
return db.prepare(`
|
|
@@ -4134,6 +4750,7 @@ function wrap(db) {
|
|
|
4134
4750
|
claimCodexDispatchReservation,
|
|
4135
4751
|
recordInboundRuntimeSelection,
|
|
4136
4752
|
getReplayProviderRecovery,
|
|
4753
|
+
prepareCodexCleanReplay,
|
|
4137
4754
|
getCodexDispatchReservation(reservationId) {
|
|
4138
4755
|
return readCodexDispatchReservation(requiredCodexString(
|
|
4139
4756
|
reservationId,
|
|
@@ -4517,7 +5134,7 @@ function wrap(db) {
|
|
|
4517
5134
|
AND handler_status IN ('dispatched', 'processing', 'replay-pending')
|
|
4518
5135
|
AND chat_id IN (${placeholders})
|
|
4519
5136
|
AND ts > ?
|
|
4520
|
-
ORDER BY ts ASC
|
|
5137
|
+
ORDER BY ts ASC, id ASC
|
|
4521
5138
|
LIMIT ?
|
|
4522
5139
|
`).all(...chatIds, cutoff, limit);
|
|
4523
5140
|
},
|