agentic-workflow-manager 9.12.0 → 9.12.2

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.
@@ -1,9 +1,10 @@
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;
7
+ exports.recoveryWhitelistBlocker = recoveryWhitelistBlocker;
7
8
  exports.runSupervisorLoop = runSupervisorLoop;
8
9
  // Loop foreground (R4.4/R4.5): tick = apply -> collect/spawn -> stall -> gate.
9
10
  // COMPLETE exige gate verde (que exige cero vivos): drenaje ANTES de declarar.
@@ -27,6 +28,7 @@ const apply_1 = require("./apply");
27
28
  const runner_1 = require("./runner");
28
29
  const tracks_1 = require("./tracks");
29
30
  const generations_1 = require("./generations");
31
+ const types_1 = require("../../core/journal/types");
30
32
  /** #166: compact v2 admission requires the runtime identity, and the supervisor
31
33
  * never supplied it — so `awm watch` blocked on every tick with
32
34
  * ADMISSION_ROUTING_FACTS_REQUIRED even with every other gate green.
@@ -190,6 +192,21 @@ exports.DEFAULT_SUPERVISOR_CONFIG = {
190
192
  function admissionForConfig(repoRoot, branch, cfg, deps = {}) {
191
193
  return defaultDispatchAdmission(repoRoot, branch, cfg.provider, cfg.routingIdentity, deps, cfg.controllerAutonomy);
192
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
+ }
193
210
  // 'frozen' (R5.2/R6.3, Task 10): SOLO puede ocurrir en el journal de un
194
211
  // TRACK individual (nunca en el del plan) — el track terminó, con las 6
195
212
  // observaciones demostrables (cero jobs vivos, gate local verde, worktree
@@ -198,12 +215,31 @@ function admissionForConfig(repoRoot, branch, cfg, deps = {}) {
198
215
  /** How many consecutive ticks an inconclusive sensor verdict may defer dispatch
199
216
  * before the supervisor says so out loud. Small: the deferral covers a verdict
200
217
  * still settling, not one that never will. */
201
- exports.MAX_SENSOR_DEFERRALS = 3;
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
+ }
202
236
  const LIVE = ['received', 'spawn-intent', 'claimed', 'running', 'cancel-requested'];
203
237
  function recoveryWhitelistBlocker(state) {
204
- if (state.cycle.status === 'BLOCKED')
205
- return 'el ciclo está bloqueado';
206
- if (state.requestProblems.length > 0)
238
+ if (state.cycle.status === 'BLOCKED') {
239
+ return state.cycle.blockedReason === 'recovery no autorizado: hay conflictos durables de requests'
240
+ ? 'hay conflictos durables de requests' : 'el ciclo está bloqueado';
241
+ }
242
+ if ((0, types_1.activeRequestProblems)(state).length > 0)
207
243
  return 'hay conflictos durables de requests';
208
244
  // Pending work is the normal reason to launch (or retry launching) the
209
245
  // controller. It is not recovery evidence and therefore cannot turn a
@@ -226,21 +262,19 @@ class Supervisor {
226
262
  cfg;
227
263
  spawner;
228
264
  dispatchAdmission;
265
+ autoBeginGeneration;
229
266
  backoff = new generations_1.Backoff();
230
267
  relaunchNotBefore = 0;
231
- /** Consecutive ticks deferred on an inconclusive sensor verdict. Bounded:
232
- * the deferral exists for a verdict that is still settling, and a verdict
233
- * that never settles must become visible rather than loop forever. */
234
- sensorDeferrals = 0;
235
268
  lastActivity = null;
236
269
  lastGenerationToken = null;
237
270
  trackRuntime;
238
- 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) {
239
272
  this.repoRoot = repoRoot;
240
273
  this.branch = branch;
241
274
  this.cfg = cfg;
242
275
  this.spawner = spawner;
243
276
  this.dispatchAdmission = dispatchAdmission;
277
+ this.autoBeginGeneration = autoBeginGeneration;
244
278
  this.trackRuntime = trackRuntime ?? (0, tracks_1.defaultTrackRuntime)(repoRoot, branch, { termGraceMs: cfg.termGraceMs, killGraceMs: cfg.killGraceMs });
245
279
  }
246
280
  fingerprintNow = (argv, paths, cwd) => {
@@ -277,7 +311,7 @@ class Supervisor {
277
311
  }
278
312
  }
279
313
  async tick() {
280
- const before0 = (0, store_1.readJournal)(this.repoRoot, this.branch);
314
+ let before0 = (0, store_1.readJournal)(this.repoRoot, this.branch);
281
315
  if (before0.corrupt || before0.state === null)
282
316
  throw new Error('journal corrupto: el supervisor no opera sobre corrupcion (R1.6)');
283
317
  // Historical/schema-1 journals are intentionally readable by migration
@@ -291,6 +325,24 @@ class Supervisor {
291
325
  // longer reconstructible.
292
326
  if (before0.state.cycle.status === 'COMPLETE')
293
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
+ if (before0.state.admissionRetry && Date.now() < Date.parse(before0.state.admissionRetry.nextRetryAt))
345
+ return 'continue';
294
346
  // A controller can create jobs and runnerTick can start them. Both are
295
347
  // downstream of the same full compact unattended admission, so it must
296
348
  // complete before any reconciliation path that could dispatch either.
@@ -305,40 +357,49 @@ class Supervisor {
305
357
  }
306
358
  if (admission.state !== 'admitted' || admission.executionMode !== 'desatendido'
307
359
  || admission.currentness !== 'current' || admission.sensors !== 'pass' || admission.journal !== 'current') {
308
- // A first empirical sensor observation can be inconclusive while
309
- // the already-started controller is still settling. It never
310
- // authorizes dispatch; retry the read-only admission next tick
311
- // instead of permanently custodying a healthy cycle.
312
- //
313
- // Bounded, because the verdict is not always transient: a
314
- // registry whose sensors are all disabled reports
315
- // `not-certified` forever. Unbounded, this returned before
316
- // `consumePendingRequests` below, so the supervisor ticked
317
- // indefinitely applying nothing and writing nothing — no event,
318
- // no state change, no exit. Refusing to progress is correct
319
- // here; refusing invisibly is not. Consuming requests cannot be
320
- // hoisted above this gate to compensate: applying a job-request
321
- // creates a Job that runnerTick can start, which is the dispatch
322
- // this admission exists to authorize.
323
- if (admission.planState === 'valid' && admission.currentness === 'current'
324
- && admission.sensors === 'not-certified' && admission.journal === 'not-required') {
325
- this.sensorDeferrals += 1;
326
- if (this.sensorDeferrals <= exports.MAX_SENSOR_DEFERRALS)
360
+ const kind = transientAdmission(admission);
361
+ if (kind) {
362
+ const binding = before0.state.planBinding;
363
+ const plan = (0, validate_1.validatePlanFile)(binding.path, this.repoRoot);
364
+ if (plan.state !== 'valid' || plan.planDigest !== binding.digest || plan.schema !== binding.schema
365
+ || (binding.executionDigest !== undefined && plan.executionDigest !== binding.executionDigest)
366
+ || (admission.planDigest !== undefined && admission.planDigest !== binding.digest)) {
367
+ (0, generations_1.enterCustody)(this.repoRoot, this.branch, 'binding del plan alterado durante admisión transitoria');
368
+ return 'custody';
369
+ }
370
+ const prior = before0.state.admissionRetry;
371
+ const attempts = (prior?.attempts ?? 0) + 1;
372
+ if (attempts <= types_1.MAX_ADMISSION_DEFERRALS) {
373
+ const now = Date.now();
374
+ before0.state.admissionRetry = { kind, attempts, firstAt: prior?.firstAt ?? new Date(now).toISOString(), nextRetryAt: new Date(now + exports.ADMISSION_RETRY_DELAY_MS).toISOString() };
375
+ (0, store_1.writeJournal)(this.repoRoot, this.branch, before0.state);
376
+ (0, store_1.appendEvent)(this.repoRoot, this.branch, { kind: 'admission-retry', cause: kind, attempts, retryAfterMs: exports.ADMISSION_RETRY_DELAY_MS });
327
377
  return 'continue';
328
- // The reason must not carry the running counter. enterCustody is
329
- // idempotent on (status, reason), so a message that changes every
330
- // tick defeats that guard and rewrites the journal plus appends a
331
- // custody-blocked event every tickMs, without bound. Naming the
332
- // bound instead of the count keeps the reason stable, so custody
333
- // is entered once and stays entered.
334
- (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('; ')}`);
378
+ }
379
+ (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('; ')}`);
335
380
  return 'custody';
336
381
  }
337
382
  (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('; ')}`);
338
383
  return 'custody';
339
384
  }
340
- // The verdict settled, which is exactly what the deferral waited for.
341
- this.sensorDeferrals = 0;
385
+ // An incomplete assertion can be repaired while the cycle is
386
+ // still waiting. Bind it only after an admitted observation, never
387
+ // on an early currentness/sensor response that has not reached the
388
+ // provider, runtime and autonomy gates yet.
389
+ if (!before0.state.admissionContext) {
390
+ before0.state.admissionContext = assertedContext;
391
+ (0, store_1.writeJournal)(this.repoRoot, this.branch, before0.state);
392
+ before0 = (0, store_1.readJournal)(this.repoRoot, this.branch);
393
+ if (before0.corrupt || !before0.state)
394
+ throw new Error('journal no verificable tras vincular admisión completa');
395
+ }
396
+ if (before0.state.admissionRetry) {
397
+ before0.state.admissionRetry = undefined;
398
+ (0, store_1.writeJournal)(this.repoRoot, this.branch, before0.state);
399
+ before0 = (0, store_1.readJournal)(this.repoRoot, this.branch);
400
+ if (before0.corrupt || !before0.state)
401
+ throw new Error('journal no verificable tras resolver reintento de admisión');
402
+ }
342
403
  }
343
404
  let recoveryResumeAction;
344
405
  // Schema-2 custody is reconciled before any controller launch. This is
@@ -392,14 +453,14 @@ class Supervisor {
392
453
  // verdicts, and an adverse verdict without a closed fix all stop
393
454
  // recovery before a controller can create more work.
394
455
  const verdictById = new Map(before0.state.verdicts.map(verdict => [verdict.id, verdict]));
395
- const openReviewOrFix = before0.state.tasks.some(task => {
456
+ const openReviewOrFix = (terminalTaskClaims && before0.state.tasks.some(task => {
396
457
  const obligations = task.reviewObligations;
397
458
  return !['spec', 'quality'].every(kind => obligations.some(obligation => obligation.kind === kind))
398
459
  || obligations.some(obligation => {
399
460
  const verdict = obligation.verdictId === undefined ? undefined : verdictById.get(obligation.verdictId);
400
461
  return verdict === undefined || verdict.result !== 'pass';
401
462
  });
402
- }) || before0.state.verdicts.some(verdict => verdict.result !== 'pass'
463
+ })) || before0.state.verdicts.some(verdict => verdict.result !== 'pass'
403
464
  && !before0.state.fixes.some(fix => fix.verdictId === verdict.id && fix.closed));
404
465
  const recovery = (0, fingerprint_1.reconcileUnattendedRecovery)({
405
466
  journal: before0.state, journalCorrupt: false, plan: before0.state.planBinding,
@@ -438,6 +499,8 @@ class Supervisor {
438
499
  // mismo resultado terminal.
439
500
  if (before.frozen !== undefined)
440
501
  return 'frozen';
502
+ if (this.autoBeginGeneration && (0, generations_1.activeGeneration)(before) === undefined)
503
+ (0, generations_1.beginGeneration)(this.repoRoot, this.branch);
441
504
  const pending = before.cycle.nextAction;
442
505
  const resumeAction = recoveryResumeAction ?? { schema: 'controller-recovery/v1', kind: pending !== undefined ? 'resume-next-action' : 'resume-cycle' };
443
506
  if (this.ensureController(resumeAction) === 'custody')
@@ -717,12 +780,8 @@ async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.d
717
780
  const onSignal = () => { shutdownRequested = true; wakeSleep?.(); };
718
781
  process.on('SIGINT', onSignal);
719
782
  process.on('SIGTERM', onSignal);
720
- const sup = new Supervisor(repoRoot, branch, cfg, spawner, trackRuntime, dispatchAdmission);
783
+ const sup = new Supervisor(repoRoot, branch, cfg, spawner, trackRuntime, dispatchAdmission, true);
721
784
  try {
722
- const s0 = (0, store_1.readJournal)(repoRoot, branch).state;
723
- if ((0, generations_1.activeGeneration)(s0) === undefined) {
724
- (0, generations_1.beginGeneration)(repoRoot, branch);
725
- }
726
785
  for (;;) {
727
786
  if (shutdownRequested)
728
787
  break;
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.digestOf = digestOf;
7
7
  exports.emitRequest = emitRequest;
8
+ exports.isWellFormedEnvelope = isWellFormedEnvelope;
8
9
  exports.listPendingRequests = listPendingRequests;
9
10
  exports.applyOutcome = applyOutcome;
10
11
  exports.ackFor = ackFor;
@@ -5,7 +5,8 @@ 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
+ exports.activeRequestProblems = activeRequestProblems;
9
10
  exports.isControllerRecoveryAction = isControllerRecoveryAction;
10
11
  exports.emptyState = emptyState;
11
12
  exports.isWellFormedState = isWellFormedState;
@@ -14,6 +15,7 @@ exports.isRoutingEnvelope = isRoutingEnvelope;
14
15
  exports.isWellFormedProcessRef = isWellFormedProcessRef;
15
16
  exports.isWellFormedJob = isWellFormedJob;
16
17
  const crypto_1 = __importDefault(require("crypto"));
18
+ const path_1 = __importDefault(require("path"));
17
19
  const types_1 = require("../tracks/types");
18
20
  const paths_1 = require("./paths");
19
21
  exports.EXECUTION_STATES = [
@@ -35,6 +37,20 @@ const COHORT_PHASES = [
35
37
  ];
36
38
  const _cohortPhasesComplete = true;
37
39
  const VERIFICATION_KINDS = ['test', 'lint', 'sensors', 'review', 'qa', 'interlock', 'track-integration'];
40
+ function activeRequestProblems(state) {
41
+ return state.requestProblems.filter(problem => {
42
+ const resolution = problem.resolution;
43
+ if (!resolution || !problem.requestId || !problem.taskId || !problem.generationToken
44
+ || path_1.default.basename(problem.file) !== `${problem.requestId}.json`
45
+ || resolution.generationToken !== problem.generationToken)
46
+ return true;
47
+ const applied = state.appliedRequests[resolution.replacementRequestId];
48
+ return applied?.outcome !== 'applied' || applied.resultRef !== problem.taskId
49
+ || applied.payloadDigest !== resolution.replacementPayloadDigest
50
+ || !state.tasks.some(task => task.id === problem.taskId);
51
+ });
52
+ }
53
+ exports.MAX_ADMISSION_DEFERRALS = 3;
38
54
  function isControllerRecoveryAction(x) {
39
55
  return isObj(x) && Object.keys(x).length === 2
40
56
  && x.schema === 'controller-recovery/v1'
@@ -102,6 +118,12 @@ function isWellFormedState(x) {
102
118
  return false;
103
119
  if (x.custodyDecisions !== undefined && (!Array.isArray(x.custodyDecisions) || !x.custodyDecisions.every(isWellFormedCustodyDecision)))
104
120
  return false;
121
+ if (x.admissionRetry !== undefined && !isWellFormedAdmissionRetry(x.admissionRetry))
122
+ return false;
123
+ if (x.admissionContext !== undefined && !isWellFormedAdmissionContext(x.admissionContext))
124
+ return false;
125
+ if (x.admissionRecoveries !== undefined && (!Array.isArray(x.admissionRecoveries) || !x.admissionRecoveries.every(isWellFormedAdmissionRecovery)))
126
+ return false;
105
127
  if (!x.generations.every(isWellFormedGeneration) || !x.tasks.every(isWellFormedTask))
106
128
  return false;
107
129
  if (!x.cycleVerificationPlan.every(isWellFormedVerificationItem))
@@ -240,12 +262,55 @@ function isRoutingEnvelope(x) { if (!isObj(x) || !Object.keys(x).every(key => ['
240
262
  function isWellFormedImplementationLineage(x) { return isObj(x) && Object.keys(x).every(key => ['id', 'obligationId', 'sliceId', 'planDigest', 'executionDigest', 'initialProfile', 'initialEffort', 'attempts'].includes(key)) && typeof x.id === 'string' && x.id.length > 0 && typeof x.obligationId === 'string' && x.obligationId.length > 0 && typeof x.sliceId === 'string' && x.sliceId.length > 0 && isDigest(x.planDigest) && isDigest(x.executionDigest) && ['mechanical', 'integration', 'judgment'].includes(String(x.initialProfile)) && ['medium', 'high', 'runtime-default'].includes(String(x.initialEffort)) && Number.isSafeInteger(x.attempts) && x.attempts >= 0 && x.attempts <= 3; }
241
263
  function isWellFormedRequestProblem(x) {
242
264
  return isObj(x) && typeof x.file === 'string' && (x.kind === 'corrupt' || x.kind === 'rejected')
243
- && typeof x.detail === 'string' && typeof x.at === 'string';
265
+ && typeof x.detail === 'string' && typeof x.at === 'string'
266
+ && (x.requestId === undefined || typeof x.requestId === 'string')
267
+ && (x.taskId === undefined || typeof x.taskId === 'string')
268
+ && (x.generationToken === undefined || typeof x.generationToken === 'string')
269
+ && (x.resolution === undefined || (x.kind === 'rejected' && isObj(x.resolution)
270
+ && Object.keys(x.resolution).every(key => ['replacementRequestId', 'replacementPayloadDigest', 'reason', 'generationToken', 'at'].includes(key))
271
+ && typeof x.requestId === 'string' && x.requestId.length > 0
272
+ && typeof x.taskId === 'string' && x.taskId.length > 0
273
+ && typeof x.generationToken === 'string' && x.generationToken.length > 0
274
+ && typeof x.resolution.replacementRequestId === 'string' && x.resolution.replacementRequestId.length > 0
275
+ && typeof x.resolution.replacementPayloadDigest === 'string' && /^[a-f0-9]{64}$/.test(x.resolution.replacementPayloadDigest)
276
+ && typeof x.resolution.reason === 'string' && x.resolution.reason.trim().length > 0
277
+ && typeof x.resolution.generationToken === 'string' && x.resolution.generationToken.length > 0
278
+ && typeof x.resolution.at === 'string' && Number.isFinite(Date.parse(x.resolution.at))));
244
279
  }
245
280
  function isWellFormedCustodyDecision(x) {
246
281
  return isObj(x) && typeof x.at === 'string' && x.decision === 'resume'
247
282
  && typeof x.reason === 'string' && typeof x.generationToken === 'string';
248
283
  }
284
+ function isWellFormedAdmissionRetry(x) {
285
+ return isObj(x) && Object.keys(x).length === 4
286
+ && (x.kind === 'currentness' || x.kind === 'sensors')
287
+ && Number.isInteger(x.attempts) && Number(x.attempts) >= 1 && Number(x.attempts) <= exports.MAX_ADMISSION_DEFERRALS
288
+ && typeof x.firstAt === 'string' && Number.isFinite(Date.parse(x.firstAt))
289
+ && typeof x.nextRetryAt === 'string' && Number.isFinite(Date.parse(x.nextRetryAt))
290
+ && Date.parse(x.nextRetryAt) >= Date.parse(x.firstAt);
291
+ }
292
+ function isWellFormedAdmissionContext(x) {
293
+ if (!isObj(x) || !Object.keys(x).every(key => ['provider', 'runtime', 'controllerAutonomy'].includes(key))
294
+ || (x.provider !== 'codex' && x.provider !== 'claude-code')
295
+ || (x.controllerAutonomy !== undefined && x.controllerAutonomy !== 'approval-free'))
296
+ return false;
297
+ if (x.runtime === undefined)
298
+ return true;
299
+ return isObj(x.runtime) && Object.keys(x.runtime).length === 3
300
+ && typeof x.runtime.kind === 'string' && x.runtime.kind.length > 0
301
+ && typeof x.runtime.version === 'string' && x.runtime.version.length > 0
302
+ && typeof x.runtime.accountScopeDigest === 'string' && /^[a-f0-9]{64}$/.test(x.runtime.accountScopeDigest);
303
+ }
304
+ function isWellFormedAdmissionRecovery(x) {
305
+ return isObj(x) && Object.keys(x).every(key => ['at', 'reason', 'blockedReason', 'generationToken', 'context', 'planDigest', 'assertion'].includes(key))
306
+ && typeof x.at === 'string' && Number.isFinite(Date.parse(x.at))
307
+ && typeof x.reason === 'string' && x.reason.trim().length > 0 && x.reason.length <= 1024
308
+ && typeof x.blockedReason === 'string' && x.blockedReason.length > 0
309
+ && (x.generationToken === undefined || (typeof x.generationToken === 'string' && x.generationToken.length > 0))
310
+ && isWellFormedAdmissionContext(x.context)
311
+ && typeof x.planDigest === 'string' && /^[a-f0-9]{64}$/.test(x.planDigest)
312
+ && (x.assertion === 'recorded' || x.assertion === 'legacy-operator-asserted');
313
+ }
249
314
  function isWellFormedProcessRef(x) {
250
315
  if (!isObj(x))
251
316
  return false;