@karmaniverous/jeeves-meta 0.13.11 → 0.15.0
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 +8 -6
- package/dist/cli/jeeves-meta/index.js +1181 -607
- package/dist/index.d.ts +181 -52
- package/dist/index.js +1266 -149
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8292,6 +8292,7 @@ function computeSummary(entries, depthWeight) {
|
|
|
8292
8292
|
let staleCount = 0;
|
|
8293
8293
|
let errorCount = 0;
|
|
8294
8294
|
let lockedCount = 0;
|
|
8295
|
+
let disabledCount = 0;
|
|
8295
8296
|
let neverSynthesizedCount = 0;
|
|
8296
8297
|
let totalArchitectTokens = 0;
|
|
8297
8298
|
let totalBuilderTokens = 0;
|
|
@@ -8307,6 +8308,8 @@ function computeSummary(entries, depthWeight) {
|
|
|
8307
8308
|
errorCount++;
|
|
8308
8309
|
if (e.locked)
|
|
8309
8310
|
lockedCount++;
|
|
8311
|
+
if (e.disabled)
|
|
8312
|
+
disabledCount++;
|
|
8310
8313
|
if (e.lastSynthesized === null)
|
|
8311
8314
|
neverSynthesizedCount++;
|
|
8312
8315
|
totalArchitectTokens += e.architectTokens ?? 0;
|
|
@@ -8331,6 +8334,7 @@ function computeSummary(entries, depthWeight) {
|
|
|
8331
8334
|
stale: staleCount,
|
|
8332
8335
|
errors: errorCount,
|
|
8333
8336
|
locked: lockedCount,
|
|
8337
|
+
disabled: disabledCount,
|
|
8334
8338
|
neverSynthesized: neverSynthesizedCount,
|
|
8335
8339
|
tokens: {
|
|
8336
8340
|
architect: totalArchitectTokens,
|
|
@@ -8808,6 +8812,7 @@ async function listMetas(config, watcher) {
|
|
|
8808
8812
|
const emphasis = meta._emphasis ?? 1;
|
|
8809
8813
|
const hasError = Boolean(meta._error);
|
|
8810
8814
|
const locked = isLocked(normalizePath(node.metaPath));
|
|
8815
|
+
const disabled = meta._disabled === true;
|
|
8811
8816
|
const neverSynth = !meta._generatedAt;
|
|
8812
8817
|
// Compute staleness
|
|
8813
8818
|
let stalenessSeconds;
|
|
@@ -8830,6 +8835,7 @@ async function listMetas(config, watcher) {
|
|
|
8830
8835
|
lastSynthesized: meta._generatedAt ?? null,
|
|
8831
8836
|
hasError,
|
|
8832
8837
|
locked,
|
|
8838
|
+
disabled,
|
|
8833
8839
|
architectTokens: archTokens > 0 ? archTokens : null,
|
|
8834
8840
|
builderTokens: buildTokens > 0 ? buildTokens : null,
|
|
8835
8841
|
criticTokens: critTokens > 0 ? critTokens : null,
|
|
@@ -9036,6 +9042,10 @@ class GatewayExecutor {
|
|
|
9036
9042
|
return undefined;
|
|
9037
9043
|
}
|
|
9038
9044
|
}
|
|
9045
|
+
/** Whether this executor has been aborted by the operator. */
|
|
9046
|
+
get aborted() {
|
|
9047
|
+
return this.controller.signal.aborted;
|
|
9048
|
+
}
|
|
9039
9049
|
/** Abort the currently running spawn, if any. */
|
|
9040
9050
|
abort() {
|
|
9041
9051
|
this.controller.abort();
|
|
@@ -9509,6 +9519,22 @@ const metaErrorSchema = z.object({
|
|
|
9509
9519
|
*
|
|
9510
9520
|
* @module schema/meta
|
|
9511
9521
|
*/
|
|
9522
|
+
/** Valid states for a synthesis phase. */
|
|
9523
|
+
const phaseStatuses = [
|
|
9524
|
+
'fresh',
|
|
9525
|
+
'stale',
|
|
9526
|
+
'pending',
|
|
9527
|
+
'running',
|
|
9528
|
+
'failed',
|
|
9529
|
+
];
|
|
9530
|
+
/** Zod schema for a per-phase status value. */
|
|
9531
|
+
const phaseStatusSchema = z.enum(phaseStatuses);
|
|
9532
|
+
/** Zod schema for the per-meta phase state record. */
|
|
9533
|
+
const phaseStateSchema = z.object({
|
|
9534
|
+
architect: phaseStatusSchema,
|
|
9535
|
+
builder: phaseStatusSchema,
|
|
9536
|
+
critic: phaseStatusSchema,
|
|
9537
|
+
});
|
|
9512
9538
|
/** Zod schema for the reserved (underscore-prefixed) meta.json properties. */
|
|
9513
9539
|
const metaJsonSchema = z
|
|
9514
9540
|
.object({
|
|
@@ -9587,6 +9613,14 @@ const metaJsonSchema = z
|
|
|
9587
9613
|
* Cleared on successful cycle.
|
|
9588
9614
|
*/
|
|
9589
9615
|
_error: metaErrorSchema.optional(),
|
|
9616
|
+
/** When true, this meta is skipped during staleness scheduling. Manual trigger still works. */
|
|
9617
|
+
_disabled: z.boolean().optional(),
|
|
9618
|
+
/**
|
|
9619
|
+
* Per-phase state machine record. Engine-managed.
|
|
9620
|
+
* Keyed by phase name (architect, builder, critic) with status values.
|
|
9621
|
+
* Persisted to survive ticks; derived on first load for back-compat.
|
|
9622
|
+
*/
|
|
9623
|
+
_phaseState: phaseStateSchema.optional(),
|
|
9590
9624
|
})
|
|
9591
9625
|
.loose();
|
|
9592
9626
|
|
|
@@ -9645,6 +9679,8 @@ async function mergeAndWrite(options) {
|
|
|
9645
9679
|
_state: options.state,
|
|
9646
9680
|
// Error handling
|
|
9647
9681
|
_error: options.error ?? undefined,
|
|
9682
|
+
// Phase state machine
|
|
9683
|
+
_phaseState: options.phaseState,
|
|
9648
9684
|
// Spread structured fields from builder
|
|
9649
9685
|
...options.builderOutput?.fields,
|
|
9650
9686
|
};
|
|
@@ -9675,6 +9711,8 @@ async function mergeAndWrite(options) {
|
|
|
9675
9711
|
delete merged._content;
|
|
9676
9712
|
if (merged._feedback === undefined)
|
|
9677
9713
|
delete merged._feedback;
|
|
9714
|
+
if (merged._phaseState === undefined)
|
|
9715
|
+
delete merged._phaseState;
|
|
9678
9716
|
// Validate
|
|
9679
9717
|
const result = metaJsonSchema.safeParse(merged);
|
|
9680
9718
|
if (!result.success) {
|
|
@@ -10370,6 +10408,686 @@ async function orchestrate(config, executor, watcher, targetPath, onProgress, lo
|
|
|
10370
10408
|
return [result];
|
|
10371
10409
|
}
|
|
10372
10410
|
|
|
10411
|
+
/**
|
|
10412
|
+
* Pure phase-state transition functions.
|
|
10413
|
+
*
|
|
10414
|
+
* Implements every row of the §8 "Transitions and invalidation cascade" table.
|
|
10415
|
+
* No I/O — pure functions over PhaseState and documented inputs.
|
|
10416
|
+
*
|
|
10417
|
+
* @module phaseState/phaseTransitions
|
|
10418
|
+
*/
|
|
10419
|
+
/**
|
|
10420
|
+
* Create a fresh (fully-complete) phase state.
|
|
10421
|
+
*/
|
|
10422
|
+
function freshPhaseState() {
|
|
10423
|
+
return { architect: 'fresh', builder: 'fresh', critic: 'fresh' };
|
|
10424
|
+
}
|
|
10425
|
+
/**
|
|
10426
|
+
* Create a phase state for a never-synthesized meta (all pending from architect).
|
|
10427
|
+
*/
|
|
10428
|
+
function initialPhaseState() {
|
|
10429
|
+
return { architect: 'pending', builder: 'stale', critic: 'stale' };
|
|
10430
|
+
}
|
|
10431
|
+
/**
|
|
10432
|
+
* Enforce the per-meta invariant: at most one phase is pending or running,
|
|
10433
|
+
* and it is the first non-fresh phase in pipeline order.
|
|
10434
|
+
*
|
|
10435
|
+
* Stale phases that become the first non-fresh phase are promoted to pending.
|
|
10436
|
+
*/
|
|
10437
|
+
function enforceInvariant(state) {
|
|
10438
|
+
const result = { ...state };
|
|
10439
|
+
let foundNonFresh = false;
|
|
10440
|
+
for (const phase of ['architect', 'builder', 'critic']) {
|
|
10441
|
+
const s = result[phase];
|
|
10442
|
+
if (s === 'fresh')
|
|
10443
|
+
continue;
|
|
10444
|
+
if (!foundNonFresh) {
|
|
10445
|
+
foundNonFresh = true;
|
|
10446
|
+
// First non-fresh: if stale, promote to pending
|
|
10447
|
+
if (s === 'stale') {
|
|
10448
|
+
result[phase] = 'pending';
|
|
10449
|
+
}
|
|
10450
|
+
// pending, running, failed stay as-is
|
|
10451
|
+
}
|
|
10452
|
+
else {
|
|
10453
|
+
// Subsequent non-fresh: must not be pending or running
|
|
10454
|
+
if (s === 'pending') {
|
|
10455
|
+
result[phase] = 'stale';
|
|
10456
|
+
}
|
|
10457
|
+
// running in non-first position would be a bug, but don't mask it
|
|
10458
|
+
}
|
|
10459
|
+
}
|
|
10460
|
+
return result;
|
|
10461
|
+
}
|
|
10462
|
+
// ── Phase success transitions ──────────────────────────────────────────
|
|
10463
|
+
/**
|
|
10464
|
+
* Architect completes successfully.
|
|
10465
|
+
* architect → fresh; builder → pending; critic → stale.
|
|
10466
|
+
*/
|
|
10467
|
+
function architectSuccess(state) {
|
|
10468
|
+
return enforceInvariant({
|
|
10469
|
+
architect: 'fresh',
|
|
10470
|
+
builder: state.builder === 'failed' ? 'failed' : 'pending',
|
|
10471
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
10472
|
+
});
|
|
10473
|
+
}
|
|
10474
|
+
/**
|
|
10475
|
+
* Builder completes successfully.
|
|
10476
|
+
* builder → fresh; critic → pending.
|
|
10477
|
+
*/
|
|
10478
|
+
function builderSuccess(state) {
|
|
10479
|
+
return enforceInvariant({
|
|
10480
|
+
...state,
|
|
10481
|
+
builder: 'fresh',
|
|
10482
|
+
critic: state.critic === 'failed' ? 'failed' : 'pending',
|
|
10483
|
+
});
|
|
10484
|
+
}
|
|
10485
|
+
/**
|
|
10486
|
+
* Critic completes successfully.
|
|
10487
|
+
* critic → fresh. Meta becomes fully fresh.
|
|
10488
|
+
*/
|
|
10489
|
+
function criticSuccess(state) {
|
|
10490
|
+
return enforceInvariant({
|
|
10491
|
+
...state,
|
|
10492
|
+
critic: 'fresh',
|
|
10493
|
+
});
|
|
10494
|
+
}
|
|
10495
|
+
// ── Failure transition ─────────────────────────────────────────────────
|
|
10496
|
+
/**
|
|
10497
|
+
* A phase fails (error, timeout, or abort).
|
|
10498
|
+
* Target phase → failed; upstream and downstream unchanged.
|
|
10499
|
+
*/
|
|
10500
|
+
function phaseFailed(state, phase) {
|
|
10501
|
+
return enforceInvariant({
|
|
10502
|
+
...state,
|
|
10503
|
+
[phase]: 'failed',
|
|
10504
|
+
});
|
|
10505
|
+
}
|
|
10506
|
+
// ── Surgical retry ─────────────────────────────────────────────────────
|
|
10507
|
+
/**
|
|
10508
|
+
* Retry a failed phase: failed → pending.
|
|
10509
|
+
* Only valid when the phase is currently failed.
|
|
10510
|
+
*/
|
|
10511
|
+
function retryPhase(state, phase) {
|
|
10512
|
+
if (state[phase] !== 'failed')
|
|
10513
|
+
return state;
|
|
10514
|
+
return enforceInvariant({
|
|
10515
|
+
...state,
|
|
10516
|
+
[phase]: 'pending',
|
|
10517
|
+
});
|
|
10518
|
+
}
|
|
10519
|
+
/**
|
|
10520
|
+
* Retry all failed phases: each failed phase → pending.
|
|
10521
|
+
* Used by scheduler ticks and queue reads to auto-promote failed phases.
|
|
10522
|
+
*/
|
|
10523
|
+
function retryAllFailed(state) {
|
|
10524
|
+
let result = state;
|
|
10525
|
+
for (const phase of ['architect', 'builder', 'critic']) {
|
|
10526
|
+
if (result[phase] === 'failed') {
|
|
10527
|
+
result = retryPhase(result, phase);
|
|
10528
|
+
}
|
|
10529
|
+
}
|
|
10530
|
+
return result;
|
|
10531
|
+
}
|
|
10532
|
+
// ── Running transition ─────────────────────────────────────────────────
|
|
10533
|
+
/**
|
|
10534
|
+
* Mark a phase as running (scheduler picks it).
|
|
10535
|
+
*/
|
|
10536
|
+
function phaseRunning(state, phase) {
|
|
10537
|
+
return {
|
|
10538
|
+
...state,
|
|
10539
|
+
[phase]: 'running',
|
|
10540
|
+
};
|
|
10541
|
+
}
|
|
10542
|
+
// ── Query helpers ──────────────────────────────────────────────────────
|
|
10543
|
+
/**
|
|
10544
|
+
* Get the owed phase: first non-fresh phase in pipeline order, or null.
|
|
10545
|
+
*/
|
|
10546
|
+
function getOwedPhase(state) {
|
|
10547
|
+
for (const phase of ['architect', 'builder', 'critic']) {
|
|
10548
|
+
if (state[phase] !== 'fresh')
|
|
10549
|
+
return phase;
|
|
10550
|
+
}
|
|
10551
|
+
return null;
|
|
10552
|
+
}
|
|
10553
|
+
/**
|
|
10554
|
+
* Check if a meta is fully fresh (all phases fresh).
|
|
10555
|
+
*/
|
|
10556
|
+
function isFullyFresh(state) {
|
|
10557
|
+
return (state.architect === 'fresh' &&
|
|
10558
|
+
state.builder === 'fresh' &&
|
|
10559
|
+
state.critic === 'fresh');
|
|
10560
|
+
}
|
|
10561
|
+
/**
|
|
10562
|
+
* Get the scheduler priority band for a meta's owed phase.
|
|
10563
|
+
* 1 = critic (highest), 2 = builder, 3 = architect, null = fully fresh.
|
|
10564
|
+
*/
|
|
10565
|
+
function getPriorityBand(state) {
|
|
10566
|
+
const owed = getOwedPhase(state);
|
|
10567
|
+
if (!owed)
|
|
10568
|
+
return null;
|
|
10569
|
+
if (owed === 'critic')
|
|
10570
|
+
return 1;
|
|
10571
|
+
if (owed === 'builder')
|
|
10572
|
+
return 2;
|
|
10573
|
+
return 3;
|
|
10574
|
+
}
|
|
10575
|
+
|
|
10576
|
+
/**
|
|
10577
|
+
* Backward-compatible derivation of _phaseState from existing meta fields.
|
|
10578
|
+
*
|
|
10579
|
+
* When a meta is loaded from disk without _phaseState, this reconstructs
|
|
10580
|
+
* the phase state from _content, _builder, _state, _error.step, and
|
|
10581
|
+
* the architect-invalidating inputs.
|
|
10582
|
+
*
|
|
10583
|
+
* @module phaseState/derivePhaseState
|
|
10584
|
+
*/
|
|
10585
|
+
/**
|
|
10586
|
+
* Derive _phaseState from existing meta fields.
|
|
10587
|
+
*
|
|
10588
|
+
* If the meta already has _phaseState, returns it as-is.
|
|
10589
|
+
*
|
|
10590
|
+
* Otherwise, reconstructs from available fields:
|
|
10591
|
+
* - Never-synthesized meta (no _content, no _builder): all phases start pending/stale.
|
|
10592
|
+
* - Errored meta: the failed phase is mapped from _error.step.
|
|
10593
|
+
* - Mid-cycle meta with cached _builder but no _content: builder pending.
|
|
10594
|
+
* - Fully-fresh meta: all phases fresh.
|
|
10595
|
+
* - Meta with stale architect inputs: architect pending, downstream stale.
|
|
10596
|
+
*
|
|
10597
|
+
* @param meta - The meta.json content.
|
|
10598
|
+
* @param inputs - Optional derivation inputs. If not provided, a simpler
|
|
10599
|
+
* heuristic is used (no architect invalidation check).
|
|
10600
|
+
* @returns The derived PhaseState.
|
|
10601
|
+
*/
|
|
10602
|
+
function derivePhaseState(meta, inputs) {
|
|
10603
|
+
// Already has _phaseState — use it
|
|
10604
|
+
if (meta._phaseState)
|
|
10605
|
+
return meta._phaseState;
|
|
10606
|
+
// Check for errors first — _error.step maps directly to failed phase
|
|
10607
|
+
if (meta._error) {
|
|
10608
|
+
const failedPhase = meta._error.step;
|
|
10609
|
+
const state = freshPhaseState();
|
|
10610
|
+
state[failedPhase] = 'failed';
|
|
10611
|
+
// If architect failed and no _builder, downstream is stale
|
|
10612
|
+
if (failedPhase === 'architect') {
|
|
10613
|
+
if (!meta._builder) {
|
|
10614
|
+
state.builder = 'stale';
|
|
10615
|
+
state.critic = 'stale';
|
|
10616
|
+
}
|
|
10617
|
+
}
|
|
10618
|
+
// If builder failed, critic is stale
|
|
10619
|
+
if (failedPhase === 'builder') {
|
|
10620
|
+
state.critic = 'stale';
|
|
10621
|
+
}
|
|
10622
|
+
return state;
|
|
10623
|
+
}
|
|
10624
|
+
// Never synthesized: no _content AND no _builder (and no error)
|
|
10625
|
+
if (!meta._content && !meta._builder) {
|
|
10626
|
+
return initialPhaseState();
|
|
10627
|
+
}
|
|
10628
|
+
// Check architect invalidation (when inputs are provided)
|
|
10629
|
+
if (inputs) {
|
|
10630
|
+
const architectInvalidated = inputs.structureChanged ||
|
|
10631
|
+
inputs.steerChanged ||
|
|
10632
|
+
inputs.architectChanged ||
|
|
10633
|
+
inputs.crossRefsChanged ||
|
|
10634
|
+
(meta._synthesisCount ?? 0) >= inputs.architectEvery;
|
|
10635
|
+
if (architectInvalidated) {
|
|
10636
|
+
return {
|
|
10637
|
+
architect: 'pending',
|
|
10638
|
+
builder: 'stale',
|
|
10639
|
+
critic: 'stale',
|
|
10640
|
+
};
|
|
10641
|
+
}
|
|
10642
|
+
}
|
|
10643
|
+
// Has _builder but no _content: builder is pending
|
|
10644
|
+
if (meta._builder && !meta._content) {
|
|
10645
|
+
return {
|
|
10646
|
+
architect: 'fresh',
|
|
10647
|
+
builder: 'pending',
|
|
10648
|
+
critic: 'stale',
|
|
10649
|
+
};
|
|
10650
|
+
}
|
|
10651
|
+
// Has _content but no _feedback: critic is pending
|
|
10652
|
+
if (meta._content && !meta._feedback) {
|
|
10653
|
+
return {
|
|
10654
|
+
architect: 'fresh',
|
|
10655
|
+
builder: 'fresh',
|
|
10656
|
+
critic: 'pending',
|
|
10657
|
+
};
|
|
10658
|
+
}
|
|
10659
|
+
// Default: fully fresh
|
|
10660
|
+
return freshPhaseState();
|
|
10661
|
+
}
|
|
10662
|
+
|
|
10663
|
+
/**
|
|
10664
|
+
* Corpus-wide phase scheduler.
|
|
10665
|
+
*
|
|
10666
|
+
* Selects the highest-priority ready phase across all metas.
|
|
10667
|
+
* Priority: critic (band 1) \> builder (band 2) \> architect (band 3).
|
|
10668
|
+
* Tiebreak within band: weighted staleness (§3.9).
|
|
10669
|
+
*
|
|
10670
|
+
* @module phaseState/phaseScheduler
|
|
10671
|
+
*/
|
|
10672
|
+
/**
|
|
10673
|
+
* Build phase candidates from listMetas entries.
|
|
10674
|
+
*
|
|
10675
|
+
* Derives phase state and auto-retries failed phases for each entry.
|
|
10676
|
+
* Used by orchestratePhase, queue route, and status route.
|
|
10677
|
+
*/
|
|
10678
|
+
function buildPhaseCandidates(entries) {
|
|
10679
|
+
return entries.map((entry) => ({
|
|
10680
|
+
node: entry.node,
|
|
10681
|
+
meta: entry.meta,
|
|
10682
|
+
phaseState: retryAllFailed(derivePhaseState(entry.meta)),
|
|
10683
|
+
actualStaleness: entry.stalenessSeconds,
|
|
10684
|
+
locked: entry.locked,
|
|
10685
|
+
disabled: entry.disabled,
|
|
10686
|
+
}));
|
|
10687
|
+
}
|
|
10688
|
+
/**
|
|
10689
|
+
* Rank all eligible phase candidates by priority.
|
|
10690
|
+
*
|
|
10691
|
+
* Filters to pending phases, computes effective staleness, and sorts by
|
|
10692
|
+
* band (ascending: critic first) then effective staleness (descending).
|
|
10693
|
+
*
|
|
10694
|
+
* Used by selectPhaseCandidate (returns first) and the queue route (returns all).
|
|
10695
|
+
*/
|
|
10696
|
+
function rankPhaseCandidates(metas, depthWeight) {
|
|
10697
|
+
// Filter to metas with a pending (scheduler-eligible) phase
|
|
10698
|
+
const eligible = metas.filter((m) => {
|
|
10699
|
+
if (m.locked)
|
|
10700
|
+
return false;
|
|
10701
|
+
if (m.disabled && !m.isOverride)
|
|
10702
|
+
return false;
|
|
10703
|
+
const owed = getOwedPhase(m.phaseState);
|
|
10704
|
+
if (!owed)
|
|
10705
|
+
return false;
|
|
10706
|
+
return m.phaseState[owed] === 'pending';
|
|
10707
|
+
});
|
|
10708
|
+
if (eligible.length === 0)
|
|
10709
|
+
return [];
|
|
10710
|
+
// Compute effective staleness for tiebreaking
|
|
10711
|
+
const withStaleness = computeEffectiveStaleness(eligible.map((m) => ({
|
|
10712
|
+
node: m.node,
|
|
10713
|
+
meta: m.meta,
|
|
10714
|
+
actualStaleness: m.actualStaleness,
|
|
10715
|
+
})), depthWeight);
|
|
10716
|
+
// Build candidates with band info
|
|
10717
|
+
const candidates = withStaleness.map((ws, i) => {
|
|
10718
|
+
const m = eligible[i];
|
|
10719
|
+
const owedPhase = getOwedPhase(m.phaseState);
|
|
10720
|
+
return {
|
|
10721
|
+
node: ws.node,
|
|
10722
|
+
meta: ws.meta,
|
|
10723
|
+
phaseState: m.phaseState,
|
|
10724
|
+
owedPhase,
|
|
10725
|
+
band: getPriorityBand(m.phaseState),
|
|
10726
|
+
actualStaleness: ws.actualStaleness,
|
|
10727
|
+
effectiveStaleness: ws.effectiveStaleness,
|
|
10728
|
+
};
|
|
10729
|
+
});
|
|
10730
|
+
// Sort by band (ascending = critic first) then effective staleness (descending)
|
|
10731
|
+
candidates.sort((a, b) => {
|
|
10732
|
+
if (a.band !== b.band)
|
|
10733
|
+
return a.band - b.band;
|
|
10734
|
+
return b.effectiveStaleness - a.effectiveStaleness;
|
|
10735
|
+
});
|
|
10736
|
+
return candidates;
|
|
10737
|
+
}
|
|
10738
|
+
/**
|
|
10739
|
+
* Select the best phase candidate across the corpus.
|
|
10740
|
+
*
|
|
10741
|
+
* @param metas - Array of (node, meta, phaseState, stalenessSeconds) tuples.
|
|
10742
|
+
* @param depthWeight - Config depthWeight for staleness tiebreak.
|
|
10743
|
+
* @returns The winning candidate, or null if no phase is ready.
|
|
10744
|
+
*/
|
|
10745
|
+
function selectPhaseCandidate(metas, depthWeight) {
|
|
10746
|
+
return rankPhaseCandidates(metas, depthWeight)[0] ?? null;
|
|
10747
|
+
}
|
|
10748
|
+
|
|
10749
|
+
/**
|
|
10750
|
+
* Per-phase executors for the phase-state machine.
|
|
10751
|
+
*
|
|
10752
|
+
* Each function runs exactly one phase on one meta, updates _phaseState
|
|
10753
|
+
* via pure transitions, and persists via the lock-staged write.
|
|
10754
|
+
*
|
|
10755
|
+
* @module orchestrator/runPhase
|
|
10756
|
+
*/
|
|
10757
|
+
/** Write updated meta with phase state via lock staging. */
|
|
10758
|
+
async function persistPhaseState(base, phaseState, updates) {
|
|
10759
|
+
const lockPath = join(base.metaPath, '.lock');
|
|
10760
|
+
const metaJsonPath = join(base.metaPath, 'meta.json');
|
|
10761
|
+
const merged = {
|
|
10762
|
+
...base.current,
|
|
10763
|
+
...updates,
|
|
10764
|
+
_phaseState: phaseState,
|
|
10765
|
+
_structureHash: base.structureHash,
|
|
10766
|
+
};
|
|
10767
|
+
// Clean undefined
|
|
10768
|
+
if (merged._error === undefined)
|
|
10769
|
+
delete merged._error;
|
|
10770
|
+
await writeFile(lockPath, JSON.stringify(merged, null, 2) + '\n');
|
|
10771
|
+
await copyFile(lockPath, metaJsonPath);
|
|
10772
|
+
return merged;
|
|
10773
|
+
}
|
|
10774
|
+
/**
|
|
10775
|
+
* Handle phase failure (abort or error).
|
|
10776
|
+
*
|
|
10777
|
+
* Shared error path for all three phase executors. When the executor was
|
|
10778
|
+
* aborted, returns immediately without persisting (abort route handles it).
|
|
10779
|
+
* Otherwise, transitions the phase to failed and persists the error.
|
|
10780
|
+
*/
|
|
10781
|
+
async function handlePhaseFailure(phase, err, executor, ps, base, additionalUpdates) {
|
|
10782
|
+
if (executor.aborted) {
|
|
10783
|
+
return {
|
|
10784
|
+
executed: true,
|
|
10785
|
+
phaseState: phaseFailed(ps, phase),
|
|
10786
|
+
error: { step: phase, code: 'ABORT', message: 'Aborted by operator' },
|
|
10787
|
+
};
|
|
10788
|
+
}
|
|
10789
|
+
const error = toMetaError(phase, err);
|
|
10790
|
+
const failedPs = phaseFailed(ps, phase);
|
|
10791
|
+
await persistPhaseState(base, failedPs, {
|
|
10792
|
+
_error: error,
|
|
10793
|
+
...additionalUpdates,
|
|
10794
|
+
});
|
|
10795
|
+
return { executed: true, phaseState: failedPs, error };
|
|
10796
|
+
}
|
|
10797
|
+
// ── Architect executor ─────────────────────────────────────────────────
|
|
10798
|
+
async function runArchitect(node, currentMeta, phaseState, config, executor, watcher, structureHash, onProgress, logger) {
|
|
10799
|
+
let ps = phaseRunning(phaseState, 'architect');
|
|
10800
|
+
const ctx = await buildContextPackage(node, currentMeta, watcher, logger);
|
|
10801
|
+
try {
|
|
10802
|
+
await onProgress?.({
|
|
10803
|
+
type: 'phase_start',
|
|
10804
|
+
path: node.ownerPath,
|
|
10805
|
+
phase: 'architect',
|
|
10806
|
+
});
|
|
10807
|
+
const phaseStart = Date.now();
|
|
10808
|
+
const architectTask = buildArchitectTask(ctx, currentMeta, config);
|
|
10809
|
+
const result = await executor.spawn(architectTask, {
|
|
10810
|
+
thinking: config.thinking,
|
|
10811
|
+
timeout: config.architectTimeout,
|
|
10812
|
+
label: 'meta-architect',
|
|
10813
|
+
});
|
|
10814
|
+
const builderBrief = parseArchitectOutput(result.output);
|
|
10815
|
+
const architectTokens = result.tokens;
|
|
10816
|
+
// Architect success: architect → fresh, _synthesisCount → 0
|
|
10817
|
+
ps = architectSuccess(ps);
|
|
10818
|
+
const updatedMeta = await persistPhaseState({ metaPath: node.metaPath, current: currentMeta, config, structureHash }, ps, {
|
|
10819
|
+
_builder: builderBrief,
|
|
10820
|
+
_architect: currentMeta._architect ?? config.defaultArchitect ?? '',
|
|
10821
|
+
_synthesisCount: 0,
|
|
10822
|
+
_architectTokens: architectTokens,
|
|
10823
|
+
_generatedAt: new Date().toISOString(),
|
|
10824
|
+
_error: undefined,
|
|
10825
|
+
});
|
|
10826
|
+
await onProgress?.({
|
|
10827
|
+
type: 'phase_complete',
|
|
10828
|
+
path: node.ownerPath,
|
|
10829
|
+
phase: 'architect',
|
|
10830
|
+
tokens: architectTokens,
|
|
10831
|
+
durationMs: Date.now() - phaseStart,
|
|
10832
|
+
});
|
|
10833
|
+
return { executed: true, phaseState: ps, updatedMeta };
|
|
10834
|
+
}
|
|
10835
|
+
catch (err) {
|
|
10836
|
+
return handlePhaseFailure('architect', err, executor, ps, {
|
|
10837
|
+
metaPath: node.metaPath,
|
|
10838
|
+
current: currentMeta,
|
|
10839
|
+
structureHash,
|
|
10840
|
+
});
|
|
10841
|
+
}
|
|
10842
|
+
}
|
|
10843
|
+
// ── Builder executor ───────────────────────────────────────────────────
|
|
10844
|
+
async function runBuilder(node, currentMeta, phaseState, config, executor, watcher, structureHash, onProgress, logger) {
|
|
10845
|
+
let ps = phaseRunning(phaseState, 'builder');
|
|
10846
|
+
const ctx = await buildContextPackage(node, currentMeta, watcher, logger);
|
|
10847
|
+
try {
|
|
10848
|
+
await onProgress?.({
|
|
10849
|
+
type: 'phase_start',
|
|
10850
|
+
path: node.ownerPath,
|
|
10851
|
+
phase: 'builder',
|
|
10852
|
+
});
|
|
10853
|
+
const builderStart = Date.now();
|
|
10854
|
+
const builderTask = buildBuilderTask(ctx, currentMeta, config);
|
|
10855
|
+
const result = await executor.spawn(builderTask, {
|
|
10856
|
+
thinking: config.thinking,
|
|
10857
|
+
timeout: config.builderTimeout,
|
|
10858
|
+
label: 'meta-builder',
|
|
10859
|
+
});
|
|
10860
|
+
const builderOutput = parseBuilderOutput(result.output);
|
|
10861
|
+
const builderTokens = result.tokens;
|
|
10862
|
+
// Builder success: builder → fresh, critic → pending
|
|
10863
|
+
ps = builderSuccess(ps);
|
|
10864
|
+
const updatedMeta = await persistPhaseState({ metaPath: node.metaPath, current: currentMeta, config, structureHash }, ps, {
|
|
10865
|
+
_content: builderOutput.content,
|
|
10866
|
+
_state: builderOutput.state,
|
|
10867
|
+
_builderTokens: builderTokens,
|
|
10868
|
+
_generatedAt: new Date().toISOString(),
|
|
10869
|
+
_error: undefined,
|
|
10870
|
+
...builderOutput.fields,
|
|
10871
|
+
});
|
|
10872
|
+
await onProgress?.({
|
|
10873
|
+
type: 'phase_complete',
|
|
10874
|
+
path: node.ownerPath,
|
|
10875
|
+
phase: 'builder',
|
|
10876
|
+
tokens: builderTokens,
|
|
10877
|
+
durationMs: Date.now() - builderStart,
|
|
10878
|
+
});
|
|
10879
|
+
return { executed: true, phaseState: ps, updatedMeta };
|
|
10880
|
+
}
|
|
10881
|
+
catch (err) {
|
|
10882
|
+
// §4.6 partial _state recovery on timeout
|
|
10883
|
+
let partialState;
|
|
10884
|
+
if (err instanceof SpawnTimeoutError) {
|
|
10885
|
+
try {
|
|
10886
|
+
const raw = await readFile(err.outputPath, 'utf8');
|
|
10887
|
+
const partial = parseBuilderOutput(raw);
|
|
10888
|
+
if (partial.state !== undefined &&
|
|
10889
|
+
JSON.stringify(partial.state) !== JSON.stringify(currentMeta._state)) {
|
|
10890
|
+
partialState = { _state: partial.state };
|
|
10891
|
+
}
|
|
10892
|
+
}
|
|
10893
|
+
catch {
|
|
10894
|
+
// Could not read partial output — no state recovery
|
|
10895
|
+
}
|
|
10896
|
+
}
|
|
10897
|
+
return handlePhaseFailure('builder', err, executor, ps, {
|
|
10898
|
+
metaPath: node.metaPath,
|
|
10899
|
+
current: currentMeta,
|
|
10900
|
+
structureHash,
|
|
10901
|
+
}, partialState);
|
|
10902
|
+
}
|
|
10903
|
+
}
|
|
10904
|
+
// ── Critic executor ────────────────────────────────────────────────────
|
|
10905
|
+
async function runCritic(node, currentMeta, phaseState, config, executor, watcher, structureHash, onProgress, logger) {
|
|
10906
|
+
let ps = phaseRunning(phaseState, 'critic');
|
|
10907
|
+
const ctx = await buildContextPackage(node, currentMeta, watcher, logger);
|
|
10908
|
+
// Build critic task using current meta's _content
|
|
10909
|
+
const metaForCritic = { ...currentMeta };
|
|
10910
|
+
try {
|
|
10911
|
+
await onProgress?.({
|
|
10912
|
+
type: 'phase_start',
|
|
10913
|
+
path: node.ownerPath,
|
|
10914
|
+
phase: 'critic',
|
|
10915
|
+
});
|
|
10916
|
+
const criticStart = Date.now();
|
|
10917
|
+
const criticTask = buildCriticTask(ctx, metaForCritic, config);
|
|
10918
|
+
const result = await executor.spawn(criticTask, {
|
|
10919
|
+
thinking: config.thinking,
|
|
10920
|
+
timeout: config.criticTimeout,
|
|
10921
|
+
label: 'meta-critic',
|
|
10922
|
+
});
|
|
10923
|
+
const feedback = parseCriticOutput(result.output);
|
|
10924
|
+
const criticTokens = result.tokens;
|
|
10925
|
+
// Critic success: critic → fresh
|
|
10926
|
+
ps = criticSuccess(ps);
|
|
10927
|
+
const cycleComplete = isFullyFresh(ps);
|
|
10928
|
+
const updates = {
|
|
10929
|
+
_feedback: feedback,
|
|
10930
|
+
_criticTokens: criticTokens,
|
|
10931
|
+
_error: undefined,
|
|
10932
|
+
};
|
|
10933
|
+
// Full-cycle completion: increment _synthesisCount, archive, emit.
|
|
10934
|
+
// Per spec: architect resets to 0, full-cycle increments on top.
|
|
10935
|
+
if (cycleComplete) {
|
|
10936
|
+
updates._synthesisCount = (currentMeta._synthesisCount ?? 0) + 1;
|
|
10937
|
+
}
|
|
10938
|
+
const updatedMeta = await persistPhaseState({ metaPath: node.metaPath, current: currentMeta, config, structureHash }, ps, updates);
|
|
10939
|
+
// Archive on full-cycle only
|
|
10940
|
+
if (cycleComplete) {
|
|
10941
|
+
await createSnapshot(node.metaPath, updatedMeta);
|
|
10942
|
+
await pruneArchive(node.metaPath, config.maxArchive);
|
|
10943
|
+
}
|
|
10944
|
+
await onProgress?.({
|
|
10945
|
+
type: 'phase_complete',
|
|
10946
|
+
path: node.ownerPath,
|
|
10947
|
+
phase: 'critic',
|
|
10948
|
+
tokens: criticTokens,
|
|
10949
|
+
durationMs: Date.now() - criticStart,
|
|
10950
|
+
});
|
|
10951
|
+
return {
|
|
10952
|
+
executed: true,
|
|
10953
|
+
phaseState: ps,
|
|
10954
|
+
updatedMeta,
|
|
10955
|
+
cycleComplete,
|
|
10956
|
+
};
|
|
10957
|
+
}
|
|
10958
|
+
catch (err) {
|
|
10959
|
+
return handlePhaseFailure('critic', err, executor, ps, {
|
|
10960
|
+
metaPath: node.metaPath,
|
|
10961
|
+
current: currentMeta,
|
|
10962
|
+
structureHash,
|
|
10963
|
+
});
|
|
10964
|
+
}
|
|
10965
|
+
}
|
|
10966
|
+
|
|
10967
|
+
/**
|
|
10968
|
+
* Phase-aware orchestration entry point.
|
|
10969
|
+
*
|
|
10970
|
+
* Replaces the old staleness-based orchestrate() with phase-state-machine
|
|
10971
|
+
* scheduling: each tick discovers all metas, computes invalidation,
|
|
10972
|
+
* auto-retries failed phases, selects the best phase candidate, and
|
|
10973
|
+
* executes exactly one phase.
|
|
10974
|
+
*
|
|
10975
|
+
* @module orchestrator/orchestratePhase
|
|
10976
|
+
*/
|
|
10977
|
+
/** Phase runner dispatch map — avoids repeating the same switch/case. */
|
|
10978
|
+
const phaseRunners = {
|
|
10979
|
+
architect: runArchitect,
|
|
10980
|
+
builder: runBuilder,
|
|
10981
|
+
critic: runCritic,
|
|
10982
|
+
};
|
|
10983
|
+
/**
|
|
10984
|
+
* Run a single phase-aware orchestration tick.
|
|
10985
|
+
*
|
|
10986
|
+
* When targetPath is provided (override entry), runs the owed phase for
|
|
10987
|
+
* that specific meta. Otherwise, discovers all metas, computes invalidation,
|
|
10988
|
+
* and selects the best phase candidate corpus-wide.
|
|
10989
|
+
*/
|
|
10990
|
+
async function orchestratePhase(config, executor, watcher, targetPath, onProgress, logger) {
|
|
10991
|
+
// ── Targeted path (override entry) ──
|
|
10992
|
+
if (targetPath) {
|
|
10993
|
+
return orchestrateTargeted(config, executor, watcher, targetPath, onProgress, logger);
|
|
10994
|
+
}
|
|
10995
|
+
// ── Corpus-wide discovery + phase selection ──
|
|
10996
|
+
let metaResult;
|
|
10997
|
+
try {
|
|
10998
|
+
metaResult = await listMetas(config, watcher);
|
|
10999
|
+
}
|
|
11000
|
+
catch (err) {
|
|
11001
|
+
logger?.warn({ err }, 'Failed to list metas for phase selection');
|
|
11002
|
+
return { executed: false };
|
|
11003
|
+
}
|
|
11004
|
+
if (metaResult.entries.length === 0)
|
|
11005
|
+
return { executed: false };
|
|
11006
|
+
// Build candidates with phase state (including invalidation + auto-retry)
|
|
11007
|
+
const candidates = buildPhaseCandidates(metaResult.entries);
|
|
11008
|
+
// Select best phase candidate
|
|
11009
|
+
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
11010
|
+
if (!winner) {
|
|
11011
|
+
return { executed: false };
|
|
11012
|
+
}
|
|
11013
|
+
// Acquire lock
|
|
11014
|
+
if (!acquireLock(winner.node.metaPath)) {
|
|
11015
|
+
logger?.debug({ path: winner.node.metaPath }, 'Selected candidate is locked, skipping');
|
|
11016
|
+
return { executed: false };
|
|
11017
|
+
}
|
|
11018
|
+
try {
|
|
11019
|
+
// Re-read meta under lock for freshness
|
|
11020
|
+
const currentMeta = await readMetaJson(winner.node.metaPath);
|
|
11021
|
+
const phaseState = retryAllFailed(derivePhaseState(currentMeta));
|
|
11022
|
+
const owedPhase = getOwedPhase(phaseState);
|
|
11023
|
+
if (!owedPhase || phaseState[owedPhase] !== 'pending') {
|
|
11024
|
+
// Nothing to do (race: became fresh between selection and lock)
|
|
11025
|
+
return { executed: false };
|
|
11026
|
+
}
|
|
11027
|
+
// Compute structure hash for the phase
|
|
11028
|
+
const { scopeFiles } = await getScopeFiles(winner.node, watcher);
|
|
11029
|
+
const structureHash = computeStructureHash(scopeFiles);
|
|
11030
|
+
// skipUnchanged: bump _generatedAt without altering _phaseState
|
|
11031
|
+
if (config.skipUnchanged && currentMeta._generatedAt) {
|
|
11032
|
+
const verifiedStale = await isStale(getScopePrefix(winner.node), currentMeta, watcher);
|
|
11033
|
+
if (!verifiedStale) {
|
|
11034
|
+
await persistPhaseState({
|
|
11035
|
+
metaPath: winner.node.metaPath,
|
|
11036
|
+
current: currentMeta,
|
|
11037
|
+
config,
|
|
11038
|
+
structureHash,
|
|
11039
|
+
}, phaseState, { _generatedAt: new Date().toISOString() });
|
|
11040
|
+
logger?.debug({ path: winner.node.ownerPath }, 'Skipped unchanged meta, bumped _generatedAt');
|
|
11041
|
+
return { executed: false };
|
|
11042
|
+
}
|
|
11043
|
+
}
|
|
11044
|
+
return await executePhase(winner.node, currentMeta, phaseState, owedPhase, config, executor, watcher, structureHash, onProgress, logger);
|
|
11045
|
+
}
|
|
11046
|
+
finally {
|
|
11047
|
+
releaseLock(winner.node.metaPath);
|
|
11048
|
+
}
|
|
11049
|
+
}
|
|
11050
|
+
/**
|
|
11051
|
+
* Orchestrate a targeted (override) meta path.
|
|
11052
|
+
* Resolves the owed phase at execution time (not enqueue time).
|
|
11053
|
+
*/
|
|
11054
|
+
async function orchestrateTargeted(config, executor, watcher, targetPath, onProgress, logger) {
|
|
11055
|
+
const normalizedTarget = normalizePath(targetPath);
|
|
11056
|
+
const node = await buildMinimalNode(normalizedTarget, watcher);
|
|
11057
|
+
if (!acquireLock(node.metaPath)) {
|
|
11058
|
+
return { executed: false };
|
|
11059
|
+
}
|
|
11060
|
+
try {
|
|
11061
|
+
const currentMeta = await readMetaJson(normalizedTarget);
|
|
11062
|
+
const phaseState = retryAllFailed(derivePhaseState(currentMeta));
|
|
11063
|
+
const owedPhase = getOwedPhase(phaseState);
|
|
11064
|
+
if (!owedPhase) {
|
|
11065
|
+
// Fully fresh — override is a no-op (silently dropped per spec)
|
|
11066
|
+
return { executed: false, metaPath: normalizedTarget };
|
|
11067
|
+
}
|
|
11068
|
+
// Compute structure hash
|
|
11069
|
+
const { scopeFiles } = await getScopeFiles(node, watcher);
|
|
11070
|
+
const structureHash = computeStructureHash(scopeFiles);
|
|
11071
|
+
return await executePhase(node, currentMeta, phaseState, owedPhase, config, executor, watcher, structureHash, onProgress, logger);
|
|
11072
|
+
}
|
|
11073
|
+
finally {
|
|
11074
|
+
releaseLock(node.metaPath);
|
|
11075
|
+
}
|
|
11076
|
+
}
|
|
11077
|
+
/**
|
|
11078
|
+
* Execute exactly one phase on a meta.
|
|
11079
|
+
*/
|
|
11080
|
+
async function executePhase(node, currentMeta, phaseState, phase, config, executor, watcher, structureHash, onProgress, logger) {
|
|
11081
|
+
const result = await phaseRunners[phase](node, currentMeta, phaseState, config, executor, watcher, structureHash, onProgress, logger);
|
|
11082
|
+
return {
|
|
11083
|
+
executed: true,
|
|
11084
|
+
metaPath: node.metaPath,
|
|
11085
|
+
phase,
|
|
11086
|
+
phaseResult: result,
|
|
11087
|
+
cycleComplete: result.cycleComplete,
|
|
11088
|
+
};
|
|
11089
|
+
}
|
|
11090
|
+
|
|
10373
11091
|
/**
|
|
10374
11092
|
* Progress reporting via OpenClaw gateway `/tools/invoke` → `message` tool.
|
|
10375
11093
|
*
|
|
@@ -10504,42 +11222,106 @@ class ProgressReporter {
|
|
|
10504
11222
|
}
|
|
10505
11223
|
|
|
10506
11224
|
/**
|
|
10507
|
-
*
|
|
11225
|
+
* Hybrid 3-layer synthesis queue.
|
|
10508
11226
|
*
|
|
10509
|
-
*
|
|
10510
|
-
*
|
|
10511
|
-
*
|
|
11227
|
+
* Layer 1: Current — the single item currently executing (at most one).
|
|
11228
|
+
* Layer 2: Overrides — items manually enqueued via POST /synthesize with path.
|
|
11229
|
+
* FIFO among overrides, ahead of automatic candidates.
|
|
11230
|
+
* Layer 3: Automatic — computed on read, not persisted. All metas with a
|
|
11231
|
+
* pending phase, ranked by scheduler priority.
|
|
11232
|
+
*
|
|
11233
|
+
* Legacy: `pending` array is the union of overrides + automatic.
|
|
10512
11234
|
*
|
|
10513
11235
|
* @module queue
|
|
10514
11236
|
*/
|
|
10515
11237
|
const DEPTH_WARNING_THRESHOLD = 3;
|
|
10516
11238
|
/**
|
|
10517
|
-
*
|
|
11239
|
+
* Hybrid 3-layer synthesis queue.
|
|
10518
11240
|
*
|
|
10519
|
-
* Only one synthesis runs at a time.
|
|
10520
|
-
*
|
|
10521
|
-
* position returned.
|
|
11241
|
+
* Only one synthesis runs at a time. Override items (explicit triggers)
|
|
11242
|
+
* take priority over automatic candidates.
|
|
10522
11243
|
*/
|
|
10523
11244
|
class SynthesisQueue {
|
|
11245
|
+
/** Legacy queue (used by processQueue for backward compat). */
|
|
10524
11246
|
queue = [];
|
|
10525
11247
|
currentItem = null;
|
|
10526
11248
|
processing = false;
|
|
10527
11249
|
logger;
|
|
10528
11250
|
onEnqueueCallback = null;
|
|
10529
|
-
/**
|
|
10530
|
-
|
|
10531
|
-
|
|
10532
|
-
|
|
10533
|
-
*/
|
|
11251
|
+
/** Explicit override entries (3-layer model). */
|
|
11252
|
+
overrideEntries = [];
|
|
11253
|
+
/** Currently executing item with phase info (3-layer model). */
|
|
11254
|
+
currentPhaseItem = null;
|
|
10534
11255
|
constructor(logger) {
|
|
10535
11256
|
this.logger = logger;
|
|
10536
11257
|
}
|
|
10537
|
-
/**
|
|
10538
|
-
* Set a callback to invoke when a new (non-duplicate) item is enqueued.
|
|
10539
|
-
*/
|
|
11258
|
+
/** Set a callback to invoke when a new (non-duplicate) item is enqueued. */
|
|
10540
11259
|
onEnqueue(callback) {
|
|
10541
11260
|
this.onEnqueueCallback = callback;
|
|
10542
11261
|
}
|
|
11262
|
+
// ── Override layer (3-layer model) ─────────────────────────────────
|
|
11263
|
+
/**
|
|
11264
|
+
* Add an explicit override entry (from POST /synthesize with path).
|
|
11265
|
+
* Deduped by path. Returns position and whether already queued.
|
|
11266
|
+
*/
|
|
11267
|
+
enqueueOverride(path) {
|
|
11268
|
+
// Check if currently executing
|
|
11269
|
+
if (this.currentPhaseItem?.path === path ||
|
|
11270
|
+
this.currentItem?.path === path) {
|
|
11271
|
+
return { position: 0, alreadyQueued: true };
|
|
11272
|
+
}
|
|
11273
|
+
// Check if already in overrides
|
|
11274
|
+
const existing = this.overrideEntries.findIndex((e) => e.path === path);
|
|
11275
|
+
if (existing !== -1) {
|
|
11276
|
+
return { position: existing, alreadyQueued: true };
|
|
11277
|
+
}
|
|
11278
|
+
this.overrideEntries.push({
|
|
11279
|
+
path,
|
|
11280
|
+
enqueuedAt: new Date().toISOString(),
|
|
11281
|
+
});
|
|
11282
|
+
const position = this.overrideEntries.length - 1;
|
|
11283
|
+
if (this.overrideEntries.length > DEPTH_WARNING_THRESHOLD) {
|
|
11284
|
+
this.logger.warn({ depth: this.overrideEntries.length }, 'Override queue depth exceeds threshold');
|
|
11285
|
+
}
|
|
11286
|
+
this.onEnqueueCallback?.();
|
|
11287
|
+
return { position, alreadyQueued: false };
|
|
11288
|
+
}
|
|
11289
|
+
/** Dequeue the next override entry, or undefined if empty. */
|
|
11290
|
+
dequeueOverride() {
|
|
11291
|
+
return this.overrideEntries.shift();
|
|
11292
|
+
}
|
|
11293
|
+
/** Get all override entries (shallow copy). */
|
|
11294
|
+
get overrides() {
|
|
11295
|
+
return [...this.overrideEntries];
|
|
11296
|
+
}
|
|
11297
|
+
/** Clear all override entries. Returns count removed. */
|
|
11298
|
+
clearOverrides() {
|
|
11299
|
+
const count = this.overrideEntries.length;
|
|
11300
|
+
this.overrideEntries = [];
|
|
11301
|
+
return count;
|
|
11302
|
+
}
|
|
11303
|
+
/** Check if a path is in the override layer. */
|
|
11304
|
+
hasOverride(path) {
|
|
11305
|
+
return this.overrideEntries.some((e) => e.path === path);
|
|
11306
|
+
}
|
|
11307
|
+
// ── Current-item tracking (3-layer model) ──────────────────────────
|
|
11308
|
+
/** Set the currently executing phase item. */
|
|
11309
|
+
setCurrentPhase(path, phase) {
|
|
11310
|
+
this.currentPhaseItem = {
|
|
11311
|
+
path,
|
|
11312
|
+
phase,
|
|
11313
|
+
startedAt: new Date().toISOString(),
|
|
11314
|
+
};
|
|
11315
|
+
}
|
|
11316
|
+
/** Clear the current phase item. */
|
|
11317
|
+
clearCurrentPhase() {
|
|
11318
|
+
this.currentPhaseItem = null;
|
|
11319
|
+
}
|
|
11320
|
+
/** The currently executing phase item, or null. */
|
|
11321
|
+
get currentPhase() {
|
|
11322
|
+
return this.currentPhaseItem;
|
|
11323
|
+
}
|
|
11324
|
+
// ── Legacy queue interface (preserved for backward compat) ─────────
|
|
10543
11325
|
/**
|
|
10544
11326
|
* Add a path to the synthesis queue.
|
|
10545
11327
|
*
|
|
@@ -10548,11 +11330,9 @@ class SynthesisQueue {
|
|
|
10548
11330
|
* @returns Position and whether the path was already queued.
|
|
10549
11331
|
*/
|
|
10550
11332
|
enqueue(path, priority = false) {
|
|
10551
|
-
// Check if currently being synthesized.
|
|
10552
11333
|
if (this.currentItem?.path === path) {
|
|
10553
11334
|
return { position: 0, alreadyQueued: true };
|
|
10554
11335
|
}
|
|
10555
|
-
// Check if already in queue.
|
|
10556
11336
|
const existingIndex = this.queue.findIndex((item) => item.path === path);
|
|
10557
11337
|
if (existingIndex !== -1) {
|
|
10558
11338
|
return { position: existingIndex, alreadyQueued: true };
|
|
@@ -10575,11 +11355,7 @@ class SynthesisQueue {
|
|
|
10575
11355
|
this.onEnqueueCallback?.();
|
|
10576
11356
|
return { position, alreadyQueued: false };
|
|
10577
11357
|
}
|
|
10578
|
-
/**
|
|
10579
|
-
* Remove and return the next item from the queue.
|
|
10580
|
-
*
|
|
10581
|
-
* @returns The next QueueItem, or undefined if the queue is empty.
|
|
10582
|
-
*/
|
|
11358
|
+
/** Remove and return the next item from the queue. */
|
|
10583
11359
|
dequeue() {
|
|
10584
11360
|
const item = this.queue.shift();
|
|
10585
11361
|
if (item) {
|
|
@@ -10609,7 +11385,6 @@ class SynthesisQueue {
|
|
|
10609
11385
|
}
|
|
10610
11386
|
/**
|
|
10611
11387
|
* Remove all pending items from the queue.
|
|
10612
|
-
*
|
|
10613
11388
|
* Does not affect the currently-running item.
|
|
10614
11389
|
*
|
|
10615
11390
|
* @returns The number of items removed.
|
|
@@ -10619,45 +11394,56 @@ class SynthesisQueue {
|
|
|
10619
11394
|
this.queue = [];
|
|
10620
11395
|
return count;
|
|
10621
11396
|
}
|
|
10622
|
-
/**
|
|
10623
|
-
* Check whether a path is in the queue or currently being synthesized.
|
|
10624
|
-
*
|
|
10625
|
-
* @param path - Meta path to look up.
|
|
10626
|
-
* @returns True if the path is queued or currently running.
|
|
10627
|
-
*/
|
|
11397
|
+
/** Check whether a path is in the queue or currently being synthesized. */
|
|
10628
11398
|
has(path) {
|
|
10629
11399
|
if (this.currentItem?.path === path)
|
|
10630
11400
|
return true;
|
|
10631
|
-
|
|
11401
|
+
if (this.currentPhaseItem?.path === path)
|
|
11402
|
+
return true;
|
|
11403
|
+
return (this.queue.some((item) => item.path === path) ||
|
|
11404
|
+
this.overrideEntries.some((e) => e.path === path));
|
|
10632
11405
|
}
|
|
10633
|
-
/**
|
|
10634
|
-
* Get the 0-indexed position of a path in the queue.
|
|
10635
|
-
*
|
|
10636
|
-
* @param path - Meta path to look up.
|
|
10637
|
-
* @returns Position index, or null if not found in the queue.
|
|
10638
|
-
*/
|
|
11406
|
+
/** Get the 0-indexed position of a path in the queue. */
|
|
10639
11407
|
getPosition(path) {
|
|
11408
|
+
// Check overrides first
|
|
11409
|
+
const overrideIdx = this.overrideEntries.findIndex((e) => e.path === path);
|
|
11410
|
+
if (overrideIdx !== -1)
|
|
11411
|
+
return overrideIdx;
|
|
10640
11412
|
const index = this.queue.findIndex((item) => item.path === path);
|
|
10641
11413
|
return index === -1 ? null : index;
|
|
10642
11414
|
}
|
|
10643
|
-
/**
|
|
10644
|
-
|
|
10645
|
-
|
|
10646
|
-
|
|
10647
|
-
|
|
11415
|
+
/** Dequeue the next item: overrides first, then legacy queue. */
|
|
11416
|
+
nextItem() {
|
|
11417
|
+
const override = this.dequeueOverride();
|
|
11418
|
+
if (override)
|
|
11419
|
+
return { path: override.path, source: 'override' };
|
|
11420
|
+
const item = this.dequeue();
|
|
11421
|
+
if (item)
|
|
11422
|
+
return { path: item.path, source: 'legacy' };
|
|
11423
|
+
return undefined;
|
|
11424
|
+
}
|
|
11425
|
+
/** Return a snapshot of queue state for the /status endpoint. */
|
|
10648
11426
|
getState() {
|
|
10649
11427
|
return {
|
|
10650
|
-
depth: this.queue.length,
|
|
10651
|
-
items:
|
|
10652
|
-
|
|
10653
|
-
|
|
10654
|
-
|
|
10655
|
-
|
|
11428
|
+
depth: this.queue.length + this.overrideEntries.length,
|
|
11429
|
+
items: [
|
|
11430
|
+
...this.overrideEntries.map((e) => ({
|
|
11431
|
+
path: e.path,
|
|
11432
|
+
priority: true,
|
|
11433
|
+
enqueuedAt: e.enqueuedAt,
|
|
11434
|
+
})),
|
|
11435
|
+
...this.queue.map((item) => ({
|
|
11436
|
+
path: item.path,
|
|
11437
|
+
priority: item.priority,
|
|
11438
|
+
enqueuedAt: item.enqueuedAt,
|
|
11439
|
+
})),
|
|
11440
|
+
],
|
|
10656
11441
|
};
|
|
10657
11442
|
}
|
|
10658
11443
|
/**
|
|
10659
|
-
* Process queued items one at a time until
|
|
11444
|
+
* Process queued items one at a time until all queues are empty.
|
|
10660
11445
|
*
|
|
11446
|
+
* Override entries are processed first (FIFO), then legacy queue items.
|
|
10661
11447
|
* Re-entry is prevented: if already processing, the call returns
|
|
10662
11448
|
* immediately. Errors are logged and do not block subsequent items.
|
|
10663
11449
|
*
|
|
@@ -10668,19 +11454,22 @@ class SynthesisQueue {
|
|
|
10668
11454
|
return;
|
|
10669
11455
|
this.processing = true;
|
|
10670
11456
|
try {
|
|
10671
|
-
let
|
|
10672
|
-
while (
|
|
11457
|
+
let next = this.nextItem();
|
|
11458
|
+
while (next) {
|
|
10673
11459
|
try {
|
|
10674
|
-
await synthesizeFn(
|
|
11460
|
+
await synthesizeFn(next.path);
|
|
10675
11461
|
}
|
|
10676
11462
|
catch (err) {
|
|
10677
|
-
this.logger.error({ path:
|
|
11463
|
+
this.logger.error({ path: next.path, err }, 'Synthesis failed');
|
|
10678
11464
|
}
|
|
10679
|
-
this.
|
|
10680
|
-
|
|
11465
|
+
this.clearCurrentPhase();
|
|
11466
|
+
if (next.source === 'legacy')
|
|
11467
|
+
this.complete();
|
|
11468
|
+
next = this.nextItem();
|
|
10681
11469
|
}
|
|
10682
11470
|
}
|
|
10683
11471
|
finally {
|
|
11472
|
+
this.clearCurrentPhase();
|
|
10684
11473
|
this.processing = false;
|
|
10685
11474
|
}
|
|
10686
11475
|
}
|
|
@@ -11152,14 +11941,15 @@ async function autoSeedPass(rules, watcher, logger) {
|
|
|
11152
11941
|
}
|
|
11153
11942
|
|
|
11154
11943
|
/**
|
|
11155
|
-
* Croner-based scheduler that discovers the
|
|
11156
|
-
* and enqueues it for
|
|
11944
|
+
* Croner-based scheduler that discovers the highest-priority ready phase
|
|
11945
|
+
* across the corpus each tick and enqueues it for execution.
|
|
11157
11946
|
*
|
|
11158
11947
|
* @module scheduler
|
|
11159
11948
|
*/
|
|
11160
11949
|
const MAX_BACKOFF_MULTIPLIER = 4;
|
|
11161
11950
|
/**
|
|
11162
|
-
* Periodic scheduler that discovers
|
|
11951
|
+
* Periodic scheduler that discovers the highest-priority ready phase
|
|
11952
|
+
* across all metas and enqueues it for execution.
|
|
11163
11953
|
*
|
|
11164
11954
|
* Supports adaptive backoff when no candidates are found and hot-reloadable
|
|
11165
11955
|
* cron expressions via {@link Scheduler.updateSchedule}.
|
|
@@ -11214,10 +12004,10 @@ class Scheduler {
|
|
|
11214
12004
|
this.logger.info({ schedule: expression }, 'Schedule updated');
|
|
11215
12005
|
}
|
|
11216
12006
|
}
|
|
11217
|
-
/** Reset backoff multiplier (call after successful
|
|
12007
|
+
/** Reset backoff multiplier (call after successful phase execution). */
|
|
11218
12008
|
resetBackoff() {
|
|
11219
12009
|
if (this.backoffMultiplier > 1) {
|
|
11220
|
-
this.logger.debug('Backoff reset after successful
|
|
12010
|
+
this.logger.debug('Backoff reset after successful phase execution');
|
|
11221
12011
|
}
|
|
11222
12012
|
this.backoffMultiplier = 1;
|
|
11223
12013
|
}
|
|
@@ -11232,10 +12022,9 @@ class Scheduler {
|
|
|
11232
12022
|
return this.job.nextRun() ?? null;
|
|
11233
12023
|
}
|
|
11234
12024
|
/**
|
|
11235
|
-
* Single tick: discover
|
|
12025
|
+
* Single tick: discover the highest-priority ready phase and enqueue it.
|
|
11236
12026
|
*
|
|
11237
|
-
*
|
|
11238
|
-
* when no candidates are found.
|
|
12027
|
+
* Applies adaptive backoff when no candidates are found.
|
|
11239
12028
|
*/
|
|
11240
12029
|
async tick() {
|
|
11241
12030
|
this.tickCount++;
|
|
@@ -11260,14 +12049,15 @@ class Scheduler {
|
|
|
11260
12049
|
this.logger.warn({ err }, 'Auto-seed pass failed');
|
|
11261
12050
|
}
|
|
11262
12051
|
}
|
|
11263
|
-
const candidate = await this.
|
|
12052
|
+
const candidate = await this.discoverNextPhase();
|
|
11264
12053
|
if (!candidate) {
|
|
11265
12054
|
this.backoffMultiplier = Math.min(this.backoffMultiplier * 2, MAX_BACKOFF_MULTIPLIER);
|
|
11266
|
-
this.logger.debug({ backoffMultiplier: this.backoffMultiplier }, 'No
|
|
12055
|
+
this.logger.debug({ backoffMultiplier: this.backoffMultiplier }, 'No ready phases found, increasing backoff');
|
|
11267
12056
|
return;
|
|
11268
12057
|
}
|
|
11269
|
-
|
|
11270
|
-
this.
|
|
12058
|
+
// Enqueue using the legacy queue path (backward compat with processQueue)
|
|
12059
|
+
this.queue.enqueue(candidate.path);
|
|
12060
|
+
this.logger.info({ path: candidate.path, phase: candidate.phase, band: candidate.band }, 'Enqueued phase candidate');
|
|
11271
12061
|
// Opportunistic watcher restart detection
|
|
11272
12062
|
if (this.registrar) {
|
|
11273
12063
|
try {
|
|
@@ -11287,22 +12077,27 @@ class Scheduler {
|
|
|
11287
12077
|
}
|
|
11288
12078
|
}
|
|
11289
12079
|
/**
|
|
11290
|
-
* Discover the
|
|
12080
|
+
* Discover the highest-priority ready phase across the corpus.
|
|
12081
|
+
*
|
|
12082
|
+
* Uses phase-state-aware scheduling: priority order is
|
|
12083
|
+
* critic (band 1) \> builder (band 2) \> architect (band 3),
|
|
12084
|
+
* with weighted staleness as tiebreaker within a band.
|
|
11291
12085
|
*/
|
|
11292
|
-
async
|
|
12086
|
+
async discoverNextPhase() {
|
|
11293
12087
|
try {
|
|
11294
12088
|
const result = await listMetas(this.config, this.watcher);
|
|
11295
|
-
const
|
|
11296
|
-
|
|
11297
|
-
|
|
11298
|
-
|
|
11299
|
-
|
|
11300
|
-
|
|
11301
|
-
|
|
11302
|
-
|
|
12089
|
+
const candidates = buildPhaseCandidates(result.entries);
|
|
12090
|
+
const winner = selectPhaseCandidate(candidates, this.config.depthWeight);
|
|
12091
|
+
if (!winner)
|
|
12092
|
+
return null;
|
|
12093
|
+
return {
|
|
12094
|
+
path: winner.node.metaPath,
|
|
12095
|
+
phase: winner.owedPhase,
|
|
12096
|
+
band: winner.band,
|
|
12097
|
+
};
|
|
11303
12098
|
}
|
|
11304
12099
|
catch (err) {
|
|
11305
|
-
this.logger.warn({ err }, 'Failed to discover
|
|
12100
|
+
this.logger.warn({ err }, 'Failed to discover next phase candidate');
|
|
11306
12101
|
return null;
|
|
11307
12102
|
}
|
|
11308
12103
|
}
|
|
@@ -11452,6 +12247,10 @@ const metasQuerySchema = z.object({
|
|
|
11452
12247
|
.enum(['true', 'false'])
|
|
11453
12248
|
.transform((v) => v === 'true')
|
|
11454
12249
|
.optional(),
|
|
12250
|
+
disabled: z
|
|
12251
|
+
.enum(['true', 'false'])
|
|
12252
|
+
.transform((v) => v === 'true')
|
|
12253
|
+
.optional(),
|
|
11455
12254
|
fields: z.string().optional(),
|
|
11456
12255
|
});
|
|
11457
12256
|
const metaDetailQuerySchema = z.object({
|
|
@@ -11482,6 +12281,9 @@ function registerMetasRoutes(app, deps) {
|
|
|
11482
12281
|
if (query.locked !== undefined) {
|
|
11483
12282
|
entries = entries.filter((e) => e.locked === query.locked);
|
|
11484
12283
|
}
|
|
12284
|
+
if (query.disabled !== undefined) {
|
|
12285
|
+
entries = entries.filter((e) => e.disabled === query.disabled);
|
|
12286
|
+
}
|
|
11485
12287
|
if (typeof query.staleHours === 'number') {
|
|
11486
12288
|
entries = entries.filter((e) => e.stalenessSeconds >= query.staleHours * 3600);
|
|
11487
12289
|
}
|
|
@@ -11497,12 +12299,16 @@ function registerMetasRoutes(app, deps) {
|
|
|
11497
12299
|
'lastSynthesized',
|
|
11498
12300
|
'hasError',
|
|
11499
12301
|
'locked',
|
|
12302
|
+
'disabled',
|
|
11500
12303
|
'architectTokens',
|
|
11501
12304
|
'builderTokens',
|
|
11502
12305
|
'criticTokens',
|
|
12306
|
+
'phaseState',
|
|
12307
|
+
'owedPhase',
|
|
11503
12308
|
];
|
|
11504
12309
|
const projectedFields = fieldList ?? defaultFields;
|
|
11505
12310
|
const metas = entries.map((e) => {
|
|
12311
|
+
const ps = derivePhaseState(e.meta);
|
|
11506
12312
|
const full = {
|
|
11507
12313
|
path: e.path,
|
|
11508
12314
|
depth: e.depth,
|
|
@@ -11513,9 +12319,12 @@ function registerMetasRoutes(app, deps) {
|
|
|
11513
12319
|
lastSynthesized: e.lastSynthesized,
|
|
11514
12320
|
hasError: e.hasError,
|
|
11515
12321
|
locked: e.locked,
|
|
12322
|
+
disabled: e.disabled,
|
|
11516
12323
|
architectTokens: e.architectTokens,
|
|
11517
12324
|
builderTokens: e.builderTokens,
|
|
11518
12325
|
criticTokens: e.criticTokens,
|
|
12326
|
+
phaseState: ps,
|
|
12327
|
+
owedPhase: getOwedPhase(ps),
|
|
11519
12328
|
};
|
|
11520
12329
|
const projected = {};
|
|
11521
12330
|
for (const f of projectedFields) {
|
|
@@ -11540,13 +12349,6 @@ function registerMetasRoutes(app, deps) {
|
|
|
11540
12349
|
}
|
|
11541
12350
|
const meta = JSON.parse(await readFile(join(targetNode.metaPath, 'meta.json'), 'utf8'));
|
|
11542
12351
|
// Field projection
|
|
11543
|
-
const defaultExclude = new Set([
|
|
11544
|
-
'_architect',
|
|
11545
|
-
'_builder',
|
|
11546
|
-
'_critic',
|
|
11547
|
-
'_content',
|
|
11548
|
-
'_feedback',
|
|
11549
|
-
]);
|
|
11550
12352
|
const fieldList = query.fields?.split(',');
|
|
11551
12353
|
const projectMeta = (m) => {
|
|
11552
12354
|
if (fieldList) {
|
|
@@ -11557,7 +12359,7 @@ function registerMetasRoutes(app, deps) {
|
|
|
11557
12359
|
}
|
|
11558
12360
|
const r = {};
|
|
11559
12361
|
for (const [k, v] of Object.entries(m)) {
|
|
11560
|
-
if (!
|
|
12362
|
+
if (!DEFAULT_EXCLUDE_FIELDS.has(k))
|
|
11561
12363
|
r[k] = v;
|
|
11562
12364
|
}
|
|
11563
12365
|
return r;
|
|
@@ -11570,6 +12372,10 @@ function registerMetasRoutes(app, deps) {
|
|
|
11570
12372
|
? Math.round((Date.now() - new Date(metaTyped._generatedAt).getTime()) / 1000)
|
|
11571
12373
|
: null;
|
|
11572
12374
|
const score = computeStalenessScore(staleSeconds, metaTyped._depth ?? 0, metaTyped._emphasis ?? 1, config.depthWeight);
|
|
12375
|
+
// Phase state
|
|
12376
|
+
const entry = result.entries.find((e) => e.node.metaPath === targetNode.metaPath);
|
|
12377
|
+
const phaseState = entry ? derivePhaseState(entry.meta) : null;
|
|
12378
|
+
const owedPhase = phaseState ? getOwedPhase(phaseState) : null;
|
|
11573
12379
|
const response = {
|
|
11574
12380
|
path: targetNode.metaPath,
|
|
11575
12381
|
meta: projectMeta(meta),
|
|
@@ -11582,6 +12388,8 @@ function registerMetasRoutes(app, deps) {
|
|
|
11582
12388
|
seconds: staleSeconds,
|
|
11583
12389
|
score: Math.round(score * 100) / 100,
|
|
11584
12390
|
},
|
|
12391
|
+
phaseState,
|
|
12392
|
+
owedPhase,
|
|
11585
12393
|
};
|
|
11586
12394
|
// Cross-refs status
|
|
11587
12395
|
const crossRefsRaw = meta._crossRefs;
|
|
@@ -11618,6 +12426,84 @@ function registerMetasRoutes(app, deps) {
|
|
|
11618
12426
|
});
|
|
11619
12427
|
}
|
|
11620
12428
|
|
|
12429
|
+
/**
|
|
12430
|
+
* PATCH /metas/:path — update user-settable reserved properties on a meta.
|
|
12431
|
+
*
|
|
12432
|
+
* Supported fields: _steer, _emphasis, _depth, _crossRefs, _disabled.
|
|
12433
|
+
* Set a field to null to remove it. Unknown keys are rejected.
|
|
12434
|
+
*
|
|
12435
|
+
* @module routes/metasUpdate
|
|
12436
|
+
*/
|
|
12437
|
+
const updateBodySchema = z
|
|
12438
|
+
.object({
|
|
12439
|
+
_steer: z.union([z.string(), z.null()]).optional(),
|
|
12440
|
+
_emphasis: z.union([z.number().min(0), z.null()]).optional(),
|
|
12441
|
+
_depth: z.union([z.number(), z.null()]).optional(),
|
|
12442
|
+
_crossRefs: z.union([z.array(z.string()), z.null()]).optional(),
|
|
12443
|
+
_disabled: z.union([z.boolean(), z.null()]).optional(),
|
|
12444
|
+
})
|
|
12445
|
+
.strict();
|
|
12446
|
+
function registerMetasUpdateRoute(app, deps) {
|
|
12447
|
+
app.patch('/metas/:path', async (request, reply) => {
|
|
12448
|
+
const parseResult = updateBodySchema.safeParse(request.body);
|
|
12449
|
+
if (!parseResult.success) {
|
|
12450
|
+
return reply.status(400).send({
|
|
12451
|
+
error: 'BAD_REQUEST',
|
|
12452
|
+
message: parseResult.error.message,
|
|
12453
|
+
});
|
|
12454
|
+
}
|
|
12455
|
+
const updates = parseResult.data;
|
|
12456
|
+
const targetPath = normalizePath(decodeURIComponent(request.params.path));
|
|
12457
|
+
const metaDir = resolveMetaDir(targetPath);
|
|
12458
|
+
let meta;
|
|
12459
|
+
try {
|
|
12460
|
+
meta = (await readMetaJson(metaDir));
|
|
12461
|
+
}
|
|
12462
|
+
catch {
|
|
12463
|
+
return reply.status(404).send({
|
|
12464
|
+
error: 'NOT_FOUND',
|
|
12465
|
+
message: 'Meta path not found: ' + targetPath,
|
|
12466
|
+
});
|
|
12467
|
+
}
|
|
12468
|
+
const metaJsonPath = join(metaDir, 'meta.json');
|
|
12469
|
+
const KEYS = [
|
|
12470
|
+
'_steer',
|
|
12471
|
+
'_emphasis',
|
|
12472
|
+
'_depth',
|
|
12473
|
+
'_crossRefs',
|
|
12474
|
+
'_disabled',
|
|
12475
|
+
];
|
|
12476
|
+
const toDelete = new Set();
|
|
12477
|
+
const toSet = {};
|
|
12478
|
+
for (const key of KEYS) {
|
|
12479
|
+
const value = updates[key];
|
|
12480
|
+
if (value === null) {
|
|
12481
|
+
toDelete.add(key);
|
|
12482
|
+
}
|
|
12483
|
+
else if (value !== undefined) {
|
|
12484
|
+
toSet[key] = value;
|
|
12485
|
+
}
|
|
12486
|
+
}
|
|
12487
|
+
const updated = {};
|
|
12488
|
+
for (const [k, v] of Object.entries(meta)) {
|
|
12489
|
+
if (!toDelete.has(k))
|
|
12490
|
+
updated[k] = v;
|
|
12491
|
+
}
|
|
12492
|
+
Object.assign(updated, toSet);
|
|
12493
|
+
await writeFile(metaJsonPath, JSON.stringify(updated, null, 2) + '\n');
|
|
12494
|
+
// Project the response — exclude the same large fields as the detail route.
|
|
12495
|
+
const projected = {};
|
|
12496
|
+
for (const [k, v] of Object.entries(updated)) {
|
|
12497
|
+
if (!DEFAULT_EXCLUDE_FIELDS.has(k))
|
|
12498
|
+
projected[k] = v;
|
|
12499
|
+
}
|
|
12500
|
+
return reply.send({
|
|
12501
|
+
path: metaDir,
|
|
12502
|
+
meta: projected,
|
|
12503
|
+
});
|
|
12504
|
+
});
|
|
12505
|
+
}
|
|
12506
|
+
|
|
11621
12507
|
/**
|
|
11622
12508
|
* GET /preview — dry-run synthesis preview.
|
|
11623
12509
|
*
|
|
@@ -11670,6 +12556,19 @@ function registerPreviewRoute(app, deps) {
|
|
|
11670
12556
|
const structureChanged = structureHash !== meta._structureHash;
|
|
11671
12557
|
const latestArchive = await readLatestArchive(targetNode.metaPath);
|
|
11672
12558
|
const steerChanged = hasSteerChanged(meta._steer, latestArchive?._steer, Boolean(latestArchive));
|
|
12559
|
+
// _architect change detection
|
|
12560
|
+
const architectChanged = latestArchive
|
|
12561
|
+
? (meta._architect ?? '') !== (latestArchive._architect ?? '')
|
|
12562
|
+
: Boolean(meta._architect);
|
|
12563
|
+
// _crossRefs declaration change detection
|
|
12564
|
+
const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
|
|
12565
|
+
const archiveRefs = (latestArchive?._crossRefs ?? [])
|
|
12566
|
+
.slice()
|
|
12567
|
+
.sort()
|
|
12568
|
+
.join(',');
|
|
12569
|
+
const crossRefsDeclChanged = latestArchive
|
|
12570
|
+
? currentRefs !== archiveRefs
|
|
12571
|
+
: currentRefs.length > 0;
|
|
11673
12572
|
const architectTriggered = isArchitectTriggered(meta, structureChanged, steerChanged, config.architectEvery);
|
|
11674
12573
|
// Delta files
|
|
11675
12574
|
const deltaFiles = getDeltaFiles(meta._generatedAt, scopeFiles);
|
|
@@ -11684,6 +12583,40 @@ function registerPreviewRoute(app, deps) {
|
|
|
11684
12583
|
? Math.round((Date.now() - new Date(meta._generatedAt).getTime()) / 1000)
|
|
11685
12584
|
: null;
|
|
11686
12585
|
const stalenessScore = computeStalenessScore(stalenessSeconds, meta._depth ?? 0, meta._emphasis ?? 1, config.depthWeight);
|
|
12586
|
+
// Phase state
|
|
12587
|
+
const phaseState = derivePhaseState(meta, {
|
|
12588
|
+
structureChanged,
|
|
12589
|
+
steerChanged,
|
|
12590
|
+
architectChanged,
|
|
12591
|
+
crossRefsChanged: crossRefsDeclChanged,
|
|
12592
|
+
architectEvery: config.architectEvery,
|
|
12593
|
+
});
|
|
12594
|
+
const owedPhase = getOwedPhase(phaseState);
|
|
12595
|
+
const priorityBand = getPriorityBand(phaseState);
|
|
12596
|
+
// Architect invalidators
|
|
12597
|
+
const architectInvalidators = [];
|
|
12598
|
+
if (owedPhase === 'architect') {
|
|
12599
|
+
if (structureChanged)
|
|
12600
|
+
architectInvalidators.push('structureHash');
|
|
12601
|
+
if (steerChanged)
|
|
12602
|
+
architectInvalidators.push('steer');
|
|
12603
|
+
if (architectChanged)
|
|
12604
|
+
architectInvalidators.push('_architect');
|
|
12605
|
+
if (crossRefsDeclChanged)
|
|
12606
|
+
architectInvalidators.push('_crossRefs');
|
|
12607
|
+
if ((meta._synthesisCount ?? 0) >= config.architectEvery) {
|
|
12608
|
+
architectInvalidators.push('architectEvery');
|
|
12609
|
+
}
|
|
12610
|
+
}
|
|
12611
|
+
// Staleness inputs
|
|
12612
|
+
const stalenessInputs = {
|
|
12613
|
+
structureHash,
|
|
12614
|
+
steerChanged,
|
|
12615
|
+
architectChanged,
|
|
12616
|
+
crossRefsDeclChanged,
|
|
12617
|
+
scopeMtimeMax: null,
|
|
12618
|
+
crossRefContentChanged: false,
|
|
12619
|
+
};
|
|
11687
12620
|
return {
|
|
11688
12621
|
path: targetNode.metaPath,
|
|
11689
12622
|
staleness: {
|
|
@@ -11708,6 +12641,12 @@ function registerPreviewRoute(app, deps) {
|
|
|
11708
12641
|
deltaCount: deltaFiles.length,
|
|
11709
12642
|
},
|
|
11710
12643
|
estimatedTokens,
|
|
12644
|
+
// New phase-state fields (additive)
|
|
12645
|
+
owedPhase,
|
|
12646
|
+
priorityBand,
|
|
12647
|
+
phaseState,
|
|
12648
|
+
stalenessInputs,
|
|
12649
|
+
architectInvalidators,
|
|
11711
12650
|
};
|
|
11712
12651
|
});
|
|
11713
12652
|
}
|
|
@@ -11715,8 +12654,8 @@ function registerPreviewRoute(app, deps) {
|
|
|
11715
12654
|
/**
|
|
11716
12655
|
* Queue management and abort routes.
|
|
11717
12656
|
*
|
|
11718
|
-
* - GET /queue —
|
|
11719
|
-
* - POST /queue/clear — remove
|
|
12657
|
+
* - GET /queue — 3-layer queue model (current, overrides, automatic, pending)
|
|
12658
|
+
* - POST /queue/clear — remove override entries only
|
|
11720
12659
|
* - POST /synthesize/abort — abort the current synthesis
|
|
11721
12660
|
*
|
|
11722
12661
|
* @module routes/queue
|
|
@@ -11724,33 +12663,134 @@ function registerPreviewRoute(app, deps) {
|
|
|
11724
12663
|
/** Register queue management routes. */
|
|
11725
12664
|
function registerQueueRoutes(app, deps) {
|
|
11726
12665
|
const { queue } = deps;
|
|
11727
|
-
app.get('/queue', () =>
|
|
11728
|
-
|
|
11729
|
-
|
|
11730
|
-
state
|
|
11731
|
-
|
|
12666
|
+
app.get('/queue', async () => {
|
|
12667
|
+
const currentPhase = queue.currentPhase;
|
|
12668
|
+
const overrides = queue.overrides;
|
|
12669
|
+
// Compute owedPhase for each override entry by reading meta state
|
|
12670
|
+
const enrichedOverrides = await Promise.all(overrides.map(async (o) => {
|
|
12671
|
+
try {
|
|
12672
|
+
const metaDir = resolveMetaDir(o.path);
|
|
12673
|
+
const meta = await readMetaJson(metaDir);
|
|
12674
|
+
const ps = derivePhaseState(meta);
|
|
12675
|
+
return {
|
|
12676
|
+
path: o.path,
|
|
12677
|
+
owedPhase: getOwedPhase(ps),
|
|
12678
|
+
enqueuedAt: o.enqueuedAt,
|
|
12679
|
+
};
|
|
12680
|
+
}
|
|
12681
|
+
catch {
|
|
12682
|
+
return {
|
|
12683
|
+
path: o.path,
|
|
12684
|
+
owedPhase: null,
|
|
12685
|
+
enqueuedAt: o.enqueuedAt,
|
|
12686
|
+
};
|
|
12687
|
+
}
|
|
12688
|
+
}));
|
|
12689
|
+
// Compute automatic layer: all metas with a pending owed phase,
|
|
12690
|
+
// ranked by scheduler priority (computed on read, not persisted)
|
|
12691
|
+
let automatic = [];
|
|
12692
|
+
try {
|
|
12693
|
+
const metaResult = await listMetas(deps.config, deps.watcher);
|
|
12694
|
+
const candidates = buildPhaseCandidates(metaResult.entries);
|
|
12695
|
+
const ranked = rankPhaseCandidates(candidates, deps.config.depthWeight);
|
|
12696
|
+
automatic = ranked.map((c) => ({
|
|
12697
|
+
path: c.node.metaPath,
|
|
12698
|
+
owedPhase: c.owedPhase,
|
|
12699
|
+
priorityBand: c.band,
|
|
12700
|
+
effectiveStaleness: c.effectiveStaleness,
|
|
12701
|
+
}));
|
|
12702
|
+
}
|
|
12703
|
+
catch {
|
|
12704
|
+
// If listing fails, automatic stays empty
|
|
12705
|
+
}
|
|
12706
|
+
// Legacy: pending is the union of overrides + automatic + legacy queue items
|
|
12707
|
+
const pendingItems = [
|
|
12708
|
+
...enrichedOverrides.map((o) => ({
|
|
12709
|
+
path: o.path,
|
|
12710
|
+
owedPhase: o.owedPhase,
|
|
12711
|
+
})),
|
|
12712
|
+
...automatic.map((a) => ({
|
|
12713
|
+
path: a.path,
|
|
12714
|
+
owedPhase: a.owedPhase,
|
|
12715
|
+
})),
|
|
12716
|
+
...queue.pending.map((item) => ({
|
|
12717
|
+
path: item.path,
|
|
12718
|
+
owedPhase: null,
|
|
12719
|
+
})),
|
|
12720
|
+
];
|
|
12721
|
+
return {
|
|
12722
|
+
current: currentPhase
|
|
12723
|
+
? {
|
|
12724
|
+
path: currentPhase.path,
|
|
12725
|
+
phase: currentPhase.phase,
|
|
12726
|
+
startedAt: currentPhase.startedAt,
|
|
12727
|
+
}
|
|
12728
|
+
: queue.current
|
|
12729
|
+
? {
|
|
12730
|
+
path: queue.current.path,
|
|
12731
|
+
phase: null,
|
|
12732
|
+
startedAt: queue.current.enqueuedAt,
|
|
12733
|
+
}
|
|
12734
|
+
: null,
|
|
12735
|
+
overrides: enrichedOverrides,
|
|
12736
|
+
automatic,
|
|
12737
|
+
pending: pendingItems,
|
|
12738
|
+
// Legacy state
|
|
12739
|
+
state: queue.getState(),
|
|
12740
|
+
};
|
|
12741
|
+
});
|
|
11732
12742
|
app.post('/queue/clear', () => {
|
|
11733
|
-
const removed = queue.
|
|
12743
|
+
const removed = queue.clearOverrides();
|
|
11734
12744
|
return { cleared: removed };
|
|
11735
12745
|
});
|
|
11736
12746
|
app.post('/synthesize/abort', async (_request, reply) => {
|
|
11737
|
-
|
|
12747
|
+
// Check 3-layer current first
|
|
12748
|
+
const currentPhase = queue.currentPhase;
|
|
12749
|
+
const current = currentPhase ?? queue.current;
|
|
11738
12750
|
if (!current) {
|
|
11739
|
-
return reply
|
|
11740
|
-
.status(404)
|
|
11741
|
-
.send({ error: 'NOT_FOUND', message: 'No synthesis in progress' });
|
|
12751
|
+
return reply.status(200).send({ status: 'idle' });
|
|
11742
12752
|
}
|
|
11743
12753
|
// Abort the executor
|
|
11744
12754
|
deps.executor?.abort();
|
|
12755
|
+
const metaDir = resolveMetaDir(current.path);
|
|
12756
|
+
const phase = currentPhase?.phase ?? null;
|
|
12757
|
+
// Transition running phase to failed and write _error to meta.json
|
|
12758
|
+
if (phase) {
|
|
12759
|
+
try {
|
|
12760
|
+
const meta = await readMetaJson(metaDir);
|
|
12761
|
+
let ps = derivePhaseState(meta);
|
|
12762
|
+
ps = phaseFailed(ps, phase);
|
|
12763
|
+
const updated = {
|
|
12764
|
+
...meta,
|
|
12765
|
+
_phaseState: ps,
|
|
12766
|
+
_error: {
|
|
12767
|
+
step: phase,
|
|
12768
|
+
code: 'ABORT',
|
|
12769
|
+
message: 'Aborted by operator',
|
|
12770
|
+
},
|
|
12771
|
+
};
|
|
12772
|
+
const lockPath = join(metaDir, '.lock');
|
|
12773
|
+
const metaJsonPath = join(metaDir, 'meta.json');
|
|
12774
|
+
await writeFile(lockPath, JSON.stringify(updated, null, 2) + '\n');
|
|
12775
|
+
await copyFile(lockPath, metaJsonPath);
|
|
12776
|
+
}
|
|
12777
|
+
catch {
|
|
12778
|
+
// Best-effort — meta may be unreadable
|
|
12779
|
+
}
|
|
12780
|
+
}
|
|
11745
12781
|
// Release the lock for the current meta path
|
|
11746
12782
|
try {
|
|
11747
|
-
releaseLock(
|
|
12783
|
+
releaseLock(metaDir);
|
|
11748
12784
|
}
|
|
11749
12785
|
catch {
|
|
11750
12786
|
// Lock may already be released
|
|
11751
12787
|
}
|
|
11752
12788
|
deps.logger.info({ path: current.path }, 'Synthesis aborted');
|
|
11753
|
-
return {
|
|
12789
|
+
return {
|
|
12790
|
+
status: 'aborted',
|
|
12791
|
+
path: current.path,
|
|
12792
|
+
...(phase ? { phase } : {}),
|
|
12793
|
+
};
|
|
11754
12794
|
});
|
|
11755
12795
|
}
|
|
11756
12796
|
|
|
@@ -11807,7 +12847,6 @@ async function checkDependency(url, path) {
|
|
|
11807
12847
|
return { url, status: 'unreachable', checkedAt };
|
|
11808
12848
|
}
|
|
11809
12849
|
}
|
|
11810
|
-
/** Check watcher, surfacing initialScan.active as indexing state. */
|
|
11811
12850
|
async function checkWatcher(url) {
|
|
11812
12851
|
const checkedAt = new Date().toISOString();
|
|
11813
12852
|
try {
|
|
@@ -11833,26 +12872,61 @@ async function checkWatcher(url) {
|
|
|
11833
12872
|
function deriveServiceState(deps) {
|
|
11834
12873
|
if (deps.shuttingDown)
|
|
11835
12874
|
return 'stopping';
|
|
11836
|
-
if (deps.queue.current)
|
|
12875
|
+
if (deps.queue.current || deps.queue.currentPhase)
|
|
11837
12876
|
return 'synthesizing';
|
|
11838
|
-
if (deps.queue.depth > 0)
|
|
12877
|
+
if (deps.queue.depth > 0 || deps.queue.overrides.length > 0)
|
|
11839
12878
|
return 'waiting';
|
|
11840
12879
|
return 'idle';
|
|
11841
12880
|
}
|
|
12881
|
+
function emptyPhaseCounts() {
|
|
12882
|
+
return { fresh: 0, stale: 0, pending: 0, running: 0, failed: 0 };
|
|
12883
|
+
}
|
|
11842
12884
|
function registerStatusRoute(app, deps) {
|
|
11843
12885
|
const statusHandler = createStatusHandler({
|
|
11844
12886
|
name: SERVICE_NAME,
|
|
11845
12887
|
version: SERVICE_VERSION,
|
|
11846
12888
|
getHealth: async () => {
|
|
11847
|
-
const { config, queue, scheduler, stats } = deps;
|
|
12889
|
+
const { config, queue, scheduler, stats, watcher } = deps;
|
|
11848
12890
|
// On-demand dependency checks
|
|
11849
12891
|
const [watcherHealth, gatewayHealth] = await Promise.all([
|
|
11850
12892
|
checkWatcher(config.watcherUrl),
|
|
11851
12893
|
checkDependency(config.gatewayUrl, '/status'),
|
|
11852
12894
|
]);
|
|
12895
|
+
// Phase state summary
|
|
12896
|
+
const phaseStateSummary = {
|
|
12897
|
+
architect: emptyPhaseCounts(),
|
|
12898
|
+
builder: emptyPhaseCounts(),
|
|
12899
|
+
critic: emptyPhaseCounts(),
|
|
12900
|
+
};
|
|
12901
|
+
let nextPhase = null;
|
|
12902
|
+
try {
|
|
12903
|
+
const metaResult = await listMetas(config, watcher);
|
|
12904
|
+
// Count raw phase states (before retry) for display
|
|
12905
|
+
for (const entry of metaResult.entries) {
|
|
12906
|
+
const ps = derivePhaseState(entry.meta);
|
|
12907
|
+
for (const phase of ['architect', 'builder', 'critic']) {
|
|
12908
|
+
phaseStateSummary[phase][ps[phase]]++;
|
|
12909
|
+
}
|
|
12910
|
+
}
|
|
12911
|
+
// Build candidates (with auto-retry) for scheduling
|
|
12912
|
+
const candidates = buildPhaseCandidates(metaResult.entries);
|
|
12913
|
+
// Find next phase candidate
|
|
12914
|
+
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
12915
|
+
if (winner) {
|
|
12916
|
+
nextPhase = {
|
|
12917
|
+
path: winner.node.metaPath,
|
|
12918
|
+
phase: winner.owedPhase,
|
|
12919
|
+
band: winner.band,
|
|
12920
|
+
staleness: winner.effectiveStaleness,
|
|
12921
|
+
};
|
|
12922
|
+
}
|
|
12923
|
+
}
|
|
12924
|
+
catch {
|
|
12925
|
+
// Watcher unreachable — phase summary unavailable
|
|
12926
|
+
}
|
|
11853
12927
|
return {
|
|
11854
12928
|
serviceState: deriveServiceState(deps),
|
|
11855
|
-
currentTarget: queue.current?.path ?? null,
|
|
12929
|
+
currentTarget: queue.current?.path ?? queue.currentPhase?.path ?? null,
|
|
11856
12930
|
queue: queue.getState(),
|
|
11857
12931
|
stats: {
|
|
11858
12932
|
totalSyntheses: stats.totalSyntheses,
|
|
@@ -11872,6 +12946,8 @@ function registerStatusRoute(app, deps) {
|
|
|
11872
12946
|
},
|
|
11873
12947
|
gateway: gatewayHealth,
|
|
11874
12948
|
},
|
|
12949
|
+
phaseStateSummary,
|
|
12950
|
+
nextPhase,
|
|
11875
12951
|
};
|
|
11876
12952
|
},
|
|
11877
12953
|
});
|
|
@@ -11884,6 +12960,9 @@ function registerStatusRoute(app, deps) {
|
|
|
11884
12960
|
/**
|
|
11885
12961
|
* POST /synthesize route handler.
|
|
11886
12962
|
*
|
|
12963
|
+
* Path-targeted triggers create explicit override entries in the queue.
|
|
12964
|
+
* Corpus-wide triggers discover the stalest candidate.
|
|
12965
|
+
*
|
|
11887
12966
|
* @module routes/synthesize
|
|
11888
12967
|
*/
|
|
11889
12968
|
const synthesizeBodySchema = z.object({
|
|
@@ -11894,44 +12973,70 @@ function registerSynthesizeRoute(app, deps) {
|
|
|
11894
12973
|
app.post('/synthesize', async (request, reply) => {
|
|
11895
12974
|
const body = synthesizeBodySchema.parse(request.body);
|
|
11896
12975
|
const { config, watcher, queue } = deps;
|
|
11897
|
-
let targetPath;
|
|
11898
12976
|
if (body.path) {
|
|
11899
|
-
|
|
11900
|
-
|
|
11901
|
-
|
|
11902
|
-
|
|
11903
|
-
let
|
|
12977
|
+
// Path-targeted trigger: create override entry
|
|
12978
|
+
const targetPath = resolveMetaDir(body.path);
|
|
12979
|
+
// Read meta to determine owed phase
|
|
12980
|
+
let owedPhase = null;
|
|
12981
|
+
let meta;
|
|
11904
12982
|
try {
|
|
11905
|
-
|
|
12983
|
+
meta = await readMetaJson(targetPath);
|
|
12984
|
+
const phaseState = derivePhaseState(meta);
|
|
12985
|
+
owedPhase = getOwedPhase(phaseState);
|
|
11906
12986
|
}
|
|
11907
12987
|
catch {
|
|
11908
|
-
|
|
11909
|
-
error: 'SERVICE_UNAVAILABLE',
|
|
11910
|
-
message: 'Watcher unreachable — cannot discover candidates',
|
|
11911
|
-
});
|
|
12988
|
+
// Meta unreadable — proceed, phase will be evaluated at dequeue time
|
|
11912
12989
|
}
|
|
11913
|
-
|
|
11914
|
-
|
|
11915
|
-
.
|
|
11916
|
-
node: e.node,
|
|
11917
|
-
meta: e.meta,
|
|
11918
|
-
actualStaleness: e.stalenessSeconds,
|
|
11919
|
-
}));
|
|
11920
|
-
const stalest = discoverStalestPath(stale, config.depthWeight);
|
|
11921
|
-
if (!stalest) {
|
|
11922
|
-
return reply.code(200).send({
|
|
12990
|
+
// Fully fresh meta → skip (reuse meta already read above)
|
|
12991
|
+
if (owedPhase === null && meta && (meta._phaseState || meta._content)) {
|
|
12992
|
+
return await reply.code(200).send({
|
|
11923
12993
|
status: 'skipped',
|
|
11924
|
-
|
|
12994
|
+
path: targetPath,
|
|
12995
|
+
owedPhase: null,
|
|
12996
|
+
queuePosition: -1,
|
|
12997
|
+
alreadyQueued: false,
|
|
11925
12998
|
});
|
|
11926
12999
|
}
|
|
11927
|
-
|
|
13000
|
+
const result = queue.enqueueOverride(targetPath);
|
|
13001
|
+
return reply.code(202).send({
|
|
13002
|
+
status: 'queued',
|
|
13003
|
+
path: targetPath,
|
|
13004
|
+
owedPhase,
|
|
13005
|
+
queuePosition: result.position,
|
|
13006
|
+
alreadyQueued: result.alreadyQueued,
|
|
13007
|
+
});
|
|
13008
|
+
}
|
|
13009
|
+
// Corpus-wide trigger: discover stalest candidate
|
|
13010
|
+
let result;
|
|
13011
|
+
try {
|
|
13012
|
+
result = await listMetas(config, watcher);
|
|
11928
13013
|
}
|
|
11929
|
-
|
|
13014
|
+
catch {
|
|
13015
|
+
return reply.status(503).send({
|
|
13016
|
+
error: 'SERVICE_UNAVAILABLE',
|
|
13017
|
+
message: 'Watcher unreachable — cannot discover candidates',
|
|
13018
|
+
});
|
|
13019
|
+
}
|
|
13020
|
+
const stale = result.entries
|
|
13021
|
+
.filter((e) => e.stalenessSeconds > 0 && !e.disabled)
|
|
13022
|
+
.map((e) => ({
|
|
13023
|
+
node: e.node,
|
|
13024
|
+
meta: e.meta,
|
|
13025
|
+
actualStaleness: e.stalenessSeconds,
|
|
13026
|
+
}));
|
|
13027
|
+
const stalest = discoverStalestPath(stale, config.depthWeight);
|
|
13028
|
+
if (!stalest) {
|
|
13029
|
+
return reply.code(200).send({
|
|
13030
|
+
status: 'skipped',
|
|
13031
|
+
message: 'No stale metas found. Nothing to synthesize.',
|
|
13032
|
+
});
|
|
13033
|
+
}
|
|
13034
|
+
const enqueueResult = queue.enqueue(stalest);
|
|
11930
13035
|
return reply.code(202).send({
|
|
11931
13036
|
status: 'accepted',
|
|
11932
|
-
path:
|
|
11933
|
-
queuePosition:
|
|
11934
|
-
alreadyQueued:
|
|
13037
|
+
path: stalest,
|
|
13038
|
+
queuePosition: enqueueResult.position,
|
|
13039
|
+
alreadyQueued: enqueueResult.alreadyQueued,
|
|
11935
13040
|
});
|
|
11936
13041
|
});
|
|
11937
13042
|
}
|
|
@@ -11969,6 +13074,17 @@ function registerUnlockRoute(app, deps) {
|
|
|
11969
13074
|
*
|
|
11970
13075
|
* @module routes
|
|
11971
13076
|
*/
|
|
13077
|
+
/**
|
|
13078
|
+
* Large generated fields excluded from detail/update response projections.
|
|
13079
|
+
* Shared between metas detail and metasUpdate routes.
|
|
13080
|
+
*/
|
|
13081
|
+
const DEFAULT_EXCLUDE_FIELDS = new Set([
|
|
13082
|
+
'_architect',
|
|
13083
|
+
'_builder',
|
|
13084
|
+
'_critic',
|
|
13085
|
+
'_content',
|
|
13086
|
+
'_feedback',
|
|
13087
|
+
]);
|
|
11972
13088
|
/** Register all HTTP routes on the Fastify instance. */
|
|
11973
13089
|
function registerRoutes(app, deps) {
|
|
11974
13090
|
// Global error handler for validation + watcher errors
|
|
@@ -11990,6 +13106,7 @@ function registerRoutes(app, deps) {
|
|
|
11990
13106
|
});
|
|
11991
13107
|
registerStatusRoute(app, deps);
|
|
11992
13108
|
registerMetasRoutes(app, deps);
|
|
13109
|
+
registerMetasUpdateRoute(app);
|
|
11993
13110
|
registerSynthesizeRoute(app, deps);
|
|
11994
13111
|
registerPreviewRoute(app, deps);
|
|
11995
13112
|
registerSeedRoute(app, deps);
|
|
@@ -12222,10 +13339,9 @@ async function startService(config, configPath) {
|
|
|
12222
13339
|
}
|
|
12223
13340
|
// Progress reporter — uses shared config reference so hot-reload propagates
|
|
12224
13341
|
const progress = new ProgressReporter(config, logger);
|
|
12225
|
-
// Wire queue processing —
|
|
13342
|
+
// Wire queue processing — execute one phase per dequeue (phase-state machine)
|
|
12226
13343
|
const synthesizeFn = async (path) => {
|
|
12227
13344
|
const startMs = Date.now();
|
|
12228
|
-
let cycleTokens = 0;
|
|
12229
13345
|
// Strip .meta suffix for human-readable progress reporting
|
|
12230
13346
|
const ownerPath = path.replace(/\/?\.meta\/?$/, '');
|
|
12231
13347
|
await progress.report({
|
|
@@ -12233,49 +13349,50 @@ async function startService(config, configPath) {
|
|
|
12233
13349
|
path: ownerPath,
|
|
12234
13350
|
});
|
|
12235
13351
|
try {
|
|
12236
|
-
const
|
|
13352
|
+
const result = await orchestratePhase(config, executor, watcher, path, async (evt) => {
|
|
13353
|
+
// Wire current-phase tracking for GET /queue and POST /synthesize/abort
|
|
13354
|
+
if (evt.type === 'phase_start' && evt.phase) {
|
|
13355
|
+
queue.setCurrentPhase(ownerPath, evt.phase);
|
|
13356
|
+
}
|
|
12237
13357
|
// Track token stats from phase completions
|
|
12238
13358
|
if (evt.type === 'phase_complete') {
|
|
12239
13359
|
if (evt.tokens !== undefined) {
|
|
12240
13360
|
stats.totalTokens += evt.tokens;
|
|
12241
|
-
if (cycleTokens !== undefined) {
|
|
12242
|
-
cycleTokens += evt.tokens;
|
|
12243
|
-
}
|
|
12244
13361
|
}
|
|
12245
13362
|
else {
|
|
12246
|
-
cycleTokens = undefined;
|
|
12247
13363
|
logger.warn({ path: ownerPath, phase: evt.phase }, 'Token count unavailable (session lookup may have timed out)');
|
|
12248
13364
|
}
|
|
12249
13365
|
}
|
|
12250
13366
|
await progress.report(evt);
|
|
12251
13367
|
}, logger);
|
|
12252
|
-
// orchestrate() always returns exactly one result
|
|
12253
|
-
const result = results[0];
|
|
12254
13368
|
const durationMs = Date.now() - startMs;
|
|
12255
|
-
if (!result.
|
|
12256
|
-
|
|
12257
|
-
logger.debug({ path: ownerPath }, 'Synthesis skipped');
|
|
13369
|
+
if (!result.executed) {
|
|
13370
|
+
logger.debug({ path: ownerPath }, 'Phase skipped (fully fresh or locked)');
|
|
12258
13371
|
return;
|
|
12259
13372
|
}
|
|
12260
13373
|
// Update stats
|
|
12261
13374
|
stats.totalSyntheses++;
|
|
12262
13375
|
stats.lastCycleDurationMs = durationMs;
|
|
12263
13376
|
stats.lastCycleAt = new Date().toISOString();
|
|
12264
|
-
|
|
13377
|
+
const phaseResult = result.phaseResult;
|
|
13378
|
+
if (phaseResult?.error) {
|
|
12265
13379
|
stats.totalErrors++;
|
|
12266
13380
|
await progress.report({
|
|
12267
13381
|
type: 'error',
|
|
12268
13382
|
path: ownerPath,
|
|
12269
|
-
phase:
|
|
12270
|
-
error:
|
|
13383
|
+
phase: phaseResult.error.step,
|
|
13384
|
+
error: phaseResult.error.message,
|
|
12271
13385
|
});
|
|
12272
13386
|
}
|
|
12273
13387
|
else {
|
|
13388
|
+
// Task #9: Reset backoff on ANY successful phase execution
|
|
12274
13389
|
scheduler.resetBackoff();
|
|
13390
|
+
}
|
|
13391
|
+
// Emit synthesis_complete only on full-cycle completion
|
|
13392
|
+
if (result.cycleComplete) {
|
|
12275
13393
|
await progress.report({
|
|
12276
13394
|
type: 'synthesis_complete',
|
|
12277
13395
|
path: ownerPath,
|
|
12278
|
-
tokens: cycleTokens,
|
|
12279
13396
|
durationMs,
|
|
12280
13397
|
});
|
|
12281
13398
|
}
|
|
@@ -12395,4 +13512,4 @@ const metaDescriptor = jeevesComponentDescriptorSchema.parse({
|
|
|
12395
13512
|
customCliCommands: registerCustomCliCommands,
|
|
12396
13513
|
});
|
|
12397
13514
|
|
|
12398
|
-
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 };
|
|
13515
|
+
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 };
|