@allocator-one/rcl 4.5.4 → 4.5.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/README.md +5 -4
- package/dist/converge/attempt-budget.d.ts +14 -0
- package/dist/converge/attempt-budget.js +216 -1
- package/dist/converge/attempt-budget.js.map +1 -1
- package/dist/converge/launch-guard.js +23 -6
- package/dist/converge/launch-guard.js.map +1 -1
- package/dist/converge/launch-record.d.ts +36 -0
- package/dist/converge/launch-record.js +9 -3
- package/dist/converge/launch-record.js.map +1 -1
- package/dist/converge/legacy-launch-health.d.ts +36 -0
- package/dist/converge/native-lock.d.ts +3 -1
- package/dist/converge/native-lock.js +4 -4
- package/dist/converge/native-lock.js.map +1 -1
- package/dist/converge/ordinary-pending-export.d.ts +3 -0
- package/dist/converge/ordinary-pending-export.js +64 -17
- package/dist/converge/ordinary-pending-export.js.map +1 -1
- package/dist/converge/ordinary-pending-package.d.ts +11 -0
- package/dist/converge/ordinary-pending-package.js +18 -4
- package/dist/converge/ordinary-pending-package.js.map +1 -1
- package/dist/converge/pending-finalization-receipt.d.ts +4 -0
- package/dist/converge/pending-finalization-receipt.js +102 -0
- package/dist/converge/pending-finalization-receipt.js.map +1 -0
- package/dist/converge/pending-legacy-resume.d.ts +48 -3
- package/dist/converge/pending-legacy-resume.js +215 -35
- package/dist/converge/pending-legacy-resume.js.map +1 -1
- package/dist/converge/pending-recovery-source.d.ts +114 -0
- package/dist/converge/pending-recovery-source.js +36 -1
- package/dist/converge/pending-recovery-source.js.map +1 -1
- package/dist/converge/process-identity.d.ts +51 -0
- package/dist/converge/process-identity.js +190 -0
- package/dist/converge/process-identity.js.map +1 -0
- package/dist/converge/recovery-launch.js +39 -17
- package/dist/converge/recovery-launch.js.map +1 -1
- package/dist/converge/recovery-state.d.ts +55 -0
- package/dist/converge/recovery-state.js +306 -0
- package/dist/converge/recovery-state.js.map +1 -0
- package/dist/converge/run-state.d.ts +39 -1
- package/dist/converge/run-state.js +246 -18
- package/dist/converge/run-state.js.map +1 -1
- package/dist/converge/semantic-state.d.ts +55 -0
- package/dist/converge/semantic-state.js +666 -0
- package/dist/converge/semantic-state.js.map +1 -0
- package/dist/converge/stale-report.d.ts +36 -0
- package/dist/dispatch/async-lane.d.ts +12 -0
- package/dist/dispatch/async-lane.js +98 -12
- package/dist/dispatch/async-lane.js.map +1 -1
- package/dist/evidence/claim-recovery/validation/native-state.d.ts +22 -0
- package/dist/evidence/claim-recovery/validation/native-state.js +91 -9
- package/dist/evidence/claim-recovery/validation/native-state.js.map +1 -1
- package/dist/evidence/claim-recovery/validation/obligations.js +2 -1
- package/dist/evidence/claim-recovery/validation/obligations.js.map +1 -1
- package/dist/evidence/claim-recovery/validation/semantic-validation.d.ts +11 -0
- package/dist/evidence/claim-recovery/validation/semantic-validation.js +105 -28
- package/dist/evidence/claim-recovery/validation/semantic-validation.js.map +1 -1
- package/dist/evidence/claim-recovery/validation/types.d.ts +10 -0
- package/dist/evidence/original-run/lock-scope.d.ts +12 -6
- package/dist/evidence/original-run/lock-scope.js +12 -9
- package/dist/evidence/original-run/lock-scope.js.map +1 -1
- package/dist/index.js +59 -13
- package/dist/index.js.map +1 -1
- package/dist/telemetry/recovery/files.d.ts +4 -0
- package/dist/telemetry/recovery/files.js +15 -5
- package/dist/telemetry/recovery/files.js.map +1 -1
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@ import { claimConvergeAttempt, convergeAttemptStatePath, previewConvergeAttemptS
|
|
|
10
10
|
import { withRecoveryTarget } from './target-ownership.js';
|
|
11
11
|
import { bindOriginalCouncil } from '../dispatch/original-execution.js';
|
|
12
12
|
import { createOriginalLaunch } from '../dispatch/original-launch.js';
|
|
13
|
+
import { consumeBoundAsyncHistory, snapshotAsyncHistory } from '../dispatch/async-lane.js';
|
|
13
14
|
import { uuidv7 } from '../report/uuid.js';
|
|
14
15
|
import { captureSupplementalAsync } from '../report/supplemental-async.js';
|
|
15
16
|
import { stableStringify } from '../report/run-header.js';
|
|
@@ -17,8 +18,9 @@ import { readStable } from '../telemetry/recovery/files.js';
|
|
|
17
18
|
import { serializeRecoveryDocument } from '../evidence/original-run/journal.js';
|
|
18
19
|
import { prepareLockRoot } from '../evidence/original-run/lock-path.js';
|
|
19
20
|
import { retainStaleFile } from './stale-report-storage.js';
|
|
20
|
-
import { createPendingRecoverySource, createOrdinaryMigrationSource, pendingRecoverySourceSchema } from './pending-recovery-source.js';
|
|
21
|
+
import { createPendingRecoverySource, createOrdinaryMigrationSource, createCycleMigrationSource, pendingRecoverySourceSchema } from './pending-recovery-source.js';
|
|
21
22
|
import { validateOrdinaryPendingPackage } from './ordinary-pending-package.js';
|
|
23
|
+
import { assertReviewCyclePair } from './fresh-review.js';
|
|
22
24
|
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
23
25
|
const ordinaryPendingFinalizationReceiptBodySchema = z.object({
|
|
24
26
|
version: z.literal(1),
|
|
@@ -41,20 +43,66 @@ const ordinaryPendingFinalizationReceiptBodySchema = z.object({
|
|
|
41
43
|
sourceAttemptStateSha256: digestSchema,
|
|
42
44
|
finalizedNativeStateSha256: digestSchema,
|
|
43
45
|
}).strict();
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
const cyclePendingFinalizationReceiptBodySchema = ordinaryPendingFinalizationReceiptBodySchema.omit({
|
|
47
|
+
version: true,
|
|
48
|
+
operation: true,
|
|
49
|
+
retainedAsyncSha256: true,
|
|
50
|
+
}).extend({
|
|
51
|
+
version: z.literal(2),
|
|
52
|
+
operation: z.literal('cycle-pending-finalize-only'),
|
|
53
|
+
retainedAsyncSha256: z.array(digestSchema),
|
|
54
|
+
cycleId: z.string().uuid(),
|
|
55
|
+
operationId: z.string().uuid(),
|
|
56
|
+
repo: z.string().min(1),
|
|
57
|
+
prNumber: z.number().int().positive().safe(),
|
|
58
|
+
attemptCap: z.number().int().positive().safe(),
|
|
59
|
+
roundCap: z.number().int().positive().safe(),
|
|
60
|
+
asyncAttribution: z.literal('cycle-history-unattributed'),
|
|
61
|
+
}).strict();
|
|
62
|
+
const pendingFinalizationReceiptBodySchema = z.union([
|
|
63
|
+
ordinaryPendingFinalizationReceiptBodySchema,
|
|
64
|
+
cyclePendingFinalizationReceiptBodySchema,
|
|
65
|
+
]);
|
|
66
|
+
const ordinaryPendingFinalizationReceiptSchema = z.union([
|
|
67
|
+
ordinaryPendingFinalizationReceiptBodySchema.extend({ receiptDigest: digestSchema }).strict(),
|
|
68
|
+
cyclePendingFinalizationReceiptBodySchema.extend({ receiptDigest: digestSchema }).strict(),
|
|
69
|
+
]).superRefine((receipt, context) => {
|
|
47
70
|
const { receiptDigest, ...body } = receipt;
|
|
48
71
|
if (receiptDigest !== createHash('sha256').update(stableStringify(body)).digest('hex')) {
|
|
49
72
|
context.addIssue({ code: 'custom', message: 'Pending finalization receipt digest is invalid' });
|
|
50
73
|
}
|
|
51
74
|
if (receipt.nextFreeAttempt !== receipt.finalizedAttempt + 1 ||
|
|
52
75
|
receipt.attemptsUsed !== receipt.finalizedAttempt || receipt.attemptsUsed > receipt.cap ||
|
|
53
|
-
|
|
76
|
+
(receipt.version === 1 && (receipt.retainedAsyncSha256.length === 0 ||
|
|
77
|
+
new Set(receipt.retainedAsyncSha256).size !== receipt.retainedAsyncSha256.length))) {
|
|
54
78
|
context.addIssue({ code: 'custom', message: 'Pending finalization accounting is inconsistent' });
|
|
55
79
|
}
|
|
80
|
+
if (receipt.version === 2 && (receipt.attemptCap !== receipt.cap || receipt.round > receipt.roundCap)) {
|
|
81
|
+
context.addIssue({ code: 'custom', message: 'Cycle pending finalization accounting is inconsistent' });
|
|
82
|
+
}
|
|
56
83
|
});
|
|
57
84
|
function fail(code) { throw new Error(`pending_legacy_resume_${code}`); }
|
|
85
|
+
function cyclePendingPreviewSha256(input) {
|
|
86
|
+
return createHash('sha256').update(stableStringify({
|
|
87
|
+
version: 1,
|
|
88
|
+
operation: 'cycle-pending-finalize-preview',
|
|
89
|
+
...input,
|
|
90
|
+
retainedAsyncSha256: [...input.retainedAsyncSha256].sort(),
|
|
91
|
+
})).digest('hex');
|
|
92
|
+
}
|
|
93
|
+
function assertCyclePendingPreviewBinding(options, sourceDigest, migrationPackageSha256) {
|
|
94
|
+
if (options.currentPrHeadSha === undefined)
|
|
95
|
+
return;
|
|
96
|
+
const expected = cyclePendingPreviewSha256({ target: options.target, headSha: options.headSha,
|
|
97
|
+
baseSha: options.baseSha, currentPrHeadSha: options.currentPrHeadSha,
|
|
98
|
+
pendingInputSha256: options.pendingInputSha256,
|
|
99
|
+
nativeStateSha256: options.nativeStateSha256,
|
|
100
|
+
attemptStateSha256: options.attemptStateSha256,
|
|
101
|
+
retainedAsyncSha256: options.retainedAsyncSha256, maxAttempts: options.maxAttempts,
|
|
102
|
+
sourceDigest, migrationPackageSha256 });
|
|
103
|
+
if (options.previewSha256 !== expected)
|
|
104
|
+
fail('preview_binding_mismatch');
|
|
105
|
+
}
|
|
58
106
|
function defaultOwnerAlive(pid) {
|
|
59
107
|
try {
|
|
60
108
|
process.kill(pid, 0);
|
|
@@ -90,12 +138,18 @@ function sourceFromFinalized(target, launch, attempts, migrationPackage) {
|
|
|
90
138
|
const migrationPackageSha256 = createHash('sha256').update(stableStringify(migrationPackage)).digest('hex');
|
|
91
139
|
if (migrationPackageSha256 !== recovery.migrationPackageSha256)
|
|
92
140
|
fail('finalization_mismatch');
|
|
93
|
-
const
|
|
141
|
+
const common = { target, headSha: launch.headSha,
|
|
94
142
|
inputSha256: launch.inputSha256, pendingAttempt: launch.attempt, round: launch.round,
|
|
95
143
|
originalPid: launch.pid, startedAt: launch.startedAt, blockingOutcome: recovery.blockingOutcome,
|
|
96
144
|
reason: recovery.reason, nativeStateSha256: recovery.nativeStateSha256,
|
|
97
145
|
attemptStateSha256: recovery.attemptStateSha256, retainedAsyncSha256: recovery.retainedAsyncSha256,
|
|
98
|
-
migrationPackageSha256 }
|
|
146
|
+
migrationPackageSha256 };
|
|
147
|
+
const source = migrationPackage.version === 2
|
|
148
|
+
? createCycleMigrationSource({ version: 2, ...common, cycle: migrationPackage.cycle,
|
|
149
|
+
cycleId: migrationPackage.cycle.id, operationId: migrationPackage.cycle.operationId,
|
|
150
|
+
attemptCap: migrationPackage.attemptCap, roundCap: migrationPackage.roundCap,
|
|
151
|
+
attemptsUsed: migrationPackage.attemptsUsed, asyncAttribution: 'cycle-history-unattributed' })
|
|
152
|
+
: createOrdinaryMigrationSource({ version: 1, ...common });
|
|
99
153
|
if (source.digest !== recovery.sourceDigest)
|
|
100
154
|
fail('finalization_mismatch');
|
|
101
155
|
return source;
|
|
@@ -112,40 +166,109 @@ function sourceFromFinalized(target, launch, attempts, migrationPackage) {
|
|
|
112
166
|
digest: recovery.sourceDigest,
|
|
113
167
|
});
|
|
114
168
|
}
|
|
169
|
+
function packageExpected(state, attempts, current, input) {
|
|
170
|
+
return {
|
|
171
|
+
target: input.target, headSha: current.headSha, inputSha256: current.inputSha256,
|
|
172
|
+
baseSha: input.baseSha, attempt: current.attempt, round: current.round, pid: current.pid,
|
|
173
|
+
retainedAsyncSha256: input.retainedAsyncSha256,
|
|
174
|
+
...(state.cycle ? { cycle: state.cycle, attemptCap: attempts.cap,
|
|
175
|
+
roundCap: state.roundCap, attemptsUsed: attempts.attemptsUsed } : {}),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
async function assertCycleRecovery(options, state, attempts, packet) {
|
|
179
|
+
if (!state.cycle) {
|
|
180
|
+
if (packet.version === 2)
|
|
181
|
+
fail('cycle_mismatch');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
await assertReviewCyclePair(options.gitCommonDir, options.target, state.cycle);
|
|
185
|
+
if (packet.version !== 2 || !isDeepStrictEqual(packet.cycle, state.cycle))
|
|
186
|
+
fail('cycle_mismatch');
|
|
187
|
+
const remote = options.cycleRemote;
|
|
188
|
+
if (!remote || remote.repo.toLowerCase() !== state.cycle.repo ||
|
|
189
|
+
remote.prNumber !== state.cycle.prNumber || remote.url !== state.cycle.url)
|
|
190
|
+
fail('cycle_remote_mismatch');
|
|
191
|
+
if ((await remote.current())?.id !== state.cycle.id)
|
|
192
|
+
fail('cycle_superseded');
|
|
193
|
+
if (attempts.cap !== packet.attemptCap || state.roundCap !== packet.roundCap ||
|
|
194
|
+
attempts.attemptsUsed !== packet.attemptsUsed)
|
|
195
|
+
fail('cycle_accounting_mismatch');
|
|
196
|
+
}
|
|
115
197
|
function validateOptions(input) {
|
|
116
198
|
const hashes = input?.retainedAsyncSha256;
|
|
199
|
+
const cycleFinalizePreview = input?.migrationPackage?.version === 2 &&
|
|
200
|
+
input.previewMode === 'finalize-only';
|
|
201
|
+
const currentPrHeadValid = input?.currentPrHeadSha === undefined ||
|
|
202
|
+
(cycleFinalizePreview && /^[a-f0-9]{40}$/.test(input.currentPrHeadSha));
|
|
117
203
|
if (!input || typeof input.gitCommonDir !== 'string' || typeof input.target !== 'string' || !input.target.trim() ||
|
|
118
204
|
!/^[a-f0-9]{40}$/.test(input.headSha ?? '') ||
|
|
119
205
|
(input.migrationPackage && !/^[a-f0-9]{40}$/.test(input.baseSha ?? '')) ||
|
|
120
206
|
!/^[a-f0-9]{64}$/.test(input.pendingInputSha256 ?? '') ||
|
|
121
207
|
!/^[a-f0-9]{64}$/.test(input.recoveryInputSha256 ?? '') ||
|
|
122
208
|
typeof input.retryReason !== 'string' || !input.retryReason.trim() || input.retryReason.length > 500 ||
|
|
123
|
-
!Array.isArray(hashes) || hashes.
|
|
124
|
-
new Set(hashes).size !== hashes.length ||
|
|
209
|
+
!Array.isArray(hashes) || hashes.some(value => !/^[a-f0-9]{64}$/.test(value)) ||
|
|
210
|
+
(!cycleFinalizePreview && (hashes.length === 0 || new Set(hashes).size !== hashes.length)) ||
|
|
211
|
+
typeof input.validate !== 'function' ||
|
|
125
212
|
typeof input.run !== 'function' || typeof input.loadRetainedAsync !== 'function' ||
|
|
126
213
|
(!input.migrationPackage && !input.legacyRetry) ||
|
|
127
214
|
!Number.isSafeInteger(input.maxAttempts) || input.maxAttempts < 1 ||
|
|
128
215
|
!Number.isSafeInteger(input.maxPhysicalCalls) || input.maxPhysicalCalls < 1 ||
|
|
129
216
|
!Number.isSafeInteger(input.maxAttemptsPerCell) || input.maxAttemptsPerCell < 1 ||
|
|
130
217
|
!Number.isSafeInteger(input.maxDurationMs) || input.maxDurationMs < 1 || input.maxDurationMs > 2_147_483_647 ||
|
|
131
|
-
(input.previewMode !== undefined && !['combined', 'finalize-only'].includes(input.previewMode))
|
|
218
|
+
(input.previewMode !== undefined && !['combined', 'finalize-only'].includes(input.previewMode)) ||
|
|
219
|
+
!currentPrHeadValid) {
|
|
132
220
|
fail('invalid_input');
|
|
133
221
|
}
|
|
134
222
|
}
|
|
135
223
|
function validateFinalizeOptions(input) {
|
|
136
224
|
const hashes = input?.retainedAsyncSha256;
|
|
225
|
+
const cycleBacked = input?.migrationPackage?.version === 2;
|
|
226
|
+
const hasCurrentPrHead = input?.currentPrHeadSha !== undefined;
|
|
227
|
+
const hasPreviewSha = input?.previewSha256 !== undefined;
|
|
228
|
+
const hasStoreDir = typeof input?.cycleHistory?.storeDir === 'string' &&
|
|
229
|
+
input.cycleHistory.storeDir.trim().length > 0;
|
|
230
|
+
const hasStoreResolver = typeof input?.cycleHistory?.resolveStoreDir === 'function';
|
|
137
231
|
if (!input || typeof input.gitCommonDir !== 'string' || typeof input.target !== 'string' || !input.target.trim() ||
|
|
138
232
|
!/^[a-f0-9]{40}$/.test(input.headSha ?? '') || !/^[a-f0-9]{40}$/.test(input.baseSha ?? '') ||
|
|
139
233
|
!/^[a-f0-9]{64}$/.test(input.pendingInputSha256 ?? '') ||
|
|
140
234
|
!/^[a-f0-9]{64}$/.test(input.nativeStateSha256 ?? '') ||
|
|
141
235
|
!/^[a-f0-9]{64}$/.test(input.attemptStateSha256 ?? '') ||
|
|
142
|
-
!Array.isArray(hashes) || hashes.
|
|
143
|
-
new Set(hashes).size !== hashes.length ||
|
|
236
|
+
!Array.isArray(hashes) || hashes.some(value => !/^[a-f0-9]{64}$/.test(value)) ||
|
|
237
|
+
(!cycleBacked && (hashes.length === 0 || new Set(hashes).size !== hashes.length)) ||
|
|
238
|
+
typeof input.loadRetainedAsync !== 'function' ||
|
|
144
239
|
(input.writeFinalizedState !== undefined && typeof input.writeFinalizedState !== 'function') ||
|
|
145
|
-
|
|
240
|
+
(input.consumeCycleHistory !== undefined && typeof input.consumeCycleHistory !== 'function') ||
|
|
241
|
+
(cycleBacked && (!input.cycleHistory || hasStoreDir === hasStoreResolver ||
|
|
242
|
+
typeof input.cycleHistory.targetKey !== 'string' || !input.cycleHistory.targetKey.trim() ||
|
|
243
|
+
!Number.isSafeInteger(input.cycleHistory.maxResults) || input.cycleHistory.maxResults < 1)) ||
|
|
244
|
+
!input.migrationPackage || !Number.isSafeInteger(input.maxAttempts) || input.maxAttempts < 1 ||
|
|
245
|
+
hasCurrentPrHead !== hasPreviewSha ||
|
|
246
|
+
(hasCurrentPrHead && (!cycleBacked || !/^[a-f0-9]{40}$/.test(input.currentPrHeadSha) ||
|
|
247
|
+
!/^[a-f0-9]{64}$/.test(input.previewSha256)))) {
|
|
146
248
|
fail('invalid_input');
|
|
147
249
|
}
|
|
148
250
|
}
|
|
251
|
+
async function reconcileFinalizedCycleHistory(options, mode) {
|
|
252
|
+
if (options.migrationPackage.version !== 2)
|
|
253
|
+
return;
|
|
254
|
+
const history = options.cycleHistory;
|
|
255
|
+
let storeDir;
|
|
256
|
+
try {
|
|
257
|
+
storeDir = history.storeDir ?? await history.resolveStoreDir();
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
if (mode !== 'new-receipt' && error.code === 'ENOENT')
|
|
261
|
+
return;
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
if (mode === 'advanced-replay') {
|
|
265
|
+
const live = await snapshotAsyncHistory(storeDir, history.targetKey, history.maxResults);
|
|
266
|
+
if (live.artifacts.length > 0)
|
|
267
|
+
fail('cycle_history_ambiguous_after_successor');
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
await (options.consumeCycleHistory ?? consumeBoundAsyncHistory)(storeDir, history.targetKey, options.retainedAsyncSha256, history.maxResults);
|
|
271
|
+
}
|
|
149
272
|
async function archiveAsync(common, source, artifacts, packet) {
|
|
150
273
|
validateAsyncArtifacts(source, artifacts, packet);
|
|
151
274
|
const directory = await prepareLockRoot(join(common, 'rcl-converge-pending-recovery', source.digest));
|
|
@@ -269,7 +392,7 @@ async function assertFinalizedStateMatchesReceipt(common, receipt) {
|
|
|
269
392
|
return { native: native.raw, attempts: attempts.raw, exact: false };
|
|
270
393
|
}
|
|
271
394
|
function createFinalizationReceipt(input) {
|
|
272
|
-
const body =
|
|
395
|
+
const body = pendingFinalizationReceiptBodySchema.parse(input);
|
|
273
396
|
return ordinaryPendingFinalizationReceiptSchema.parse({
|
|
274
397
|
...body,
|
|
275
398
|
receiptDigest: createHash('sha256').update(stableStringify(body)).digest('hex'),
|
|
@@ -284,10 +407,16 @@ function assertReceiptMatchesInput(receipt, input, migrationPackageSha256) {
|
|
|
284
407
|
receipt.retainedAsyncSha256.join(',') !== [...input.retainedAsyncSha256].sort().join(',')) {
|
|
285
408
|
fail('finalization_receipt_mismatch');
|
|
286
409
|
}
|
|
410
|
+
if (receipt.version === 2 && (input.migrationPackage.cycle?.id !== receipt.cycleId ||
|
|
411
|
+
input.migrationPackage.cycle.operationId !== receipt.operationId))
|
|
412
|
+
fail('finalization_receipt_mismatch');
|
|
287
413
|
return validateOrdinaryPendingPackage(input.migrationPackage, {
|
|
288
414
|
target: receipt.target, headSha: receipt.headSha, inputSha256: receipt.inputSha256,
|
|
289
415
|
baseSha: receipt.baseSha, attempt: receipt.finalizedAttempt, round: receipt.round,
|
|
290
416
|
pid: receipt.originalPid, retainedAsyncSha256: receipt.retainedAsyncSha256,
|
|
417
|
+
...(receipt.version === 2 ? { cycle: input.migrationPackage.cycle,
|
|
418
|
+
attemptCap: receipt.attemptCap, roundCap: receipt.roundCap,
|
|
419
|
+
attemptsUsed: receipt.attemptsUsed } : {}),
|
|
291
420
|
});
|
|
292
421
|
}
|
|
293
422
|
async function readArchivedAsync(common, sourceDigest, retainedAsyncSha256) {
|
|
@@ -327,14 +456,26 @@ export async function finalizeOrdinaryPendingLaunch(input) {
|
|
|
327
456
|
const existingReceipt = await readFinalizationReceipt(options.gitCommonDir, migrationPackageSha256);
|
|
328
457
|
if (existingReceipt) {
|
|
329
458
|
const packet = assertReceiptMatchesInput(existingReceipt, options, migrationPackageSha256);
|
|
459
|
+
assertCyclePendingPreviewBinding(options, existingReceipt.sourceDigest, migrationPackageSha256);
|
|
330
460
|
const state = await assertFinalizedStateMatchesReceipt(options.gitCommonDir, existingReceipt);
|
|
461
|
+
const [currentRun, currentAttempts] = await Promise.all([
|
|
462
|
+
loadConvergeRunState(options.gitCommonDir, options.target),
|
|
463
|
+
previewConvergeAttemptState(options.gitCommonDir, options.target),
|
|
464
|
+
]);
|
|
465
|
+
if (!currentRun || !currentAttempts)
|
|
466
|
+
fail('finalization_receipt_state_mismatch');
|
|
467
|
+
await assertCycleRecovery(options, currentRun, currentAttempts, packet.version === 2 ? { ...packet, attemptsUsed: currentAttempts.attemptsUsed } : packet);
|
|
331
468
|
validateAsyncArtifacts({ retainedAsyncSha256: existingReceipt.retainedAsyncSha256 }, await readArchivedAsync(options.gitCommonDir, existingReceipt.sourceDigest, existingReceipt.retainedAsyncSha256), packet);
|
|
332
469
|
const snapshotsPresent = await finalizationSnapshotsPresent(options.gitCommonDir, existingReceipt);
|
|
470
|
+
// Revalidate at the last supported boundary before replay can publish
|
|
471
|
+
// missing snapshots or consume retained cycle history.
|
|
472
|
+
await assertCycleRecovery(options, currentRun, currentAttempts, packet.version === 2 ? { ...packet, attemptsUsed: currentAttempts.attemptsUsed } : packet);
|
|
333
473
|
if (!snapshotsPresent) {
|
|
334
474
|
if (!state.exact)
|
|
335
475
|
fail('finalization_receipt_state_mismatch');
|
|
336
476
|
await retainFinalizationSnapshots(options.gitCommonDir, existingReceipt, state.attempts, state.native);
|
|
337
477
|
}
|
|
478
|
+
await reconcileFinalizedCycleHistory(options, state.exact ? 'exact-replay' : 'advanced-replay');
|
|
338
479
|
return { receipt: existingReceipt, reusedReceipt: true,
|
|
339
480
|
snapshotBindingUpgraded: !snapshotsPresent };
|
|
340
481
|
}
|
|
@@ -351,11 +492,8 @@ export async function finalizeOrdinaryPendingLaunch(input) {
|
|
|
351
492
|
current.inputSha256 !== options.pendingInputSha256 || !['pending', 'failed'].includes(current.status)) {
|
|
352
493
|
fail('launch_mismatch');
|
|
353
494
|
}
|
|
354
|
-
const packet = validateOrdinaryPendingPackage(options.migrationPackage,
|
|
355
|
-
|
|
356
|
-
baseSha: options.baseSha, attempt: current.attempt, round: current.round, pid: current.pid,
|
|
357
|
-
retainedAsyncSha256: options.retainedAsyncSha256,
|
|
358
|
-
});
|
|
495
|
+
const packet = validateOrdinaryPendingPackage(options.migrationPackage, packageExpected(state, attempts, current, options));
|
|
496
|
+
await assertCycleRecovery(options, state, attempts, packet);
|
|
359
497
|
let source;
|
|
360
498
|
let sourceAttemptStateBytes;
|
|
361
499
|
if (current.status === 'pending') {
|
|
@@ -366,12 +504,18 @@ export async function finalizeOrdinaryPendingLaunch(input) {
|
|
|
366
504
|
readStable(convergeAttemptStatePath(options.gitCommonDir, options.target)),
|
|
367
505
|
]);
|
|
368
506
|
sourceAttemptStateBytes = attemptBytes.raw;
|
|
369
|
-
|
|
507
|
+
const commonSource = { target: options.target,
|
|
370
508
|
headSha: current.headSha, inputSha256: current.inputSha256, pendingAttempt: current.attempt,
|
|
371
509
|
round: current.round, originalPid: current.pid, startedAt: current.startedAt,
|
|
372
510
|
blockingOutcome: 'unknown', reason: 'coordinator_exited_without_durable_blocking_receipts',
|
|
373
511
|
nativeStateSha256: nativeBytes.sha256, attemptStateSha256: attemptBytes.sha256,
|
|
374
|
-
retainedAsyncSha256: options.retainedAsyncSha256, migrationPackageSha256 }
|
|
512
|
+
retainedAsyncSha256: options.retainedAsyncSha256, migrationPackageSha256 };
|
|
513
|
+
source = packet.version === 2
|
|
514
|
+
? createCycleMigrationSource({ version: 2, ...commonSource, cycle: state.cycle,
|
|
515
|
+
cycleId: state.cycle.id, operationId: state.cycle.operationId,
|
|
516
|
+
attemptCap: attempts.cap, roundCap: state.roundCap, attemptsUsed: attempts.attemptsUsed,
|
|
517
|
+
asyncAttribution: 'cycle-history-unattributed' })
|
|
518
|
+
: createOrdinaryMigrationSource({ version: 1, ...commonSource });
|
|
375
519
|
}
|
|
376
520
|
else {
|
|
377
521
|
source = sourceFromFinalized(options.target, current, attempts, packet);
|
|
@@ -379,6 +523,7 @@ export async function finalizeOrdinaryPendingLaunch(input) {
|
|
|
379
523
|
if (source.nativeStateSha256 !== options.nativeStateSha256 ||
|
|
380
524
|
source.attemptStateSha256 !== options.attemptStateSha256)
|
|
381
525
|
fail('source_digest_mismatch');
|
|
526
|
+
assertCyclePendingPreviewBinding(options, source.digest, migrationPackageSha256);
|
|
382
527
|
// Once the failed/unknown native state exists, archive publication already
|
|
383
528
|
// completed. Resume from those immutable bytes so a lost acknowledgment
|
|
384
529
|
// does not depend on the original async store still being available.
|
|
@@ -399,7 +544,7 @@ export async function finalizeOrdinaryPendingLaunch(input) {
|
|
|
399
544
|
// Construct and validate the exact receipt before the first write. The
|
|
400
545
|
// native serializer is shared with writeState, so its post-state digest is
|
|
401
546
|
// known without publishing a partially validated operation.
|
|
402
|
-
const
|
|
547
|
+
const receiptCommon = {
|
|
403
548
|
target: options.target, headSha: current.headSha, baseSha: options.baseSha,
|
|
404
549
|
inputSha256: current.inputSha256, sourceDigest: source.digest, migrationPackageSha256,
|
|
405
550
|
finalizedAttempt: current.attempt, round: current.round, originalPid: current.pid,
|
|
@@ -407,13 +552,24 @@ export async function finalizeOrdinaryPendingLaunch(input) {
|
|
|
407
552
|
blockingOutcome: 'unknown', retainedAsyncSha256: options.retainedAsyncSha256,
|
|
408
553
|
sourceNativeStateSha256: source.nativeStateSha256,
|
|
409
554
|
sourceAttemptStateSha256: source.attemptStateSha256,
|
|
410
|
-
finalizedNativeStateSha256
|
|
555
|
+
finalizedNativeStateSha256
|
|
556
|
+
};
|
|
557
|
+
const receipt = packet.version === 2
|
|
558
|
+
? createFinalizationReceipt({ version: 2, operation: 'cycle-pending-finalize-only', ...receiptCommon,
|
|
559
|
+
cycleId: state.cycle.id, operationId: state.cycle.operationId,
|
|
560
|
+
repo: state.cycle.repo, prNumber: state.cycle.prNumber,
|
|
561
|
+
attemptCap: attempts.cap, roundCap: state.roundCap,
|
|
562
|
+
asyncAttribution: 'cycle-history-unattributed' })
|
|
563
|
+
: createFinalizationReceipt({ version: 1, operation: 'ordinary-pending-finalize-only', ...receiptCommon });
|
|
411
564
|
if (current.status === 'pending') {
|
|
412
565
|
await preflightConvergeRunStateWrite(options.gitCommonDir, state, ownership);
|
|
413
566
|
}
|
|
414
567
|
// Every fallible authentication and accounting check above precedes the
|
|
415
568
|
// first durable write. Archive publication is idempotent if this process
|
|
416
569
|
// stops before the native failed/unknown state or receipt is published.
|
|
570
|
+
// Revalidate remote cycle membership after those reads and immediately
|
|
571
|
+
// before archive publication so live PR or cycle drift fails closed.
|
|
572
|
+
await assertCycleRecovery(options, state, attempts, packet);
|
|
417
573
|
await archiveAsync(options.gitCommonDir, source, artifacts, packet);
|
|
418
574
|
if (current.status === 'pending') {
|
|
419
575
|
await (options.writeFinalizedState ?? writeState)(options.gitCommonDir, state, ownership);
|
|
@@ -424,6 +580,10 @@ export async function finalizeOrdinaryPendingLaunch(input) {
|
|
|
424
580
|
const finalizedAttempts = await readStable(convergeAttemptStatePath(options.gitCommonDir, options.target));
|
|
425
581
|
sourceAttemptStateBytes ??= finalizedAttempts.raw;
|
|
426
582
|
await retainFinalizationSnapshots(options.gitCommonDir, receipt, sourceAttemptStateBytes, finalizedNative.raw);
|
|
583
|
+
// Cycle history is consumed while the target remains recovery-locked and
|
|
584
|
+
// before the terminal receipt makes a successor eligible. Partial cleanup
|
|
585
|
+
// is safe to retry because consumption validates occurrence subsets.
|
|
586
|
+
await reconcileFinalizedCycleHistory(options, 'new-receipt');
|
|
427
587
|
await retainFinalizationReceipt(options.gitCommonDir, receipt);
|
|
428
588
|
return { receipt, reusedReceipt: false, snapshotBindingUpgraded: false };
|
|
429
589
|
});
|
|
@@ -434,12 +594,12 @@ function validateAsyncArtifacts(source, artifacts, packet) {
|
|
|
434
594
|
if (expected.length !== actual.length || expected.some((item, index) => item !== actual[index])) {
|
|
435
595
|
fail('async_binding_mismatch');
|
|
436
596
|
}
|
|
597
|
+
const actualDescriptors = [];
|
|
437
598
|
for (const artifact of artifacts) {
|
|
438
599
|
const bytes = Buffer.from(artifact.bytesBase64, 'base64');
|
|
439
600
|
if (createHash('sha256').update(bytes).digest('hex') !== artifact.sha256)
|
|
440
601
|
fail('async_binding_mismatch');
|
|
441
602
|
if (packet) {
|
|
442
|
-
const descriptor = packet.retainedAsync.find(item => item.sha256 === artifact.sha256);
|
|
443
603
|
let row;
|
|
444
604
|
try {
|
|
445
605
|
row = JSON.parse(bytes.toString('utf8'));
|
|
@@ -447,9 +607,15 @@ function validateAsyncArtifacts(source, artifacts, packet) {
|
|
|
447
607
|
catch {
|
|
448
608
|
fail('async_binding_mismatch');
|
|
449
609
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
610
|
+
actualDescriptors.push(JSON.stringify([artifact.sha256, row.model, row.role, row.provider]));
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (packet) {
|
|
614
|
+
const expectedDescriptors = packet.retainedAsync.map(item => JSON.stringify([item.sha256, item.model, item.role, item.provider])).sort();
|
|
615
|
+
actualDescriptors.sort();
|
|
616
|
+
if (expectedDescriptors.length !== actualDescriptors.length ||
|
|
617
|
+
expectedDescriptors.some((item, index) => item !== actualDescriptors[index])) {
|
|
618
|
+
fail('async_binding_mismatch');
|
|
453
619
|
}
|
|
454
620
|
}
|
|
455
621
|
}
|
|
@@ -481,26 +647,40 @@ export async function previewOrdinaryPendingLaunch(input) {
|
|
|
481
647
|
}
|
|
482
648
|
if ((options.ownerAlive ?? defaultOwnerAlive)(current.pid))
|
|
483
649
|
fail('owner_alive');
|
|
484
|
-
const packet = validateOrdinaryPendingPackage(options.migrationPackage,
|
|
485
|
-
|
|
486
|
-
baseSha: options.baseSha, attempt: current.attempt, round: current.round, pid: current.pid,
|
|
487
|
-
retainedAsyncSha256: options.retainedAsyncSha256,
|
|
488
|
-
});
|
|
650
|
+
const packet = validateOrdinaryPendingPackage(options.migrationPackage, packageExpected(state, attempts, current, options));
|
|
651
|
+
await assertCycleRecovery(options, state, attempts, packet);
|
|
489
652
|
const [nativeBytes, attemptBytes] = await Promise.all([
|
|
490
653
|
readStable(convergeRunStatePath(options.gitCommonDir, options.target)),
|
|
491
654
|
readStable(convergeAttemptStatePath(options.gitCommonDir, options.target)),
|
|
492
655
|
]);
|
|
493
|
-
const
|
|
656
|
+
const commonSource = { target: options.target,
|
|
494
657
|
headSha: current.headSha, inputSha256: current.inputSha256, pendingAttempt: current.attempt,
|
|
495
658
|
round: current.round, originalPid: current.pid, startedAt: current.startedAt,
|
|
496
659
|
blockingOutcome: 'unknown', reason: 'coordinator_exited_without_durable_blocking_receipts',
|
|
497
660
|
nativeStateSha256: nativeBytes.sha256, attemptStateSha256: attemptBytes.sha256,
|
|
498
661
|
retainedAsyncSha256: options.retainedAsyncSha256,
|
|
499
662
|
migrationPackageSha256: createHash('sha256').update(stableStringify(packet)).digest('hex'),
|
|
500
|
-
}
|
|
663
|
+
};
|
|
664
|
+
const source = packet.version === 2
|
|
665
|
+
? createCycleMigrationSource({ version: 2, ...commonSource, cycle: state.cycle,
|
|
666
|
+
cycleId: state.cycle.id, operationId: state.cycle.operationId,
|
|
667
|
+
attemptCap: attempts.cap, roundCap: state.roundCap, attemptsUsed: attempts.attemptsUsed,
|
|
668
|
+
asyncAttribution: 'cycle-history-unattributed' })
|
|
669
|
+
: createOrdinaryMigrationSource({ version: 1, ...commonSource });
|
|
501
670
|
validateAsyncArtifacts(source, await options.loadRetainedAsync(), packet);
|
|
671
|
+
const currentPrBinding = options.currentPrHeadSha === undefined ? {} : {
|
|
672
|
+
currentPrHeadSha: options.currentPrHeadSha,
|
|
673
|
+
previewSha256: cyclePendingPreviewSha256({ target: options.target, headSha: options.headSha,
|
|
674
|
+
baseSha: options.baseSha, currentPrHeadSha: options.currentPrHeadSha,
|
|
675
|
+
pendingInputSha256: options.pendingInputSha256,
|
|
676
|
+
nativeStateSha256: nativeBytes.sha256, attemptStateSha256: attemptBytes.sha256,
|
|
677
|
+
retainedAsyncSha256: options.retainedAsyncSha256, maxAttempts: options.maxAttempts,
|
|
678
|
+
sourceDigest: source.digest,
|
|
679
|
+
migrationPackageSha256: createHash('sha256').update(stableStringify(packet)).digest('hex') }),
|
|
680
|
+
};
|
|
502
681
|
return { source, nativeStateSha256: nativeBytes.sha256, attemptStateSha256: attemptBytes.sha256,
|
|
503
|
-
attemptsUsed: attempts.attemptsUsed, cap: attempts.cap, nextAttempt: current.attempt + 1
|
|
682
|
+
attemptsUsed: attempts.attemptsUsed, cap: attempts.cap, nextAttempt: current.attempt + 1,
|
|
683
|
+
...currentPrBinding };
|
|
504
684
|
});
|
|
505
685
|
}
|
|
506
686
|
async function runBound(options, ownership, state, claim, source, launch, resume) {
|