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.
@@ -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 = 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
+ }
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
- const before0 = (0, store_1.readJournal)(this.repoRoot, this.branch);
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 (before0.state.schema === 2 && before0.state.planBinding) {
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
- // A first empirical sensor observation can be inconclusive while
313
- // the already-started controller is still settling. It never
314
- // authorizes dispatch; retry the read-only admission next tick
315
- // instead of permanently custodying a healthy cycle.
316
- //
317
- // Bounded, because the verdict is not always transient: a
318
- // registry whose sensors are all disabled reports
319
- // `not-certified` forever. Unbounded, this returned before
320
- // `consumePendingRequests` below, so the supervisor ticked
321
- // indefinitely applying nothing and writing nothing — no event,
322
- // no state change, no exit. Refusing to progress is correct
323
- // here; refusing invisibly is not. Consuming requests cannot be
324
- // hoisted above this gate to compensate: applying a job-request
325
- // creates a Job that runnerTick can start, which is the dispatch
326
- // this admission exists to authorize.
327
- if (admission.planState === 'valid' && admission.currentness === 'current'
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
- // The reason must not carry the running counter. enterCustody is
333
- // idempotent on (status, reason), so a message that changes every
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
- // The verdict settled, which is exactly what the deferral waited for.
345
- this.sensorDeferrals = 0;
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, dispatch: !freezing,
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
- /** true => custodia (el caller NO libera lock ni sale). */
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
- if (gen?.processRef === undefined)
647
- return false; // sin controlador propio: nada que supervisar
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 decision = (0, generations_1.decideStall)({ heartbeatAgeMs, activityFrozenMs, safeToReplace: adapter.safeToReplace(gen.processRef) }, { heartbeatTimeoutMs: this.cfg.heartbeatTimeoutMs, activityWindowMs: this.cfg.activityWindowMs });
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 false;
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 false;
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 true;
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 true;
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 true;
808
+ return 'custody';
684
809
  }
685
810
  if (Date.now() < this.relaunchNotBefore)
686
- return false; // esperando backoff, auditando
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 true;
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 false;
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) && typeof x.id === 'string' && typeof x.taskId === 'string' && typeof x.at === 'string';
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;