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/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 = 20;
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 (['request-prepared', 'queued-send-cancelled'].includes(kind)) return;
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
- db.prepare(`
1503
- INSERT INTO codex_turn_attempts (
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
- generation.session_key,
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
- ts,
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
- JOIN codex_generations generation
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 && attempt.method !== 'turn/start') {
2458
- return unknown('codex-evidence-conflict');
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 !== 'turn/start') {
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
  },