polygram 0.38.4 → 0.38.6
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 +8 -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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "polygram",
|
|
4
|
-
"version": "0.38.
|
|
4
|
+
"version": "0.38.6",
|
|
5
5
|
"description": "Telegram integration for Claude Code that preserves the OpenClaw per-chat session model. Migration target for OpenClaw users. Multi-bot, multi-chat, per-topic isolation; SQLite transcripts; inline-keyboard approvals. Bundles /polygram:status|logs|pair-code|approvals admin commands plus history (transcript queries) and polygram-send (out-of-turn IPC sends with file-upload validation) skills.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"telegram",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function changedProcessError() {
|
|
4
|
+
const error = new Error(
|
|
5
|
+
'Clean Codex replay no longer owns the process that rearmed its input',
|
|
6
|
+
);
|
|
7
|
+
error.code = 'CODEX_CLEAN_REPLAY_PROCESS_CHANGED';
|
|
8
|
+
return error;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function authorizeCleanReplayDispatch({
|
|
12
|
+
sessionKey,
|
|
13
|
+
currentProcess,
|
|
14
|
+
expectedProcess,
|
|
15
|
+
reservation,
|
|
16
|
+
controller,
|
|
17
|
+
} = {}) {
|
|
18
|
+
const generationId = reservation?.generationId;
|
|
19
|
+
const reservationId = reservation?.reservationId;
|
|
20
|
+
if (
|
|
21
|
+
typeof sessionKey !== 'string'
|
|
22
|
+
|| sessionKey.length === 0
|
|
23
|
+
|| currentProcess !== expectedProcess
|
|
24
|
+
|| !currentProcess
|
|
25
|
+
|| currentProcess.closed === true
|
|
26
|
+
|| (currentProcess.runtime ?? currentProcess.backend) !== 'codex'
|
|
27
|
+
|| !['Active', 'Idle', 'StartingTurn'].includes(currentProcess.state)
|
|
28
|
+
|| typeof generationId !== 'string'
|
|
29
|
+
|| generationId.length === 0
|
|
30
|
+
|| currentProcess.generationId !== generationId
|
|
31
|
+
|| typeof reservationId !== 'string'
|
|
32
|
+
|| reservationId.length === 0
|
|
33
|
+
|| typeof controller?.markDispatchDisposition !== 'function'
|
|
34
|
+
) {
|
|
35
|
+
throw changedProcessError();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
controller.markDispatchDisposition({
|
|
39
|
+
sessionKey,
|
|
40
|
+
generationId,
|
|
41
|
+
reservationId,
|
|
42
|
+
disposition: 'queue-authorized',
|
|
43
|
+
});
|
|
44
|
+
return Object.freeze({
|
|
45
|
+
reservationId,
|
|
46
|
+
generationId,
|
|
47
|
+
state: 'queue-authorized',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { authorizeCleanReplayDispatch };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createHash } = require('node:crypto');
|
|
4
|
+
|
|
5
|
+
function codexDispatchReservationId({
|
|
6
|
+
botName,
|
|
7
|
+
telegramChatId,
|
|
8
|
+
telegramMessageId,
|
|
9
|
+
}) {
|
|
10
|
+
const digest = createHash('sha256')
|
|
11
|
+
.update(JSON.stringify({
|
|
12
|
+
botName,
|
|
13
|
+
telegramChatId,
|
|
14
|
+
telegramMessageId,
|
|
15
|
+
}))
|
|
16
|
+
.digest('hex');
|
|
17
|
+
return `codex-dispatch:v1:${digest}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { codexDispatchReservationId };
|
|
@@ -18,6 +18,10 @@ const {
|
|
|
18
18
|
resolveCodexRuntimeRequest,
|
|
19
19
|
resolveRuntimeConfig,
|
|
20
20
|
} = require('../runtime-config');
|
|
21
|
+
const { CODEX_APP_SERVER_NAMESPACE } = require('../db/sessions');
|
|
22
|
+
const {
|
|
23
|
+
codexDispatchReservationId,
|
|
24
|
+
} = require('./dispatch-reservation-id');
|
|
21
25
|
|
|
22
26
|
const DEFAULT_CATALOG_MAX_AGE_MS = 15 * 60 * 1_000;
|
|
23
27
|
|
|
@@ -734,7 +738,9 @@ function createCodexRuntimeController({
|
|
|
734
738
|
return processReplacementReason(sessionKey, proc) !== null;
|
|
735
739
|
}
|
|
736
740
|
|
|
737
|
-
function registerProcess(proc
|
|
741
|
+
function registerProcess(proc, {
|
|
742
|
+
expectedProviderGenerationId = null,
|
|
743
|
+
} = {}) {
|
|
738
744
|
if (
|
|
739
745
|
proc?.runtime !== 'codex'
|
|
740
746
|
|| typeof proc.generationId !== 'string'
|
|
@@ -742,6 +748,21 @@ function createCodexRuntimeController({
|
|
|
742
748
|
) {
|
|
743
749
|
throw new TypeError('Codex runtime controller requires a Codex process');
|
|
744
750
|
}
|
|
751
|
+
const strictResume = (
|
|
752
|
+
proc.spawnOptions?.resumePolicy === 'require-interrupted-turn'
|
|
753
|
+
);
|
|
754
|
+
if (
|
|
755
|
+
(strictResume && (
|
|
756
|
+
typeof expectedProviderGenerationId !== 'string'
|
|
757
|
+
|| expectedProviderGenerationId.length === 0
|
|
758
|
+
|| Buffer.byteLength(expectedProviderGenerationId) > 4096
|
|
759
|
+
))
|
|
760
|
+
|| (!strictResume && expectedProviderGenerationId !== null)
|
|
761
|
+
) {
|
|
762
|
+
throw new TypeError(
|
|
763
|
+
'Codex provider lineage is valid only for an exact strict resume',
|
|
764
|
+
);
|
|
765
|
+
}
|
|
745
766
|
const receipt = resolveReceipt(proc.sessionKey, {
|
|
746
767
|
runtime: 'codex',
|
|
747
768
|
spawnProfileId: proc.spawnProfileId,
|
|
@@ -765,6 +786,8 @@ function createCodexRuntimeController({
|
|
|
765
786
|
retirementRequested: false,
|
|
766
787
|
retirementWaiters: [],
|
|
767
788
|
retired: false,
|
|
789
|
+
expectedProviderGenerationId,
|
|
790
|
+
strictResumeAttemptId: null,
|
|
768
791
|
credentialStateFingerprint: receiptsBySession
|
|
769
792
|
.get(proc.sessionKey)
|
|
770
793
|
.credentialStateFingerprint,
|
|
@@ -852,18 +875,6 @@ function createCodexRuntimeController({
|
|
|
852
875
|
};
|
|
853
876
|
}
|
|
854
877
|
|
|
855
|
-
function dispatchReservationId({
|
|
856
|
-
botName,
|
|
857
|
-
telegramChatId,
|
|
858
|
-
telegramMessageId,
|
|
859
|
-
}) {
|
|
860
|
-
return `codex-dispatch:v1:${digest({
|
|
861
|
-
botName,
|
|
862
|
-
telegramChatId,
|
|
863
|
-
telegramMessageId,
|
|
864
|
-
})}`;
|
|
865
|
-
}
|
|
866
|
-
|
|
867
878
|
function claimDispatchReservation({
|
|
868
879
|
sessionKey,
|
|
869
880
|
generationId,
|
|
@@ -881,7 +892,7 @@ function createCodexRuntimeController({
|
|
|
881
892
|
telegramMessageId,
|
|
882
893
|
'Telegram message ID',
|
|
883
894
|
);
|
|
884
|
-
const reservationId =
|
|
895
|
+
const reservationId = codexDispatchReservationId({
|
|
885
896
|
botName: exactBotName,
|
|
886
897
|
telegramChatId: exactChatId,
|
|
887
898
|
telegramMessageId: exactMessageId,
|
|
@@ -982,7 +993,7 @@ function createCodexRuntimeController({
|
|
|
982
993
|
reservationId,
|
|
983
994
|
'dispatch reservation ID',
|
|
984
995
|
);
|
|
985
|
-
if (exactReservationId !==
|
|
996
|
+
if (exactReservationId !== codexDispatchReservationId({
|
|
986
997
|
botName: exactBotName,
|
|
987
998
|
telegramChatId: exactChatId,
|
|
988
999
|
telegramMessageId: exactMessageId,
|
|
@@ -1172,7 +1183,10 @@ function createCodexRuntimeController({
|
|
|
1172
1183
|
}
|
|
1173
1184
|
|
|
1174
1185
|
function activateWithPreparedCheckpoint(record, payload) {
|
|
1175
|
-
if (
|
|
1186
|
+
if (
|
|
1187
|
+
payload.kind !== 'request-prepared'
|
|
1188
|
+
|| !['thread/start', 'thread/resume'].includes(payload.method)
|
|
1189
|
+
) {
|
|
1176
1190
|
throw controllerError(
|
|
1177
1191
|
'Codex generation reached durability out of order.',
|
|
1178
1192
|
'CODEX_GENERATION_NOT_DURABLE',
|
|
@@ -1205,6 +1219,100 @@ function createCodexRuntimeController({
|
|
|
1205
1219
|
record.durable = true;
|
|
1206
1220
|
}
|
|
1207
1221
|
|
|
1222
|
+
function persistPreparedTurnIdentity(record, payload, state) {
|
|
1223
|
+
const proc = record.proc;
|
|
1224
|
+
const pending = proc.current;
|
|
1225
|
+
const settings = pending?.admittedSettings;
|
|
1226
|
+
const sourceMsgId = pending?.context?.sourceMsgId ?? null;
|
|
1227
|
+
const starting = (
|
|
1228
|
+
proc.state === 'StartingTurn'
|
|
1229
|
+
&& proc.settingsAdmissionClosed === false
|
|
1230
|
+
);
|
|
1231
|
+
const quiescing = (
|
|
1232
|
+
proc.state === 'Quiescing'
|
|
1233
|
+
&& proc.settingsAdmissionClosed === true
|
|
1234
|
+
);
|
|
1235
|
+
const reject = () => {
|
|
1236
|
+
throw controllerError(
|
|
1237
|
+
'Codex turn preparation does not match the exact admitted turn.',
|
|
1238
|
+
'CODEX_TURN_PREPARATION_REJECTED',
|
|
1239
|
+
'Fence the generation before retrying the user turn.',
|
|
1240
|
+
);
|
|
1241
|
+
};
|
|
1242
|
+
if (
|
|
1243
|
+
currentProcessBySession.get(proc.sessionKey) !== record
|
|
1244
|
+
|| record.closed
|
|
1245
|
+
|| record.retired
|
|
1246
|
+
|| record.durabilityBlocked
|
|
1247
|
+
|| record.retirementVerificationFailed
|
|
1248
|
+
|| !record.durable
|
|
1249
|
+
|| proc.closed === true
|
|
1250
|
+
|| proc.runtime !== 'codex'
|
|
1251
|
+
|| proc.backend !== 'codex'
|
|
1252
|
+
|| proc.generationId !== payload.generationId
|
|
1253
|
+
|| proc.hostIdentity !== state.identity.stableHostId
|
|
1254
|
+
|| proc.bootSessionIdentity !== state.identity.bootSessionId
|
|
1255
|
+
|| (!starting && !quiescing)
|
|
1256
|
+
|| !pending
|
|
1257
|
+
|| proc.pendingQueue?.[0] !== pending
|
|
1258
|
+
|| pending.settled !== false
|
|
1259
|
+
|| pending.startDeliveryState !== 'preparing'
|
|
1260
|
+
|| pending.attemptId !== payload.attemptId
|
|
1261
|
+
|| typeof pending.clientUserMessageId !== 'string'
|
|
1262
|
+
|| pending.clientUserMessageId.length === 0
|
|
1263
|
+
|| (pending.clientUserMessageId ?? null)
|
|
1264
|
+
!== (payload.clientUserMessageId ?? null)
|
|
1265
|
+
|| sourceMsgId !== (payload.source ?? null)
|
|
1266
|
+
|| pending.admittedSettings !== proc.admittingTurnSettings
|
|
1267
|
+
|| !Object.isFrozen(settings)
|
|
1268
|
+
|| typeof settings?.model !== 'string'
|
|
1269
|
+
|| settings.model.length === 0
|
|
1270
|
+
|| typeof settings?.effort !== 'string'
|
|
1271
|
+
|| settings.effort.length === 0
|
|
1272
|
+
|| typeof proc.providerSessionId !== 'string'
|
|
1273
|
+
|| proc.providerSessionId.length === 0
|
|
1274
|
+
|| proc.providerSessionId !== payload.threadId
|
|
1275
|
+
|| proc.spawnProfileId !== record.receipt.spawnProfileId
|
|
1276
|
+
|| proc.cwd !== record.receipt.expectedStaticProfile.cwd
|
|
1277
|
+
) {
|
|
1278
|
+
reject();
|
|
1279
|
+
}
|
|
1280
|
+
const expectedProviderGenerationId = (
|
|
1281
|
+
record.expectedProviderGenerationId !== null
|
|
1282
|
+
&& (
|
|
1283
|
+
record.strictResumeAttemptId === null
|
|
1284
|
+
|| record.strictResumeAttemptId === payload.attemptId
|
|
1285
|
+
)
|
|
1286
|
+
)
|
|
1287
|
+
? record.expectedProviderGenerationId
|
|
1288
|
+
: null;
|
|
1289
|
+
const result = db.recordCodexTurnStartPrepared({
|
|
1290
|
+
checkpoint: payload,
|
|
1291
|
+
provider: {
|
|
1292
|
+
session_key: proc.sessionKey,
|
|
1293
|
+
namespace: CODEX_APP_SERVER_NAMESPACE,
|
|
1294
|
+
provider: 'codex',
|
|
1295
|
+
provider_session_id: proc.providerSessionId,
|
|
1296
|
+
app_server_session_id: null,
|
|
1297
|
+
agent: null,
|
|
1298
|
+
cwd: proc.cwd,
|
|
1299
|
+
model: settings.model,
|
|
1300
|
+
effort: settings.effort,
|
|
1301
|
+
pm_backend: 'codex',
|
|
1302
|
+
spawn_profile_id: proc.spawnProfileId,
|
|
1303
|
+
ts: payload.ts ?? Date.now(),
|
|
1304
|
+
},
|
|
1305
|
+
expectedProviderGenerationId,
|
|
1306
|
+
});
|
|
1307
|
+
if (
|
|
1308
|
+
expectedProviderGenerationId !== null
|
|
1309
|
+
&& record.strictResumeAttemptId === null
|
|
1310
|
+
) {
|
|
1311
|
+
record.strictResumeAttemptId = payload.attemptId;
|
|
1312
|
+
}
|
|
1313
|
+
return result;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1208
1316
|
function maybeRetire(record) {
|
|
1209
1317
|
if (
|
|
1210
1318
|
record.retired
|
|
@@ -1488,7 +1596,7 @@ function createCodexRuntimeController({
|
|
|
1488
1596
|
}
|
|
1489
1597
|
}
|
|
1490
1598
|
|
|
1491
|
-
|
|
1599
|
+
function checkpointSinkImpl(payload) {
|
|
1492
1600
|
const record = processesByGeneration.get(payload?.generationId);
|
|
1493
1601
|
const state = requireAvailableInitialization();
|
|
1494
1602
|
if (
|
|
@@ -1553,7 +1661,19 @@ function createCodexRuntimeController({
|
|
|
1553
1661
|
return Object.freeze({ ...result });
|
|
1554
1662
|
}
|
|
1555
1663
|
|
|
1556
|
-
if (
|
|
1664
|
+
if (
|
|
1665
|
+
payload.kind === 'request-prepared'
|
|
1666
|
+
&& payload.method === 'turn/start'
|
|
1667
|
+
) {
|
|
1668
|
+
if (!record.durable) {
|
|
1669
|
+
throw controllerError(
|
|
1670
|
+
'Codex turn reached persistence before generation initialization.',
|
|
1671
|
+
'CODEX_GENERATION_NOT_DURABLE',
|
|
1672
|
+
'Quarantine the generation and inspect its initialization checkpoints.',
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
persistPreparedTurnIdentity(record, payload, state);
|
|
1676
|
+
} else if (!record.durable) {
|
|
1557
1677
|
activateWithPreparedCheckpoint(record, payload);
|
|
1558
1678
|
} else {
|
|
1559
1679
|
db.recordCodexCheckpoint(payload);
|
|
@@ -1567,6 +1687,20 @@ function createCodexRuntimeController({
|
|
|
1567
1687
|
maybeRetire(record);
|
|
1568
1688
|
}
|
|
1569
1689
|
|
|
1690
|
+
async function checkpointSink(payload) {
|
|
1691
|
+
try {
|
|
1692
|
+
return checkpointSinkImpl(payload);
|
|
1693
|
+
} catch (error) {
|
|
1694
|
+
if (
|
|
1695
|
+
payload?.kind === 'request-prepared'
|
|
1696
|
+
&& payload?.method === 'turn/start'
|
|
1697
|
+
) {
|
|
1698
|
+
error.checkpointDisposition = 'not-committed';
|
|
1699
|
+
}
|
|
1700
|
+
throw error;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1570
1704
|
return Object.freeze({
|
|
1571
1705
|
initialize,
|
|
1572
1706
|
prepareSession,
|
|
@@ -64,6 +64,7 @@ async function buildCodexSpawnContext({
|
|
|
64
64
|
existingSessionId,
|
|
65
65
|
resumePolicy: 'require-interrupted-turn',
|
|
66
66
|
expectedInterruptedTurnId: strictResume.expectedInterruptedTurnId,
|
|
67
|
+
expectedProviderGenerationId: strictResume.expectedGenerationId,
|
|
67
68
|
noWaitForCapacity: true,
|
|
68
69
|
};
|
|
69
70
|
}
|
package/lib/db/auto-resume.js
CHANGED
|
@@ -80,15 +80,172 @@ function createAutoResumeTracker({ cooldownMs = DEFAULT_COOLDOWN_MS, now = Date.
|
|
|
80
80
|
* Classify the durable provider ledger for one Codex Telegram input.
|
|
81
81
|
*
|
|
82
82
|
* A request is safe to submit automatically only when the ledger proves that
|
|
83
|
-
* no provider write occurred.
|
|
84
|
-
*
|
|
85
|
-
* state fails closed because re-submission could
|
|
86
|
-
* effects.
|
|
83
|
+
* no provider write occurred. A deploy-owned pre-write cancellation is safe
|
|
84
|
+
* only with exact clean-restart provenance; other cancellations remain
|
|
85
|
+
* cancelled. Every ambiguous state fails closed because re-submission could
|
|
86
|
+
* duplicate text or tool effects.
|
|
87
87
|
*
|
|
88
88
|
* Linked steering inputs are never independent work. Their state follows the
|
|
89
89
|
* target turn, which is returned only for diagnostics and reconciliation.
|
|
90
90
|
*/
|
|
91
91
|
function classifyCodexRecoveryEvidence(evidence = {}) {
|
|
92
|
+
const structured = (
|
|
93
|
+
evidence?.kind != null
|
|
94
|
+
|| evidence?.selection != null
|
|
95
|
+
|| evidence?.reservation != null
|
|
96
|
+
|| evidence?.attempt != null
|
|
97
|
+
|| evidence?.linkedInput != null
|
|
98
|
+
|| evidence?.targetAttempt != null
|
|
99
|
+
|| evidence?.cancellationProof != null
|
|
100
|
+
);
|
|
101
|
+
if (structured) {
|
|
102
|
+
const selection = evidence?.selection;
|
|
103
|
+
const reservation = evidence?.reservation;
|
|
104
|
+
const attempt = evidence?.attempt;
|
|
105
|
+
const linkedInput = evidence?.linkedInput;
|
|
106
|
+
const targetAttempt = evidence?.targetAttempt;
|
|
107
|
+
const cancellationProof = evidence?.cancellationProof;
|
|
108
|
+
const exactSelection = (
|
|
109
|
+
evidence?.provider === 'codex'
|
|
110
|
+
&& selection?.provider === 'codex'
|
|
111
|
+
&& typeof selection.sessionKey === 'string'
|
|
112
|
+
&& selection.sessionKey.length > 0
|
|
113
|
+
&& Number.isSafeInteger(selection.selectedTs)
|
|
114
|
+
&& selection.selectedTs >= 0
|
|
115
|
+
);
|
|
116
|
+
const exactReservation = (
|
|
117
|
+
typeof reservation?.reservationId === 'string'
|
|
118
|
+
&& reservation.reservationId.length > 0
|
|
119
|
+
&& typeof reservation?.generationId === 'string'
|
|
120
|
+
&& reservation.generationId.length > 0
|
|
121
|
+
&& ['reserved', 'queue-authorized'].includes(reservation.state)
|
|
122
|
+
&& reservation.steerAttemptId == null
|
|
123
|
+
&& reservation.targetAttemptId == null
|
|
124
|
+
&& reservation.settledTs == null
|
|
125
|
+
);
|
|
126
|
+
const exactAttempt = (
|
|
127
|
+
typeof attempt?.attemptId === 'string'
|
|
128
|
+
&& attempt.attemptId.length > 0
|
|
129
|
+
&& typeof attempt?.generationId === 'string'
|
|
130
|
+
&& attempt.generationId.length > 0
|
|
131
|
+
&& attempt.deliveryState === 'prepared'
|
|
132
|
+
&& attempt.turnId == null
|
|
133
|
+
&& attempt.terminalStatus == null
|
|
134
|
+
);
|
|
135
|
+
const noLinkedEvidence = linkedInput == null && targetAttempt == null;
|
|
136
|
+
const matchingGeneration = (
|
|
137
|
+
reservation == null
|
|
138
|
+
|| attempt == null
|
|
139
|
+
|| reservation.generationId === attempt.generationId
|
|
140
|
+
);
|
|
141
|
+
const exactOptionalQueueReservation = (
|
|
142
|
+
reservation == null
|
|
143
|
+
|| (exactReservation && reservation.state === 'queue-authorized')
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (!exactSelection) {
|
|
147
|
+
return {
|
|
148
|
+
action: 'defer',
|
|
149
|
+
reason: 'provider-acceptance-not-disproven',
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (!noLinkedEvidence) {
|
|
153
|
+
return {
|
|
154
|
+
action: 'defer',
|
|
155
|
+
reason: 'linked-input',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (
|
|
159
|
+
evidence.kind === 'selection-only'
|
|
160
|
+
&& reservation == null
|
|
161
|
+
&& attempt == null
|
|
162
|
+
&& cancellationProof == null
|
|
163
|
+
) {
|
|
164
|
+
return {
|
|
165
|
+
action: 'recover',
|
|
166
|
+
reason: 'selection-proven-not-dispatched',
|
|
167
|
+
cleanRestartSafe: true,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (
|
|
171
|
+
evidence.kind === 'dispatch-reservation'
|
|
172
|
+
&& exactReservation
|
|
173
|
+
&& attempt == null
|
|
174
|
+
&& cancellationProof == null
|
|
175
|
+
) {
|
|
176
|
+
return {
|
|
177
|
+
action: 'recover',
|
|
178
|
+
reason: 'reservation-proven-not-dispatched',
|
|
179
|
+
cleanRestartSafe: true,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const exactPrimary = (
|
|
184
|
+
exactAttempt
|
|
185
|
+
&& attempt.method === 'turn/start'
|
|
186
|
+
&& matchingGeneration
|
|
187
|
+
&& exactOptionalQueueReservation
|
|
188
|
+
&& (
|
|
189
|
+
(reservation == null && evidence.kind === 'primary-turn')
|
|
190
|
+
|| (
|
|
191
|
+
reservation != null
|
|
192
|
+
&& evidence.kind === 'dispatch-reservation'
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
if (
|
|
197
|
+
exactPrimary
|
|
198
|
+
&& attempt.recoveryState === 'prepared'
|
|
199
|
+
&& cancellationProof == null
|
|
200
|
+
) {
|
|
201
|
+
return {
|
|
202
|
+
action: 'recover',
|
|
203
|
+
reason: 'request-proven-not-accepted',
|
|
204
|
+
cleanRestartSafe: true,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (exactPrimary && attempt.recoveryState === 'cancelled') {
|
|
208
|
+
if (
|
|
209
|
+
cancellationProof?.kind === 'active-start-cancelled'
|
|
210
|
+
&& cancellationProof?.reason === 'clean-restart'
|
|
211
|
+
&& Object.keys(cancellationProof).length === 2
|
|
212
|
+
) {
|
|
213
|
+
return {
|
|
214
|
+
action: 'recover',
|
|
215
|
+
reason: 'clean-restart-request-cancelled-before-acceptance',
|
|
216
|
+
cleanRestartSafe: true,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
action: 'skip',
|
|
221
|
+
reason: 'request-cancelled-before-acceptance',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const exactQueuedCancellation = (
|
|
226
|
+
evidence.kind === 'queued-send'
|
|
227
|
+
&& exactAttempt
|
|
228
|
+
&& attempt.method === 'queued/send'
|
|
229
|
+
&& attempt.recoveryState === 'cancelled'
|
|
230
|
+
&& matchingGeneration
|
|
231
|
+
&& exactOptionalQueueReservation
|
|
232
|
+
&& cancellationProof?.kind === 'queued-send-cancelled'
|
|
233
|
+
&& cancellationProof?.reason === 'clean-restart'
|
|
234
|
+
&& Object.keys(cancellationProof).length === 2
|
|
235
|
+
);
|
|
236
|
+
if (exactQueuedCancellation) {
|
|
237
|
+
return {
|
|
238
|
+
action: 'recover',
|
|
239
|
+
reason: 'clean-restart-queued-send-cancelled',
|
|
240
|
+
cleanRestartSafe: true,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
action: 'defer',
|
|
245
|
+
reason: 'provider-acceptance-not-disproven',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
92
249
|
const deliveryState = evidence?.delivery_state ?? evidence?.deliveryState;
|
|
93
250
|
const recoveryState = evidence?.recovery_state ?? evidence?.recoveryState;
|
|
94
251
|
const linkedState = (
|
|
@@ -278,6 +278,14 @@ const FIELDS = {
|
|
|
278
278
|
replies: 'int',
|
|
279
279
|
reply_count: 'int',
|
|
280
280
|
receipt_count: 'int',
|
|
281
|
+
recovered_count: 'int',
|
|
282
|
+
skipped_count: 'int',
|
|
283
|
+
codex_scheduled_count: 'int',
|
|
284
|
+
admitted_count: 'int',
|
|
285
|
+
terminal_count: 'int',
|
|
286
|
+
noticed_count: 'int',
|
|
287
|
+
notice_failed_count: 'int',
|
|
288
|
+
deferred_count: 'int',
|
|
281
289
|
interim_count: 'int',
|
|
282
290
|
block_count: 'int',
|
|
283
291
|
media_count: 'int',
|