agentic-workflow-manager 9.12.2 → 9.12.4
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 +13 -2
- package/dist/src/commands/watch/apply.js +118 -2
- package/dist/src/commands/watch/generations.js +1 -1
- package/dist/src/commands/watch/index.js +12 -2
- package/dist/src/commands/watch/supervisor.js +114 -24
- package/dist/src/core/admission/index.js +2 -0
- package/dist/src/core/admission/registry-contracts.js +74 -3
- package/dist/src/core/journal/requests.js +6 -0
- package/dist/src/core/journal/types.js +40 -3
- package/dist/tests/commands/watch/admission-recovery.test.js +44 -0
- package/dist/tests/commands/watch/plan-lifecycle.test.js +47 -1
- package/dist/tests/commands/watch/reopened-196.test.js +1042 -0
- package/dist/tests/commands/watch/supervisor-loop.test.js +23 -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
|
@@ -251,6 +251,67 @@ function registerJobCommand(program) {
|
|
|
251
251
|
assertAuthenticatedCwd(repo, branch);
|
|
252
252
|
process.stdout.write(JSON.stringify((0, query_1.queryPs)(repo, branch), null, 2) + '\n');
|
|
253
253
|
});
|
|
254
|
+
job.command('ack')
|
|
255
|
+
.description('consulta read-only el acuse durable de una request por ID exacto')
|
|
256
|
+
.argument('<requestId>')
|
|
257
|
+
.action((requestId) => {
|
|
258
|
+
if (!/^req-[A-Za-z0-9-]{1,124}$/.test(requestId))
|
|
259
|
+
throw new Error('requestId invalido');
|
|
260
|
+
const repo = process.cwd();
|
|
261
|
+
const branch = branchOf(repo);
|
|
262
|
+
assertAuthenticatedCwd(repo, branch);
|
|
263
|
+
const read = (0, store_1.readJournal)(repo, branch);
|
|
264
|
+
if (read.corrupt || !read.state) {
|
|
265
|
+
process.stdout.write(JSON.stringify({ requestId, state: 'unverifiable', journal: read.corrupt ? 'corrupt' : 'absent' }) + '\n');
|
|
266
|
+
process.exitCode = 1;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
(0, lock_1.verifyBranchInvariant)(repo, read.state.branch);
|
|
270
|
+
const ack = (0, requests_1.ackFor)(read.state, requestId);
|
|
271
|
+
if (ack) {
|
|
272
|
+
process.stdout.write(JSON.stringify({ requestId, state: ack.outcome === 'applied' ? 'applied' : 'rejected',
|
|
273
|
+
outcome: ack.outcome, ...(ack.resultRef ? { resultRef: ack.resultRef } : {}) }) + '\n');
|
|
274
|
+
if (ack.outcome !== 'applied')
|
|
275
|
+
process.exitCode = 1;
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const problem = read.state.requestProblems.find(item => item.requestId === requestId
|
|
279
|
+
|| (item.kind === 'corrupt' && path_1.default.basename(item.file) === `${requestId}.json`));
|
|
280
|
+
if (problem) {
|
|
281
|
+
process.stdout.write(JSON.stringify({ requestId, state: problem.kind === 'corrupt' ? 'unverifiable' : 'rejected', reasonCode: problem.kind }) + '\n');
|
|
282
|
+
process.exitCode = 1;
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
let pending;
|
|
286
|
+
try {
|
|
287
|
+
pending = (0, requests_1.listPendingRequests)(repo, branch).find(item => item.requestId === requestId
|
|
288
|
+
|| (item.corrupt && path_1.default.basename(item.file) === `${requestId}.json`));
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
process.stdout.write(JSON.stringify({ requestId, state: 'unverifiable', reasonCode: 'request-directory' }) + '\n');
|
|
292
|
+
process.exitCode = 1;
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
// The supervisor may have consumed the request between the first
|
|
296
|
+
// journal snapshot and the queue read. Recheck before saying unknown.
|
|
297
|
+
const latest = (0, store_1.readJournal)(repo, branch);
|
|
298
|
+
if (latest.corrupt || !latest.state) {
|
|
299
|
+
process.stdout.write(JSON.stringify({ requestId, state: 'unverifiable', journal: latest.corrupt ? 'corrupt' : 'absent' }) + '\n');
|
|
300
|
+
process.exitCode = 1;
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const latestAck = (0, requests_1.ackFor)(latest.state, requestId);
|
|
304
|
+
if (latestAck) {
|
|
305
|
+
process.stdout.write(JSON.stringify({ requestId, state: latestAck.outcome === 'applied' ? 'applied' : 'rejected',
|
|
306
|
+
outcome: latestAck.outcome, ...(latestAck.resultRef ? { resultRef: latestAck.resultRef } : {}) }) + '\n');
|
|
307
|
+
if (latestAck.outcome !== 'applied')
|
|
308
|
+
process.exitCode = 1;
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
process.stdout.write(JSON.stringify({ requestId, state: pending?.corrupt ? 'unverifiable' : pending ? 'pending' : 'unknown' }) + '\n');
|
|
312
|
+
if (!pending || pending.corrupt)
|
|
313
|
+
process.exitCode = 1;
|
|
314
|
+
});
|
|
254
315
|
job.command('list').action(() => {
|
|
255
316
|
const repo = process.cwd();
|
|
256
317
|
const branch = branchOf(repo);
|
|
@@ -32,7 +32,12 @@ function sameContext(a, b) {
|
|
|
32
32
|
&& a.runtime?.accountScopeDigest === b.runtime?.accountScopeDigest;
|
|
33
33
|
}
|
|
34
34
|
function admissionCustodyCause(reason) {
|
|
35
|
-
if (!reason
|
|
35
|
+
if (!reason)
|
|
36
|
+
return undefined;
|
|
37
|
+
if (reason === 'runtime local cambió durante generacion admitida'
|
|
38
|
+
|| /^runtime local no verificable: (?:NATIVE_QUERY_FAILED|ACCOUNT_UNVERIFIED|MACHINE_KEY_MISSING|HOOK_CAPTURE_FAILED)$/.test(reason))
|
|
39
|
+
return 'runtime';
|
|
40
|
+
if ((reason.match(/ADMISSION_[A-Z_]+/g) ?? []).length !== 1)
|
|
36
41
|
return undefined;
|
|
37
42
|
const old = reason.startsWith('admisión compacta desatendida bloqueada antes de dispatch: ');
|
|
38
43
|
if ((old || reason.startsWith('admisión currentness no verificable tras 3 reintentos: '))
|
|
@@ -46,6 +51,12 @@ function admissionCustodyCause(reason) {
|
|
|
46
51
|
function assertNoAmbiguousWork(repoRoot, branch, state) {
|
|
47
52
|
if ((0, types_1.activeRequestProblems)(state).length > 0)
|
|
48
53
|
throw new Error('recovery bloqueado: request rechazada o corrupta activa');
|
|
54
|
+
if (state.routingAttempts?.some(attempt => attempt.state !== 'reserved'
|
|
55
|
+
|| attempt.nativeAgentId !== undefined || attempt.nativeEventDigest !== undefined
|
|
56
|
+
|| attempt.observed !== undefined))
|
|
57
|
+
throw new Error('recovery bloqueado: intento nativo activo o ambiguo');
|
|
58
|
+
if (state.dispatches.some(dispatch => dispatch.routingAttemptId !== undefined))
|
|
59
|
+
throw new Error('recovery bloqueado: dispatch nativo ACK-applied sin prueba de child terminado');
|
|
49
60
|
for (const generation of state.generations) {
|
|
50
61
|
if ((0, generations_1.controllerGenerationHasUnresolvedClaim)(repoRoot, branch, generation))
|
|
51
62
|
throw new Error('recovery bloqueado: claim de controller sin identidad demostrable');
|
|
@@ -87,7 +98,7 @@ async function recoverAdmissionCustody(repoRoot, branch, input, dispatchAdmissio
|
|
|
87
98
|
&& sameContext(previous.context, context))
|
|
88
99
|
return 'already-recovered';
|
|
89
100
|
if (state.cycle.status !== 'BLOCKED' || !admissionCustodyCause(state.cycle.blockedReason)) {
|
|
90
|
-
throw new Error('recovery solo admite custodia causada por admisión currentness o
|
|
101
|
+
throw new Error('recovery solo admite custodia causada por admisión currentness, sensores o runtime local verificable');
|
|
91
102
|
}
|
|
92
103
|
const latest = state.generations.at(-1);
|
|
93
104
|
if (latest?.token !== input.generationToken || (latest === undefined && input.generationToken !== undefined)) {
|
|
@@ -29,6 +29,7 @@ const machine_key_1 = require("../../core/model-policy/machine-key");
|
|
|
29
29
|
const paths_2 = require("../../core/paths");
|
|
30
30
|
const native_routing_proof_1 = require("../../core/model-policy/native-routing-proof");
|
|
31
31
|
const selection_policy_digest_1 = require("../../core/model-policy/selection-policy-digest");
|
|
32
|
+
const process_1 = require("../../core/journal/process");
|
|
32
33
|
function now() { return new Date().toISOString(); }
|
|
33
34
|
function currentNativeAttempt(repoRoot, envelope, eventScope, at) {
|
|
34
35
|
if (!eventScope || JSON.stringify(eventScope.runtime) !== JSON.stringify(envelope.runtime))
|
|
@@ -155,6 +156,14 @@ function applyRequestToState(s, env, digest, repoRoot, eventScope) {
|
|
|
155
156
|
const capabilities = (0, capabilities_1.readCapabilities)((0, capabilities_1.validateRuntimeKey)(envelope.runtime), checkedAt);
|
|
156
157
|
return { checkedAt, policy: policy.state === 'approved' ? policy.policy : undefined, capabilities: capabilities.state === 'current' ? capabilities.receipt : undefined };
|
|
157
158
|
} }, p.at ?? now());
|
|
159
|
+
const routingAttempt = reserved.state.routingAttempts?.find(item => item.id === reserved.attemptId);
|
|
160
|
+
if (!routingAttempt)
|
|
161
|
+
throw new Error('routing reservation produced no attempt');
|
|
162
|
+
const priorReservationAck = routingAttempt.reservationRequestId === undefined ? undefined
|
|
163
|
+
: reserved.state.appliedRequests[routingAttempt.reservationRequestId];
|
|
164
|
+
if (!priorReservationAck || priorReservationAck.outcome !== 'applied' || priorReservationAck.resultRef !== routingAttempt.id) {
|
|
165
|
+
routingAttempt.reservationRequestId = env.requestId;
|
|
166
|
+
}
|
|
158
167
|
Object.assign(s, reserved.state);
|
|
159
168
|
(0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied', resultRef: reserved.attemptId });
|
|
160
169
|
return;
|
|
@@ -166,6 +175,15 @@ function applyRequestToState(s, env, digest, repoRoot, eventScope) {
|
|
|
166
175
|
const attempt = s.routingAttempts?.find(item => item.id === p.attemptId);
|
|
167
176
|
if (!attempt)
|
|
168
177
|
throw new Error('routing-observe attempt is unknown');
|
|
178
|
+
if (s.planBinding?.schema === 'compact-slices/v2' && attempt.envelope.role === 'implementer') {
|
|
179
|
+
if (attempt.nativeEvidenceRequired !== true)
|
|
180
|
+
throw new Error('routing-observe v2 requires a native event receipt');
|
|
181
|
+
const dispatch = s.dispatches.find(item => item.routingAttemptId === attempt.id);
|
|
182
|
+
const ack = dispatch?.dispatchRequestId ? s.appliedRequests[dispatch.dispatchRequestId] : undefined;
|
|
183
|
+
if (!dispatch || ack?.outcome !== 'applied' || ack.requestId !== dispatch.dispatchRequestId
|
|
184
|
+
|| ack.resultRef !== dispatch.id)
|
|
185
|
+
throw new Error('routing-observe requires applied dispatch ack');
|
|
186
|
+
}
|
|
169
187
|
let key = null;
|
|
170
188
|
if (attempt.nativeEvidenceRequired) {
|
|
171
189
|
try {
|
|
@@ -454,8 +472,106 @@ function applyRequestToState(s, env, digest, repoRoot, eventScope) {
|
|
|
454
472
|
const taskId = p.taskId;
|
|
455
473
|
if (!s.tasks.some((task) => task.id === taskId))
|
|
456
474
|
throw new Error('register --entity dispatch: taskId desconocido');
|
|
457
|
-
|
|
458
|
-
|
|
475
|
+
const v2 = s.planBinding?.schema === 'compact-slices/v2';
|
|
476
|
+
let routingAttemptId;
|
|
477
|
+
let reservedForNewDispatch = true;
|
|
478
|
+
if (v2) {
|
|
479
|
+
if (typeof p.routingAttemptId !== 'string' || p.routingAttemptId.length === 0)
|
|
480
|
+
throw new Error('register --entity dispatch v2 requiere routingAttemptId');
|
|
481
|
+
const attempt = s.routingAttempts?.find(candidate => candidate.id === p.routingAttemptId);
|
|
482
|
+
if (!attempt || attempt.envelope.role !== 'implementer'
|
|
483
|
+
|| attempt.envelope.sliceId !== taskId || attempt.envelope.planDigest !== s.planBinding.digest
|
|
484
|
+
|| attempt.envelope.executionDigest !== s.planBinding.executionDigest
|
|
485
|
+
|| !attempt.reservationRequestId
|
|
486
|
+
|| s.appliedRequests[attempt.reservationRequestId]?.outcome !== 'applied'
|
|
487
|
+
|| s.appliedRequests[attempt.reservationRequestId]?.requestId !== attempt.reservationRequestId
|
|
488
|
+
|| s.appliedRequests[attempt.reservationRequestId]?.resultRef !== attempt.id) {
|
|
489
|
+
throw new Error('register --entity dispatch: routing reservation for this slice and plan lacks applied ack');
|
|
490
|
+
}
|
|
491
|
+
routingAttemptId = attempt.id;
|
|
492
|
+
if (attempt.nativeEvidenceRequired !== true)
|
|
493
|
+
throw new Error('register --entity dispatch v2 requires native event receipt');
|
|
494
|
+
reservedForNewDispatch = attempt.state === 'reserved';
|
|
495
|
+
}
|
|
496
|
+
else if (p.routingAttemptId !== undefined)
|
|
497
|
+
throw new Error('register --entity dispatch: routingAttemptId solo para compact v2');
|
|
498
|
+
const noLiveJobs = () => Object.values(s.jobs).every(job => ['exited', 'cancelled'].includes(job.executionState)
|
|
499
|
+
&& [job.processRef, job.wrapperRef].every(ref => !ref || (!(0, process_1.refIsAlive)(ref) && (0, process_1.groupIsGone)(ref.processGroup))));
|
|
500
|
+
const historical = v2 ? s.dispatches.filter(d => d.taskId === taskId && !d.routingAttemptId) : [];
|
|
501
|
+
// A blocked routing verdict does not prove its native child has
|
|
502
|
+
// stopped. This migration permits exactly the first linked handoff;
|
|
503
|
+
// a subsequent retry needs a separately verified ownership path.
|
|
504
|
+
if (historical.some(d => d.id !== dispatchId))
|
|
505
|
+
throw new Error('register --entity dispatch: historical unlinked dispatch ID conflictivo');
|
|
506
|
+
const existing = s.dispatches.find(d => d.id === dispatchId);
|
|
507
|
+
// A 9.12.2 controller could ACK the unlinked intent before it
|
|
508
|
+
// reserved the native route. Keep that record and ACK as history;
|
|
509
|
+
// only a fresh request can create the first linked handoff.
|
|
510
|
+
if (v2 && existing?.taskId === taskId && !existing.routingAttemptId && !existing.dispatchRequestId && routingAttemptId) {
|
|
511
|
+
const attempt = s.routingAttempts.find(candidate => candidate.id === routingAttemptId);
|
|
512
|
+
const linkedReplay = s.dispatches.find(d => d.dispatchRequestId === env.requestId);
|
|
513
|
+
if (linkedReplay) {
|
|
514
|
+
const replayAck = s.appliedRequests[env.requestId];
|
|
515
|
+
if (linkedReplay.taskId !== taskId || linkedReplay.routingAttemptId !== routingAttemptId
|
|
516
|
+
|| replayAck?.outcome !== 'applied' || replayAck.idempotencyKey !== env.idempotencyKey
|
|
517
|
+
|| replayAck.payloadDigest !== digest || replayAck.resultRef !== linkedReplay.id)
|
|
518
|
+
throw new Error('register --entity dispatch: routed replay conflictivo');
|
|
519
|
+
(0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied', resultRef: linkedReplay.id });
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const linkedPrior = s.dispatches.find(d => d.routingAttemptId === routingAttemptId);
|
|
523
|
+
if (linkedPrior) {
|
|
524
|
+
const priorAck = linkedPrior.dispatchRequestId ? s.appliedRequests[linkedPrior.dispatchRequestId] : undefined;
|
|
525
|
+
if (linkedPrior.taskId !== taskId || priorAck?.outcome !== 'applied'
|
|
526
|
+
|| priorAck.idempotencyKey !== env.idempotencyKey || priorAck.payloadDigest !== digest)
|
|
527
|
+
throw new Error('register --entity dispatch: routed replay conflictivo');
|
|
528
|
+
(0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied', resultRef: linkedPrior.id });
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const oldAt = Date.parse(existing.at);
|
|
532
|
+
const reservedAt = Date.parse(attempt.reservedAt ?? '');
|
|
533
|
+
// 9.12.2 emits {...JSON.parse(--json), entity}; JSON.stringify
|
|
534
|
+
// preserves caller key order. The old record has no request
|
|
535
|
+
// link, so accept only permutations of these exact three
|
|
536
|
+
// fields, not an unrelated resultRef or extra payload fields.
|
|
537
|
+
const legacyDigests = [
|
|
538
|
+
{ entity: 'dispatch', dispatchId: existing.id, taskId },
|
|
539
|
+
{ entity: 'dispatch', taskId, dispatchId: existing.id },
|
|
540
|
+
{ dispatchId: existing.id, entity: 'dispatch', taskId },
|
|
541
|
+
{ dispatchId: existing.id, taskId, entity: 'dispatch' },
|
|
542
|
+
{ taskId, entity: 'dispatch', dispatchId: existing.id },
|
|
543
|
+
{ taskId, dispatchId: existing.id, entity: 'dispatch' },
|
|
544
|
+
].map(payload => (0, requests_1.digestOf)(payload));
|
|
545
|
+
const oldAckApplied = Object.values(s.appliedRequests).some(ack => ack.outcome === 'applied'
|
|
546
|
+
&& ack.resultRef === existing.id && legacyDigests.includes(ack.payloadDigest));
|
|
547
|
+
const safeLegacy = reservedForNewDispatch && Number.isFinite(oldAt) && Number.isFinite(reservedAt)
|
|
548
|
+
&& oldAt < reservedAt
|
|
549
|
+
&& oldAckApplied
|
|
550
|
+
&& s.dispatches.filter(d => d.taskId === taskId).length === 1
|
|
551
|
+
&& s.tasks.find(t => t.id === taskId)?.attempts === 1
|
|
552
|
+
&& s.tasks.find(t => t.id === taskId)?.status === 'in-progress'
|
|
553
|
+
&& noLiveJobs()
|
|
554
|
+
&& !s.routingAttempts?.some(candidate => candidate.envelope.sliceId === taskId
|
|
555
|
+
&& (candidate.nativeAgentId || candidate.nativeEventDigest || candidate.observed));
|
|
556
|
+
if (!safeLegacy)
|
|
557
|
+
throw new Error('register --entity dispatch: legacy handoff no reconciliable sin evidencia exclusiva');
|
|
558
|
+
const linkedId = `routed-${crypto_1.default.createHash('sha256').update(env.requestId).digest('hex').slice(0, 32)}`;
|
|
559
|
+
if (s.dispatches.some(d => d.id === linkedId) || s.dispatches.some(d => d.routingAttemptId === routingAttemptId))
|
|
560
|
+
throw new Error('register --entity dispatch: routed ID o reservation conflictivo');
|
|
561
|
+
s.dispatches.push({ id: linkedId, taskId, at: now(), routingAttemptId, dispatchRequestId: env.requestId });
|
|
562
|
+
(0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied', resultRef: linkedId });
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (existing && (existing.taskId !== taskId || existing.routingAttemptId !== routingAttemptId)) {
|
|
566
|
+
throw new Error('register --entity dispatch: dispatchId conflictivo');
|
|
567
|
+
}
|
|
568
|
+
if (!existing) {
|
|
569
|
+
if (routingAttemptId && s.dispatches.some(d => d.routingAttemptId === routingAttemptId)) {
|
|
570
|
+
throw new Error('register --entity dispatch: routing reservation ya tiene un dispatch');
|
|
571
|
+
}
|
|
572
|
+
if (!reservedForNewDispatch)
|
|
573
|
+
throw new Error('register --entity dispatch: routing attempt ya no esta reservado');
|
|
574
|
+
s.dispatches.push({ id: dispatchId, taskId, at: now(), ...(routingAttemptId ? { routingAttemptId, dispatchRequestId: env.requestId } : {}) });
|
|
459
575
|
const task = s.tasks.find((t) => t.id === taskId);
|
|
460
576
|
if (task !== undefined)
|
|
461
577
|
task.attempts += 1;
|
|
@@ -95,7 +95,7 @@ function promptForGeneration(gen) {
|
|
|
95
95
|
'resume-next-action': 'Retoma el next_action vigente del journal.',
|
|
96
96
|
'reconcile-active-jobs': 'Reconcilia los jobs activos existentes del journal sin duplicarlos.',
|
|
97
97
|
};
|
|
98
|
-
return `${instructions[gen.resumeAction.kind]}\nGeneracion activa: ${gen.token}. Incluye --generation ${gen.token} en
|
|
98
|
+
return `${instructions[gen.resumeAction.kind]}\nGeneracion activa: ${gen.token}. Incluye --generation ${gen.token} solo en awm job request, register, verdict, controller-heartbeat, routing-reserve y routing-observe. Ejecuta awm job reconcile sin --generation; awm job ack <requestId> tambien es de solo lectura y no acepta ese flag. En compact v2: espera el ack applied de routing-reserve, toma su resultRef como routingAttemptId, registra dispatch con ese routingAttemptId y espera su ack applied antes de lanzar al implementador nativo. Ningun implementador escribe antes de ese ack. Luego emite routing-observe y espera su ack; un recibo de emision no es un ack aplicado.`;
|
|
99
99
|
}
|
|
100
100
|
function recoveryAction(action) {
|
|
101
101
|
if (!(0, types_1.isControllerRecoveryAction)(action))
|
|
@@ -143,8 +143,18 @@ function registerWatchCommand(program) {
|
|
|
143
143
|
...(opts.controllerAutonomy === undefined ? {} : { controllerAutonomy: opts.controllerAutonomy }),
|
|
144
144
|
};
|
|
145
145
|
process.stdout.write(`awm watch: supervisor activo (${cfg.provider}) — Ctrl-C para terminar\n`);
|
|
146
|
-
await (0, supervisor_1.runSupervisorLoop)(repo, branch, cfg);
|
|
147
|
-
|
|
146
|
+
const outcome = await (0, supervisor_1.runSupervisorLoop)(repo, branch, cfg);
|
|
147
|
+
const final = (0, archive_unused_1.watchJournalStatus)(repo, branch);
|
|
148
|
+
if (outcome === 'complete' && final.state === 'present' && final.cycleState === 'COMPLETE') {
|
|
149
|
+
process.stdout.write('gate verde: ciclo COMPLETE — drenado, lock liberado, apagando\n');
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
process.stdout.write(`awm watch: ${outcome === 'stopped' ? 'detenido' : 'track congelado'}; ciclo ${final.cycleState ?? final.state} — sin certificacion de cierre\n`);
|
|
153
|
+
if (outcome === 'stopped')
|
|
154
|
+
process.exitCode = 130;
|
|
155
|
+
else if (outcome === 'complete' || final.state !== 'present')
|
|
156
|
+
process.exitCode = 1;
|
|
157
|
+
}
|
|
148
158
|
});
|
|
149
159
|
watch.command('recover-request')
|
|
150
160
|
.description('reconcilia una task rechazada con una request correctiva aplicada bajo lock exclusivo')
|
|
@@ -53,7 +53,7 @@ function routingFacts(provider, identity, cwd, deps = {}) {
|
|
|
53
53
|
capabilities: capabilities.state === 'current' ? capabilities.receipt : undefined, now: at };
|
|
54
54
|
}
|
|
55
55
|
function defaultDispatchAdmission(repoRoot, branch, provider, identity, deps = {}, autonomy) {
|
|
56
|
-
return async () => {
|
|
56
|
+
return async (request = {}) => {
|
|
57
57
|
const observed = (0, store_1.readJournal)(repoRoot, branch);
|
|
58
58
|
const binding = observed.state?.schema === 2 ? observed.state.planBinding : undefined;
|
|
59
59
|
if (!binding || binding.executionMode !== 'desatendido') {
|
|
@@ -92,6 +92,7 @@ function defaultDispatchAdmission(repoRoot, branch, provider, identity, deps = {
|
|
|
92
92
|
routing = routingFacts(provider, identity, repoRoot, deps);
|
|
93
93
|
return (deps.admit ?? registry_contracts_1.admitRegistryPlan)({ plan, provider, cwd: repoRoot, enabledAgents: preferences.enabledAgents,
|
|
94
94
|
executionMode: 'desatendido', requireCurrent: true, verifySensors: true,
|
|
95
|
+
freshSensorsRequired: request.freshSensorsRequired === true,
|
|
95
96
|
journalState: observed.state, journalCorrupt: observed.corrupt, planPath: binding.path,
|
|
96
97
|
routing,
|
|
97
98
|
controllerAutonomy: autonomy });
|
|
@@ -341,12 +342,67 @@ class Supervisor {
|
|
|
341
342
|
if (before0.corrupt || !before0.state)
|
|
342
343
|
throw new Error('journal no verificable tras vincular identidad de admisión');
|
|
343
344
|
}
|
|
344
|
-
|
|
345
|
+
// Full sensor admission belongs to the launch boundary. Once that
|
|
346
|
+
// generation has a durable controller launch intent, RED edits are
|
|
347
|
+
// normal work, not a new pre-dispatch baseline. Preserve binding
|
|
348
|
+
// checks while it runs; the next generation gets a fresh admission.
|
|
349
|
+
const binding = before0.state.planBinding;
|
|
350
|
+
if (!binding)
|
|
351
|
+
throw new Error('binding compacto ausente tras lectura del journal');
|
|
352
|
+
const launched = (0, generations_1.activeGeneration)(before0.state);
|
|
353
|
+
const ownedLaunch = launched !== undefined && (launched.processRef !== undefined || launched.wrapperRef !== undefined
|
|
354
|
+
|| (0, generations_1.controllerGenerationHasUnresolvedClaim)(this.repoRoot, this.branch, launched));
|
|
355
|
+
const generationAdmitted = launched?.controllerJobId !== undefined && launched.launchArgvDigest !== undefined && ownedLaunch;
|
|
356
|
+
if (generationAdmitted) {
|
|
357
|
+
const plan = (0, validate_1.validatePlanFile)(binding.path, this.repoRoot);
|
|
358
|
+
if (plan.state !== 'valid' || plan.planDigest !== binding.digest || plan.schema !== binding.schema
|
|
359
|
+
|| (binding.executionDigest !== undefined && plan.executionDigest !== binding.executionDigest)) {
|
|
360
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, 'binding del plan alterado durante generacion admitida');
|
|
361
|
+
return 'custody';
|
|
362
|
+
}
|
|
363
|
+
if (binding.schema === 'compact-slices/v2') {
|
|
364
|
+
const asserted = assertedContext.runtime;
|
|
365
|
+
if (!asserted) {
|
|
366
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, 'runtime de routing ausente durante generacion v2');
|
|
367
|
+
return 'custody';
|
|
368
|
+
}
|
|
369
|
+
const expected = (0, capabilities_1.validateRuntimeKey)({ target: assertedContext.provider, ...asserted });
|
|
370
|
+
let local;
|
|
371
|
+
try {
|
|
372
|
+
local = await (0, local_event_scope_1.queryLocalEventScope)(this.repoRoot, expected.target, expected.kind);
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
// Stable, non-sensitive custody reason remains recoverable
|
|
376
|
+
// after a transient native query or permission failure.
|
|
377
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, 'runtime local no verificable: NATIVE_QUERY_FAILED');
|
|
378
|
+
return 'custody';
|
|
379
|
+
}
|
|
380
|
+
if (local.state !== 'current') {
|
|
381
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, `runtime local no verificable: ${local.reason}`);
|
|
382
|
+
return 'custody';
|
|
383
|
+
}
|
|
384
|
+
if (JSON.stringify(local.scope.runtime) !== JSON.stringify(expected)) {
|
|
385
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, 'runtime local cambió durante generacion admitida');
|
|
386
|
+
return 'custody';
|
|
387
|
+
}
|
|
388
|
+
const stored = (0, event_store_1.readStoredEventReceipt)(expected);
|
|
389
|
+
if (stored.state === 'invalid' || (stored.state === 'present' &&
|
|
390
|
+
(stored.receipt.binaryDigest !== local.scope.binaryDigest || stored.receipt.configDigest !== local.scope.configDigest))) {
|
|
391
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, 'recibo nativo cambió durante generacion admitida');
|
|
392
|
+
return 'custody';
|
|
393
|
+
}
|
|
394
|
+
if (stored.state === 'absent' && (0, capabilities_1.readCapabilities)(expected, new Date()).state !== 'current') {
|
|
395
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, 'capacidades de routing no verificables durante generacion admitida');
|
|
396
|
+
return 'custody';
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (!generationAdmitted && before0.state.admissionRetry && Date.now() < Date.parse(before0.state.admissionRetry.nextRetryAt))
|
|
345
401
|
return 'continue';
|
|
346
402
|
// A controller can create jobs and runnerTick can start them. Both are
|
|
347
403
|
// downstream of the same full compact unattended admission, so it must
|
|
348
404
|
// complete before any reconciliation path that could dispatch either.
|
|
349
|
-
if (
|
|
405
|
+
if (!generationAdmitted) {
|
|
350
406
|
let admission;
|
|
351
407
|
try {
|
|
352
408
|
admission = await this.dispatchAdmission();
|
|
@@ -359,7 +415,6 @@ class Supervisor {
|
|
|
359
415
|
|| admission.currentness !== 'current' || admission.sensors !== 'pass' || admission.journal !== 'current') {
|
|
360
416
|
const kind = transientAdmission(admission);
|
|
361
417
|
if (kind) {
|
|
362
|
-
const binding = before0.state.planBinding;
|
|
363
418
|
const plan = (0, validate_1.validatePlanFile)(binding.path, this.repoRoot);
|
|
364
419
|
if (plan.state !== 'valid' || plan.planDigest !== binding.digest || plan.schema !== binding.schema
|
|
365
420
|
|| (binding.executionDigest !== undefined && plan.executionDigest !== binding.executionDigest)
|
|
@@ -565,6 +620,10 @@ class Supervisor {
|
|
|
565
620
|
const finalizing = afterRequests.state?.cohortPhase === 'FINAL_INTEGRATION' || afterRequests.state?.cohortPhase === 'FINAL_INTERLOCK';
|
|
566
621
|
if (!finalizing && afterRequests.state !== null && (0, generations_1.activeGeneration)(afterRequests.state) === undefined
|
|
567
622
|
&& afterRequests.state.generations.length > 0 && afterRequests.state.cycle.status === 'IN_PROGRESS') {
|
|
623
|
+
// The current tick used its launched generation's admission. A
|
|
624
|
+
// replacement must wait until the next tick's full admission.
|
|
625
|
+
if (generationAdmitted)
|
|
626
|
+
return 'continue';
|
|
568
627
|
(0, generations_1.beginGeneration)(this.repoRoot, this.branch);
|
|
569
628
|
if (this.ensureController(resumeAction) === 'custody')
|
|
570
629
|
return 'custody';
|
|
@@ -577,12 +636,15 @@ class Supervisor {
|
|
|
577
636
|
const freezeCheck = (0, store_1.readJournal)(this.repoRoot, this.branch);
|
|
578
637
|
const freezing = !freezeCheck.corrupt && freezeCheck.state !== null
|
|
579
638
|
&& freezeCheck.state.freezeRequested === true && freezeCheck.state.frozen === undefined;
|
|
639
|
+
// Reconcile controller ownership and, if needed, freshly admit its
|
|
640
|
+
// replacement before runnerTick can launch received jobs.
|
|
641
|
+
const supervision = await this.superviseController();
|
|
642
|
+
if (supervision === 'custody')
|
|
643
|
+
return 'custody';
|
|
580
644
|
(0, runner_1.runnerTick)(this.repoRoot, this.branch, this.spawner, {
|
|
581
|
-
reconcileGraceMs: this.cfg.reconcileGraceMs, stallObservationMs: this.cfg.jobStallObservationMs,
|
|
645
|
+
reconcileGraceMs: this.cfg.reconcileGraceMs, stallObservationMs: this.cfg.jobStallObservationMs,
|
|
646
|
+
dispatch: !freezing && supervision === 'ok',
|
|
582
647
|
});
|
|
583
|
-
const custody = await this.superviseController();
|
|
584
|
-
if (custody)
|
|
585
|
-
return 'custody';
|
|
586
648
|
if (freezing)
|
|
587
649
|
return this.attemptFreeze();
|
|
588
650
|
const r = (0, store_1.readJournal)(this.repoRoot, this.branch);
|
|
@@ -695,15 +757,23 @@ class Supervisor {
|
|
|
695
757
|
(0, store_1.appendEvent)(this.repoRoot, this.branch, { kind: 'track-frozen', headSha: s.frozen.headSha });
|
|
696
758
|
return 'frozen'; // paso 6 ("libera el lock y sale"): runSupervisorLoop trata 'frozen' igual que 'complete'
|
|
697
759
|
}
|
|
698
|
-
/**
|
|
760
|
+
/** Deferred ownership drains existing jobs but never starts new work. */
|
|
699
761
|
async superviseController() {
|
|
700
762
|
const r = (0, store_1.readJournal)(this.repoRoot, this.branch);
|
|
701
763
|
if (r.corrupt || r.state === null)
|
|
702
764
|
throw new Error('journal corrupto (R1.6)');
|
|
703
765
|
const s = r.state;
|
|
704
766
|
const gen = (0, generations_1.activeGeneration)(s);
|
|
705
|
-
|
|
706
|
-
|
|
767
|
+
// Track worktrees may be driven by the plan controller without a
|
|
768
|
+
// local generation. The canonical final-integration job also runs
|
|
769
|
+
// after the plan controller is deliberately paused. An existing but
|
|
770
|
+
// unadopted launch is different: claim recovery/backoff must never
|
|
771
|
+
// dispatch received jobs under that generation.
|
|
772
|
+
if (gen === undefined)
|
|
773
|
+
return s.trackContext || s.cohortPhase === 'FINAL_INTEGRATION'
|
|
774
|
+
|| s.cohortPhase === 'FINAL_INTERLOCK' ? 'ok' : 'deferred';
|
|
775
|
+
if (gen.processRef === undefined)
|
|
776
|
+
return 'deferred';
|
|
707
777
|
if (this.lastGenerationToken !== gen.token) {
|
|
708
778
|
this.lastGenerationToken = gen.token;
|
|
709
779
|
this.lastActivity = null;
|
|
@@ -716,10 +786,13 @@ class Supervisor {
|
|
|
716
786
|
this.lastActivity = { key, changedAt: Date.now() };
|
|
717
787
|
}
|
|
718
788
|
const activityFrozenMs = Date.now() - this.lastActivity.changedAt;
|
|
719
|
-
const
|
|
789
|
+
const safeToReplace = adapter.safeToReplace(gen.processRef);
|
|
790
|
+
// A dead process with proven identity is not healthy merely because
|
|
791
|
+
// its last heartbeat was recent. Resolve it before any new job starts.
|
|
792
|
+
const decision = snap === null && safeToReplace === 'safe' ? 'resolve-generation' : (0, generations_1.decideStall)({ heartbeatAgeMs, activityFrozenMs, safeToReplace }, { heartbeatTimeoutMs: this.cfg.heartbeatTimeoutMs, activityWindowMs: this.cfg.activityWindowMs });
|
|
720
793
|
if (decision === 'healthy') {
|
|
721
794
|
this.backoff.reset();
|
|
722
|
-
return
|
|
795
|
+
return 'ok';
|
|
723
796
|
}
|
|
724
797
|
if (decision === 'suspected-stall-observe') {
|
|
725
798
|
if (gen.state !== 'controller-suspected-stall') {
|
|
@@ -727,39 +800,53 @@ class Supervisor {
|
|
|
727
800
|
(0, store_1.writeJournal)(this.repoRoot, this.branch, s);
|
|
728
801
|
(0, store_1.appendEvent)(this.repoRoot, this.branch, { kind: 'controller-suspected-stall', n: gen.n });
|
|
729
802
|
}
|
|
730
|
-
return
|
|
803
|
+
return 'ok';
|
|
731
804
|
}
|
|
732
805
|
if (decision === 'custody-blocked') {
|
|
733
806
|
(0, generations_1.enterCustody)(this.repoRoot, this.branch, 'doble senial de stall sin safeToReplace positivo del adapter (R4.2b)');
|
|
734
|
-
return
|
|
807
|
+
return 'custody';
|
|
735
808
|
}
|
|
736
809
|
// resolve-generation
|
|
737
810
|
const resolved = await (0, generations_1.resolveGeneration)(this.repoRoot, this.branch, adapter, { termGraceMs: this.cfg.termGraceMs, killGraceMs: this.cfg.killGraceMs });
|
|
738
811
|
if (resolved === 'custody-blocked')
|
|
739
|
-
return
|
|
812
|
+
return 'custody';
|
|
740
813
|
if (this.backoff.exhausted()) {
|
|
741
814
|
(0, generations_1.enterCustody)(this.repoRoot, this.branch, 'tope de relanzamientos por hora alcanzado (R4.3)');
|
|
742
|
-
return
|
|
815
|
+
return 'custody';
|
|
743
816
|
}
|
|
744
817
|
if (Date.now() < this.relaunchNotBefore)
|
|
745
|
-
return
|
|
818
|
+
return 'deferred'; // esperando backoff, auditando
|
|
819
|
+
// This is a different controller generation, even though the tick
|
|
820
|
+
// began under the old one's admission. Do not reuse that admission
|
|
821
|
+
// after RED edits, runtime drift, or a currentness change.
|
|
822
|
+
let replacementAdmission;
|
|
823
|
+
try {
|
|
824
|
+
replacementAdmission = await this.dispatchAdmission({ freshSensorsRequired: true });
|
|
825
|
+
}
|
|
826
|
+
catch (error) {
|
|
827
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, `admisión de reemplazo no verificable: ${error.message}`);
|
|
828
|
+
return 'custody';
|
|
829
|
+
}
|
|
830
|
+
if (replacementAdmission.state !== 'admitted' || replacementAdmission.executionMode !== 'desatendido'
|
|
831
|
+
|| replacementAdmission.currentness !== 'current' || replacementAdmission.sensors !== 'pass'
|
|
832
|
+
|| replacementAdmission.journal !== 'current' || replacementAdmission.sensorEvidence === 'generation-bound') {
|
|
833
|
+
(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('; ')}`);
|
|
834
|
+
return 'custody';
|
|
835
|
+
}
|
|
746
836
|
(0, generations_1.beginGeneration)(this.repoRoot, this.branch);
|
|
747
837
|
const nextAction = (0, store_1.readJournal)(this.repoRoot, this.branch).state.cycle.nextAction;
|
|
748
838
|
const action = { schema: 'controller-recovery/v1', kind: nextAction !== undefined ? 'resume-next-action' : 'resume-cycle' };
|
|
749
839
|
const launched = this.ensureController(action);
|
|
750
840
|
if (launched === 'custody')
|
|
751
|
-
return
|
|
841
|
+
return 'custody';
|
|
752
842
|
if (launched === 'ok') {
|
|
753
843
|
this.backoff.recordRelaunch();
|
|
754
844
|
this.relaunchNotBefore = Date.now() + this.backoff.nextMs();
|
|
755
845
|
}
|
|
756
|
-
return
|
|
846
|
+
return launched === 'deferred' ? 'deferred' : 'ok';
|
|
757
847
|
}
|
|
758
848
|
}
|
|
759
849
|
exports.Supervisor = Supervisor;
|
|
760
|
-
/** Foreground, visible, terminable (R2.4): sin daemons. SIGINT/SIGTERM libera
|
|
761
|
-
* el lock y sale; COMPLETE => auto-exit liberando lock y terminando la
|
|
762
|
-
* generacion propia (cero huerfanos). */
|
|
763
850
|
async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.defaultWrapperSpawner)(), trackRuntime, dispatchAdmission) {
|
|
764
851
|
const r = (0, store_1.readJournal)(repoRoot, branch);
|
|
765
852
|
if (r.corrupt || r.state === null) {
|
|
@@ -772,11 +859,12 @@ async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.d
|
|
|
772
859
|
}
|
|
773
860
|
(0, lock_1.verifyBranchInvariant)(repoRoot, r.state.branch);
|
|
774
861
|
if (r.state.cycle.status === 'COMPLETE')
|
|
775
|
-
return;
|
|
862
|
+
return 'complete';
|
|
776
863
|
const handle = (0, lock_1.acquireLock)(repoRoot);
|
|
777
864
|
let shutdownRequested = false;
|
|
778
865
|
let wakeSleep = null;
|
|
779
866
|
let safeToRelease = false;
|
|
867
|
+
let terminal = 'stopped';
|
|
780
868
|
const onSignal = () => { shutdownRequested = true; wakeSleep?.(); };
|
|
781
869
|
process.on('SIGINT', onSignal);
|
|
782
870
|
process.on('SIGTERM', onSignal);
|
|
@@ -790,6 +878,7 @@ async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.d
|
|
|
790
878
|
// drenar ownership y liberar el lock, el track ya cumplió su
|
|
791
879
|
// freeze y no debe seguir despachando ni corriendo su loop.
|
|
792
880
|
if (out === 'complete' || out === 'frozen') {
|
|
881
|
+
terminal = out;
|
|
793
882
|
const finalState = (0, store_1.readJournal)(repoRoot, branch);
|
|
794
883
|
if (!finalState.corrupt && finalState.state?.routingIncidents?.length)
|
|
795
884
|
process.stderr.write(`AWM final routing summary ${JSON.stringify((0, journal_1.routingReport)(finalState.state))}\n`);
|
|
@@ -831,6 +920,7 @@ async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.d
|
|
|
831
920
|
if (shutdownRequested)
|
|
832
921
|
(0, store_1.writeJournal)(repoRoot, branch, sEnd);
|
|
833
922
|
safeToRelease = true;
|
|
923
|
+
return terminal;
|
|
834
924
|
}
|
|
835
925
|
finally {
|
|
836
926
|
process.removeListener('SIGINT', onSignal);
|
|
@@ -166,6 +166,8 @@ function sanitizeAdmissionReport(report) {
|
|
|
166
166
|
invalidAdmission();
|
|
167
167
|
if (value.cliCurrentness !== undefined && !CLI_CURRENTNESS.has(value.cliCurrentness))
|
|
168
168
|
invalidAdmission();
|
|
169
|
+
if (value.sensorEvidence !== undefined && value.sensorEvidence !== 'generation-bound')
|
|
170
|
+
invalidAdmission();
|
|
169
171
|
if (value.planDigest !== undefined && (typeof value.planDigest !== 'string' || !/^[a-f0-9]{64}$/.test(value.planDigest)))
|
|
170
172
|
invalidAdmission();
|
|
171
173
|
if (value.executionMode !== undefined && value.executionMode !== 'interactivo' && value.executionMode !== 'desatendido')
|