agentic-workflow-manager 9.10.0 → 9.10.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.
- package/dist/src/commands/plan/index.js +33 -4
- package/dist/src/commands/watch/supervisor.js +40 -3
- package/dist/tests/commands/plan/index.test.js +58 -0
- package/dist/tests/commands/watch/supervisor-loop.test.js +53 -0
- 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
|
@@ -183,10 +183,39 @@ function registerPlanCommand(program, deps = { validatePlanFile: validate_1.vali
|
|
|
183
183
|
if (!roles.includes(options.role))
|
|
184
184
|
throw new Error('--role is invalid');
|
|
185
185
|
const runtime = (0, capabilities_1.validateRuntimeKey)({ target: options.provider, kind: options.runtimeKind, version: options.runtimeVersion, accountScopeDigest: options.accountScopeDigest });
|
|
186
|
-
const result = report.schema === 'compact-slices/v1' && !options.optInV1 ? (0, resolve_1.resolveDispatch)({ plan: report, role: options.role, policy: undefined, capabilities: undefined, runtime, now: new Date(), optInV1: false }) : (() => {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
186
|
+
const result = report.schema === 'compact-slices/v1' && !options.optInV1 ? (0, resolve_1.resolveDispatch)({ plan: report, role: options.role, policy: undefined, capabilities: undefined, runtime, now: new Date(), optInV1: false }) : (() => {
|
|
187
|
+
const policy = (deps.readEffectivePolicy ?? store_2.readEffectivePolicy)(cwd);
|
|
188
|
+
const capabilities = (deps.readCapabilities ?? capabilities_1.readCapabilities)(runtime, new Date());
|
|
189
|
+
if (!options.lineage) {
|
|
190
|
+
const first = (0, resolve_1.resolveDispatch)({ plan: report, role: options.role, sliceId: options.slice, policy: policy.state === 'approved' ? policy.policy : undefined, capabilities: capabilities.state === 'current' ? capabilities.receipt : undefined, runtime, now: new Date(), optInV1: options.optInV1 === true });
|
|
191
|
+
// The consumer forwards THIS envelope to `job routing-reserve`, which
|
|
192
|
+
// is what creates the lineage the --lineage escalation path below
|
|
193
|
+
// then reads. Emitting it only there made the first attempt of any
|
|
194
|
+
// lineage unreservable: envelope <= lineage <= reserve <= envelope.
|
|
195
|
+
// Every field comes from values already in hand, so the CLI stays the
|
|
196
|
+
// sole author and the consumer never assembles a protocol document.
|
|
197
|
+
// No custodyHandoff here on purpose: the obligation and lineage ids
|
|
198
|
+
// are the caller's to choose on a first attempt, and inventing them
|
|
199
|
+
// would put identity the CLI cannot know into custody.
|
|
200
|
+
if (first.state !== 'resolved' || report.schema !== 'compact-slices/v2' || report.executionDigest === undefined)
|
|
201
|
+
return first;
|
|
202
|
+
const slice = report.manifest.slices.find((candidate) => candidate.id === options.slice);
|
|
203
|
+
const requestedProfile = options.role === 'implementer' ? slice?.implementerProfile : 'full';
|
|
204
|
+
if (requestedProfile === undefined)
|
|
205
|
+
return first;
|
|
206
|
+
return { ...first, envelope: { schema: 'routing-envelope/v1', runtime, role: options.role, planDigest: report.planDigest, executionDigest: report.executionDigest, ...(options.slice === undefined ? {} : { sliceId: options.slice }), requestedProfile, effectiveProfile: first.effectiveProfile, resolved: first.selection, policyDigest: first.policyDigest, capabilityDigest: first.capabilityDigest, outcome: first.outcome, unavailableEvidence: first.unavailableEvidence } };
|
|
207
|
+
}
|
|
208
|
+
const state = journalObservation(cwd).journalState;
|
|
209
|
+
const lineage = state.implementationLineages.find(candidate => candidate.id === options.lineage);
|
|
210
|
+
const next = (0, journal_1.resolveLineageEscalation)(state, options.lineage, lineage.initialProfile);
|
|
211
|
+
const resolved = (0, resolve_1.resolveSelection)({ role: 'implementer', requestedProfile: next.profile, policy: policy.state === 'approved' ? policy.policy : undefined, capabilities: capabilities.state === 'current' ? capabilities.receipt : undefined, runtime, now: new Date() });
|
|
212
|
+
if (resolved.state === 'blocked')
|
|
213
|
+
return resolved;
|
|
214
|
+
const highRequiresFull = next.effort !== 'high' || (policy.state === 'approved' && (() => { const full = policy.policy.content.mappings.find(row => row.target === runtime.target && row.runtimeKind === runtime.kind)?.fullCapability; return full?.selector.kind === resolved.selection.selector.kind && full.selector.id === resolved.selection.selector.id && full.effort.kind === 'explicit' && full.effort.value === 'high'; })());
|
|
215
|
+
if (resolved.selection.effort.kind !== 'explicit' || resolved.selection.effort.value !== next.effort || !highRequiresFull)
|
|
216
|
+
return { state: 'blocked', diagnostics: [{ code: 'ROUTING_ESCALATION_EFFORT_UNAVAILABLE', message: 'The approved selected effort does not match the next lineage escalation.' }] };
|
|
217
|
+
return { ...resolved, envelope: { schema: 'routing-envelope/v1', runtime, role: 'implementer', planDigest: report.planDigest, executionDigest: report.executionDigest, sliceId: lineage.sliceId, requestedProfile: lineage.initialProfile, effectiveProfile: next.profile, resolved: resolved.selection, policyDigest: resolved.policyDigest, capabilityDigest: resolved.capabilityDigest, outcome: resolved.outcome, unavailableEvidence: resolved.unavailableEvidence }, custodyHandoff: { kind: 'routing-reserve', obligationId: lineage.obligationId, lineageId: lineage.id } };
|
|
218
|
+
})();
|
|
190
219
|
process.stdout.write(options.json ? `${JSON.stringify(result)}\n` : `Plan routing: ${result.state}\n`);
|
|
191
220
|
if (result.state === 'blocked')
|
|
192
221
|
process.exitCode = 2;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Supervisor = exports.DEFAULT_SUPERVISOR_CONFIG = void 0;
|
|
3
|
+
exports.Supervisor = exports.MAX_SENSOR_DEFERRALS = exports.DEFAULT_SUPERVISOR_CONFIG = void 0;
|
|
4
4
|
exports.runSupervisorLoop = runSupervisorLoop;
|
|
5
5
|
const store_1 = require("../../core/journal/store");
|
|
6
6
|
const fingerprint_1 = require("../../core/journal/fingerprint");
|
|
@@ -120,6 +120,15 @@ exports.DEFAULT_SUPERVISOR_CONFIG = {
|
|
|
120
120
|
jobStallObservationMs: 5 * 60000, // R3.5 default: mismo orden de magnitud que heartbeatTimeoutMs, concern independiente
|
|
121
121
|
maxParallelTracks: 1, // overridden en tiempo de ejecución con loadDefaultParallelism() (watch/index.ts)
|
|
122
122
|
};
|
|
123
|
+
// 'frozen' (R5.2/R6.3, Task 10): SOLO puede ocurrir en el journal de un
|
|
124
|
+
// TRACK individual (nunca en el del plan) — el track terminó, con las 6
|
|
125
|
+
// observaciones demostrables (cero jobs vivos, gate local verde, worktree
|
|
126
|
+
// limpio, generación propia terminada con identidad confirmada), de
|
|
127
|
+
// atender un `track-freeze-request` pedido por el supervisor del plan.
|
|
128
|
+
/** How many consecutive ticks an inconclusive sensor verdict may defer dispatch
|
|
129
|
+
* before the supervisor says so out loud. Small: the deferral covers a verdict
|
|
130
|
+
* still settling, not one that never will. */
|
|
131
|
+
exports.MAX_SENSOR_DEFERRALS = 3;
|
|
123
132
|
const LIVE = ['received', 'spawn-intent', 'claimed', 'running', 'cancel-requested'];
|
|
124
133
|
function recoveryWhitelistBlocker(state) {
|
|
125
134
|
if (state.cycle.status === 'BLOCKED')
|
|
@@ -149,6 +158,10 @@ class Supervisor {
|
|
|
149
158
|
dispatchAdmission;
|
|
150
159
|
backoff = new generations_1.Backoff();
|
|
151
160
|
relaunchNotBefore = 0;
|
|
161
|
+
/** Consecutive ticks deferred on an inconclusive sensor verdict. Bounded:
|
|
162
|
+
* the deferral exists for a verdict that is still settling, and a verdict
|
|
163
|
+
* that never settles must become visible rather than loop forever. */
|
|
164
|
+
sensorDeferrals = 0;
|
|
152
165
|
lastActivity = null;
|
|
153
166
|
lastGenerationToken = null;
|
|
154
167
|
trackRuntime;
|
|
@@ -226,12 +239,36 @@ class Supervisor {
|
|
|
226
239
|
// the already-started controller is still settling. It never
|
|
227
240
|
// authorizes dispatch; retry the read-only admission next tick
|
|
228
241
|
// instead of permanently custodying a healthy cycle.
|
|
242
|
+
//
|
|
243
|
+
// Bounded, because the verdict is not always transient: a
|
|
244
|
+
// registry whose sensors are all disabled reports
|
|
245
|
+
// `not-certified` forever. Unbounded, this returned before
|
|
246
|
+
// `consumePendingRequests` below, so the supervisor ticked
|
|
247
|
+
// indefinitely applying nothing and writing nothing — no event,
|
|
248
|
+
// no state change, no exit. Refusing to progress is correct
|
|
249
|
+
// here; refusing invisibly is not. Consuming requests cannot be
|
|
250
|
+
// hoisted above this gate to compensate: applying a job-request
|
|
251
|
+
// creates a Job that runnerTick can start, which is the dispatch
|
|
252
|
+
// this admission exists to authorize.
|
|
229
253
|
if (admission.planState === 'valid' && admission.currentness === 'current'
|
|
230
|
-
&& admission.sensors === 'not-certified' && admission.journal === 'not-required')
|
|
231
|
-
|
|
254
|
+
&& admission.sensors === 'not-certified' && admission.journal === 'not-required') {
|
|
255
|
+
this.sensorDeferrals += 1;
|
|
256
|
+
if (this.sensorDeferrals <= exports.MAX_SENSOR_DEFERRALS)
|
|
257
|
+
return 'continue';
|
|
258
|
+
// The reason must not carry the running counter. enterCustody is
|
|
259
|
+
// idempotent on (status, reason), so a message that changes every
|
|
260
|
+
// tick defeats that guard and rewrites the journal plus appends a
|
|
261
|
+
// custody-blocked event every tickMs, without bound. Naming the
|
|
262
|
+
// bound instead of the count keeps the reason stable, so custody
|
|
263
|
+
// is entered once and stays entered.
|
|
264
|
+
(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('; ')}`);
|
|
265
|
+
return 'custody';
|
|
266
|
+
}
|
|
232
267
|
(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('; ')}`);
|
|
233
268
|
return 'custody';
|
|
234
269
|
}
|
|
270
|
+
// The verdict settled, which is exactly what the deferral waited for.
|
|
271
|
+
this.sensorDeferrals = 0;
|
|
235
272
|
}
|
|
236
273
|
let recoveryResumeAction;
|
|
237
274
|
// Schema-2 custody is reconciled before any controller launch. This is
|
|
@@ -13,12 +13,20 @@ const plan_1 = require("../../../src/commands/plan");
|
|
|
13
13
|
const init_1 = require("../../../src/commands/watch/init");
|
|
14
14
|
const providers_1 = require("../../../src/providers");
|
|
15
15
|
const registry_1 = require("../../../src/core/renderers/registry");
|
|
16
|
+
const canonical_1 = require("../../../src/core/model-policy/canonical");
|
|
17
|
+
const capabilities_1 = require("../../../src/core/model-policy/capabilities");
|
|
18
|
+
const journal_1 = require("../../../src/core/model-policy/journal");
|
|
19
|
+
const types_1 = require("../../../src/core/journal/types");
|
|
16
20
|
const stdoutWrite = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
17
21
|
const repositoryRoot = path_1.default.resolve(__dirname, '../../../..');
|
|
18
22
|
const approved = (0, validate_1.validatePlanFile)('docs/plans/2026-09-14-compact-only-bootstrap-plan.md', repositoryRoot);
|
|
19
23
|
if (approved.state !== 'valid')
|
|
20
24
|
throw new Error(`tracked bootstrap plan must validate before command tests: ${approved.state}`);
|
|
21
25
|
const valid = approved;
|
|
26
|
+
const approvedV2 = (0, validate_1.validatePlanFile)('valid.md', path_1.default.join(__dirname, '../../core/plan/fixtures/compact-slices-v2'));
|
|
27
|
+
if (approvedV2.state !== 'valid')
|
|
28
|
+
throw new Error(`v2 fixture must validate before command tests: ${approvedV2.state}`);
|
|
29
|
+
const v2Valid = approvedV2;
|
|
22
30
|
const invalid = {
|
|
23
31
|
state: 'invalid', diagnostics: [{ code: 'PLAN_MARKERS', message: 'compact markers must occur once in order' }],
|
|
24
32
|
};
|
|
@@ -60,6 +68,56 @@ describe('plan validate Commander wiring', () => {
|
|
|
60
68
|
expect(policy).not.toHaveBeenCalled();
|
|
61
69
|
expect(capabilities).not.toHaveBeenCalled();
|
|
62
70
|
});
|
|
71
|
+
// The consumer reference says: call `plan resolve` for the exact role and
|
|
72
|
+
// local slice, then send ITS frozen envelope to `job routing-reserve`. The
|
|
73
|
+
// envelope was only ever built on the --lineage branch, which resolves the
|
|
74
|
+
// lineage out of journal state — and lineages are created by
|
|
75
|
+
// reserveRoutingAttempt, i.e. by routing-reserve itself. envelope <= lineage
|
|
76
|
+
// <= reserve <= envelope, so the first attempt of any lineage could never be
|
|
77
|
+
// reserved. This test goes from resolve output to a reserved attempt with no
|
|
78
|
+
// hand-built envelope literal, which is the only shape that can catch it:
|
|
79
|
+
// tests/commands/job/routing.test.ts passes a literal and so starts one step
|
|
80
|
+
// past the gap.
|
|
81
|
+
it('emits a reservable envelope on a first v2 attempt, closing the resolve to reserve path', async () => {
|
|
82
|
+
// A genuinely validated report: the resolver rejects any object that did
|
|
83
|
+
// not come out of validatePlanFile, so a spread-and-relabel fixture
|
|
84
|
+
// cannot reach this path at all.
|
|
85
|
+
const sliceId = 'S1';
|
|
86
|
+
const sel = { selector: { kind: 'model', id: 'gpt-5.6-luna' }, effort: { kind: 'explicit', value: 'medium' } };
|
|
87
|
+
const full = { selector: { kind: 'model', id: 'gpt-5.6-sol' }, effort: { kind: 'explicit', value: 'high' } };
|
|
88
|
+
const sha = 'a'.repeat(64);
|
|
89
|
+
const content = { schema: 'model-policy/v1', mappings: [{ target: 'codex', runtimeKind: 'native', profiles: { mechanical: sel, integration: sel, judgment: sel }, fullCapability: full, degradation: { allowMissingModelOverride: false, allowMissingEffortOverride: false, allowMissingObservedIdentity: false } }], implementationBudget: { maxAttempts: 3, escalation: ['mechanical', 'integration', 'judgment'], judgmentEfforts: ['medium', 'high'] } };
|
|
90
|
+
const readEffectivePolicy = () => ({ state: 'approved', provenance: 'user', policy: { schema: 'approved-model-policy/v1', content, contentDigest: (0, canonical_1.canonicalPolicyDigest)(content), approval: { approvedAt: '2026-09-19T00:00:00.000Z', approvalId: 'a1' }, lineage: { previousDigest: null } } });
|
|
91
|
+
const receipt = {
|
|
92
|
+
schema: 'routing-capabilities/v1',
|
|
93
|
+
runtime: { target: 'codex', kind: 'native', version: '1.0.0', accountScopeDigest: sha },
|
|
94
|
+
recordedAt: new Date(Date.now() - 60_000).toISOString(), expiresAt: new Date(Date.now() + 6 * 3600_000).toISOString(),
|
|
95
|
+
capabilities: { artifactDelivery: 'supported', interactiveExecution: 'supported', unattendedController: 'supported', nativeSubagents: 'supported', modelOverride: 'supported', effortOverride: 'supported', observedModelEvidence: 'supported', durableResume: 'unverified' },
|
|
96
|
+
availableSelections: [sel, full], runtimeDefaultSelection: full,
|
|
97
|
+
evidence: ['modelOverride', 'effortOverride', 'observedModelEvidence', 'nativeSubagents', 'artifactDelivery', 'interactiveExecution', 'unattendedController'].map(capability => ({ capability, kind: 'native-control', receiptDigest: 'b'.repeat(64) })),
|
|
98
|
+
approval: { approvalId: 'r1', snapshotDigest: 'c'.repeat(64) },
|
|
99
|
+
};
|
|
100
|
+
const readCapabilities = () => ({ state: 'current', receipt, digest: (0, capabilities_1.capabilityReceiptDigest)(receipt) });
|
|
101
|
+
const program = new commander_1.Command();
|
|
102
|
+
program.exitOverride();
|
|
103
|
+
program.configureOutput({ writeErr: () => undefined });
|
|
104
|
+
(0, plan_1.registerPlanCommand)(program, { validatePlanFile: () => v2Valid, readEffectivePolicy: readEffectivePolicy, readCapabilities: readCapabilities });
|
|
105
|
+
await program.parseAsync(['node', 'awm', 'plan', 'resolve', 'plans/r4.md', '--provider', 'codex', '--runtime-kind', 'native', '--runtime-version', '1.0.0', '--account-scope-digest', sha, '--role', 'implementer', '--slice', sliceId, '--cwd', repositoryRoot, '--json']);
|
|
106
|
+
const emitted = JSON.parse(String(stdoutWrite.mock.calls[stdoutWrite.mock.calls.length - 1][0]));
|
|
107
|
+
expect(emitted.state).toBe('resolved');
|
|
108
|
+
expect(emitted.envelope).toBeDefined();
|
|
109
|
+
// The CLI must be the sole author: the envelope has to satisfy the same
|
|
110
|
+
// predicate routing-reserve applies, with nothing filled in by the caller.
|
|
111
|
+
expect((0, types_1.isRoutingEnvelope)(emitted.envelope)).toBe(true);
|
|
112
|
+
expect(emitted.envelope).toMatchObject({ schema: 'routing-envelope/v1', role: 'implementer', sliceId, requestedProfile: 'mechanical', effectiveProfile: 'mechanical', resolved: sel });
|
|
113
|
+
expect(emitted.envelope.planDigest).toBe(v2Valid.planDigest);
|
|
114
|
+
expect(emitted.envelope.executionDigest).toBe(v2Valid.executionDigest);
|
|
115
|
+
// The point of the whole thing: that envelope reserves a first attempt,
|
|
116
|
+
// which is what creates the lineage the escalation path later needs.
|
|
117
|
+
const reserved = (0, journal_1.reserveRoutingAttempt)((0, types_1.emptyState)('main'), { obligationId: 'o1', lineageId: 'l1', envelope: emitted.envelope, fingerprint: 'e'.repeat(64) }, '2026-09-19T00:00:00.000Z');
|
|
118
|
+
expect(reserved.attemptId).toEqual(expect.any(String));
|
|
119
|
+
expect(reserved.state.implementationLineages).toEqual([expect.objectContaining({ id: 'l1', sliceId, initialProfile: 'mechanical' })]);
|
|
120
|
+
});
|
|
63
121
|
it('emits deterministic human output for a valid compact plan', async () => {
|
|
64
122
|
await commandFor(valid).parseAsync(['node', 'awm', 'plan', 'validate', 'plans/r4.md']);
|
|
65
123
|
expect(String(stdoutWrite.mock.calls[0][0])).toBe('Plan validation: valid "plans/r4.md" (compact-slices/v1; 1 slices; 5 requirements; complete ownership)\n');
|
|
@@ -390,6 +390,59 @@ describe('supervisor loop', () => {
|
|
|
390
390
|
await expect(new supervisor_1.Supervisor(repo, 'main', cfg, spy, undefined, admitted).tick()).rejects.toThrow(/rama|branch/i);
|
|
391
391
|
expect(calls).toBe(0);
|
|
392
392
|
});
|
|
393
|
+
// The sensor deferral at the top of tick() covers a first inconclusive read
|
|
394
|
+
// while a controller settles, and returns 'continue' so a transient does not
|
|
395
|
+
// custody a healthy cycle. It returns BEFORE consumePendingRequests, and
|
|
396
|
+
// `sensors: 'not-certified'` is not always transient: it is the steady state
|
|
397
|
+
// of any registry whose sensors are all disabled, which the baseline registry
|
|
398
|
+
// documents about itself. Unbounded, the supervisor ticked forever applying
|
|
399
|
+
// nothing and writing nothing — no event, no state change, no exit. Observed
|
|
400
|
+
// against published 9.10.0: a routing-reserve and an unrelated
|
|
401
|
+
// controller-heartbeat both sat unconsumed for ~36 ticks with appliedRequests
|
|
402
|
+
// empty and the cycle still IN_PROGRESS.
|
|
403
|
+
test('an inconclusive sensor verdict defers dispatch a bounded number of ticks, then says so', async () => {
|
|
404
|
+
const repo = setupRepo();
|
|
405
|
+
initUnattendedFixture(repo);
|
|
406
|
+
(0, generations_1.beginGeneration)(repo, 'main');
|
|
407
|
+
const token = (0, generations_1.activeGeneration)((0, store_1.readJournal)(repo, 'main').state).token;
|
|
408
|
+
(0, request_1.requestJob)(repo, 'main', token, ['node', '-e', 'process.exit(0)'], [], '.');
|
|
409
|
+
// Exactly what `awm plan admit --require-current --verify-sensors` returns
|
|
410
|
+
// for that registry: valid plan, current contracts, sensors not certified.
|
|
411
|
+
// Every field matches the deferral condition.
|
|
412
|
+
const notCertified = async () => ({
|
|
413
|
+
state: 'blocked', planState: 'valid', executionMode: 'desatendido',
|
|
414
|
+
journal: 'not-required', currentness: 'current', sensors: 'not-certified',
|
|
415
|
+
diagnostics: [{ code: 'ADMISSION_SENSORS_BLOCKED', message: 'sensors not certified' }],
|
|
416
|
+
});
|
|
417
|
+
let spawned = 0;
|
|
418
|
+
const sup = new supervisor_1.Supervisor(repo, 'main', supervisor_1.DEFAULT_SUPERVISOR_CONFIG, () => { spawned += 1; }, undefined, notCertified);
|
|
419
|
+
for (let i = 0; i < supervisor_1.MAX_SENSOR_DEFERRALS; i += 1) {
|
|
420
|
+
expect(await sup.tick()).toBe('continue');
|
|
421
|
+
expect((0, store_1.readJournal)(repo, 'main').state.cycle.status).toBe('IN_PROGRESS');
|
|
422
|
+
}
|
|
423
|
+
// The verdict never settled, so the supervisor must stop pretending it might.
|
|
424
|
+
expect(await sup.tick()).toBe('custody');
|
|
425
|
+
const state = (0, store_1.readJournal)(repo, 'main').state;
|
|
426
|
+
expect(state.cycle.status).toBe('BLOCKED');
|
|
427
|
+
expect(state.cycle.blockedReason).toMatch(/sensor/i);
|
|
428
|
+
// Entered once, not once per tick. enterCustody is idempotent on
|
|
429
|
+
// (status, reason), so the reason must not carry the running counter —
|
|
430
|
+
// otherwise every tick rewrites the journal and appends another
|
|
431
|
+
// custody-blocked event, unbounded. Observed against published 9.10.1
|
|
432
|
+
// before this was fixed: "tras 16 ticks", "tras 17 ticks", "tras 18
|
|
433
|
+
// ticks", one every 5s.
|
|
434
|
+
const eventsFile = path_1.default.join(repo, '.awm', 'journal', 'main', 'events.jsonl');
|
|
435
|
+
const custodyEvents = () => fs_1.default.readFileSync(eventsFile, 'utf8').split('\n').filter(line => line.includes('custody-blocked')).length;
|
|
436
|
+
const afterFirst = custodyEvents();
|
|
437
|
+
expect(afterFirst).toBe(1);
|
|
438
|
+
const revisionAfterFirst = state.revision;
|
|
439
|
+
for (let i = 0; i < 3; i += 1)
|
|
440
|
+
expect(await sup.tick()).toBe('custody');
|
|
441
|
+
expect(custodyEvents()).toBe(afterFirst);
|
|
442
|
+
expect((0, store_1.readJournal)(repo, 'main').state.revision).toBe(revisionAfterFirst);
|
|
443
|
+
// Deferring dispatch was right throughout, and stays right.
|
|
444
|
+
expect(spawned).toBe(0);
|
|
445
|
+
});
|
|
393
446
|
test('una admisión compacta bloqueada no lanza controlador ni wrappers', async () => {
|
|
394
447
|
initUnattendedFixture(repo);
|
|
395
448
|
const state = (0, store_1.readJournal)(repo, 'main').state;
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|