@karmaniverous/jeeves-meta 0.14.0 → 0.15.1
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/README.md +7 -6
- package/dist/cli/jeeves-meta/index.js +1091 -621
- package/dist/index.d.ts +177 -52
- package/dist/index.js +1176 -163
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9026,12 +9026,12 @@ class GatewayExecutor {
|
|
|
9026
9026
|
return data;
|
|
9027
9027
|
}
|
|
9028
9028
|
/** Look up totalTokens for a session via sessions_list. */
|
|
9029
|
-
async getSessionTokens(sessionKey
|
|
9029
|
+
async getSessionTokens(sessionKey) {
|
|
9030
9030
|
try {
|
|
9031
9031
|
const result = await this.invoke('sessions_list', {
|
|
9032
9032
|
limit: 20,
|
|
9033
9033
|
messageLimit: 0,
|
|
9034
|
-
}
|
|
9034
|
+
});
|
|
9035
9035
|
const sessions = (result.result?.details?.sessions ??
|
|
9036
9036
|
result.result?.sessions ??
|
|
9037
9037
|
[]);
|
|
@@ -9042,6 +9042,10 @@ class GatewayExecutor {
|
|
|
9042
9042
|
return undefined;
|
|
9043
9043
|
}
|
|
9044
9044
|
}
|
|
9045
|
+
/** Whether this executor has been aborted by the operator. */
|
|
9046
|
+
get aborted() {
|
|
9047
|
+
return this.controller.signal.aborted;
|
|
9048
|
+
}
|
|
9045
9049
|
/** Abort the currently running spawn, if any. */
|
|
9046
9050
|
abort() {
|
|
9047
9051
|
this.controller.abort();
|
|
@@ -9059,7 +9063,6 @@ class GatewayExecutor {
|
|
|
9059
9063
|
// Generate unique output path for file-based output
|
|
9060
9064
|
const outputId = randomUUID();
|
|
9061
9065
|
const outputPath = this.workspaceDir + '/output-' + outputId + '.json';
|
|
9062
|
-
const invokeSessionKey = 'agent:main:meta-invoke:' + outputId;
|
|
9063
9066
|
// Append file output instruction to the task
|
|
9064
9067
|
const taskWithOutput = task +
|
|
9065
9068
|
'\n\n## OUTPUT DELIVERY\n\n' +
|
|
@@ -9077,7 +9080,7 @@ class GatewayExecutor {
|
|
|
9077
9080
|
runTimeoutSeconds: timeoutSeconds,
|
|
9078
9081
|
...(options?.thinking ? { thinking: options.thinking } : {}),
|
|
9079
9082
|
...(options?.model ? { model: options.model } : {}),
|
|
9080
|
-
}
|
|
9083
|
+
});
|
|
9081
9084
|
const details = (spawnResult.result?.details ?? spawnResult.result);
|
|
9082
9085
|
const sessionKey = details?.childSessionKey ?? details?.sessionKey;
|
|
9083
9086
|
if (typeof sessionKey !== 'string' || !sessionKey) {
|
|
@@ -9097,7 +9100,7 @@ class GatewayExecutor {
|
|
|
9097
9100
|
sessionKey,
|
|
9098
9101
|
limit: 5,
|
|
9099
9102
|
includeTools: false,
|
|
9100
|
-
}
|
|
9103
|
+
});
|
|
9101
9104
|
const messages = historyResult.result?.details?.messages ??
|
|
9102
9105
|
historyResult.result?.messages ??
|
|
9103
9106
|
[];
|
|
@@ -9110,7 +9113,7 @@ class GatewayExecutor {
|
|
|
9110
9113
|
lastMsg.stopReason !== 'toolUse' &&
|
|
9111
9114
|
lastMsg.stopReason !== 'error') {
|
|
9112
9115
|
// Fetch token usage from session metadata
|
|
9113
|
-
const tokens = await this.getSessionTokens(sessionKey
|
|
9116
|
+
const tokens = await this.getSessionTokens(sessionKey);
|
|
9114
9117
|
// Read output from file (sub-agent wrote it via Write tool)
|
|
9115
9118
|
if (existsSync(outputPath)) {
|
|
9116
9119
|
try {
|
|
@@ -9515,6 +9518,22 @@ const metaErrorSchema = z.object({
|
|
|
9515
9518
|
*
|
|
9516
9519
|
* @module schema/meta
|
|
9517
9520
|
*/
|
|
9521
|
+
/** Valid states for a synthesis phase. */
|
|
9522
|
+
const phaseStatuses = [
|
|
9523
|
+
'fresh',
|
|
9524
|
+
'stale',
|
|
9525
|
+
'pending',
|
|
9526
|
+
'running',
|
|
9527
|
+
'failed',
|
|
9528
|
+
];
|
|
9529
|
+
/** Zod schema for a per-phase status value. */
|
|
9530
|
+
const phaseStatusSchema = z.enum(phaseStatuses);
|
|
9531
|
+
/** Zod schema for the per-meta phase state record. */
|
|
9532
|
+
const phaseStateSchema = z.object({
|
|
9533
|
+
architect: phaseStatusSchema,
|
|
9534
|
+
builder: phaseStatusSchema,
|
|
9535
|
+
critic: phaseStatusSchema,
|
|
9536
|
+
});
|
|
9518
9537
|
/** Zod schema for the reserved (underscore-prefixed) meta.json properties. */
|
|
9519
9538
|
const metaJsonSchema = z
|
|
9520
9539
|
.object({
|
|
@@ -9595,6 +9614,12 @@ const metaJsonSchema = z
|
|
|
9595
9614
|
_error: metaErrorSchema.optional(),
|
|
9596
9615
|
/** When true, this meta is skipped during staleness scheduling. Manual trigger still works. */
|
|
9597
9616
|
_disabled: z.boolean().optional(),
|
|
9617
|
+
/**
|
|
9618
|
+
* Per-phase state machine record. Engine-managed.
|
|
9619
|
+
* Keyed by phase name (architect, builder, critic) with status values.
|
|
9620
|
+
* Persisted to survive ticks; derived on first load for back-compat.
|
|
9621
|
+
*/
|
|
9622
|
+
_phaseState: phaseStateSchema.optional(),
|
|
9598
9623
|
})
|
|
9599
9624
|
.loose();
|
|
9600
9625
|
|
|
@@ -9653,6 +9678,8 @@ async function mergeAndWrite(options) {
|
|
|
9653
9678
|
_state: options.state,
|
|
9654
9679
|
// Error handling
|
|
9655
9680
|
_error: options.error ?? undefined,
|
|
9681
|
+
// Phase state machine
|
|
9682
|
+
_phaseState: options.phaseState,
|
|
9656
9683
|
// Spread structured fields from builder
|
|
9657
9684
|
...options.builderOutput?.fields,
|
|
9658
9685
|
};
|
|
@@ -9683,6 +9710,8 @@ async function mergeAndWrite(options) {
|
|
|
9683
9710
|
delete merged._content;
|
|
9684
9711
|
if (merged._feedback === undefined)
|
|
9685
9712
|
delete merged._feedback;
|
|
9713
|
+
if (merged._phaseState === undefined)
|
|
9714
|
+
delete merged._phaseState;
|
|
9686
9715
|
// Validate
|
|
9687
9716
|
const result = metaJsonSchema.safeParse(merged);
|
|
9688
9717
|
if (!result.success) {
|
|
@@ -10378,6 +10407,686 @@ async function orchestrate(config, executor, watcher, targetPath, onProgress, lo
|
|
|
10378
10407
|
return [result];
|
|
10379
10408
|
}
|
|
10380
10409
|
|
|
10410
|
+
/**
|
|
10411
|
+
* Pure phase-state transition functions.
|
|
10412
|
+
*
|
|
10413
|
+
* Implements every row of the §8 "Transitions and invalidation cascade" table.
|
|
10414
|
+
* No I/O — pure functions over PhaseState and documented inputs.
|
|
10415
|
+
*
|
|
10416
|
+
* @module phaseState/phaseTransitions
|
|
10417
|
+
*/
|
|
10418
|
+
/**
|
|
10419
|
+
* Create a fresh (fully-complete) phase state.
|
|
10420
|
+
*/
|
|
10421
|
+
function freshPhaseState() {
|
|
10422
|
+
return { architect: 'fresh', builder: 'fresh', critic: 'fresh' };
|
|
10423
|
+
}
|
|
10424
|
+
/**
|
|
10425
|
+
* Create a phase state for a never-synthesized meta (all pending from architect).
|
|
10426
|
+
*/
|
|
10427
|
+
function initialPhaseState() {
|
|
10428
|
+
return { architect: 'pending', builder: 'stale', critic: 'stale' };
|
|
10429
|
+
}
|
|
10430
|
+
/**
|
|
10431
|
+
* Enforce the per-meta invariant: at most one phase is pending or running,
|
|
10432
|
+
* and it is the first non-fresh phase in pipeline order.
|
|
10433
|
+
*
|
|
10434
|
+
* Stale phases that become the first non-fresh phase are promoted to pending.
|
|
10435
|
+
*/
|
|
10436
|
+
function enforceInvariant(state) {
|
|
10437
|
+
const result = { ...state };
|
|
10438
|
+
let foundNonFresh = false;
|
|
10439
|
+
for (const phase of ['architect', 'builder', 'critic']) {
|
|
10440
|
+
const s = result[phase];
|
|
10441
|
+
if (s === 'fresh')
|
|
10442
|
+
continue;
|
|
10443
|
+
if (!foundNonFresh) {
|
|
10444
|
+
foundNonFresh = true;
|
|
10445
|
+
// First non-fresh: if stale, promote to pending
|
|
10446
|
+
if (s === 'stale') {
|
|
10447
|
+
result[phase] = 'pending';
|
|
10448
|
+
}
|
|
10449
|
+
// pending, running, failed stay as-is
|
|
10450
|
+
}
|
|
10451
|
+
else {
|
|
10452
|
+
// Subsequent non-fresh: must not be pending or running
|
|
10453
|
+
if (s === 'pending') {
|
|
10454
|
+
result[phase] = 'stale';
|
|
10455
|
+
}
|
|
10456
|
+
// running in non-first position would be a bug, but don't mask it
|
|
10457
|
+
}
|
|
10458
|
+
}
|
|
10459
|
+
return result;
|
|
10460
|
+
}
|
|
10461
|
+
// ── Phase success transitions ──────────────────────────────────────────
|
|
10462
|
+
/**
|
|
10463
|
+
* Architect completes successfully.
|
|
10464
|
+
* architect → fresh; builder → pending; critic → stale.
|
|
10465
|
+
*/
|
|
10466
|
+
function architectSuccess(state) {
|
|
10467
|
+
return enforceInvariant({
|
|
10468
|
+
architect: 'fresh',
|
|
10469
|
+
builder: state.builder === 'failed' ? 'failed' : 'pending',
|
|
10470
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
10471
|
+
});
|
|
10472
|
+
}
|
|
10473
|
+
/**
|
|
10474
|
+
* Builder completes successfully.
|
|
10475
|
+
* builder → fresh; critic → pending.
|
|
10476
|
+
*/
|
|
10477
|
+
function builderSuccess(state) {
|
|
10478
|
+
return enforceInvariant({
|
|
10479
|
+
...state,
|
|
10480
|
+
builder: 'fresh',
|
|
10481
|
+
critic: state.critic === 'failed' ? 'failed' : 'pending',
|
|
10482
|
+
});
|
|
10483
|
+
}
|
|
10484
|
+
/**
|
|
10485
|
+
* Critic completes successfully.
|
|
10486
|
+
* critic → fresh. Meta becomes fully fresh.
|
|
10487
|
+
*/
|
|
10488
|
+
function criticSuccess(state) {
|
|
10489
|
+
return enforceInvariant({
|
|
10490
|
+
...state,
|
|
10491
|
+
critic: 'fresh',
|
|
10492
|
+
});
|
|
10493
|
+
}
|
|
10494
|
+
// ── Failure transition ─────────────────────────────────────────────────
|
|
10495
|
+
/**
|
|
10496
|
+
* A phase fails (error, timeout, or abort).
|
|
10497
|
+
* Target phase → failed; upstream and downstream unchanged.
|
|
10498
|
+
*/
|
|
10499
|
+
function phaseFailed(state, phase) {
|
|
10500
|
+
return enforceInvariant({
|
|
10501
|
+
...state,
|
|
10502
|
+
[phase]: 'failed',
|
|
10503
|
+
});
|
|
10504
|
+
}
|
|
10505
|
+
// ── Surgical retry ─────────────────────────────────────────────────────
|
|
10506
|
+
/**
|
|
10507
|
+
* Retry a failed phase: failed → pending.
|
|
10508
|
+
* Only valid when the phase is currently failed.
|
|
10509
|
+
*/
|
|
10510
|
+
function retryPhase(state, phase) {
|
|
10511
|
+
if (state[phase] !== 'failed')
|
|
10512
|
+
return state;
|
|
10513
|
+
return enforceInvariant({
|
|
10514
|
+
...state,
|
|
10515
|
+
[phase]: 'pending',
|
|
10516
|
+
});
|
|
10517
|
+
}
|
|
10518
|
+
/**
|
|
10519
|
+
* Retry all failed phases: each failed phase → pending.
|
|
10520
|
+
* Used by scheduler ticks and queue reads to auto-promote failed phases.
|
|
10521
|
+
*/
|
|
10522
|
+
function retryAllFailed(state) {
|
|
10523
|
+
let result = state;
|
|
10524
|
+
for (const phase of ['architect', 'builder', 'critic']) {
|
|
10525
|
+
if (result[phase] === 'failed') {
|
|
10526
|
+
result = retryPhase(result, phase);
|
|
10527
|
+
}
|
|
10528
|
+
}
|
|
10529
|
+
return result;
|
|
10530
|
+
}
|
|
10531
|
+
// ── Running transition ─────────────────────────────────────────────────
|
|
10532
|
+
/**
|
|
10533
|
+
* Mark a phase as running (scheduler picks it).
|
|
10534
|
+
*/
|
|
10535
|
+
function phaseRunning(state, phase) {
|
|
10536
|
+
return {
|
|
10537
|
+
...state,
|
|
10538
|
+
[phase]: 'running',
|
|
10539
|
+
};
|
|
10540
|
+
}
|
|
10541
|
+
// ── Query helpers ──────────────────────────────────────────────────────
|
|
10542
|
+
/**
|
|
10543
|
+
* Get the owed phase: first non-fresh phase in pipeline order, or null.
|
|
10544
|
+
*/
|
|
10545
|
+
function getOwedPhase(state) {
|
|
10546
|
+
for (const phase of ['architect', 'builder', 'critic']) {
|
|
10547
|
+
if (state[phase] !== 'fresh')
|
|
10548
|
+
return phase;
|
|
10549
|
+
}
|
|
10550
|
+
return null;
|
|
10551
|
+
}
|
|
10552
|
+
/**
|
|
10553
|
+
* Check if a meta is fully fresh (all phases fresh).
|
|
10554
|
+
*/
|
|
10555
|
+
function isFullyFresh(state) {
|
|
10556
|
+
return (state.architect === 'fresh' &&
|
|
10557
|
+
state.builder === 'fresh' &&
|
|
10558
|
+
state.critic === 'fresh');
|
|
10559
|
+
}
|
|
10560
|
+
/**
|
|
10561
|
+
* Get the scheduler priority band for a meta's owed phase.
|
|
10562
|
+
* 1 = critic (highest), 2 = builder, 3 = architect, null = fully fresh.
|
|
10563
|
+
*/
|
|
10564
|
+
function getPriorityBand(state) {
|
|
10565
|
+
const owed = getOwedPhase(state);
|
|
10566
|
+
if (!owed)
|
|
10567
|
+
return null;
|
|
10568
|
+
if (owed === 'critic')
|
|
10569
|
+
return 1;
|
|
10570
|
+
if (owed === 'builder')
|
|
10571
|
+
return 2;
|
|
10572
|
+
return 3;
|
|
10573
|
+
}
|
|
10574
|
+
|
|
10575
|
+
/**
|
|
10576
|
+
* Backward-compatible derivation of _phaseState from existing meta fields.
|
|
10577
|
+
*
|
|
10578
|
+
* When a meta is loaded from disk without _phaseState, this reconstructs
|
|
10579
|
+
* the phase state from _content, _builder, _state, _error.step, and
|
|
10580
|
+
* the architect-invalidating inputs.
|
|
10581
|
+
*
|
|
10582
|
+
* @module phaseState/derivePhaseState
|
|
10583
|
+
*/
|
|
10584
|
+
/**
|
|
10585
|
+
* Derive _phaseState from existing meta fields.
|
|
10586
|
+
*
|
|
10587
|
+
* If the meta already has _phaseState, returns it as-is.
|
|
10588
|
+
*
|
|
10589
|
+
* Otherwise, reconstructs from available fields:
|
|
10590
|
+
* - Never-synthesized meta (no _content, no _builder): all phases start pending/stale.
|
|
10591
|
+
* - Errored meta: the failed phase is mapped from _error.step.
|
|
10592
|
+
* - Mid-cycle meta with cached _builder but no _content: builder pending.
|
|
10593
|
+
* - Fully-fresh meta: all phases fresh.
|
|
10594
|
+
* - Meta with stale architect inputs: architect pending, downstream stale.
|
|
10595
|
+
*
|
|
10596
|
+
* @param meta - The meta.json content.
|
|
10597
|
+
* @param inputs - Optional derivation inputs. If not provided, a simpler
|
|
10598
|
+
* heuristic is used (no architect invalidation check).
|
|
10599
|
+
* @returns The derived PhaseState.
|
|
10600
|
+
*/
|
|
10601
|
+
function derivePhaseState(meta, inputs) {
|
|
10602
|
+
// Already has _phaseState — use it
|
|
10603
|
+
if (meta._phaseState)
|
|
10604
|
+
return meta._phaseState;
|
|
10605
|
+
// Check for errors first — _error.step maps directly to failed phase
|
|
10606
|
+
if (meta._error) {
|
|
10607
|
+
const failedPhase = meta._error.step;
|
|
10608
|
+
const state = freshPhaseState();
|
|
10609
|
+
state[failedPhase] = 'failed';
|
|
10610
|
+
// If architect failed and no _builder, downstream is stale
|
|
10611
|
+
if (failedPhase === 'architect') {
|
|
10612
|
+
if (!meta._builder) {
|
|
10613
|
+
state.builder = 'stale';
|
|
10614
|
+
state.critic = 'stale';
|
|
10615
|
+
}
|
|
10616
|
+
}
|
|
10617
|
+
// If builder failed, critic is stale
|
|
10618
|
+
if (failedPhase === 'builder') {
|
|
10619
|
+
state.critic = 'stale';
|
|
10620
|
+
}
|
|
10621
|
+
return state;
|
|
10622
|
+
}
|
|
10623
|
+
// Never synthesized: no _content AND no _builder (and no error)
|
|
10624
|
+
if (!meta._content && !meta._builder) {
|
|
10625
|
+
return initialPhaseState();
|
|
10626
|
+
}
|
|
10627
|
+
// Check architect invalidation (when inputs are provided)
|
|
10628
|
+
if (inputs) {
|
|
10629
|
+
const architectInvalidated = inputs.structureChanged ||
|
|
10630
|
+
inputs.steerChanged ||
|
|
10631
|
+
inputs.architectChanged ||
|
|
10632
|
+
inputs.crossRefsChanged ||
|
|
10633
|
+
(meta._synthesisCount ?? 0) >= inputs.architectEvery;
|
|
10634
|
+
if (architectInvalidated) {
|
|
10635
|
+
return {
|
|
10636
|
+
architect: 'pending',
|
|
10637
|
+
builder: 'stale',
|
|
10638
|
+
critic: 'stale',
|
|
10639
|
+
};
|
|
10640
|
+
}
|
|
10641
|
+
}
|
|
10642
|
+
// Has _builder but no _content: builder is pending
|
|
10643
|
+
if (meta._builder && !meta._content) {
|
|
10644
|
+
return {
|
|
10645
|
+
architect: 'fresh',
|
|
10646
|
+
builder: 'pending',
|
|
10647
|
+
critic: 'stale',
|
|
10648
|
+
};
|
|
10649
|
+
}
|
|
10650
|
+
// Has _content but no _feedback: critic is pending
|
|
10651
|
+
if (meta._content && !meta._feedback) {
|
|
10652
|
+
return {
|
|
10653
|
+
architect: 'fresh',
|
|
10654
|
+
builder: 'fresh',
|
|
10655
|
+
critic: 'pending',
|
|
10656
|
+
};
|
|
10657
|
+
}
|
|
10658
|
+
// Default: fully fresh
|
|
10659
|
+
return freshPhaseState();
|
|
10660
|
+
}
|
|
10661
|
+
|
|
10662
|
+
/**
|
|
10663
|
+
* Corpus-wide phase scheduler.
|
|
10664
|
+
*
|
|
10665
|
+
* Selects the highest-priority ready phase across all metas.
|
|
10666
|
+
* Priority: critic (band 1) \> builder (band 2) \> architect (band 3).
|
|
10667
|
+
* Tiebreak within band: weighted staleness (§3.9).
|
|
10668
|
+
*
|
|
10669
|
+
* @module phaseState/phaseScheduler
|
|
10670
|
+
*/
|
|
10671
|
+
/**
|
|
10672
|
+
* Build phase candidates from listMetas entries.
|
|
10673
|
+
*
|
|
10674
|
+
* Derives phase state and auto-retries failed phases for each entry.
|
|
10675
|
+
* Used by orchestratePhase, queue route, and status route.
|
|
10676
|
+
*/
|
|
10677
|
+
function buildPhaseCandidates(entries) {
|
|
10678
|
+
return entries.map((entry) => ({
|
|
10679
|
+
node: entry.node,
|
|
10680
|
+
meta: entry.meta,
|
|
10681
|
+
phaseState: retryAllFailed(derivePhaseState(entry.meta)),
|
|
10682
|
+
actualStaleness: entry.stalenessSeconds,
|
|
10683
|
+
locked: entry.locked,
|
|
10684
|
+
disabled: entry.disabled,
|
|
10685
|
+
}));
|
|
10686
|
+
}
|
|
10687
|
+
/**
|
|
10688
|
+
* Rank all eligible phase candidates by priority.
|
|
10689
|
+
*
|
|
10690
|
+
* Filters to pending phases, computes effective staleness, and sorts by
|
|
10691
|
+
* band (ascending: critic first) then effective staleness (descending).
|
|
10692
|
+
*
|
|
10693
|
+
* Used by selectPhaseCandidate (returns first) and the queue route (returns all).
|
|
10694
|
+
*/
|
|
10695
|
+
function rankPhaseCandidates(metas, depthWeight) {
|
|
10696
|
+
// Filter to metas with a pending (scheduler-eligible) phase
|
|
10697
|
+
const eligible = metas.filter((m) => {
|
|
10698
|
+
if (m.locked)
|
|
10699
|
+
return false;
|
|
10700
|
+
if (m.disabled && !m.isOverride)
|
|
10701
|
+
return false;
|
|
10702
|
+
const owed = getOwedPhase(m.phaseState);
|
|
10703
|
+
if (!owed)
|
|
10704
|
+
return false;
|
|
10705
|
+
return m.phaseState[owed] === 'pending';
|
|
10706
|
+
});
|
|
10707
|
+
if (eligible.length === 0)
|
|
10708
|
+
return [];
|
|
10709
|
+
// Compute effective staleness for tiebreaking
|
|
10710
|
+
const withStaleness = computeEffectiveStaleness(eligible.map((m) => ({
|
|
10711
|
+
node: m.node,
|
|
10712
|
+
meta: m.meta,
|
|
10713
|
+
actualStaleness: m.actualStaleness,
|
|
10714
|
+
})), depthWeight);
|
|
10715
|
+
// Build candidates with band info
|
|
10716
|
+
const candidates = withStaleness.map((ws, i) => {
|
|
10717
|
+
const m = eligible[i];
|
|
10718
|
+
const owedPhase = getOwedPhase(m.phaseState);
|
|
10719
|
+
return {
|
|
10720
|
+
node: ws.node,
|
|
10721
|
+
meta: ws.meta,
|
|
10722
|
+
phaseState: m.phaseState,
|
|
10723
|
+
owedPhase,
|
|
10724
|
+
band: getPriorityBand(m.phaseState),
|
|
10725
|
+
actualStaleness: ws.actualStaleness,
|
|
10726
|
+
effectiveStaleness: ws.effectiveStaleness,
|
|
10727
|
+
};
|
|
10728
|
+
});
|
|
10729
|
+
// Sort by band (ascending = critic first) then effective staleness (descending)
|
|
10730
|
+
candidates.sort((a, b) => {
|
|
10731
|
+
if (a.band !== b.band)
|
|
10732
|
+
return a.band - b.band;
|
|
10733
|
+
return b.effectiveStaleness - a.effectiveStaleness;
|
|
10734
|
+
});
|
|
10735
|
+
return candidates;
|
|
10736
|
+
}
|
|
10737
|
+
/**
|
|
10738
|
+
* Select the best phase candidate across the corpus.
|
|
10739
|
+
*
|
|
10740
|
+
* @param metas - Array of (node, meta, phaseState, stalenessSeconds) tuples.
|
|
10741
|
+
* @param depthWeight - Config depthWeight for staleness tiebreak.
|
|
10742
|
+
* @returns The winning candidate, or null if no phase is ready.
|
|
10743
|
+
*/
|
|
10744
|
+
function selectPhaseCandidate(metas, depthWeight) {
|
|
10745
|
+
return rankPhaseCandidates(metas, depthWeight)[0] ?? null;
|
|
10746
|
+
}
|
|
10747
|
+
|
|
10748
|
+
/**
|
|
10749
|
+
* Per-phase executors for the phase-state machine.
|
|
10750
|
+
*
|
|
10751
|
+
* Each function runs exactly one phase on one meta, updates _phaseState
|
|
10752
|
+
* via pure transitions, and persists via the lock-staged write.
|
|
10753
|
+
*
|
|
10754
|
+
* @module orchestrator/runPhase
|
|
10755
|
+
*/
|
|
10756
|
+
/** Write updated meta with phase state via lock staging. */
|
|
10757
|
+
async function persistPhaseState(base, phaseState, updates) {
|
|
10758
|
+
const lockPath = join(base.metaPath, '.lock');
|
|
10759
|
+
const metaJsonPath = join(base.metaPath, 'meta.json');
|
|
10760
|
+
const merged = {
|
|
10761
|
+
...base.current,
|
|
10762
|
+
...updates,
|
|
10763
|
+
_phaseState: phaseState,
|
|
10764
|
+
_structureHash: base.structureHash,
|
|
10765
|
+
};
|
|
10766
|
+
// Clean undefined
|
|
10767
|
+
if (merged._error === undefined)
|
|
10768
|
+
delete merged._error;
|
|
10769
|
+
await writeFile(lockPath, JSON.stringify(merged, null, 2) + '\n');
|
|
10770
|
+
await copyFile(lockPath, metaJsonPath);
|
|
10771
|
+
return merged;
|
|
10772
|
+
}
|
|
10773
|
+
/**
|
|
10774
|
+
* Handle phase failure (abort or error).
|
|
10775
|
+
*
|
|
10776
|
+
* Shared error path for all three phase executors. When the executor was
|
|
10777
|
+
* aborted, returns immediately without persisting (abort route handles it).
|
|
10778
|
+
* Otherwise, transitions the phase to failed and persists the error.
|
|
10779
|
+
*/
|
|
10780
|
+
async function handlePhaseFailure(phase, err, executor, ps, base, additionalUpdates) {
|
|
10781
|
+
if (executor.aborted) {
|
|
10782
|
+
return {
|
|
10783
|
+
executed: true,
|
|
10784
|
+
phaseState: phaseFailed(ps, phase),
|
|
10785
|
+
error: { step: phase, code: 'ABORT', message: 'Aborted by operator' },
|
|
10786
|
+
};
|
|
10787
|
+
}
|
|
10788
|
+
const error = toMetaError(phase, err);
|
|
10789
|
+
const failedPs = phaseFailed(ps, phase);
|
|
10790
|
+
await persistPhaseState(base, failedPs, {
|
|
10791
|
+
_error: error,
|
|
10792
|
+
...additionalUpdates,
|
|
10793
|
+
});
|
|
10794
|
+
return { executed: true, phaseState: failedPs, error };
|
|
10795
|
+
}
|
|
10796
|
+
// ── Architect executor ─────────────────────────────────────────────────
|
|
10797
|
+
async function runArchitect(node, currentMeta, phaseState, config, executor, watcher, structureHash, onProgress, logger) {
|
|
10798
|
+
let ps = phaseRunning(phaseState, 'architect');
|
|
10799
|
+
const ctx = await buildContextPackage(node, currentMeta, watcher, logger);
|
|
10800
|
+
try {
|
|
10801
|
+
await onProgress?.({
|
|
10802
|
+
type: 'phase_start',
|
|
10803
|
+
path: node.ownerPath,
|
|
10804
|
+
phase: 'architect',
|
|
10805
|
+
});
|
|
10806
|
+
const phaseStart = Date.now();
|
|
10807
|
+
const architectTask = buildArchitectTask(ctx, currentMeta, config);
|
|
10808
|
+
const result = await executor.spawn(architectTask, {
|
|
10809
|
+
thinking: config.thinking,
|
|
10810
|
+
timeout: config.architectTimeout,
|
|
10811
|
+
label: 'meta-architect',
|
|
10812
|
+
});
|
|
10813
|
+
const builderBrief = parseArchitectOutput(result.output);
|
|
10814
|
+
const architectTokens = result.tokens;
|
|
10815
|
+
// Architect success: architect → fresh, _synthesisCount → 0
|
|
10816
|
+
ps = architectSuccess(ps);
|
|
10817
|
+
const updatedMeta = await persistPhaseState({ metaPath: node.metaPath, current: currentMeta, config, structureHash }, ps, {
|
|
10818
|
+
_builder: builderBrief,
|
|
10819
|
+
_architect: currentMeta._architect ?? config.defaultArchitect ?? '',
|
|
10820
|
+
_synthesisCount: 0,
|
|
10821
|
+
_architectTokens: architectTokens,
|
|
10822
|
+
_generatedAt: new Date().toISOString(),
|
|
10823
|
+
_error: undefined,
|
|
10824
|
+
});
|
|
10825
|
+
await onProgress?.({
|
|
10826
|
+
type: 'phase_complete',
|
|
10827
|
+
path: node.ownerPath,
|
|
10828
|
+
phase: 'architect',
|
|
10829
|
+
tokens: architectTokens,
|
|
10830
|
+
durationMs: Date.now() - phaseStart,
|
|
10831
|
+
});
|
|
10832
|
+
return { executed: true, phaseState: ps, updatedMeta };
|
|
10833
|
+
}
|
|
10834
|
+
catch (err) {
|
|
10835
|
+
return handlePhaseFailure('architect', err, executor, ps, {
|
|
10836
|
+
metaPath: node.metaPath,
|
|
10837
|
+
current: currentMeta,
|
|
10838
|
+
structureHash,
|
|
10839
|
+
});
|
|
10840
|
+
}
|
|
10841
|
+
}
|
|
10842
|
+
// ── Builder executor ───────────────────────────────────────────────────
|
|
10843
|
+
async function runBuilder(node, currentMeta, phaseState, config, executor, watcher, structureHash, onProgress, logger) {
|
|
10844
|
+
let ps = phaseRunning(phaseState, 'builder');
|
|
10845
|
+
const ctx = await buildContextPackage(node, currentMeta, watcher, logger);
|
|
10846
|
+
try {
|
|
10847
|
+
await onProgress?.({
|
|
10848
|
+
type: 'phase_start',
|
|
10849
|
+
path: node.ownerPath,
|
|
10850
|
+
phase: 'builder',
|
|
10851
|
+
});
|
|
10852
|
+
const builderStart = Date.now();
|
|
10853
|
+
const builderTask = buildBuilderTask(ctx, currentMeta, config);
|
|
10854
|
+
const result = await executor.spawn(builderTask, {
|
|
10855
|
+
thinking: config.thinking,
|
|
10856
|
+
timeout: config.builderTimeout,
|
|
10857
|
+
label: 'meta-builder',
|
|
10858
|
+
});
|
|
10859
|
+
const builderOutput = parseBuilderOutput(result.output);
|
|
10860
|
+
const builderTokens = result.tokens;
|
|
10861
|
+
// Builder success: builder → fresh, critic → pending
|
|
10862
|
+
ps = builderSuccess(ps);
|
|
10863
|
+
const updatedMeta = await persistPhaseState({ metaPath: node.metaPath, current: currentMeta, config, structureHash }, ps, {
|
|
10864
|
+
_content: builderOutput.content,
|
|
10865
|
+
_state: builderOutput.state,
|
|
10866
|
+
_builderTokens: builderTokens,
|
|
10867
|
+
_generatedAt: new Date().toISOString(),
|
|
10868
|
+
_error: undefined,
|
|
10869
|
+
...builderOutput.fields,
|
|
10870
|
+
});
|
|
10871
|
+
await onProgress?.({
|
|
10872
|
+
type: 'phase_complete',
|
|
10873
|
+
path: node.ownerPath,
|
|
10874
|
+
phase: 'builder',
|
|
10875
|
+
tokens: builderTokens,
|
|
10876
|
+
durationMs: Date.now() - builderStart,
|
|
10877
|
+
});
|
|
10878
|
+
return { executed: true, phaseState: ps, updatedMeta };
|
|
10879
|
+
}
|
|
10880
|
+
catch (err) {
|
|
10881
|
+
// §4.6 partial _state recovery on timeout
|
|
10882
|
+
let partialState;
|
|
10883
|
+
if (err instanceof SpawnTimeoutError) {
|
|
10884
|
+
try {
|
|
10885
|
+
const raw = await readFile(err.outputPath, 'utf8');
|
|
10886
|
+
const partial = parseBuilderOutput(raw);
|
|
10887
|
+
if (partial.state !== undefined &&
|
|
10888
|
+
JSON.stringify(partial.state) !== JSON.stringify(currentMeta._state)) {
|
|
10889
|
+
partialState = { _state: partial.state };
|
|
10890
|
+
}
|
|
10891
|
+
}
|
|
10892
|
+
catch {
|
|
10893
|
+
// Could not read partial output — no state recovery
|
|
10894
|
+
}
|
|
10895
|
+
}
|
|
10896
|
+
return handlePhaseFailure('builder', err, executor, ps, {
|
|
10897
|
+
metaPath: node.metaPath,
|
|
10898
|
+
current: currentMeta,
|
|
10899
|
+
structureHash,
|
|
10900
|
+
}, partialState);
|
|
10901
|
+
}
|
|
10902
|
+
}
|
|
10903
|
+
// ── Critic executor ────────────────────────────────────────────────────
|
|
10904
|
+
async function runCritic(node, currentMeta, phaseState, config, executor, watcher, structureHash, onProgress, logger) {
|
|
10905
|
+
let ps = phaseRunning(phaseState, 'critic');
|
|
10906
|
+
const ctx = await buildContextPackage(node, currentMeta, watcher, logger);
|
|
10907
|
+
// Build critic task using current meta's _content
|
|
10908
|
+
const metaForCritic = { ...currentMeta };
|
|
10909
|
+
try {
|
|
10910
|
+
await onProgress?.({
|
|
10911
|
+
type: 'phase_start',
|
|
10912
|
+
path: node.ownerPath,
|
|
10913
|
+
phase: 'critic',
|
|
10914
|
+
});
|
|
10915
|
+
const criticStart = Date.now();
|
|
10916
|
+
const criticTask = buildCriticTask(ctx, metaForCritic, config);
|
|
10917
|
+
const result = await executor.spawn(criticTask, {
|
|
10918
|
+
thinking: config.thinking,
|
|
10919
|
+
timeout: config.criticTimeout,
|
|
10920
|
+
label: 'meta-critic',
|
|
10921
|
+
});
|
|
10922
|
+
const feedback = parseCriticOutput(result.output);
|
|
10923
|
+
const criticTokens = result.tokens;
|
|
10924
|
+
// Critic success: critic → fresh
|
|
10925
|
+
ps = criticSuccess(ps);
|
|
10926
|
+
const cycleComplete = isFullyFresh(ps);
|
|
10927
|
+
const updates = {
|
|
10928
|
+
_feedback: feedback,
|
|
10929
|
+
_criticTokens: criticTokens,
|
|
10930
|
+
_error: undefined,
|
|
10931
|
+
};
|
|
10932
|
+
// Full-cycle completion: increment _synthesisCount, archive, emit.
|
|
10933
|
+
// Per spec: architect resets to 0, full-cycle increments on top.
|
|
10934
|
+
if (cycleComplete) {
|
|
10935
|
+
updates._synthesisCount = (currentMeta._synthesisCount ?? 0) + 1;
|
|
10936
|
+
}
|
|
10937
|
+
const updatedMeta = await persistPhaseState({ metaPath: node.metaPath, current: currentMeta, config, structureHash }, ps, updates);
|
|
10938
|
+
// Archive on full-cycle only
|
|
10939
|
+
if (cycleComplete) {
|
|
10940
|
+
await createSnapshot(node.metaPath, updatedMeta);
|
|
10941
|
+
await pruneArchive(node.metaPath, config.maxArchive);
|
|
10942
|
+
}
|
|
10943
|
+
await onProgress?.({
|
|
10944
|
+
type: 'phase_complete',
|
|
10945
|
+
path: node.ownerPath,
|
|
10946
|
+
phase: 'critic',
|
|
10947
|
+
tokens: criticTokens,
|
|
10948
|
+
durationMs: Date.now() - criticStart,
|
|
10949
|
+
});
|
|
10950
|
+
return {
|
|
10951
|
+
executed: true,
|
|
10952
|
+
phaseState: ps,
|
|
10953
|
+
updatedMeta,
|
|
10954
|
+
cycleComplete,
|
|
10955
|
+
};
|
|
10956
|
+
}
|
|
10957
|
+
catch (err) {
|
|
10958
|
+
return handlePhaseFailure('critic', err, executor, ps, {
|
|
10959
|
+
metaPath: node.metaPath,
|
|
10960
|
+
current: currentMeta,
|
|
10961
|
+
structureHash,
|
|
10962
|
+
});
|
|
10963
|
+
}
|
|
10964
|
+
}
|
|
10965
|
+
|
|
10966
|
+
/**
|
|
10967
|
+
* Phase-aware orchestration entry point.
|
|
10968
|
+
*
|
|
10969
|
+
* Replaces the old staleness-based orchestrate() with phase-state-machine
|
|
10970
|
+
* scheduling: each tick discovers all metas, computes invalidation,
|
|
10971
|
+
* auto-retries failed phases, selects the best phase candidate, and
|
|
10972
|
+
* executes exactly one phase.
|
|
10973
|
+
*
|
|
10974
|
+
* @module orchestrator/orchestratePhase
|
|
10975
|
+
*/
|
|
10976
|
+
/** Phase runner dispatch map — avoids repeating the same switch/case. */
|
|
10977
|
+
const phaseRunners = {
|
|
10978
|
+
architect: runArchitect,
|
|
10979
|
+
builder: runBuilder,
|
|
10980
|
+
critic: runCritic,
|
|
10981
|
+
};
|
|
10982
|
+
/**
|
|
10983
|
+
* Run a single phase-aware orchestration tick.
|
|
10984
|
+
*
|
|
10985
|
+
* When targetPath is provided (override entry), runs the owed phase for
|
|
10986
|
+
* that specific meta. Otherwise, discovers all metas, computes invalidation,
|
|
10987
|
+
* and selects the best phase candidate corpus-wide.
|
|
10988
|
+
*/
|
|
10989
|
+
async function orchestratePhase(config, executor, watcher, targetPath, onProgress, logger) {
|
|
10990
|
+
// ── Targeted path (override entry) ──
|
|
10991
|
+
if (targetPath) {
|
|
10992
|
+
return orchestrateTargeted(config, executor, watcher, targetPath, onProgress, logger);
|
|
10993
|
+
}
|
|
10994
|
+
// ── Corpus-wide discovery + phase selection ──
|
|
10995
|
+
let metaResult;
|
|
10996
|
+
try {
|
|
10997
|
+
metaResult = await listMetas(config, watcher);
|
|
10998
|
+
}
|
|
10999
|
+
catch (err) {
|
|
11000
|
+
logger?.warn({ err }, 'Failed to list metas for phase selection');
|
|
11001
|
+
return { executed: false };
|
|
11002
|
+
}
|
|
11003
|
+
if (metaResult.entries.length === 0)
|
|
11004
|
+
return { executed: false };
|
|
11005
|
+
// Build candidates with phase state (including invalidation + auto-retry)
|
|
11006
|
+
const candidates = buildPhaseCandidates(metaResult.entries);
|
|
11007
|
+
// Select best phase candidate
|
|
11008
|
+
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
11009
|
+
if (!winner) {
|
|
11010
|
+
return { executed: false };
|
|
11011
|
+
}
|
|
11012
|
+
// Acquire lock
|
|
11013
|
+
if (!acquireLock(winner.node.metaPath)) {
|
|
11014
|
+
logger?.debug({ path: winner.node.metaPath }, 'Selected candidate is locked, skipping');
|
|
11015
|
+
return { executed: false };
|
|
11016
|
+
}
|
|
11017
|
+
try {
|
|
11018
|
+
// Re-read meta under lock for freshness
|
|
11019
|
+
const currentMeta = await readMetaJson(winner.node.metaPath);
|
|
11020
|
+
const phaseState = retryAllFailed(derivePhaseState(currentMeta));
|
|
11021
|
+
const owedPhase = getOwedPhase(phaseState);
|
|
11022
|
+
if (!owedPhase || phaseState[owedPhase] !== 'pending') {
|
|
11023
|
+
// Nothing to do (race: became fresh between selection and lock)
|
|
11024
|
+
return { executed: false };
|
|
11025
|
+
}
|
|
11026
|
+
// Compute structure hash for the phase
|
|
11027
|
+
const { scopeFiles } = await getScopeFiles(winner.node, watcher);
|
|
11028
|
+
const structureHash = computeStructureHash(scopeFiles);
|
|
11029
|
+
// skipUnchanged: bump _generatedAt without altering _phaseState
|
|
11030
|
+
if (config.skipUnchanged && currentMeta._generatedAt) {
|
|
11031
|
+
const verifiedStale = await isStale(getScopePrefix(winner.node), currentMeta, watcher);
|
|
11032
|
+
if (!verifiedStale) {
|
|
11033
|
+
await persistPhaseState({
|
|
11034
|
+
metaPath: winner.node.metaPath,
|
|
11035
|
+
current: currentMeta,
|
|
11036
|
+
config,
|
|
11037
|
+
structureHash,
|
|
11038
|
+
}, phaseState, { _generatedAt: new Date().toISOString() });
|
|
11039
|
+
logger?.debug({ path: winner.node.ownerPath }, 'Skipped unchanged meta, bumped _generatedAt');
|
|
11040
|
+
return { executed: false };
|
|
11041
|
+
}
|
|
11042
|
+
}
|
|
11043
|
+
return await executePhase(winner.node, currentMeta, phaseState, owedPhase, config, executor, watcher, structureHash, onProgress, logger);
|
|
11044
|
+
}
|
|
11045
|
+
finally {
|
|
11046
|
+
releaseLock(winner.node.metaPath);
|
|
11047
|
+
}
|
|
11048
|
+
}
|
|
11049
|
+
/**
|
|
11050
|
+
* Orchestrate a targeted (override) meta path.
|
|
11051
|
+
* Resolves the owed phase at execution time (not enqueue time).
|
|
11052
|
+
*/
|
|
11053
|
+
async function orchestrateTargeted(config, executor, watcher, targetPath, onProgress, logger) {
|
|
11054
|
+
const normalizedTarget = normalizePath(targetPath);
|
|
11055
|
+
const node = await buildMinimalNode(normalizedTarget, watcher);
|
|
11056
|
+
if (!acquireLock(node.metaPath)) {
|
|
11057
|
+
return { executed: false };
|
|
11058
|
+
}
|
|
11059
|
+
try {
|
|
11060
|
+
const currentMeta = await readMetaJson(normalizedTarget);
|
|
11061
|
+
const phaseState = retryAllFailed(derivePhaseState(currentMeta));
|
|
11062
|
+
const owedPhase = getOwedPhase(phaseState);
|
|
11063
|
+
if (!owedPhase) {
|
|
11064
|
+
// Fully fresh — override is a no-op (silently dropped per spec)
|
|
11065
|
+
return { executed: false, metaPath: normalizedTarget };
|
|
11066
|
+
}
|
|
11067
|
+
// Compute structure hash
|
|
11068
|
+
const { scopeFiles } = await getScopeFiles(node, watcher);
|
|
11069
|
+
const structureHash = computeStructureHash(scopeFiles);
|
|
11070
|
+
return await executePhase(node, currentMeta, phaseState, owedPhase, config, executor, watcher, structureHash, onProgress, logger);
|
|
11071
|
+
}
|
|
11072
|
+
finally {
|
|
11073
|
+
releaseLock(node.metaPath);
|
|
11074
|
+
}
|
|
11075
|
+
}
|
|
11076
|
+
/**
|
|
11077
|
+
* Execute exactly one phase on a meta.
|
|
11078
|
+
*/
|
|
11079
|
+
async function executePhase(node, currentMeta, phaseState, phase, config, executor, watcher, structureHash, onProgress, logger) {
|
|
11080
|
+
const result = await phaseRunners[phase](node, currentMeta, phaseState, config, executor, watcher, structureHash, onProgress, logger);
|
|
11081
|
+
return {
|
|
11082
|
+
executed: true,
|
|
11083
|
+
metaPath: node.metaPath,
|
|
11084
|
+
phase,
|
|
11085
|
+
phaseResult: result,
|
|
11086
|
+
cycleComplete: result.cycleComplete,
|
|
11087
|
+
};
|
|
11088
|
+
}
|
|
11089
|
+
|
|
10381
11090
|
/**
|
|
10382
11091
|
* Progress reporting via OpenClaw gateway `/tools/invoke` → `message` tool.
|
|
10383
11092
|
*
|
|
@@ -10512,42 +11221,106 @@ class ProgressReporter {
|
|
|
10512
11221
|
}
|
|
10513
11222
|
|
|
10514
11223
|
/**
|
|
10515
|
-
*
|
|
11224
|
+
* Hybrid 3-layer synthesis queue.
|
|
10516
11225
|
*
|
|
10517
|
-
*
|
|
10518
|
-
*
|
|
10519
|
-
*
|
|
11226
|
+
* Layer 1: Current — the single item currently executing (at most one).
|
|
11227
|
+
* Layer 2: Overrides — items manually enqueued via POST /synthesize with path.
|
|
11228
|
+
* FIFO among overrides, ahead of automatic candidates.
|
|
11229
|
+
* Layer 3: Automatic — computed on read, not persisted. All metas with a
|
|
11230
|
+
* pending phase, ranked by scheduler priority.
|
|
11231
|
+
*
|
|
11232
|
+
* Legacy: `pending` array is the union of overrides + automatic.
|
|
10520
11233
|
*
|
|
10521
11234
|
* @module queue
|
|
10522
11235
|
*/
|
|
10523
11236
|
const DEPTH_WARNING_THRESHOLD = 3;
|
|
10524
11237
|
/**
|
|
10525
|
-
*
|
|
11238
|
+
* Hybrid 3-layer synthesis queue.
|
|
10526
11239
|
*
|
|
10527
|
-
* Only one synthesis runs at a time.
|
|
10528
|
-
*
|
|
10529
|
-
* position returned.
|
|
11240
|
+
* Only one synthesis runs at a time. Override items (explicit triggers)
|
|
11241
|
+
* take priority over automatic candidates.
|
|
10530
11242
|
*/
|
|
10531
11243
|
class SynthesisQueue {
|
|
11244
|
+
/** Legacy queue (used by processQueue for backward compat). */
|
|
10532
11245
|
queue = [];
|
|
10533
11246
|
currentItem = null;
|
|
10534
11247
|
processing = false;
|
|
10535
11248
|
logger;
|
|
10536
11249
|
onEnqueueCallback = null;
|
|
10537
|
-
/**
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
|
|
10541
|
-
*/
|
|
11250
|
+
/** Explicit override entries (3-layer model). */
|
|
11251
|
+
overrideEntries = [];
|
|
11252
|
+
/** Currently executing item with phase info (3-layer model). */
|
|
11253
|
+
currentPhaseItem = null;
|
|
10542
11254
|
constructor(logger) {
|
|
10543
11255
|
this.logger = logger;
|
|
10544
11256
|
}
|
|
10545
|
-
/**
|
|
10546
|
-
* Set a callback to invoke when a new (non-duplicate) item is enqueued.
|
|
10547
|
-
*/
|
|
11257
|
+
/** Set a callback to invoke when a new (non-duplicate) item is enqueued. */
|
|
10548
11258
|
onEnqueue(callback) {
|
|
10549
11259
|
this.onEnqueueCallback = callback;
|
|
10550
11260
|
}
|
|
11261
|
+
// ── Override layer (3-layer model) ─────────────────────────────────
|
|
11262
|
+
/**
|
|
11263
|
+
* Add an explicit override entry (from POST /synthesize with path).
|
|
11264
|
+
* Deduped by path. Returns position and whether already queued.
|
|
11265
|
+
*/
|
|
11266
|
+
enqueueOverride(path) {
|
|
11267
|
+
// Check if currently executing
|
|
11268
|
+
if (this.currentPhaseItem?.path === path ||
|
|
11269
|
+
this.currentItem?.path === path) {
|
|
11270
|
+
return { position: 0, alreadyQueued: true };
|
|
11271
|
+
}
|
|
11272
|
+
// Check if already in overrides
|
|
11273
|
+
const existing = this.overrideEntries.findIndex((e) => e.path === path);
|
|
11274
|
+
if (existing !== -1) {
|
|
11275
|
+
return { position: existing, alreadyQueued: true };
|
|
11276
|
+
}
|
|
11277
|
+
this.overrideEntries.push({
|
|
11278
|
+
path,
|
|
11279
|
+
enqueuedAt: new Date().toISOString(),
|
|
11280
|
+
});
|
|
11281
|
+
const position = this.overrideEntries.length - 1;
|
|
11282
|
+
if (this.overrideEntries.length > DEPTH_WARNING_THRESHOLD) {
|
|
11283
|
+
this.logger.warn({ depth: this.overrideEntries.length }, 'Override queue depth exceeds threshold');
|
|
11284
|
+
}
|
|
11285
|
+
this.onEnqueueCallback?.();
|
|
11286
|
+
return { position, alreadyQueued: false };
|
|
11287
|
+
}
|
|
11288
|
+
/** Dequeue the next override entry, or undefined if empty. */
|
|
11289
|
+
dequeueOverride() {
|
|
11290
|
+
return this.overrideEntries.shift();
|
|
11291
|
+
}
|
|
11292
|
+
/** Get all override entries (shallow copy). */
|
|
11293
|
+
get overrides() {
|
|
11294
|
+
return [...this.overrideEntries];
|
|
11295
|
+
}
|
|
11296
|
+
/** Clear all override entries. Returns count removed. */
|
|
11297
|
+
clearOverrides() {
|
|
11298
|
+
const count = this.overrideEntries.length;
|
|
11299
|
+
this.overrideEntries = [];
|
|
11300
|
+
return count;
|
|
11301
|
+
}
|
|
11302
|
+
/** Check if a path is in the override layer. */
|
|
11303
|
+
hasOverride(path) {
|
|
11304
|
+
return this.overrideEntries.some((e) => e.path === path);
|
|
11305
|
+
}
|
|
11306
|
+
// ── Current-item tracking (3-layer model) ──────────────────────────
|
|
11307
|
+
/** Set the currently executing phase item. */
|
|
11308
|
+
setCurrentPhase(path, phase) {
|
|
11309
|
+
this.currentPhaseItem = {
|
|
11310
|
+
path,
|
|
11311
|
+
phase,
|
|
11312
|
+
startedAt: new Date().toISOString(),
|
|
11313
|
+
};
|
|
11314
|
+
}
|
|
11315
|
+
/** Clear the current phase item. */
|
|
11316
|
+
clearCurrentPhase() {
|
|
11317
|
+
this.currentPhaseItem = null;
|
|
11318
|
+
}
|
|
11319
|
+
/** The currently executing phase item, or null. */
|
|
11320
|
+
get currentPhase() {
|
|
11321
|
+
return this.currentPhaseItem;
|
|
11322
|
+
}
|
|
11323
|
+
// ── Legacy queue interface (preserved for backward compat) ─────────
|
|
10551
11324
|
/**
|
|
10552
11325
|
* Add a path to the synthesis queue.
|
|
10553
11326
|
*
|
|
@@ -10556,11 +11329,9 @@ class SynthesisQueue {
|
|
|
10556
11329
|
* @returns Position and whether the path was already queued.
|
|
10557
11330
|
*/
|
|
10558
11331
|
enqueue(path, priority = false) {
|
|
10559
|
-
// Check if currently being synthesized.
|
|
10560
11332
|
if (this.currentItem?.path === path) {
|
|
10561
11333
|
return { position: 0, alreadyQueued: true };
|
|
10562
11334
|
}
|
|
10563
|
-
// Check if already in queue.
|
|
10564
11335
|
const existingIndex = this.queue.findIndex((item) => item.path === path);
|
|
10565
11336
|
if (existingIndex !== -1) {
|
|
10566
11337
|
return { position: existingIndex, alreadyQueued: true };
|
|
@@ -10583,11 +11354,7 @@ class SynthesisQueue {
|
|
|
10583
11354
|
this.onEnqueueCallback?.();
|
|
10584
11355
|
return { position, alreadyQueued: false };
|
|
10585
11356
|
}
|
|
10586
|
-
/**
|
|
10587
|
-
* Remove and return the next item from the queue.
|
|
10588
|
-
*
|
|
10589
|
-
* @returns The next QueueItem, or undefined if the queue is empty.
|
|
10590
|
-
*/
|
|
11357
|
+
/** Remove and return the next item from the queue. */
|
|
10591
11358
|
dequeue() {
|
|
10592
11359
|
const item = this.queue.shift();
|
|
10593
11360
|
if (item) {
|
|
@@ -10617,7 +11384,6 @@ class SynthesisQueue {
|
|
|
10617
11384
|
}
|
|
10618
11385
|
/**
|
|
10619
11386
|
* Remove all pending items from the queue.
|
|
10620
|
-
*
|
|
10621
11387
|
* Does not affect the currently-running item.
|
|
10622
11388
|
*
|
|
10623
11389
|
* @returns The number of items removed.
|
|
@@ -10627,45 +11393,56 @@ class SynthesisQueue {
|
|
|
10627
11393
|
this.queue = [];
|
|
10628
11394
|
return count;
|
|
10629
11395
|
}
|
|
10630
|
-
/**
|
|
10631
|
-
* Check whether a path is in the queue or currently being synthesized.
|
|
10632
|
-
*
|
|
10633
|
-
* @param path - Meta path to look up.
|
|
10634
|
-
* @returns True if the path is queued or currently running.
|
|
10635
|
-
*/
|
|
11396
|
+
/** Check whether a path is in the queue or currently being synthesized. */
|
|
10636
11397
|
has(path) {
|
|
10637
11398
|
if (this.currentItem?.path === path)
|
|
10638
11399
|
return true;
|
|
10639
|
-
|
|
11400
|
+
if (this.currentPhaseItem?.path === path)
|
|
11401
|
+
return true;
|
|
11402
|
+
return (this.queue.some((item) => item.path === path) ||
|
|
11403
|
+
this.overrideEntries.some((e) => e.path === path));
|
|
10640
11404
|
}
|
|
10641
|
-
/**
|
|
10642
|
-
* Get the 0-indexed position of a path in the queue.
|
|
10643
|
-
*
|
|
10644
|
-
* @param path - Meta path to look up.
|
|
10645
|
-
* @returns Position index, or null if not found in the queue.
|
|
10646
|
-
*/
|
|
11405
|
+
/** Get the 0-indexed position of a path in the queue. */
|
|
10647
11406
|
getPosition(path) {
|
|
11407
|
+
// Check overrides first
|
|
11408
|
+
const overrideIdx = this.overrideEntries.findIndex((e) => e.path === path);
|
|
11409
|
+
if (overrideIdx !== -1)
|
|
11410
|
+
return overrideIdx;
|
|
10648
11411
|
const index = this.queue.findIndex((item) => item.path === path);
|
|
10649
11412
|
return index === -1 ? null : index;
|
|
10650
11413
|
}
|
|
10651
|
-
/**
|
|
10652
|
-
|
|
10653
|
-
|
|
10654
|
-
|
|
10655
|
-
|
|
11414
|
+
/** Dequeue the next item: overrides first, then legacy queue. */
|
|
11415
|
+
nextItem() {
|
|
11416
|
+
const override = this.dequeueOverride();
|
|
11417
|
+
if (override)
|
|
11418
|
+
return { path: override.path, source: 'override' };
|
|
11419
|
+
const item = this.dequeue();
|
|
11420
|
+
if (item)
|
|
11421
|
+
return { path: item.path, source: 'legacy' };
|
|
11422
|
+
return undefined;
|
|
11423
|
+
}
|
|
11424
|
+
/** Return a snapshot of queue state for the /status endpoint. */
|
|
10656
11425
|
getState() {
|
|
10657
11426
|
return {
|
|
10658
|
-
depth: this.queue.length,
|
|
10659
|
-
items:
|
|
10660
|
-
|
|
10661
|
-
|
|
10662
|
-
|
|
10663
|
-
|
|
11427
|
+
depth: this.queue.length + this.overrideEntries.length,
|
|
11428
|
+
items: [
|
|
11429
|
+
...this.overrideEntries.map((e) => ({
|
|
11430
|
+
path: e.path,
|
|
11431
|
+
priority: true,
|
|
11432
|
+
enqueuedAt: e.enqueuedAt,
|
|
11433
|
+
})),
|
|
11434
|
+
...this.queue.map((item) => ({
|
|
11435
|
+
path: item.path,
|
|
11436
|
+
priority: item.priority,
|
|
11437
|
+
enqueuedAt: item.enqueuedAt,
|
|
11438
|
+
})),
|
|
11439
|
+
],
|
|
10664
11440
|
};
|
|
10665
11441
|
}
|
|
10666
11442
|
/**
|
|
10667
|
-
* Process queued items one at a time until
|
|
11443
|
+
* Process queued items one at a time until all queues are empty.
|
|
10668
11444
|
*
|
|
11445
|
+
* Override entries are processed first (FIFO), then legacy queue items.
|
|
10669
11446
|
* Re-entry is prevented: if already processing, the call returns
|
|
10670
11447
|
* immediately. Errors are logged and do not block subsequent items.
|
|
10671
11448
|
*
|
|
@@ -10676,19 +11453,22 @@ class SynthesisQueue {
|
|
|
10676
11453
|
return;
|
|
10677
11454
|
this.processing = true;
|
|
10678
11455
|
try {
|
|
10679
|
-
let
|
|
10680
|
-
while (
|
|
11456
|
+
let next = this.nextItem();
|
|
11457
|
+
while (next) {
|
|
10681
11458
|
try {
|
|
10682
|
-
await synthesizeFn(
|
|
11459
|
+
await synthesizeFn(next.path);
|
|
10683
11460
|
}
|
|
10684
11461
|
catch (err) {
|
|
10685
|
-
this.logger.error({ path:
|
|
11462
|
+
this.logger.error({ path: next.path, err }, 'Synthesis failed');
|
|
10686
11463
|
}
|
|
10687
|
-
this.
|
|
10688
|
-
|
|
11464
|
+
this.clearCurrentPhase();
|
|
11465
|
+
if (next.source === 'legacy')
|
|
11466
|
+
this.complete();
|
|
11467
|
+
next = this.nextItem();
|
|
10689
11468
|
}
|
|
10690
11469
|
}
|
|
10691
11470
|
finally {
|
|
11471
|
+
this.clearCurrentPhase();
|
|
10692
11472
|
this.processing = false;
|
|
10693
11473
|
}
|
|
10694
11474
|
}
|
|
@@ -11160,14 +11940,15 @@ async function autoSeedPass(rules, watcher, logger) {
|
|
|
11160
11940
|
}
|
|
11161
11941
|
|
|
11162
11942
|
/**
|
|
11163
|
-
* Croner-based scheduler that discovers the
|
|
11164
|
-
* and enqueues it for
|
|
11943
|
+
* Croner-based scheduler that discovers the highest-priority ready phase
|
|
11944
|
+
* across the corpus each tick and enqueues it for execution.
|
|
11165
11945
|
*
|
|
11166
11946
|
* @module scheduler
|
|
11167
11947
|
*/
|
|
11168
11948
|
const MAX_BACKOFF_MULTIPLIER = 4;
|
|
11169
11949
|
/**
|
|
11170
|
-
* Periodic scheduler that discovers
|
|
11950
|
+
* Periodic scheduler that discovers the highest-priority ready phase
|
|
11951
|
+
* across all metas and enqueues it for execution.
|
|
11171
11952
|
*
|
|
11172
11953
|
* Supports adaptive backoff when no candidates are found and hot-reloadable
|
|
11173
11954
|
* cron expressions via {@link Scheduler.updateSchedule}.
|
|
@@ -11222,10 +12003,10 @@ class Scheduler {
|
|
|
11222
12003
|
this.logger.info({ schedule: expression }, 'Schedule updated');
|
|
11223
12004
|
}
|
|
11224
12005
|
}
|
|
11225
|
-
/** Reset backoff multiplier (call after successful
|
|
12006
|
+
/** Reset backoff multiplier (call after successful phase execution). */
|
|
11226
12007
|
resetBackoff() {
|
|
11227
12008
|
if (this.backoffMultiplier > 1) {
|
|
11228
|
-
this.logger.debug('Backoff reset after successful
|
|
12009
|
+
this.logger.debug('Backoff reset after successful phase execution');
|
|
11229
12010
|
}
|
|
11230
12011
|
this.backoffMultiplier = 1;
|
|
11231
12012
|
}
|
|
@@ -11240,10 +12021,9 @@ class Scheduler {
|
|
|
11240
12021
|
return this.job.nextRun() ?? null;
|
|
11241
12022
|
}
|
|
11242
12023
|
/**
|
|
11243
|
-
* Single tick: discover
|
|
12024
|
+
* Single tick: discover the highest-priority ready phase and enqueue it.
|
|
11244
12025
|
*
|
|
11245
|
-
*
|
|
11246
|
-
* when no candidates are found.
|
|
12026
|
+
* Applies adaptive backoff when no candidates are found.
|
|
11247
12027
|
*/
|
|
11248
12028
|
async tick() {
|
|
11249
12029
|
this.tickCount++;
|
|
@@ -11268,14 +12048,15 @@ class Scheduler {
|
|
|
11268
12048
|
this.logger.warn({ err }, 'Auto-seed pass failed');
|
|
11269
12049
|
}
|
|
11270
12050
|
}
|
|
11271
|
-
const candidate = await this.
|
|
12051
|
+
const candidate = await this.discoverNextPhase();
|
|
11272
12052
|
if (!candidate) {
|
|
11273
12053
|
this.backoffMultiplier = Math.min(this.backoffMultiplier * 2, MAX_BACKOFF_MULTIPLIER);
|
|
11274
|
-
this.logger.debug({ backoffMultiplier: this.backoffMultiplier }, 'No
|
|
12054
|
+
this.logger.debug({ backoffMultiplier: this.backoffMultiplier }, 'No ready phases found, increasing backoff');
|
|
11275
12055
|
return;
|
|
11276
12056
|
}
|
|
11277
|
-
|
|
11278
|
-
this.
|
|
12057
|
+
// Enqueue using the legacy queue path (backward compat with processQueue)
|
|
12058
|
+
this.queue.enqueue(candidate.path);
|
|
12059
|
+
this.logger.info({ path: candidate.path, phase: candidate.phase, band: candidate.band }, 'Enqueued phase candidate');
|
|
11279
12060
|
// Opportunistic watcher restart detection
|
|
11280
12061
|
if (this.registrar) {
|
|
11281
12062
|
try {
|
|
@@ -11295,22 +12076,27 @@ class Scheduler {
|
|
|
11295
12076
|
}
|
|
11296
12077
|
}
|
|
11297
12078
|
/**
|
|
11298
|
-
* Discover the
|
|
12079
|
+
* Discover the highest-priority ready phase across the corpus.
|
|
12080
|
+
*
|
|
12081
|
+
* Uses phase-state-aware scheduling: priority order is
|
|
12082
|
+
* critic (band 1) \> builder (band 2) \> architect (band 3),
|
|
12083
|
+
* with weighted staleness as tiebreaker within a band.
|
|
11299
12084
|
*/
|
|
11300
|
-
async
|
|
12085
|
+
async discoverNextPhase() {
|
|
11301
12086
|
try {
|
|
11302
12087
|
const result = await listMetas(this.config, this.watcher);
|
|
11303
|
-
const
|
|
11304
|
-
|
|
11305
|
-
|
|
11306
|
-
|
|
11307
|
-
|
|
11308
|
-
|
|
11309
|
-
|
|
11310
|
-
|
|
12088
|
+
const candidates = buildPhaseCandidates(result.entries);
|
|
12089
|
+
const winner = selectPhaseCandidate(candidates, this.config.depthWeight);
|
|
12090
|
+
if (!winner)
|
|
12091
|
+
return null;
|
|
12092
|
+
return {
|
|
12093
|
+
path: winner.node.metaPath,
|
|
12094
|
+
phase: winner.owedPhase,
|
|
12095
|
+
band: winner.band,
|
|
12096
|
+
};
|
|
11311
12097
|
}
|
|
11312
12098
|
catch (err) {
|
|
11313
|
-
this.logger.warn({ err }, 'Failed to discover
|
|
12099
|
+
this.logger.warn({ err }, 'Failed to discover next phase candidate');
|
|
11314
12100
|
return null;
|
|
11315
12101
|
}
|
|
11316
12102
|
}
|
|
@@ -11516,9 +12302,12 @@ function registerMetasRoutes(app, deps) {
|
|
|
11516
12302
|
'architectTokens',
|
|
11517
12303
|
'builderTokens',
|
|
11518
12304
|
'criticTokens',
|
|
12305
|
+
'phaseState',
|
|
12306
|
+
'owedPhase',
|
|
11519
12307
|
];
|
|
11520
12308
|
const projectedFields = fieldList ?? defaultFields;
|
|
11521
12309
|
const metas = entries.map((e) => {
|
|
12310
|
+
const ps = derivePhaseState(e.meta);
|
|
11522
12311
|
const full = {
|
|
11523
12312
|
path: e.path,
|
|
11524
12313
|
depth: e.depth,
|
|
@@ -11533,6 +12322,8 @@ function registerMetasRoutes(app, deps) {
|
|
|
11533
12322
|
architectTokens: e.architectTokens,
|
|
11534
12323
|
builderTokens: e.builderTokens,
|
|
11535
12324
|
criticTokens: e.criticTokens,
|
|
12325
|
+
phaseState: ps,
|
|
12326
|
+
owedPhase: getOwedPhase(ps),
|
|
11536
12327
|
};
|
|
11537
12328
|
const projected = {};
|
|
11538
12329
|
for (const f of projectedFields) {
|
|
@@ -11557,13 +12348,6 @@ function registerMetasRoutes(app, deps) {
|
|
|
11557
12348
|
}
|
|
11558
12349
|
const meta = JSON.parse(await readFile(join(targetNode.metaPath, 'meta.json'), 'utf8'));
|
|
11559
12350
|
// Field projection
|
|
11560
|
-
const defaultExclude = new Set([
|
|
11561
|
-
'_architect',
|
|
11562
|
-
'_builder',
|
|
11563
|
-
'_critic',
|
|
11564
|
-
'_content',
|
|
11565
|
-
'_feedback',
|
|
11566
|
-
]);
|
|
11567
12351
|
const fieldList = query.fields?.split(',');
|
|
11568
12352
|
const projectMeta = (m) => {
|
|
11569
12353
|
if (fieldList) {
|
|
@@ -11574,7 +12358,7 @@ function registerMetasRoutes(app, deps) {
|
|
|
11574
12358
|
}
|
|
11575
12359
|
const r = {};
|
|
11576
12360
|
for (const [k, v] of Object.entries(m)) {
|
|
11577
|
-
if (!
|
|
12361
|
+
if (!DEFAULT_EXCLUDE_FIELDS.has(k))
|
|
11578
12362
|
r[k] = v;
|
|
11579
12363
|
}
|
|
11580
12364
|
return r;
|
|
@@ -11587,6 +12371,10 @@ function registerMetasRoutes(app, deps) {
|
|
|
11587
12371
|
? Math.round((Date.now() - new Date(metaTyped._generatedAt).getTime()) / 1000)
|
|
11588
12372
|
: null;
|
|
11589
12373
|
const score = computeStalenessScore(staleSeconds, metaTyped._depth ?? 0, metaTyped._emphasis ?? 1, config.depthWeight);
|
|
12374
|
+
// Phase state
|
|
12375
|
+
const entry = result.entries.find((e) => e.node.metaPath === targetNode.metaPath);
|
|
12376
|
+
const phaseState = entry ? derivePhaseState(entry.meta) : null;
|
|
12377
|
+
const owedPhase = phaseState ? getOwedPhase(phaseState) : null;
|
|
11590
12378
|
const response = {
|
|
11591
12379
|
path: targetNode.metaPath,
|
|
11592
12380
|
meta: projectMeta(meta),
|
|
@@ -11599,6 +12387,8 @@ function registerMetasRoutes(app, deps) {
|
|
|
11599
12387
|
seconds: staleSeconds,
|
|
11600
12388
|
score: Math.round(score * 100) / 100,
|
|
11601
12389
|
},
|
|
12390
|
+
phaseState,
|
|
12391
|
+
owedPhase,
|
|
11602
12392
|
};
|
|
11603
12393
|
// Cross-refs status
|
|
11604
12394
|
const crossRefsRaw = meta._crossRefs;
|
|
@@ -11701,16 +12491,9 @@ function registerMetasUpdateRoute(app, deps) {
|
|
|
11701
12491
|
Object.assign(updated, toSet);
|
|
11702
12492
|
await writeFile(metaJsonPath, JSON.stringify(updated, null, 2) + '\n');
|
|
11703
12493
|
// Project the response — exclude the same large fields as the detail route.
|
|
11704
|
-
const defaultExclude = new Set([
|
|
11705
|
-
'_architect',
|
|
11706
|
-
'_builder',
|
|
11707
|
-
'_critic',
|
|
11708
|
-
'_content',
|
|
11709
|
-
'_feedback',
|
|
11710
|
-
]);
|
|
11711
12494
|
const projected = {};
|
|
11712
12495
|
for (const [k, v] of Object.entries(updated)) {
|
|
11713
|
-
if (!
|
|
12496
|
+
if (!DEFAULT_EXCLUDE_FIELDS.has(k))
|
|
11714
12497
|
projected[k] = v;
|
|
11715
12498
|
}
|
|
11716
12499
|
return reply.send({
|
|
@@ -11772,6 +12555,19 @@ function registerPreviewRoute(app, deps) {
|
|
|
11772
12555
|
const structureChanged = structureHash !== meta._structureHash;
|
|
11773
12556
|
const latestArchive = await readLatestArchive(targetNode.metaPath);
|
|
11774
12557
|
const steerChanged = hasSteerChanged(meta._steer, latestArchive?._steer, Boolean(latestArchive));
|
|
12558
|
+
// _architect change detection
|
|
12559
|
+
const architectChanged = latestArchive
|
|
12560
|
+
? (meta._architect ?? '') !== (latestArchive._architect ?? '')
|
|
12561
|
+
: Boolean(meta._architect);
|
|
12562
|
+
// _crossRefs declaration change detection
|
|
12563
|
+
const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
|
|
12564
|
+
const archiveRefs = (latestArchive?._crossRefs ?? [])
|
|
12565
|
+
.slice()
|
|
12566
|
+
.sort()
|
|
12567
|
+
.join(',');
|
|
12568
|
+
const crossRefsDeclChanged = latestArchive
|
|
12569
|
+
? currentRefs !== archiveRefs
|
|
12570
|
+
: currentRefs.length > 0;
|
|
11775
12571
|
const architectTriggered = isArchitectTriggered(meta, structureChanged, steerChanged, config.architectEvery);
|
|
11776
12572
|
// Delta files
|
|
11777
12573
|
const deltaFiles = getDeltaFiles(meta._generatedAt, scopeFiles);
|
|
@@ -11786,6 +12582,40 @@ function registerPreviewRoute(app, deps) {
|
|
|
11786
12582
|
? Math.round((Date.now() - new Date(meta._generatedAt).getTime()) / 1000)
|
|
11787
12583
|
: null;
|
|
11788
12584
|
const stalenessScore = computeStalenessScore(stalenessSeconds, meta._depth ?? 0, meta._emphasis ?? 1, config.depthWeight);
|
|
12585
|
+
// Phase state
|
|
12586
|
+
const phaseState = derivePhaseState(meta, {
|
|
12587
|
+
structureChanged,
|
|
12588
|
+
steerChanged,
|
|
12589
|
+
architectChanged,
|
|
12590
|
+
crossRefsChanged: crossRefsDeclChanged,
|
|
12591
|
+
architectEvery: config.architectEvery,
|
|
12592
|
+
});
|
|
12593
|
+
const owedPhase = getOwedPhase(phaseState);
|
|
12594
|
+
const priorityBand = getPriorityBand(phaseState);
|
|
12595
|
+
// Architect invalidators
|
|
12596
|
+
const architectInvalidators = [];
|
|
12597
|
+
if (owedPhase === 'architect') {
|
|
12598
|
+
if (structureChanged)
|
|
12599
|
+
architectInvalidators.push('structureHash');
|
|
12600
|
+
if (steerChanged)
|
|
12601
|
+
architectInvalidators.push('steer');
|
|
12602
|
+
if (architectChanged)
|
|
12603
|
+
architectInvalidators.push('_architect');
|
|
12604
|
+
if (crossRefsDeclChanged)
|
|
12605
|
+
architectInvalidators.push('_crossRefs');
|
|
12606
|
+
if ((meta._synthesisCount ?? 0) >= config.architectEvery) {
|
|
12607
|
+
architectInvalidators.push('architectEvery');
|
|
12608
|
+
}
|
|
12609
|
+
}
|
|
12610
|
+
// Staleness inputs
|
|
12611
|
+
const stalenessInputs = {
|
|
12612
|
+
structureHash,
|
|
12613
|
+
steerChanged,
|
|
12614
|
+
architectChanged,
|
|
12615
|
+
crossRefsDeclChanged,
|
|
12616
|
+
scopeMtimeMax: null,
|
|
12617
|
+
crossRefContentChanged: false,
|
|
12618
|
+
};
|
|
11789
12619
|
return {
|
|
11790
12620
|
path: targetNode.metaPath,
|
|
11791
12621
|
staleness: {
|
|
@@ -11810,6 +12640,12 @@ function registerPreviewRoute(app, deps) {
|
|
|
11810
12640
|
deltaCount: deltaFiles.length,
|
|
11811
12641
|
},
|
|
11812
12642
|
estimatedTokens,
|
|
12643
|
+
// New phase-state fields (additive)
|
|
12644
|
+
owedPhase,
|
|
12645
|
+
priorityBand,
|
|
12646
|
+
phaseState,
|
|
12647
|
+
stalenessInputs,
|
|
12648
|
+
architectInvalidators,
|
|
11813
12649
|
};
|
|
11814
12650
|
});
|
|
11815
12651
|
}
|
|
@@ -11817,8 +12653,8 @@ function registerPreviewRoute(app, deps) {
|
|
|
11817
12653
|
/**
|
|
11818
12654
|
* Queue management and abort routes.
|
|
11819
12655
|
*
|
|
11820
|
-
* - GET /queue —
|
|
11821
|
-
* - POST /queue/clear — remove
|
|
12656
|
+
* - GET /queue — 3-layer queue model (current, overrides, automatic, pending)
|
|
12657
|
+
* - POST /queue/clear — remove override entries only
|
|
11822
12658
|
* - POST /synthesize/abort — abort the current synthesis
|
|
11823
12659
|
*
|
|
11824
12660
|
* @module routes/queue
|
|
@@ -11826,33 +12662,134 @@ function registerPreviewRoute(app, deps) {
|
|
|
11826
12662
|
/** Register queue management routes. */
|
|
11827
12663
|
function registerQueueRoutes(app, deps) {
|
|
11828
12664
|
const { queue } = deps;
|
|
11829
|
-
app.get('/queue', () =>
|
|
11830
|
-
|
|
11831
|
-
|
|
11832
|
-
state
|
|
11833
|
-
|
|
12665
|
+
app.get('/queue', async () => {
|
|
12666
|
+
const currentPhase = queue.currentPhase;
|
|
12667
|
+
const overrides = queue.overrides;
|
|
12668
|
+
// Compute owedPhase for each override entry by reading meta state
|
|
12669
|
+
const enrichedOverrides = await Promise.all(overrides.map(async (o) => {
|
|
12670
|
+
try {
|
|
12671
|
+
const metaDir = resolveMetaDir(o.path);
|
|
12672
|
+
const meta = await readMetaJson(metaDir);
|
|
12673
|
+
const ps = derivePhaseState(meta);
|
|
12674
|
+
return {
|
|
12675
|
+
path: o.path,
|
|
12676
|
+
owedPhase: getOwedPhase(ps),
|
|
12677
|
+
enqueuedAt: o.enqueuedAt,
|
|
12678
|
+
};
|
|
12679
|
+
}
|
|
12680
|
+
catch {
|
|
12681
|
+
return {
|
|
12682
|
+
path: o.path,
|
|
12683
|
+
owedPhase: null,
|
|
12684
|
+
enqueuedAt: o.enqueuedAt,
|
|
12685
|
+
};
|
|
12686
|
+
}
|
|
12687
|
+
}));
|
|
12688
|
+
// Compute automatic layer: all metas with a pending owed phase,
|
|
12689
|
+
// ranked by scheduler priority (computed on read, not persisted)
|
|
12690
|
+
let automatic = [];
|
|
12691
|
+
try {
|
|
12692
|
+
const metaResult = await listMetas(deps.config, deps.watcher);
|
|
12693
|
+
const candidates = buildPhaseCandidates(metaResult.entries);
|
|
12694
|
+
const ranked = rankPhaseCandidates(candidates, deps.config.depthWeight);
|
|
12695
|
+
automatic = ranked.map((c) => ({
|
|
12696
|
+
path: c.node.metaPath,
|
|
12697
|
+
owedPhase: c.owedPhase,
|
|
12698
|
+
priorityBand: c.band,
|
|
12699
|
+
effectiveStaleness: c.effectiveStaleness,
|
|
12700
|
+
}));
|
|
12701
|
+
}
|
|
12702
|
+
catch {
|
|
12703
|
+
// If listing fails, automatic stays empty
|
|
12704
|
+
}
|
|
12705
|
+
// Legacy: pending is the union of overrides + automatic + legacy queue items
|
|
12706
|
+
const pendingItems = [
|
|
12707
|
+
...enrichedOverrides.map((o) => ({
|
|
12708
|
+
path: o.path,
|
|
12709
|
+
owedPhase: o.owedPhase,
|
|
12710
|
+
})),
|
|
12711
|
+
...automatic.map((a) => ({
|
|
12712
|
+
path: a.path,
|
|
12713
|
+
owedPhase: a.owedPhase,
|
|
12714
|
+
})),
|
|
12715
|
+
...queue.pending.map((item) => ({
|
|
12716
|
+
path: item.path,
|
|
12717
|
+
owedPhase: null,
|
|
12718
|
+
})),
|
|
12719
|
+
];
|
|
12720
|
+
return {
|
|
12721
|
+
current: currentPhase
|
|
12722
|
+
? {
|
|
12723
|
+
path: currentPhase.path,
|
|
12724
|
+
phase: currentPhase.phase,
|
|
12725
|
+
startedAt: currentPhase.startedAt,
|
|
12726
|
+
}
|
|
12727
|
+
: queue.current
|
|
12728
|
+
? {
|
|
12729
|
+
path: queue.current.path,
|
|
12730
|
+
phase: null,
|
|
12731
|
+
startedAt: queue.current.enqueuedAt,
|
|
12732
|
+
}
|
|
12733
|
+
: null,
|
|
12734
|
+
overrides: enrichedOverrides,
|
|
12735
|
+
automatic,
|
|
12736
|
+
pending: pendingItems,
|
|
12737
|
+
// Legacy state
|
|
12738
|
+
state: queue.getState(),
|
|
12739
|
+
};
|
|
12740
|
+
});
|
|
11834
12741
|
app.post('/queue/clear', () => {
|
|
11835
|
-
const removed = queue.
|
|
12742
|
+
const removed = queue.clearOverrides();
|
|
11836
12743
|
return { cleared: removed };
|
|
11837
12744
|
});
|
|
11838
12745
|
app.post('/synthesize/abort', async (_request, reply) => {
|
|
11839
|
-
|
|
12746
|
+
// Check 3-layer current first
|
|
12747
|
+
const currentPhase = queue.currentPhase;
|
|
12748
|
+
const current = currentPhase ?? queue.current;
|
|
11840
12749
|
if (!current) {
|
|
11841
|
-
return reply
|
|
11842
|
-
.status(404)
|
|
11843
|
-
.send({ error: 'NOT_FOUND', message: 'No synthesis in progress' });
|
|
12750
|
+
return reply.status(200).send({ status: 'idle' });
|
|
11844
12751
|
}
|
|
11845
12752
|
// Abort the executor
|
|
11846
12753
|
deps.executor?.abort();
|
|
12754
|
+
const metaDir = resolveMetaDir(current.path);
|
|
12755
|
+
const phase = currentPhase?.phase ?? null;
|
|
12756
|
+
// Transition running phase to failed and write _error to meta.json
|
|
12757
|
+
if (phase) {
|
|
12758
|
+
try {
|
|
12759
|
+
const meta = await readMetaJson(metaDir);
|
|
12760
|
+
let ps = derivePhaseState(meta);
|
|
12761
|
+
ps = phaseFailed(ps, phase);
|
|
12762
|
+
const updated = {
|
|
12763
|
+
...meta,
|
|
12764
|
+
_phaseState: ps,
|
|
12765
|
+
_error: {
|
|
12766
|
+
step: phase,
|
|
12767
|
+
code: 'ABORT',
|
|
12768
|
+
message: 'Aborted by operator',
|
|
12769
|
+
},
|
|
12770
|
+
};
|
|
12771
|
+
const lockPath = join(metaDir, '.lock');
|
|
12772
|
+
const metaJsonPath = join(metaDir, 'meta.json');
|
|
12773
|
+
await writeFile(lockPath, JSON.stringify(updated, null, 2) + '\n');
|
|
12774
|
+
await copyFile(lockPath, metaJsonPath);
|
|
12775
|
+
}
|
|
12776
|
+
catch {
|
|
12777
|
+
// Best-effort — meta may be unreadable
|
|
12778
|
+
}
|
|
12779
|
+
}
|
|
11847
12780
|
// Release the lock for the current meta path
|
|
11848
12781
|
try {
|
|
11849
|
-
releaseLock(
|
|
12782
|
+
releaseLock(metaDir);
|
|
11850
12783
|
}
|
|
11851
12784
|
catch {
|
|
11852
12785
|
// Lock may already be released
|
|
11853
12786
|
}
|
|
11854
12787
|
deps.logger.info({ path: current.path }, 'Synthesis aborted');
|
|
11855
|
-
return {
|
|
12788
|
+
return {
|
|
12789
|
+
status: 'aborted',
|
|
12790
|
+
path: current.path,
|
|
12791
|
+
...(phase ? { phase } : {}),
|
|
12792
|
+
};
|
|
11856
12793
|
});
|
|
11857
12794
|
}
|
|
11858
12795
|
|
|
@@ -11909,7 +12846,6 @@ async function checkDependency(url, path) {
|
|
|
11909
12846
|
return { url, status: 'unreachable', checkedAt };
|
|
11910
12847
|
}
|
|
11911
12848
|
}
|
|
11912
|
-
/** Check watcher, surfacing initialScan.active as indexing state. */
|
|
11913
12849
|
async function checkWatcher(url) {
|
|
11914
12850
|
const checkedAt = new Date().toISOString();
|
|
11915
12851
|
try {
|
|
@@ -11935,26 +12871,61 @@ async function checkWatcher(url) {
|
|
|
11935
12871
|
function deriveServiceState(deps) {
|
|
11936
12872
|
if (deps.shuttingDown)
|
|
11937
12873
|
return 'stopping';
|
|
11938
|
-
if (deps.queue.current)
|
|
12874
|
+
if (deps.queue.current || deps.queue.currentPhase)
|
|
11939
12875
|
return 'synthesizing';
|
|
11940
|
-
if (deps.queue.depth > 0)
|
|
12876
|
+
if (deps.queue.depth > 0 || deps.queue.overrides.length > 0)
|
|
11941
12877
|
return 'waiting';
|
|
11942
12878
|
return 'idle';
|
|
11943
12879
|
}
|
|
12880
|
+
function emptyPhaseCounts() {
|
|
12881
|
+
return { fresh: 0, stale: 0, pending: 0, running: 0, failed: 0 };
|
|
12882
|
+
}
|
|
11944
12883
|
function registerStatusRoute(app, deps) {
|
|
11945
12884
|
const statusHandler = createStatusHandler({
|
|
11946
12885
|
name: SERVICE_NAME,
|
|
11947
12886
|
version: SERVICE_VERSION,
|
|
11948
12887
|
getHealth: async () => {
|
|
11949
|
-
const { config, queue, scheduler, stats } = deps;
|
|
12888
|
+
const { config, queue, scheduler, stats, watcher } = deps;
|
|
11950
12889
|
// On-demand dependency checks
|
|
11951
12890
|
const [watcherHealth, gatewayHealth] = await Promise.all([
|
|
11952
12891
|
checkWatcher(config.watcherUrl),
|
|
11953
12892
|
checkDependency(config.gatewayUrl, '/status'),
|
|
11954
12893
|
]);
|
|
12894
|
+
// Phase state summary
|
|
12895
|
+
const phaseStateSummary = {
|
|
12896
|
+
architect: emptyPhaseCounts(),
|
|
12897
|
+
builder: emptyPhaseCounts(),
|
|
12898
|
+
critic: emptyPhaseCounts(),
|
|
12899
|
+
};
|
|
12900
|
+
let nextPhase = null;
|
|
12901
|
+
try {
|
|
12902
|
+
const metaResult = await listMetas(config, watcher);
|
|
12903
|
+
// Count raw phase states (before retry) for display
|
|
12904
|
+
for (const entry of metaResult.entries) {
|
|
12905
|
+
const ps = derivePhaseState(entry.meta);
|
|
12906
|
+
for (const phase of ['architect', 'builder', 'critic']) {
|
|
12907
|
+
phaseStateSummary[phase][ps[phase]]++;
|
|
12908
|
+
}
|
|
12909
|
+
}
|
|
12910
|
+
// Build candidates (with auto-retry) for scheduling
|
|
12911
|
+
const candidates = buildPhaseCandidates(metaResult.entries);
|
|
12912
|
+
// Find next phase candidate
|
|
12913
|
+
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
12914
|
+
if (winner) {
|
|
12915
|
+
nextPhase = {
|
|
12916
|
+
path: winner.node.metaPath,
|
|
12917
|
+
phase: winner.owedPhase,
|
|
12918
|
+
band: winner.band,
|
|
12919
|
+
staleness: winner.effectiveStaleness,
|
|
12920
|
+
};
|
|
12921
|
+
}
|
|
12922
|
+
}
|
|
12923
|
+
catch {
|
|
12924
|
+
// Watcher unreachable — phase summary unavailable
|
|
12925
|
+
}
|
|
11955
12926
|
return {
|
|
11956
12927
|
serviceState: deriveServiceState(deps),
|
|
11957
|
-
currentTarget: queue.current?.path ?? null,
|
|
12928
|
+
currentTarget: queue.current?.path ?? queue.currentPhase?.path ?? null,
|
|
11958
12929
|
queue: queue.getState(),
|
|
11959
12930
|
stats: {
|
|
11960
12931
|
totalSyntheses: stats.totalSyntheses,
|
|
@@ -11974,6 +12945,8 @@ function registerStatusRoute(app, deps) {
|
|
|
11974
12945
|
},
|
|
11975
12946
|
gateway: gatewayHealth,
|
|
11976
12947
|
},
|
|
12948
|
+
phaseStateSummary,
|
|
12949
|
+
nextPhase,
|
|
11977
12950
|
};
|
|
11978
12951
|
},
|
|
11979
12952
|
});
|
|
@@ -11986,6 +12959,9 @@ function registerStatusRoute(app, deps) {
|
|
|
11986
12959
|
/**
|
|
11987
12960
|
* POST /synthesize route handler.
|
|
11988
12961
|
*
|
|
12962
|
+
* Path-targeted triggers create explicit override entries in the queue.
|
|
12963
|
+
* Corpus-wide triggers discover the stalest candidate.
|
|
12964
|
+
*
|
|
11989
12965
|
* @module routes/synthesize
|
|
11990
12966
|
*/
|
|
11991
12967
|
const synthesizeBodySchema = z.object({
|
|
@@ -11996,44 +12972,70 @@ function registerSynthesizeRoute(app, deps) {
|
|
|
11996
12972
|
app.post('/synthesize', async (request, reply) => {
|
|
11997
12973
|
const body = synthesizeBodySchema.parse(request.body);
|
|
11998
12974
|
const { config, watcher, queue } = deps;
|
|
11999
|
-
let targetPath;
|
|
12000
12975
|
if (body.path) {
|
|
12001
|
-
|
|
12002
|
-
|
|
12003
|
-
|
|
12004
|
-
|
|
12005
|
-
let
|
|
12976
|
+
// Path-targeted trigger: create override entry
|
|
12977
|
+
const targetPath = resolveMetaDir(body.path);
|
|
12978
|
+
// Read meta to determine owed phase
|
|
12979
|
+
let owedPhase = null;
|
|
12980
|
+
let meta;
|
|
12006
12981
|
try {
|
|
12007
|
-
|
|
12982
|
+
meta = await readMetaJson(targetPath);
|
|
12983
|
+
const phaseState = derivePhaseState(meta);
|
|
12984
|
+
owedPhase = getOwedPhase(phaseState);
|
|
12008
12985
|
}
|
|
12009
12986
|
catch {
|
|
12010
|
-
|
|
12011
|
-
error: 'SERVICE_UNAVAILABLE',
|
|
12012
|
-
message: 'Watcher unreachable — cannot discover candidates',
|
|
12013
|
-
});
|
|
12987
|
+
// Meta unreadable — proceed, phase will be evaluated at dequeue time
|
|
12014
12988
|
}
|
|
12015
|
-
|
|
12016
|
-
|
|
12017
|
-
.
|
|
12018
|
-
node: e.node,
|
|
12019
|
-
meta: e.meta,
|
|
12020
|
-
actualStaleness: e.stalenessSeconds,
|
|
12021
|
-
}));
|
|
12022
|
-
const stalest = discoverStalestPath(stale, config.depthWeight);
|
|
12023
|
-
if (!stalest) {
|
|
12024
|
-
return reply.code(200).send({
|
|
12989
|
+
// Fully fresh meta → skip (reuse meta already read above)
|
|
12990
|
+
if (owedPhase === null && meta && (meta._phaseState || meta._content)) {
|
|
12991
|
+
return await reply.code(200).send({
|
|
12025
12992
|
status: 'skipped',
|
|
12026
|
-
|
|
12993
|
+
path: targetPath,
|
|
12994
|
+
owedPhase: null,
|
|
12995
|
+
queuePosition: -1,
|
|
12996
|
+
alreadyQueued: false,
|
|
12027
12997
|
});
|
|
12028
12998
|
}
|
|
12029
|
-
|
|
12999
|
+
const result = queue.enqueueOverride(targetPath);
|
|
13000
|
+
return reply.code(202).send({
|
|
13001
|
+
status: 'queued',
|
|
13002
|
+
path: targetPath,
|
|
13003
|
+
owedPhase,
|
|
13004
|
+
queuePosition: result.position,
|
|
13005
|
+
alreadyQueued: result.alreadyQueued,
|
|
13006
|
+
});
|
|
13007
|
+
}
|
|
13008
|
+
// Corpus-wide trigger: discover stalest candidate
|
|
13009
|
+
let result;
|
|
13010
|
+
try {
|
|
13011
|
+
result = await listMetas(config, watcher);
|
|
12030
13012
|
}
|
|
12031
|
-
|
|
13013
|
+
catch {
|
|
13014
|
+
return reply.status(503).send({
|
|
13015
|
+
error: 'SERVICE_UNAVAILABLE',
|
|
13016
|
+
message: 'Watcher unreachable — cannot discover candidates',
|
|
13017
|
+
});
|
|
13018
|
+
}
|
|
13019
|
+
const stale = result.entries
|
|
13020
|
+
.filter((e) => e.stalenessSeconds > 0 && !e.disabled)
|
|
13021
|
+
.map((e) => ({
|
|
13022
|
+
node: e.node,
|
|
13023
|
+
meta: e.meta,
|
|
13024
|
+
actualStaleness: e.stalenessSeconds,
|
|
13025
|
+
}));
|
|
13026
|
+
const stalest = discoverStalestPath(stale, config.depthWeight);
|
|
13027
|
+
if (!stalest) {
|
|
13028
|
+
return reply.code(200).send({
|
|
13029
|
+
status: 'skipped',
|
|
13030
|
+
message: 'No stale metas found. Nothing to synthesize.',
|
|
13031
|
+
});
|
|
13032
|
+
}
|
|
13033
|
+
const enqueueResult = queue.enqueue(stalest);
|
|
12032
13034
|
return reply.code(202).send({
|
|
12033
13035
|
status: 'accepted',
|
|
12034
|
-
path:
|
|
12035
|
-
queuePosition:
|
|
12036
|
-
alreadyQueued:
|
|
13036
|
+
path: stalest,
|
|
13037
|
+
queuePosition: enqueueResult.position,
|
|
13038
|
+
alreadyQueued: enqueueResult.alreadyQueued,
|
|
12037
13039
|
});
|
|
12038
13040
|
});
|
|
12039
13041
|
}
|
|
@@ -12071,6 +13073,17 @@ function registerUnlockRoute(app, deps) {
|
|
|
12071
13073
|
*
|
|
12072
13074
|
* @module routes
|
|
12073
13075
|
*/
|
|
13076
|
+
/**
|
|
13077
|
+
* Large generated fields excluded from detail/update response projections.
|
|
13078
|
+
* Shared between metas detail and metasUpdate routes.
|
|
13079
|
+
*/
|
|
13080
|
+
const DEFAULT_EXCLUDE_FIELDS = new Set([
|
|
13081
|
+
'_architect',
|
|
13082
|
+
'_builder',
|
|
13083
|
+
'_critic',
|
|
13084
|
+
'_content',
|
|
13085
|
+
'_feedback',
|
|
13086
|
+
]);
|
|
12074
13087
|
/** Register all HTTP routes on the Fastify instance. */
|
|
12075
13088
|
function registerRoutes(app, deps) {
|
|
12076
13089
|
// Global error handler for validation + watcher errors
|
|
@@ -12325,10 +13338,9 @@ async function startService(config, configPath) {
|
|
|
12325
13338
|
}
|
|
12326
13339
|
// Progress reporter — uses shared config reference so hot-reload propagates
|
|
12327
13340
|
const progress = new ProgressReporter(config, logger);
|
|
12328
|
-
// Wire queue processing —
|
|
13341
|
+
// Wire queue processing — execute one phase per dequeue (phase-state machine)
|
|
12329
13342
|
const synthesizeFn = async (path) => {
|
|
12330
13343
|
const startMs = Date.now();
|
|
12331
|
-
let cycleTokens = 0;
|
|
12332
13344
|
// Strip .meta suffix for human-readable progress reporting
|
|
12333
13345
|
const ownerPath = path.replace(/\/?\.meta\/?$/, '');
|
|
12334
13346
|
await progress.report({
|
|
@@ -12336,49 +13348,50 @@ async function startService(config, configPath) {
|
|
|
12336
13348
|
path: ownerPath,
|
|
12337
13349
|
});
|
|
12338
13350
|
try {
|
|
12339
|
-
const
|
|
13351
|
+
const result = await orchestratePhase(config, executor, watcher, path, async (evt) => {
|
|
13352
|
+
// Wire current-phase tracking for GET /queue and POST /synthesize/abort
|
|
13353
|
+
if (evt.type === 'phase_start' && evt.phase) {
|
|
13354
|
+
queue.setCurrentPhase(ownerPath, evt.phase);
|
|
13355
|
+
}
|
|
12340
13356
|
// Track token stats from phase completions
|
|
12341
13357
|
if (evt.type === 'phase_complete') {
|
|
12342
13358
|
if (evt.tokens !== undefined) {
|
|
12343
13359
|
stats.totalTokens += evt.tokens;
|
|
12344
|
-
if (cycleTokens !== undefined) {
|
|
12345
|
-
cycleTokens += evt.tokens;
|
|
12346
|
-
}
|
|
12347
13360
|
}
|
|
12348
13361
|
else {
|
|
12349
|
-
cycleTokens = undefined;
|
|
12350
13362
|
logger.warn({ path: ownerPath, phase: evt.phase }, 'Token count unavailable (session lookup may have timed out)');
|
|
12351
13363
|
}
|
|
12352
13364
|
}
|
|
12353
13365
|
await progress.report(evt);
|
|
12354
13366
|
}, logger);
|
|
12355
|
-
// orchestrate() always returns exactly one result
|
|
12356
|
-
const result = results[0];
|
|
12357
13367
|
const durationMs = Date.now() - startMs;
|
|
12358
|
-
if (!result.
|
|
12359
|
-
|
|
12360
|
-
logger.debug({ path: ownerPath }, 'Synthesis skipped');
|
|
13368
|
+
if (!result.executed) {
|
|
13369
|
+
logger.debug({ path: ownerPath }, 'Phase skipped (fully fresh or locked)');
|
|
12361
13370
|
return;
|
|
12362
13371
|
}
|
|
12363
13372
|
// Update stats
|
|
12364
13373
|
stats.totalSyntheses++;
|
|
12365
13374
|
stats.lastCycleDurationMs = durationMs;
|
|
12366
13375
|
stats.lastCycleAt = new Date().toISOString();
|
|
12367
|
-
|
|
13376
|
+
const phaseResult = result.phaseResult;
|
|
13377
|
+
if (phaseResult?.error) {
|
|
12368
13378
|
stats.totalErrors++;
|
|
12369
13379
|
await progress.report({
|
|
12370
13380
|
type: 'error',
|
|
12371
13381
|
path: ownerPath,
|
|
12372
|
-
phase:
|
|
12373
|
-
error:
|
|
13382
|
+
phase: phaseResult.error.step,
|
|
13383
|
+
error: phaseResult.error.message,
|
|
12374
13384
|
});
|
|
12375
13385
|
}
|
|
12376
13386
|
else {
|
|
13387
|
+
// Task #9: Reset backoff on ANY successful phase execution
|
|
12377
13388
|
scheduler.resetBackoff();
|
|
13389
|
+
}
|
|
13390
|
+
// Emit synthesis_complete only on full-cycle completion
|
|
13391
|
+
if (result.cycleComplete) {
|
|
12378
13392
|
await progress.report({
|
|
12379
13393
|
type: 'synthesis_complete',
|
|
12380
13394
|
path: ownerPath,
|
|
12381
|
-
tokens: cycleTokens,
|
|
12382
13395
|
durationMs,
|
|
12383
13396
|
});
|
|
12384
13397
|
}
|
|
@@ -12498,4 +13511,4 @@ const metaDescriptor = jeevesComponentDescriptorSchema.parse({
|
|
|
12498
13511
|
customCliCommands: registerCustomCliCommands,
|
|
12499
13512
|
});
|
|
12500
13513
|
|
|
12501
|
-
export { DEFAULT_PORT, DEFAULT_PORT_STR, GatewayExecutor, HttpWatcherClient, MAX_STALENESS_SECONDS, ProgressReporter, RESTART_REQUIRED_FIELDS, RuleRegistrar, SERVICE_NAME, SERVICE_VERSION, Scheduler, SynthesisQueue, acquireLock, actualStaleness, buildArchitectTask, buildBuilderTask, buildContextPackage, buildCriticTask, buildOwnershipTree, cleanupStaleLocks, computeEffectiveStaleness, computeEma, computeStructureHash, createLogger, createServer, createSnapshot, discoverMetas, filterInScope, findNode, formatProgressEvent, getScopePrefix, hasSteerChanged, isArchitectTriggered, isLocked, isStale, listArchiveFiles, listMetas, loadServiceConfig, mergeAndWrite, metaConfigSchema, metaDescriptor, metaErrorSchema, metaJsonSchema, migrateConfigPath, normalizePath, orchestrate, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, readLockState, registerCustomCliCommands, registerRoutes, registerShutdownHandlers, releaseLock, resolveConfigPath, resolveMetaDir, selectCandidate, serviceConfigSchema, sleepAsync as sleep, startService, toMetaError, verifyRuleApplication };
|
|
13514
|
+
export { DEFAULT_PORT, DEFAULT_PORT_STR, GatewayExecutor, HttpWatcherClient, MAX_STALENESS_SECONDS, ProgressReporter, RESTART_REQUIRED_FIELDS, RuleRegistrar, SERVICE_NAME, SERVICE_VERSION, Scheduler, SynthesisQueue, acquireLock, actualStaleness, buildArchitectTask, buildBuilderTask, buildContextPackage, buildCriticTask, buildOwnershipTree, cleanupStaleLocks, computeEffectiveStaleness, computeEma, computeStructureHash, createLogger, createServer, createSnapshot, discoverMetas, filterInScope, findNode, formatProgressEvent, getScopePrefix, hasSteerChanged, isArchitectTriggered, isLocked, isStale, listArchiveFiles, listMetas, loadServiceConfig, mergeAndWrite, metaConfigSchema, metaDescriptor, metaErrorSchema, metaJsonSchema, migrateConfigPath, normalizePath, orchestrate, orchestratePhase, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, readLockState, registerCustomCliCommands, registerRoutes, registerShutdownHandlers, releaseLock, resolveConfigPath, resolveMetaDir, runArchitect, runBuilder, runCritic, selectCandidate, serviceConfigSchema, sleepAsync as sleep, startService, toMetaError, verifyRuleApplication };
|