agentic-workflow-manager 9.12.1 → 9.12.3
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/dist/src/commands/job/index.js +61 -0
- package/dist/src/commands/watch/admission-recovery.js +161 -0
- package/dist/src/commands/watch/apply.js +51 -2
- package/dist/src/commands/watch/generations.js +1 -1
- package/dist/src/commands/watch/index.js +43 -2
- package/dist/src/commands/watch/supervisor.js +202 -64
- package/dist/src/core/journal/requests.js +6 -0
- package/dist/src/core/journal/types.js +78 -4
- package/dist/tests/commands/watch/admission-recovery.test.js +278 -0
- package/dist/tests/commands/watch/plan-lifecycle.test.js +47 -1
- package/dist/tests/commands/watch/reopened-196.test.js +749 -0
- package/dist/tests/commands/watch/supervisor-loop.test.js +111 -2
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Supervisor = exports.MAX_SENSOR_DEFERRALS = exports.DEFAULT_SUPERVISOR_CONFIG = void 0;
|
|
3
|
+
exports.Supervisor = exports.ADMISSION_RETRY_DELAY_MS = exports.MAX_SENSOR_DEFERRALS = exports.DEFAULT_SUPERVISOR_CONFIG = void 0;
|
|
4
4
|
exports.routingFacts = routingFacts;
|
|
5
5
|
exports.defaultDispatchAdmission = defaultDispatchAdmission;
|
|
6
6
|
exports.admissionForConfig = admissionForConfig;
|
|
@@ -192,6 +192,21 @@ exports.DEFAULT_SUPERVISOR_CONFIG = {
|
|
|
192
192
|
function admissionForConfig(repoRoot, branch, cfg, deps = {}) {
|
|
193
193
|
return defaultDispatchAdmission(repoRoot, branch, cfg.provider, cfg.routingIdentity, deps, cfg.controllerAutonomy);
|
|
194
194
|
}
|
|
195
|
+
function admissionContextForConfig(cfg) {
|
|
196
|
+
if (!(0, adapter_1.isWatchProvider)(cfg.provider))
|
|
197
|
+
throw new Error('provider de supervisor invalido');
|
|
198
|
+
if (cfg.controllerAutonomy !== undefined && !(0, adapter_1.isControllerAutonomy)(cfg.controllerAutonomy))
|
|
199
|
+
throw new Error('postura del controller invalida');
|
|
200
|
+
const runtime = cfg.routingIdentity === undefined ? undefined : (0, capabilities_1.validateRuntimeKey)({ target: cfg.provider, ...cfg.routingIdentity });
|
|
201
|
+
return { provider: cfg.provider,
|
|
202
|
+
...(runtime ? { runtime: { kind: runtime.kind, version: runtime.version, accountScopeDigest: runtime.accountScopeDigest } } : {}),
|
|
203
|
+
...(cfg.controllerAutonomy ? { controllerAutonomy: cfg.controllerAutonomy } : {}) };
|
|
204
|
+
}
|
|
205
|
+
function sameAdmissionContext(left, right) {
|
|
206
|
+
return left.provider === right.provider && left.controllerAutonomy === right.controllerAutonomy
|
|
207
|
+
&& left.runtime?.kind === right.runtime?.kind && left.runtime?.version === right.runtime?.version
|
|
208
|
+
&& left.runtime?.accountScopeDigest === right.runtime?.accountScopeDigest;
|
|
209
|
+
}
|
|
195
210
|
// 'frozen' (R5.2/R6.3, Task 10): SOLO puede ocurrir en el journal de un
|
|
196
211
|
// TRACK individual (nunca en el del plan) — el track terminó, con las 6
|
|
197
212
|
// observaciones demostrables (cero jobs vivos, gate local verde, worktree
|
|
@@ -200,7 +215,24 @@ function admissionForConfig(repoRoot, branch, cfg, deps = {}) {
|
|
|
200
215
|
/** How many consecutive ticks an inconclusive sensor verdict may defer dispatch
|
|
201
216
|
* before the supervisor says so out loud. Small: the deferral covers a verdict
|
|
202
217
|
* still settling, not one that never will. */
|
|
203
|
-
exports.MAX_SENSOR_DEFERRALS =
|
|
218
|
+
exports.MAX_SENSOR_DEFERRALS = types_1.MAX_ADMISSION_DEFERRALS;
|
|
219
|
+
exports.ADMISSION_RETRY_DELAY_MS = 5000;
|
|
220
|
+
function transientAdmission(report) {
|
|
221
|
+
// Currentness and sensors are evaluated before journalStatus, so their
|
|
222
|
+
// blocked reports legitimately carry journal=not-required. The caller
|
|
223
|
+
// verifies the bound journal identity independently before deferring.
|
|
224
|
+
if (report.state !== 'blocked' || report.executionMode !== 'desatendido'
|
|
225
|
+
|| report.planState !== 'valid' || !['not-required', 'current'].includes(report.journal)
|
|
226
|
+
|| report.diagnostics.length !== 1)
|
|
227
|
+
return undefined;
|
|
228
|
+
if (report.currentness === 'unverifiable' && report.sensors === 'not-required'
|
|
229
|
+
&& report.diagnostics[0].code === 'ADMISSION_CURRENTNESS_BLOCKED')
|
|
230
|
+
return 'currentness';
|
|
231
|
+
if (report.currentness === 'current' && report.sensors === 'not-certified'
|
|
232
|
+
&& report.diagnostics[0].code === 'ADMISSION_SENSORS_BLOCKED')
|
|
233
|
+
return 'sensors';
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
204
236
|
const LIVE = ['received', 'spawn-intent', 'claimed', 'running', 'cancel-requested'];
|
|
205
237
|
function recoveryWhitelistBlocker(state) {
|
|
206
238
|
if (state.cycle.status === 'BLOCKED') {
|
|
@@ -230,21 +262,19 @@ class Supervisor {
|
|
|
230
262
|
cfg;
|
|
231
263
|
spawner;
|
|
232
264
|
dispatchAdmission;
|
|
265
|
+
autoBeginGeneration;
|
|
233
266
|
backoff = new generations_1.Backoff();
|
|
234
267
|
relaunchNotBefore = 0;
|
|
235
|
-
/** Consecutive ticks deferred on an inconclusive sensor verdict. Bounded:
|
|
236
|
-
* the deferral exists for a verdict that is still settling, and a verdict
|
|
237
|
-
* that never settles must become visible rather than loop forever. */
|
|
238
|
-
sensorDeferrals = 0;
|
|
239
268
|
lastActivity = null;
|
|
240
269
|
lastGenerationToken = null;
|
|
241
270
|
trackRuntime;
|
|
242
|
-
constructor(repoRoot, branch, cfg, spawner, trackRuntime, dispatchAdmission = admissionForConfig(repoRoot, branch, cfg)) {
|
|
271
|
+
constructor(repoRoot, branch, cfg, spawner, trackRuntime, dispatchAdmission = admissionForConfig(repoRoot, branch, cfg), autoBeginGeneration = false) {
|
|
243
272
|
this.repoRoot = repoRoot;
|
|
244
273
|
this.branch = branch;
|
|
245
274
|
this.cfg = cfg;
|
|
246
275
|
this.spawner = spawner;
|
|
247
276
|
this.dispatchAdmission = dispatchAdmission;
|
|
277
|
+
this.autoBeginGeneration = autoBeginGeneration;
|
|
248
278
|
this.trackRuntime = trackRuntime ?? (0, tracks_1.defaultTrackRuntime)(repoRoot, branch, { termGraceMs: cfg.termGraceMs, killGraceMs: cfg.killGraceMs });
|
|
249
279
|
}
|
|
250
280
|
fingerprintNow = (argv, paths, cwd) => {
|
|
@@ -281,7 +311,7 @@ class Supervisor {
|
|
|
281
311
|
}
|
|
282
312
|
}
|
|
283
313
|
async tick() {
|
|
284
|
-
|
|
314
|
+
let before0 = (0, store_1.readJournal)(this.repoRoot, this.branch);
|
|
285
315
|
if (before0.corrupt || before0.state === null)
|
|
286
316
|
throw new Error('journal corrupto: el supervisor no opera sobre corrupcion (R1.6)');
|
|
287
317
|
// Historical/schema-1 journals are intentionally readable by migration
|
|
@@ -295,10 +325,77 @@ class Supervisor {
|
|
|
295
325
|
// longer reconstructible.
|
|
296
326
|
if (before0.state.cycle.status === 'COMPLETE')
|
|
297
327
|
return 'complete';
|
|
328
|
+
// A historical custody cannot be silently converted into another
|
|
329
|
+
// cause on a later tick. Only the locked offline recovery may resume.
|
|
330
|
+
if (before0.state.cycle.status === 'BLOCKED')
|
|
331
|
+
return 'custody';
|
|
332
|
+
const assertedContext = admissionContextForConfig(this.cfg);
|
|
333
|
+
if (before0.state.admissionContext && !sameAdmissionContext(before0.state.admissionContext, assertedContext)) {
|
|
334
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, 'identidad de admisión cambió al reiniciar: provider/runtime/postura');
|
|
335
|
+
return 'custody';
|
|
336
|
+
}
|
|
337
|
+
if (!before0.state.admissionContext && assertedContext.runtime && assertedContext.controllerAutonomy) {
|
|
338
|
+
before0.state.admissionContext = assertedContext;
|
|
339
|
+
(0, store_1.writeJournal)(this.repoRoot, this.branch, before0.state);
|
|
340
|
+
before0 = (0, store_1.readJournal)(this.repoRoot, this.branch);
|
|
341
|
+
if (before0.corrupt || !before0.state)
|
|
342
|
+
throw new Error('journal no verificable tras vincular identidad de admisión');
|
|
343
|
+
}
|
|
344
|
+
// Full sensor admission belongs to the launch boundary. Once that
|
|
345
|
+
// generation has a durable controller launch intent, RED edits are
|
|
346
|
+
// normal work, not a new pre-dispatch baseline. Preserve binding
|
|
347
|
+
// checks while it runs; the next generation gets a fresh admission.
|
|
348
|
+
const binding = before0.state.planBinding;
|
|
349
|
+
if (!binding)
|
|
350
|
+
throw new Error('binding compacto ausente tras lectura del journal');
|
|
351
|
+
const launched = (0, generations_1.activeGeneration)(before0.state);
|
|
352
|
+
const ownedLaunch = launched !== undefined && (launched.processRef !== undefined || launched.wrapperRef !== undefined
|
|
353
|
+
|| (0, generations_1.controllerGenerationHasUnresolvedClaim)(this.repoRoot, this.branch, launched));
|
|
354
|
+
const generationAdmitted = launched?.controllerJobId !== undefined && launched.launchArgvDigest !== undefined && ownedLaunch;
|
|
355
|
+
if (generationAdmitted) {
|
|
356
|
+
const plan = (0, validate_1.validatePlanFile)(binding.path, this.repoRoot);
|
|
357
|
+
if (plan.state !== 'valid' || plan.planDigest !== binding.digest || plan.schema !== binding.schema
|
|
358
|
+
|| (binding.executionDigest !== undefined && plan.executionDigest !== binding.executionDigest)) {
|
|
359
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, 'binding del plan alterado durante generacion admitida');
|
|
360
|
+
return 'custody';
|
|
361
|
+
}
|
|
362
|
+
if (binding.schema === 'compact-slices/v2') {
|
|
363
|
+
const asserted = assertedContext.runtime;
|
|
364
|
+
if (!asserted) {
|
|
365
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, 'runtime de routing ausente durante generacion v2');
|
|
366
|
+
return 'custody';
|
|
367
|
+
}
|
|
368
|
+
const expected = (0, capabilities_1.validateRuntimeKey)({ target: assertedContext.provider, ...asserted });
|
|
369
|
+
let local;
|
|
370
|
+
try {
|
|
371
|
+
local = await (0, local_event_scope_1.queryLocalEventScope)(this.repoRoot, expected.target, expected.kind);
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, `runtime local no verificable: ${error.message}`);
|
|
375
|
+
return 'custody';
|
|
376
|
+
}
|
|
377
|
+
if (local.state !== 'current' || JSON.stringify(local.scope.runtime) !== JSON.stringify(expected)) {
|
|
378
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, 'runtime local cambió durante generacion admitida');
|
|
379
|
+
return 'custody';
|
|
380
|
+
}
|
|
381
|
+
const stored = (0, event_store_1.readStoredEventReceipt)(expected);
|
|
382
|
+
if (stored.state === 'invalid' || (stored.state === 'present' &&
|
|
383
|
+
(stored.receipt.binaryDigest !== local.scope.binaryDigest || stored.receipt.configDigest !== local.scope.configDigest))) {
|
|
384
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, 'recibo nativo cambió durante generacion admitida');
|
|
385
|
+
return 'custody';
|
|
386
|
+
}
|
|
387
|
+
if (stored.state === 'absent' && (0, capabilities_1.readCapabilities)(expected, new Date()).state !== 'current') {
|
|
388
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, 'capacidades de routing no verificables durante generacion admitida');
|
|
389
|
+
return 'custody';
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (!generationAdmitted && before0.state.admissionRetry && Date.now() < Date.parse(before0.state.admissionRetry.nextRetryAt))
|
|
394
|
+
return 'continue';
|
|
298
395
|
// A controller can create jobs and runnerTick can start them. Both are
|
|
299
396
|
// downstream of the same full compact unattended admission, so it must
|
|
300
397
|
// complete before any reconciliation path that could dispatch either.
|
|
301
|
-
if (
|
|
398
|
+
if (!generationAdmitted) {
|
|
302
399
|
let admission;
|
|
303
400
|
try {
|
|
304
401
|
admission = await this.dispatchAdmission();
|
|
@@ -309,40 +406,48 @@ class Supervisor {
|
|
|
309
406
|
}
|
|
310
407
|
if (admission.state !== 'admitted' || admission.executionMode !== 'desatendido'
|
|
311
408
|
|| admission.currentness !== 'current' || admission.sensors !== 'pass' || admission.journal !== 'current') {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
&& admission.sensors === 'not-certified' && admission.journal === 'not-required') {
|
|
329
|
-
this.sensorDeferrals += 1;
|
|
330
|
-
if (this.sensorDeferrals <= exports.MAX_SENSOR_DEFERRALS)
|
|
409
|
+
const kind = transientAdmission(admission);
|
|
410
|
+
if (kind) {
|
|
411
|
+
const plan = (0, validate_1.validatePlanFile)(binding.path, this.repoRoot);
|
|
412
|
+
if (plan.state !== 'valid' || plan.planDigest !== binding.digest || plan.schema !== binding.schema
|
|
413
|
+
|| (binding.executionDigest !== undefined && plan.executionDigest !== binding.executionDigest)
|
|
414
|
+
|| (admission.planDigest !== undefined && admission.planDigest !== binding.digest)) {
|
|
415
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, 'binding del plan alterado durante admisión transitoria');
|
|
416
|
+
return 'custody';
|
|
417
|
+
}
|
|
418
|
+
const prior = before0.state.admissionRetry;
|
|
419
|
+
const attempts = (prior?.attempts ?? 0) + 1;
|
|
420
|
+
if (attempts <= types_1.MAX_ADMISSION_DEFERRALS) {
|
|
421
|
+
const now = Date.now();
|
|
422
|
+
before0.state.admissionRetry = { kind, attempts, firstAt: prior?.firstAt ?? new Date(now).toISOString(), nextRetryAt: new Date(now + exports.ADMISSION_RETRY_DELAY_MS).toISOString() };
|
|
423
|
+
(0, store_1.writeJournal)(this.repoRoot, this.branch, before0.state);
|
|
424
|
+
(0, store_1.appendEvent)(this.repoRoot, this.branch, { kind: 'admission-retry', cause: kind, attempts, retryAfterMs: exports.ADMISSION_RETRY_DELAY_MS });
|
|
331
425
|
return 'continue';
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
// tick defeats that guard and rewrites the journal plus appends a
|
|
335
|
-
// custody-blocked event every tickMs, without bound. Naming the
|
|
336
|
-
// bound instead of the count keeps the reason stable, so custody
|
|
337
|
-
// is entered once and stays entered.
|
|
338
|
-
(0, generations_1.enterCustody)(this.repoRoot, this.branch, `veredicto de sensores no concluyente tras ${exports.MAX_SENSOR_DEFERRALS} ticks: ${admission.diagnostics.map(diagnostic => `${diagnostic.code}: ${diagnostic.message}`).join('; ')}`);
|
|
426
|
+
}
|
|
427
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, `admisión ${kind} no verificable tras ${types_1.MAX_ADMISSION_DEFERRALS} reintentos: ${admission.diagnostics.map(diagnostic => `${diagnostic.code}: ${diagnostic.message}`).join('; ')}`);
|
|
339
428
|
return 'custody';
|
|
340
429
|
}
|
|
341
430
|
(0, generations_1.enterCustody)(this.repoRoot, this.branch, `admisión compacta desatendida bloqueada antes de dispatch: ${admission.diagnostics.map(diagnostic => `${diagnostic.code}: ${diagnostic.message}`).join('; ')}`);
|
|
342
431
|
return 'custody';
|
|
343
432
|
}
|
|
344
|
-
//
|
|
345
|
-
|
|
433
|
+
// An incomplete assertion can be repaired while the cycle is
|
|
434
|
+
// still waiting. Bind it only after an admitted observation, never
|
|
435
|
+
// on an early currentness/sensor response that has not reached the
|
|
436
|
+
// provider, runtime and autonomy gates yet.
|
|
437
|
+
if (!before0.state.admissionContext) {
|
|
438
|
+
before0.state.admissionContext = assertedContext;
|
|
439
|
+
(0, store_1.writeJournal)(this.repoRoot, this.branch, before0.state);
|
|
440
|
+
before0 = (0, store_1.readJournal)(this.repoRoot, this.branch);
|
|
441
|
+
if (before0.corrupt || !before0.state)
|
|
442
|
+
throw new Error('journal no verificable tras vincular admisión completa');
|
|
443
|
+
}
|
|
444
|
+
if (before0.state.admissionRetry) {
|
|
445
|
+
before0.state.admissionRetry = undefined;
|
|
446
|
+
(0, store_1.writeJournal)(this.repoRoot, this.branch, before0.state);
|
|
447
|
+
before0 = (0, store_1.readJournal)(this.repoRoot, this.branch);
|
|
448
|
+
if (before0.corrupt || !before0.state)
|
|
449
|
+
throw new Error('journal no verificable tras resolver reintento de admisión');
|
|
450
|
+
}
|
|
346
451
|
}
|
|
347
452
|
let recoveryResumeAction;
|
|
348
453
|
// Schema-2 custody is reconciled before any controller launch. This is
|
|
@@ -396,14 +501,14 @@ class Supervisor {
|
|
|
396
501
|
// verdicts, and an adverse verdict without a closed fix all stop
|
|
397
502
|
// recovery before a controller can create more work.
|
|
398
503
|
const verdictById = new Map(before0.state.verdicts.map(verdict => [verdict.id, verdict]));
|
|
399
|
-
const openReviewOrFix = before0.state.tasks.some(task => {
|
|
504
|
+
const openReviewOrFix = (terminalTaskClaims && before0.state.tasks.some(task => {
|
|
400
505
|
const obligations = task.reviewObligations;
|
|
401
506
|
return !['spec', 'quality'].every(kind => obligations.some(obligation => obligation.kind === kind))
|
|
402
507
|
|| obligations.some(obligation => {
|
|
403
508
|
const verdict = obligation.verdictId === undefined ? undefined : verdictById.get(obligation.verdictId);
|
|
404
509
|
return verdict === undefined || verdict.result !== 'pass';
|
|
405
510
|
});
|
|
406
|
-
}) || before0.state.verdicts.some(verdict => verdict.result !== 'pass'
|
|
511
|
+
})) || before0.state.verdicts.some(verdict => verdict.result !== 'pass'
|
|
407
512
|
&& !before0.state.fixes.some(fix => fix.verdictId === verdict.id && fix.closed));
|
|
408
513
|
const recovery = (0, fingerprint_1.reconcileUnattendedRecovery)({
|
|
409
514
|
journal: before0.state, journalCorrupt: false, plan: before0.state.planBinding,
|
|
@@ -442,6 +547,8 @@ class Supervisor {
|
|
|
442
547
|
// mismo resultado terminal.
|
|
443
548
|
if (before.frozen !== undefined)
|
|
444
549
|
return 'frozen';
|
|
550
|
+
if (this.autoBeginGeneration && (0, generations_1.activeGeneration)(before) === undefined)
|
|
551
|
+
(0, generations_1.beginGeneration)(this.repoRoot, this.branch);
|
|
445
552
|
const pending = before.cycle.nextAction;
|
|
446
553
|
const resumeAction = recoveryResumeAction ?? { schema: 'controller-recovery/v1', kind: pending !== undefined ? 'resume-next-action' : 'resume-cycle' };
|
|
447
554
|
if (this.ensureController(resumeAction) === 'custody')
|
|
@@ -506,6 +613,10 @@ class Supervisor {
|
|
|
506
613
|
const finalizing = afterRequests.state?.cohortPhase === 'FINAL_INTEGRATION' || afterRequests.state?.cohortPhase === 'FINAL_INTERLOCK';
|
|
507
614
|
if (!finalizing && afterRequests.state !== null && (0, generations_1.activeGeneration)(afterRequests.state) === undefined
|
|
508
615
|
&& afterRequests.state.generations.length > 0 && afterRequests.state.cycle.status === 'IN_PROGRESS') {
|
|
616
|
+
// The current tick used its launched generation's admission. A
|
|
617
|
+
// replacement must wait until the next tick's full admission.
|
|
618
|
+
if (generationAdmitted)
|
|
619
|
+
return 'continue';
|
|
509
620
|
(0, generations_1.beginGeneration)(this.repoRoot, this.branch);
|
|
510
621
|
if (this.ensureController(resumeAction) === 'custody')
|
|
511
622
|
return 'custody';
|
|
@@ -518,12 +629,15 @@ class Supervisor {
|
|
|
518
629
|
const freezeCheck = (0, store_1.readJournal)(this.repoRoot, this.branch);
|
|
519
630
|
const freezing = !freezeCheck.corrupt && freezeCheck.state !== null
|
|
520
631
|
&& freezeCheck.state.freezeRequested === true && freezeCheck.state.frozen === undefined;
|
|
632
|
+
// Reconcile controller ownership and, if needed, freshly admit its
|
|
633
|
+
// replacement before runnerTick can launch received jobs.
|
|
634
|
+
const supervision = await this.superviseController();
|
|
635
|
+
if (supervision === 'custody')
|
|
636
|
+
return 'custody';
|
|
521
637
|
(0, runner_1.runnerTick)(this.repoRoot, this.branch, this.spawner, {
|
|
522
|
-
reconcileGraceMs: this.cfg.reconcileGraceMs, stallObservationMs: this.cfg.jobStallObservationMs,
|
|
638
|
+
reconcileGraceMs: this.cfg.reconcileGraceMs, stallObservationMs: this.cfg.jobStallObservationMs,
|
|
639
|
+
dispatch: !freezing && supervision === 'ok',
|
|
523
640
|
});
|
|
524
|
-
const custody = await this.superviseController();
|
|
525
|
-
if (custody)
|
|
526
|
-
return 'custody';
|
|
527
641
|
if (freezing)
|
|
528
642
|
return this.attemptFreeze();
|
|
529
643
|
const r = (0, store_1.readJournal)(this.repoRoot, this.branch);
|
|
@@ -636,15 +750,23 @@ class Supervisor {
|
|
|
636
750
|
(0, store_1.appendEvent)(this.repoRoot, this.branch, { kind: 'track-frozen', headSha: s.frozen.headSha });
|
|
637
751
|
return 'frozen'; // paso 6 ("libera el lock y sale"): runSupervisorLoop trata 'frozen' igual que 'complete'
|
|
638
752
|
}
|
|
639
|
-
/**
|
|
753
|
+
/** Deferred ownership drains existing jobs but never starts new work. */
|
|
640
754
|
async superviseController() {
|
|
641
755
|
const r = (0, store_1.readJournal)(this.repoRoot, this.branch);
|
|
642
756
|
if (r.corrupt || r.state === null)
|
|
643
757
|
throw new Error('journal corrupto (R1.6)');
|
|
644
758
|
const s = r.state;
|
|
645
759
|
const gen = (0, generations_1.activeGeneration)(s);
|
|
646
|
-
|
|
647
|
-
|
|
760
|
+
// Track worktrees may be driven by the plan controller without a
|
|
761
|
+
// local generation. The canonical final-integration job also runs
|
|
762
|
+
// after the plan controller is deliberately paused. An existing but
|
|
763
|
+
// unadopted launch is different: claim recovery/backoff must never
|
|
764
|
+
// dispatch received jobs under that generation.
|
|
765
|
+
if (gen === undefined)
|
|
766
|
+
return s.trackContext || s.cohortPhase === 'FINAL_INTEGRATION'
|
|
767
|
+
|| s.cohortPhase === 'FINAL_INTERLOCK' ? 'ok' : 'deferred';
|
|
768
|
+
if (gen.processRef === undefined)
|
|
769
|
+
return 'deferred';
|
|
648
770
|
if (this.lastGenerationToken !== gen.token) {
|
|
649
771
|
this.lastGenerationToken = gen.token;
|
|
650
772
|
this.lastActivity = null;
|
|
@@ -657,10 +779,13 @@ class Supervisor {
|
|
|
657
779
|
this.lastActivity = { key, changedAt: Date.now() };
|
|
658
780
|
}
|
|
659
781
|
const activityFrozenMs = Date.now() - this.lastActivity.changedAt;
|
|
660
|
-
const
|
|
782
|
+
const safeToReplace = adapter.safeToReplace(gen.processRef);
|
|
783
|
+
// A dead process with proven identity is not healthy merely because
|
|
784
|
+
// its last heartbeat was recent. Resolve it before any new job starts.
|
|
785
|
+
const decision = snap === null && safeToReplace === 'safe' ? 'resolve-generation' : (0, generations_1.decideStall)({ heartbeatAgeMs, activityFrozenMs, safeToReplace }, { heartbeatTimeoutMs: this.cfg.heartbeatTimeoutMs, activityWindowMs: this.cfg.activityWindowMs });
|
|
661
786
|
if (decision === 'healthy') {
|
|
662
787
|
this.backoff.reset();
|
|
663
|
-
return
|
|
788
|
+
return 'ok';
|
|
664
789
|
}
|
|
665
790
|
if (decision === 'suspected-stall-observe') {
|
|
666
791
|
if (gen.state !== 'controller-suspected-stall') {
|
|
@@ -668,39 +793,53 @@ class Supervisor {
|
|
|
668
793
|
(0, store_1.writeJournal)(this.repoRoot, this.branch, s);
|
|
669
794
|
(0, store_1.appendEvent)(this.repoRoot, this.branch, { kind: 'controller-suspected-stall', n: gen.n });
|
|
670
795
|
}
|
|
671
|
-
return
|
|
796
|
+
return 'ok';
|
|
672
797
|
}
|
|
673
798
|
if (decision === 'custody-blocked') {
|
|
674
799
|
(0, generations_1.enterCustody)(this.repoRoot, this.branch, 'doble senial de stall sin safeToReplace positivo del adapter (R4.2b)');
|
|
675
|
-
return
|
|
800
|
+
return 'custody';
|
|
676
801
|
}
|
|
677
802
|
// resolve-generation
|
|
678
803
|
const resolved = await (0, generations_1.resolveGeneration)(this.repoRoot, this.branch, adapter, { termGraceMs: this.cfg.termGraceMs, killGraceMs: this.cfg.killGraceMs });
|
|
679
804
|
if (resolved === 'custody-blocked')
|
|
680
|
-
return
|
|
805
|
+
return 'custody';
|
|
681
806
|
if (this.backoff.exhausted()) {
|
|
682
807
|
(0, generations_1.enterCustody)(this.repoRoot, this.branch, 'tope de relanzamientos por hora alcanzado (R4.3)');
|
|
683
|
-
return
|
|
808
|
+
return 'custody';
|
|
684
809
|
}
|
|
685
810
|
if (Date.now() < this.relaunchNotBefore)
|
|
686
|
-
return
|
|
811
|
+
return 'deferred'; // esperando backoff, auditando
|
|
812
|
+
// This is a different controller generation, even though the tick
|
|
813
|
+
// began under the old one's admission. Do not reuse that admission
|
|
814
|
+
// after RED edits, runtime drift, or a currentness change.
|
|
815
|
+
let replacementAdmission;
|
|
816
|
+
try {
|
|
817
|
+
replacementAdmission = await this.dispatchAdmission();
|
|
818
|
+
}
|
|
819
|
+
catch (error) {
|
|
820
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, `admisión de reemplazo no verificable: ${error.message}`);
|
|
821
|
+
return 'custody';
|
|
822
|
+
}
|
|
823
|
+
if (replacementAdmission.state !== 'admitted' || replacementAdmission.executionMode !== 'desatendido'
|
|
824
|
+
|| replacementAdmission.currentness !== 'current' || replacementAdmission.sensors !== 'pass'
|
|
825
|
+
|| replacementAdmission.journal !== 'current') {
|
|
826
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, `admisión de reemplazo bloqueada antes de dispatch: ${replacementAdmission.diagnostics.map(d => `${d.code}: ${d.message}`).join('; ')}`);
|
|
827
|
+
return 'custody';
|
|
828
|
+
}
|
|
687
829
|
(0, generations_1.beginGeneration)(this.repoRoot, this.branch);
|
|
688
830
|
const nextAction = (0, store_1.readJournal)(this.repoRoot, this.branch).state.cycle.nextAction;
|
|
689
831
|
const action = { schema: 'controller-recovery/v1', kind: nextAction !== undefined ? 'resume-next-action' : 'resume-cycle' };
|
|
690
832
|
const launched = this.ensureController(action);
|
|
691
833
|
if (launched === 'custody')
|
|
692
|
-
return
|
|
834
|
+
return 'custody';
|
|
693
835
|
if (launched === 'ok') {
|
|
694
836
|
this.backoff.recordRelaunch();
|
|
695
837
|
this.relaunchNotBefore = Date.now() + this.backoff.nextMs();
|
|
696
838
|
}
|
|
697
|
-
return
|
|
839
|
+
return launched === 'deferred' ? 'deferred' : 'ok';
|
|
698
840
|
}
|
|
699
841
|
}
|
|
700
842
|
exports.Supervisor = Supervisor;
|
|
701
|
-
/** Foreground, visible, terminable (R2.4): sin daemons. SIGINT/SIGTERM libera
|
|
702
|
-
* el lock y sale; COMPLETE => auto-exit liberando lock y terminando la
|
|
703
|
-
* generacion propia (cero huerfanos). */
|
|
704
843
|
async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.defaultWrapperSpawner)(), trackRuntime, dispatchAdmission) {
|
|
705
844
|
const r = (0, store_1.readJournal)(repoRoot, branch);
|
|
706
845
|
if (r.corrupt || r.state === null) {
|
|
@@ -713,20 +852,17 @@ async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.d
|
|
|
713
852
|
}
|
|
714
853
|
(0, lock_1.verifyBranchInvariant)(repoRoot, r.state.branch);
|
|
715
854
|
if (r.state.cycle.status === 'COMPLETE')
|
|
716
|
-
return;
|
|
855
|
+
return 'complete';
|
|
717
856
|
const handle = (0, lock_1.acquireLock)(repoRoot);
|
|
718
857
|
let shutdownRequested = false;
|
|
719
858
|
let wakeSleep = null;
|
|
720
859
|
let safeToRelease = false;
|
|
860
|
+
let terminal = 'stopped';
|
|
721
861
|
const onSignal = () => { shutdownRequested = true; wakeSleep?.(); };
|
|
722
862
|
process.on('SIGINT', onSignal);
|
|
723
863
|
process.on('SIGTERM', onSignal);
|
|
724
|
-
const sup = new Supervisor(repoRoot, branch, cfg, spawner, trackRuntime, dispatchAdmission);
|
|
864
|
+
const sup = new Supervisor(repoRoot, branch, cfg, spawner, trackRuntime, dispatchAdmission, true);
|
|
725
865
|
try {
|
|
726
|
-
const s0 = (0, store_1.readJournal)(repoRoot, branch).state;
|
|
727
|
-
if ((0, generations_1.activeGeneration)(s0) === undefined) {
|
|
728
|
-
(0, generations_1.beginGeneration)(repoRoot, branch);
|
|
729
|
-
}
|
|
730
866
|
for (;;) {
|
|
731
867
|
if (shutdownRequested)
|
|
732
868
|
break;
|
|
@@ -735,6 +871,7 @@ async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.d
|
|
|
735
871
|
// drenar ownership y liberar el lock, el track ya cumplió su
|
|
736
872
|
// freeze y no debe seguir despachando ni corriendo su loop.
|
|
737
873
|
if (out === 'complete' || out === 'frozen') {
|
|
874
|
+
terminal = out;
|
|
738
875
|
const finalState = (0, store_1.readJournal)(repoRoot, branch);
|
|
739
876
|
if (!finalState.corrupt && finalState.state?.routingIncidents?.length)
|
|
740
877
|
process.stderr.write(`AWM final routing summary ${JSON.stringify((0, journal_1.routingReport)(finalState.state))}\n`);
|
|
@@ -776,6 +913,7 @@ async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.d
|
|
|
776
913
|
if (shutdownRequested)
|
|
777
914
|
(0, store_1.writeJournal)(repoRoot, branch, sEnd);
|
|
778
915
|
safeToRelease = true;
|
|
916
|
+
return terminal;
|
|
779
917
|
}
|
|
780
918
|
finally {
|
|
781
919
|
process.removeListener('SIGINT', onSignal);
|
|
@@ -85,9 +85,15 @@ function isWellFormedEnvelope(x) {
|
|
|
85
85
|
}
|
|
86
86
|
function listPendingRequests(repoRoot, branch) {
|
|
87
87
|
const dir = (0, paths_1.requestsDir)(repoRoot, branch);
|
|
88
|
+
const directory = fs_1.default.lstatSync(dir);
|
|
89
|
+
if (directory.isSymbolicLink() || !directory.isDirectory())
|
|
90
|
+
throw new Error('request directory is not a controlled directory');
|
|
88
91
|
return fs_1.default.readdirSync(dir).filter((f) => f.endsWith('.json')).sort().map((f) => {
|
|
89
92
|
const file = path_1.default.join(dir, f);
|
|
90
93
|
try {
|
|
94
|
+
const stat = fs_1.default.lstatSync(file);
|
|
95
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
96
|
+
return { requestId: f, envelope: null, file, corrupt: true };
|
|
91
97
|
const parsed = JSON.parse(fs_1.default.readFileSync(file, 'utf8'));
|
|
92
98
|
// Shape completa antes de cualquier uso downstream. JSON valido no
|
|
93
99
|
// implica envelope valido: payload/generation/idempotency ausentes
|
|
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
6
6
|
};
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.GENERATION_STATES = exports.EXECUTION_STATES = void 0;
|
|
8
|
+
exports.MAX_ADMISSION_DEFERRALS = exports.GENERATION_STATES = exports.EXECUTION_STATES = void 0;
|
|
9
9
|
exports.activeRequestProblems = activeRequestProblems;
|
|
10
10
|
exports.isControllerRecoveryAction = isControllerRecoveryAction;
|
|
11
11
|
exports.emptyState = emptyState;
|
|
@@ -50,6 +50,7 @@ function activeRequestProblems(state) {
|
|
|
50
50
|
|| !state.tasks.some(task => task.id === problem.taskId);
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
|
+
exports.MAX_ADMISSION_DEFERRALS = 3;
|
|
53
54
|
function isControllerRecoveryAction(x) {
|
|
54
55
|
return isObj(x) && Object.keys(x).length === 2
|
|
55
56
|
&& x.schema === 'controller-recovery/v1'
|
|
@@ -98,6 +99,8 @@ function isWellFormedState(x) {
|
|
|
98
99
|
return false;
|
|
99
100
|
if (!Array.isArray(x.generations) || !Array.isArray(x.tasks))
|
|
100
101
|
return false;
|
|
102
|
+
if (!x.tasks.every(isWellFormedTask))
|
|
103
|
+
return false;
|
|
101
104
|
if (!Array.isArray(x.cycleVerificationPlan) || !Array.isArray(x.verdicts) || !Array.isArray(x.fixes))
|
|
102
105
|
return false;
|
|
103
106
|
if (!Array.isArray(x.requiredVerifiers) || !x.requiredVerifiers.every((kind) => VERIFICATION_KINDS.includes(kind))
|
|
@@ -107,8 +110,40 @@ function isWellFormedState(x) {
|
|
|
107
110
|
return false;
|
|
108
111
|
if (!isObj(x.appliedRequests) || !Object.values(x.appliedRequests).every(isWellFormedAppliedRequest))
|
|
109
112
|
return false;
|
|
113
|
+
if (!Object.entries(x.appliedRequests).every(([requestId, ack]) => requestId === ack.requestId))
|
|
114
|
+
return false;
|
|
110
115
|
if (x.routingAttempts !== undefined && (!Array.isArray(x.routingAttempts) || x.routingAttempts.length > 4096 || !x.routingAttempts.every(isWellFormedRoutingAttempt)))
|
|
111
116
|
return false;
|
|
117
|
+
// A linked v2 dispatch is evidence of a native handoff. Reject a broken
|
|
118
|
+
// link or a second dispatch for one reservation on journal read as well as
|
|
119
|
+
// at request-application time. Historical unlinked records remain readable
|
|
120
|
+
// but cannot masquerade as this new acknowledged handoff.
|
|
121
|
+
if (x.planBinding?.schema === 'compact-slices/v2') {
|
|
122
|
+
const binding = x.planBinding;
|
|
123
|
+
const attempts = (x.routingAttempts ?? []);
|
|
124
|
+
const dispatches = x.dispatches;
|
|
125
|
+
const linked = new Set();
|
|
126
|
+
for (const dispatch of dispatches) {
|
|
127
|
+
if (!dispatch.routingAttemptId)
|
|
128
|
+
continue;
|
|
129
|
+
if (linked.has(dispatch.routingAttemptId))
|
|
130
|
+
return false;
|
|
131
|
+
linked.add(dispatch.routingAttemptId);
|
|
132
|
+
const attempt = attempts.find(candidate => candidate.id === dispatch.routingAttemptId);
|
|
133
|
+
if (!attempt || !(x.tasks.some(task => task.id === dispatch.taskId))
|
|
134
|
+
|| attempt.envelope.role !== 'implementer' || attempt.envelope.sliceId !== dispatch.taskId
|
|
135
|
+
|| attempt.envelope.planDigest !== binding.digest || attempt.envelope.executionDigest !== binding.executionDigest
|
|
136
|
+
|| !attempt.reservationRequestId || !dispatch.dispatchRequestId)
|
|
137
|
+
return false;
|
|
138
|
+
const acknowledgements = x.appliedRequests;
|
|
139
|
+
const reservationAck = acknowledgements[attempt.reservationRequestId];
|
|
140
|
+
const dispatchAck = acknowledgements[dispatch.dispatchRequestId];
|
|
141
|
+
if (reservationAck?.outcome !== 'applied' || reservationAck.requestId !== attempt.reservationRequestId
|
|
142
|
+
|| reservationAck.resultRef !== attempt.id || dispatchAck?.outcome !== 'applied'
|
|
143
|
+
|| dispatchAck.requestId !== dispatch.dispatchRequestId || dispatchAck.resultRef !== dispatch.id)
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
112
147
|
if (x.routingIncidents !== undefined && (!Array.isArray(x.routingIncidents) || x.routingIncidents.length > 4096 || !x.routingIncidents.every(isWellFormedRoutingIncident)))
|
|
113
148
|
return false;
|
|
114
149
|
if (x.implementationLineages !== undefined && (!Array.isArray(x.implementationLineages) || x.implementationLineages.length > 4096 || !x.implementationLineages.every(isWellFormedImplementationLineage)))
|
|
@@ -117,6 +152,12 @@ function isWellFormedState(x) {
|
|
|
117
152
|
return false;
|
|
118
153
|
if (x.custodyDecisions !== undefined && (!Array.isArray(x.custodyDecisions) || !x.custodyDecisions.every(isWellFormedCustodyDecision)))
|
|
119
154
|
return false;
|
|
155
|
+
if (x.admissionRetry !== undefined && !isWellFormedAdmissionRetry(x.admissionRetry))
|
|
156
|
+
return false;
|
|
157
|
+
if (x.admissionContext !== undefined && !isWellFormedAdmissionContext(x.admissionContext))
|
|
158
|
+
return false;
|
|
159
|
+
if (x.admissionRecoveries !== undefined && (!Array.isArray(x.admissionRecoveries) || !x.admissionRecoveries.every(isWellFormedAdmissionRecovery)))
|
|
160
|
+
return false;
|
|
120
161
|
if (!x.generations.every(isWellFormedGeneration) || !x.tasks.every(isWellFormedTask))
|
|
121
162
|
return false;
|
|
122
163
|
if (!x.cycleVerificationPlan.every(isWellFormedVerificationItem))
|
|
@@ -176,7 +217,10 @@ function isWellFormedNextAction(x) {
|
|
|
176
217
|
&& (x.state === 'pending' || x.state === 'in-progress');
|
|
177
218
|
}
|
|
178
219
|
function isWellFormedDispatch(x) {
|
|
179
|
-
return isObj(x) &&
|
|
220
|
+
return isObj(x) && Object.keys(x).every(key => ['id', 'taskId', 'at', 'routingAttemptId', 'dispatchRequestId'].includes(key))
|
|
221
|
+
&& typeof x.id === 'string' && x.id.length > 0 && typeof x.taskId === 'string' && x.taskId.length > 0
|
|
222
|
+
&& typeof x.at === 'string' && (x.routingAttemptId === undefined || (typeof x.routingAttemptId === 'string' && x.routingAttemptId.length > 0))
|
|
223
|
+
&& (x.dispatchRequestId === undefined || (typeof x.dispatchRequestId === 'string' && x.dispatchRequestId.length > 0));
|
|
180
224
|
}
|
|
181
225
|
function strings(x) {
|
|
182
226
|
return Array.isArray(x) && x.every((item) => typeof item === 'string');
|
|
@@ -228,11 +272,11 @@ function isWellFormedAppliedRequest(x) {
|
|
|
228
272
|
&& (x.resultRef === undefined || typeof x.resultRef === 'string');
|
|
229
273
|
}
|
|
230
274
|
function isWellFormedRoutingAttempt(x) {
|
|
231
|
-
return isObj(x) && Object.keys(x).every(key => ['schema', 'id', 'obligationId', 'lineageId', 'attempt', 'envelope', 'envelopeDigest', 'fingerprint', 'state', 'reservedAt', 'nativeEvidenceRequired', 'nativeEventDigest', 'nativeAgentId', 'observed', 'observedBackendModel', 'verdict', 'reasonCode', 'administrativeRepair'].includes(key))
|
|
275
|
+
return isObj(x) && Object.keys(x).every(key => ['schema', 'id', 'obligationId', 'lineageId', 'attempt', 'envelope', 'envelopeDigest', 'fingerprint', 'state', 'reservedAt', 'reservationRequestId', 'nativeEvidenceRequired', 'nativeEventDigest', 'nativeAgentId', 'observed', 'observedBackendModel', 'verdict', 'reasonCode', 'administrativeRepair'].includes(key))
|
|
232
276
|
&& x.schema === 'routing-attempt/v1'
|
|
233
277
|
&& typeof x.id === 'string' && typeof x.obligationId === 'string' && typeof x.lineageId === 'string' && Number.isSafeInteger(x.attempt) && x.attempt > 0
|
|
234
278
|
&& typeof x.envelopeDigest === 'string' && /^[a-f0-9]{64}$/.test(x.envelopeDigest) && typeof x.fingerprint === 'string' && /^[a-f0-9]{64}$/.test(x.fingerprint)
|
|
235
|
-
&& ['reserved', 'active', 'complete', 'blocked', 'unknown'].includes(String(x.state)) && (x.reservedAt === undefined || (typeof x.reservedAt === 'string' && Number.isFinite(Date.parse(x.reservedAt)))) && (x.nativeEvidenceRequired === undefined || x.nativeEvidenceRequired === true) && (x.nativeEventDigest === undefined || isDigest(x.nativeEventDigest)) && (x.nativeAgentId === undefined || typeof x.nativeAgentId === 'string') && (x.observed === undefined || isRoutingSelection(x.observed)) && (x.observedBackendModel === undefined || (typeof x.observedBackendModel === 'string' && x.observedBackendModel.length > 0 && x.observedBackendModel.length <= 128)) && (x.verdict === undefined || ['pass', 'fail', 'inconclusive'].includes(String(x.verdict))) && (x.reasonCode === undefined || typeof x.reasonCode === 'string') && (x.administrativeRepair === undefined || x.administrativeRepair === true) && isRoutingEnvelope(x.envelope);
|
|
279
|
+
&& ['reserved', 'active', 'complete', 'blocked', 'unknown'].includes(String(x.state)) && (x.reservedAt === undefined || (typeof x.reservedAt === 'string' && Number.isFinite(Date.parse(x.reservedAt)))) && (x.reservationRequestId === undefined || (typeof x.reservationRequestId === 'string' && x.reservationRequestId.length > 0 && x.reservationRequestId.length <= 128)) && (x.nativeEvidenceRequired === undefined || x.nativeEvidenceRequired === true) && (x.nativeEventDigest === undefined || isDigest(x.nativeEventDigest)) && (x.nativeAgentId === undefined || typeof x.nativeAgentId === 'string') && (x.observed === undefined || isRoutingSelection(x.observed)) && (x.observedBackendModel === undefined || (typeof x.observedBackendModel === 'string' && x.observedBackendModel.length > 0 && x.observedBackendModel.length <= 128)) && (x.verdict === undefined || ['pass', 'fail', 'inconclusive'].includes(String(x.verdict))) && (x.reasonCode === undefined || typeof x.reasonCode === 'string') && (x.administrativeRepair === undefined || x.administrativeRepair === true) && isRoutingEnvelope(x.envelope);
|
|
236
280
|
}
|
|
237
281
|
function isWellFormedRoutingIncident(x) {
|
|
238
282
|
if (!isObj(x) || Object.keys(x).length !== 12 || !Object.keys(x).every(key => ['schema', 'id', 'runStartedAt', 'target', 'selection', 'reasonCode', 'firstAt', 'lastAt', 'affectedObligations', 'fallbackCount', 'blockedCount', 'alertState'].includes(key)))
|
|
@@ -274,6 +318,36 @@ function isWellFormedCustodyDecision(x) {
|
|
|
274
318
|
return isObj(x) && typeof x.at === 'string' && x.decision === 'resume'
|
|
275
319
|
&& typeof x.reason === 'string' && typeof x.generationToken === 'string';
|
|
276
320
|
}
|
|
321
|
+
function isWellFormedAdmissionRetry(x) {
|
|
322
|
+
return isObj(x) && Object.keys(x).length === 4
|
|
323
|
+
&& (x.kind === 'currentness' || x.kind === 'sensors')
|
|
324
|
+
&& Number.isInteger(x.attempts) && Number(x.attempts) >= 1 && Number(x.attempts) <= exports.MAX_ADMISSION_DEFERRALS
|
|
325
|
+
&& typeof x.firstAt === 'string' && Number.isFinite(Date.parse(x.firstAt))
|
|
326
|
+
&& typeof x.nextRetryAt === 'string' && Number.isFinite(Date.parse(x.nextRetryAt))
|
|
327
|
+
&& Date.parse(x.nextRetryAt) >= Date.parse(x.firstAt);
|
|
328
|
+
}
|
|
329
|
+
function isWellFormedAdmissionContext(x) {
|
|
330
|
+
if (!isObj(x) || !Object.keys(x).every(key => ['provider', 'runtime', 'controllerAutonomy'].includes(key))
|
|
331
|
+
|| (x.provider !== 'codex' && x.provider !== 'claude-code')
|
|
332
|
+
|| (x.controllerAutonomy !== undefined && x.controllerAutonomy !== 'approval-free'))
|
|
333
|
+
return false;
|
|
334
|
+
if (x.runtime === undefined)
|
|
335
|
+
return true;
|
|
336
|
+
return isObj(x.runtime) && Object.keys(x.runtime).length === 3
|
|
337
|
+
&& typeof x.runtime.kind === 'string' && x.runtime.kind.length > 0
|
|
338
|
+
&& typeof x.runtime.version === 'string' && x.runtime.version.length > 0
|
|
339
|
+
&& typeof x.runtime.accountScopeDigest === 'string' && /^[a-f0-9]{64}$/.test(x.runtime.accountScopeDigest);
|
|
340
|
+
}
|
|
341
|
+
function isWellFormedAdmissionRecovery(x) {
|
|
342
|
+
return isObj(x) && Object.keys(x).every(key => ['at', 'reason', 'blockedReason', 'generationToken', 'context', 'planDigest', 'assertion'].includes(key))
|
|
343
|
+
&& typeof x.at === 'string' && Number.isFinite(Date.parse(x.at))
|
|
344
|
+
&& typeof x.reason === 'string' && x.reason.trim().length > 0 && x.reason.length <= 1024
|
|
345
|
+
&& typeof x.blockedReason === 'string' && x.blockedReason.length > 0
|
|
346
|
+
&& (x.generationToken === undefined || (typeof x.generationToken === 'string' && x.generationToken.length > 0))
|
|
347
|
+
&& isWellFormedAdmissionContext(x.context)
|
|
348
|
+
&& typeof x.planDigest === 'string' && /^[a-f0-9]{64}$/.test(x.planDigest)
|
|
349
|
+
&& (x.assertion === 'recorded' || x.assertion === 'legacy-operator-asserted');
|
|
350
|
+
}
|
|
277
351
|
function isWellFormedProcessRef(x) {
|
|
278
352
|
if (!isObj(x))
|
|
279
353
|
return false;
|