@karmaniverous/jeeves-meta 0.15.12 → 0.16.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 +1 -1
- package/dist/cli/jeeves-meta/architect.md +1 -4
- package/dist/cli/jeeves-meta/index.js +235 -321
- package/dist/executor/GatewayExecutor.d.ts +26 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +235 -327
- package/dist/interfaces/MetaExecutor.d.ts +0 -2
- package/dist/prompts/architect.md +1 -4
- package/dist/queue/index.d.ts +25 -66
- package/dist/routes/metasUpdate.d.ts +1 -1
- package/dist/routes/queue.d.ts +2 -2
- package/dist/schema/config.d.ts +0 -9
- package/dist/schema/meta.d.ts +0 -3
- package/dist/seed/createMeta.d.ts +0 -6
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1271,12 +1271,6 @@ const autoSeedRuleSchema = z.object({
|
|
|
1271
1271
|
crossRefs: z.array(z.string()).optional(),
|
|
1272
1272
|
/** Walk up this many extra parent levels from the matched file's directory. Default 0. */
|
|
1273
1273
|
parentDepth: z.number().int().min(0).optional(),
|
|
1274
|
-
/** Per-category timeout override for the architect phase (seconds, min 30). */
|
|
1275
|
-
architectTimeout: z.number().int().min(30).optional(),
|
|
1276
|
-
/** Per-category timeout override for the builder phase (seconds, min 30). */
|
|
1277
|
-
builderTimeout: z.number().int().min(30).optional(),
|
|
1278
|
-
/** Per-category timeout override for the critic phase (seconds, min 30). */
|
|
1279
|
-
criticTimeout: z.number().int().min(30).optional(),
|
|
1280
1274
|
});
|
|
1281
1275
|
/** Zod schema for jeeves-meta service configuration (superset of MetaConfig). */
|
|
1282
1276
|
const serviceConfigSchema = metaConfigSchema.extend({
|
|
@@ -1459,7 +1453,7 @@ class SpawnTimeoutError extends Error {
|
|
|
1459
1453
|
* @module executor/GatewayExecutor
|
|
1460
1454
|
*/
|
|
1461
1455
|
const DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
1462
|
-
const
|
|
1456
|
+
const DEFAULT_SAFETY_VALVE_MS = 3_600_000; // 1 hour fallback
|
|
1463
1457
|
/**
|
|
1464
1458
|
* MetaExecutor that spawns OpenClaw sessions via the gateway's
|
|
1465
1459
|
* `/tools/invoke` endpoint.
|
|
@@ -1490,6 +1484,46 @@ class GatewayExecutor {
|
|
|
1490
1484
|
/* best-effort cleanup */
|
|
1491
1485
|
}
|
|
1492
1486
|
}
|
|
1487
|
+
/** Read and clean up the staging output file. Returns content or undefined if absent. */
|
|
1488
|
+
readStagingFile(outputPath) {
|
|
1489
|
+
if (!existsSync(outputPath))
|
|
1490
|
+
return undefined;
|
|
1491
|
+
try {
|
|
1492
|
+
return readFileSync(outputPath, 'utf8');
|
|
1493
|
+
}
|
|
1494
|
+
finally {
|
|
1495
|
+
this.cleanupOutputFile(outputPath);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
/** Extract plain text from a message content field, skipping ANNOUNCE_SKIP sentinels. */
|
|
1499
|
+
static extractMessageText(content) {
|
|
1500
|
+
if (!content)
|
|
1501
|
+
return undefined;
|
|
1502
|
+
const text = typeof content === 'string'
|
|
1503
|
+
? content
|
|
1504
|
+
: Array.isArray(content)
|
|
1505
|
+
? content
|
|
1506
|
+
.filter((b) => b.type === 'text' && b.text)
|
|
1507
|
+
.map((b) => b.text)
|
|
1508
|
+
.join('\n')
|
|
1509
|
+
: '';
|
|
1510
|
+
return text && text.trim() !== 'ANNOUNCE_SKIP' ? text : undefined;
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Check history messages for timeout detection.
|
|
1514
|
+
*
|
|
1515
|
+
* Does NOT determine completion — session completion is authoritative
|
|
1516
|
+
* (via sessions_list). History stop reasons can false-positive on
|
|
1517
|
+
* sessions_yield artifacts (#200).
|
|
1518
|
+
*/
|
|
1519
|
+
static checkHistoryCompletion(messages) {
|
|
1520
|
+
if (messages.length === 0)
|
|
1521
|
+
return { timedOut: false };
|
|
1522
|
+
const last = messages[messages.length - 1];
|
|
1523
|
+
if (last.role !== 'assistant' || !last.stopReason)
|
|
1524
|
+
return { timedOut: false };
|
|
1525
|
+
return { timedOut: last.stopReason === 'timeout' };
|
|
1526
|
+
}
|
|
1493
1527
|
/** Invoke a gateway tool via the /tools/invoke HTTP endpoint. */
|
|
1494
1528
|
async invoke(tool, args, sessionKey) {
|
|
1495
1529
|
const headers = {
|
|
@@ -1516,7 +1550,13 @@ class GatewayExecutor {
|
|
|
1516
1550
|
}
|
|
1517
1551
|
return data;
|
|
1518
1552
|
}
|
|
1519
|
-
/**
|
|
1553
|
+
/**
|
|
1554
|
+
* Look up session metadata (tokens, completion status) via sessions_list.
|
|
1555
|
+
*
|
|
1556
|
+
* Detects gateway-side timeout (`status: "timeout"`) and killed sessions
|
|
1557
|
+
* (`status: "killed"`) as completed, with a `timedOut` flag to distinguish
|
|
1558
|
+
* timeout from normal completion.
|
|
1559
|
+
*/
|
|
1520
1560
|
async getSessionInfo(sessionKey) {
|
|
1521
1561
|
try {
|
|
1522
1562
|
const result = await this.invoke('sessions_list', {
|
|
@@ -1532,13 +1572,18 @@ class GatewayExecutor {
|
|
|
1532
1572
|
// With limit=200 this is reliable; a false positive here only
|
|
1533
1573
|
// means we read the output file slightly early (still correct
|
|
1534
1574
|
// if the file exists).
|
|
1535
|
-
return { completed: true };
|
|
1575
|
+
return { completed: true, timedOut: false };
|
|
1536
1576
|
}
|
|
1537
|
-
const
|
|
1538
|
-
|
|
1577
|
+
const status = match.status;
|
|
1578
|
+
const done = status === 'completed' ||
|
|
1579
|
+
status === 'done' ||
|
|
1580
|
+
status === 'timeout' ||
|
|
1581
|
+
status === 'killed';
|
|
1582
|
+
const timedOut = status === 'timeout';
|
|
1583
|
+
return { tokens: match.totalTokens, completed: done, timedOut };
|
|
1539
1584
|
}
|
|
1540
1585
|
catch {
|
|
1541
|
-
return { completed: false };
|
|
1586
|
+
return { completed: false, timedOut: false };
|
|
1542
1587
|
}
|
|
1543
1588
|
}
|
|
1544
1589
|
/** Whether this executor has been aborted by the operator. */
|
|
@@ -1549,12 +1594,38 @@ class GatewayExecutor {
|
|
|
1549
1594
|
abort() {
|
|
1550
1595
|
this.controller.abort();
|
|
1551
1596
|
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Query the gateway's configured subagent run timeout.
|
|
1599
|
+
*
|
|
1600
|
+
* Returns the value in milliseconds, or `undefined` if the query fails
|
|
1601
|
+
* or the value is absent/zero (no timeout configured).
|
|
1602
|
+
*/
|
|
1603
|
+
async queryGatewayRunTimeout() {
|
|
1604
|
+
try {
|
|
1605
|
+
const result = await this.invoke('session_status', {});
|
|
1606
|
+
const details = (result.result?.details ?? result.result ?? {});
|
|
1607
|
+
const runTimeoutSeconds = details.runTimeoutSeconds ??
|
|
1608
|
+
details.timeout;
|
|
1609
|
+
if (typeof runTimeoutSeconds === 'number' && runTimeoutSeconds > 0) {
|
|
1610
|
+
return runTimeoutSeconds * 1000;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
catch {
|
|
1614
|
+
// Gateway unreachable or field not exposed — fall back to default
|
|
1615
|
+
}
|
|
1616
|
+
return undefined;
|
|
1617
|
+
}
|
|
1552
1618
|
async spawn(task, options) {
|
|
1553
1619
|
// Fresh controller for each spawn call
|
|
1554
1620
|
this.controller = new AbortController();
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1621
|
+
// Safety-valve deadline: gateway's runTimeoutSeconds + 60s buffer,
|
|
1622
|
+
// defaulting to 1 hour if the gateway value is 0/absent/query fails.
|
|
1623
|
+
// This is a circuit breaker, not a timeout mechanism.
|
|
1624
|
+
const gatewayTimeoutMs = await this.queryGatewayRunTimeout();
|
|
1625
|
+
const safetyValveMs = gatewayTimeoutMs
|
|
1626
|
+
? gatewayTimeoutMs + 60_000
|
|
1627
|
+
: DEFAULT_SAFETY_VALVE_MS;
|
|
1628
|
+
const safetyDeadline = Date.now() + safetyValveMs;
|
|
1558
1629
|
// Ensure workspace dir exists
|
|
1559
1630
|
if (!existsSync(this.workspaceDir)) {
|
|
1560
1631
|
mkdirSync(this.workspaceDir, { recursive: true });
|
|
@@ -1576,7 +1647,6 @@ class GatewayExecutor {
|
|
|
1576
1647
|
const spawnResult = await this.invoke('sessions_spawn', {
|
|
1577
1648
|
task: taskWithOutput,
|
|
1578
1649
|
label,
|
|
1579
|
-
runTimeoutSeconds: timeoutSeconds,
|
|
1580
1650
|
...(options?.thinking ? { thinking: options.thinking } : {}),
|
|
1581
1651
|
...(options?.model ? { model: options.model } : {}),
|
|
1582
1652
|
});
|
|
@@ -1588,9 +1658,18 @@ class GatewayExecutor {
|
|
|
1588
1658
|
throw new Error('Gateway sessions_spawn returned no sessionKey: ' +
|
|
1589
1659
|
JSON.stringify(spawnResult));
|
|
1590
1660
|
}
|
|
1591
|
-
// Step 2: Poll for completion
|
|
1661
|
+
// Step 2: Poll for completion — gateway owns the subagent lifecycle.
|
|
1662
|
+
// Loop exits via: (a) completion detection, (b) abort signal,
|
|
1663
|
+
// (c) gateway-side timeout detection, or (d) safety-valve circuit breaker.
|
|
1592
1664
|
await sleepAsync(3000);
|
|
1593
|
-
while (
|
|
1665
|
+
while (true) {
|
|
1666
|
+
// Safety-valve circuit breaker
|
|
1667
|
+
if (Date.now() >= safetyDeadline) {
|
|
1668
|
+
this.cleanupOutputFile(outputPath);
|
|
1669
|
+
throw new SpawnTimeoutError('Safety-valve deadline exceeded (' +
|
|
1670
|
+
safetyValveMs.toString() +
|
|
1671
|
+
'ms) — gateway timeout may be misconfigured', outputPath);
|
|
1672
|
+
}
|
|
1594
1673
|
// Check for abort before each poll iteration
|
|
1595
1674
|
if (this.controller.signal.aborted) {
|
|
1596
1675
|
this.cleanupOutputFile(outputPath);
|
|
@@ -1606,63 +1685,51 @@ class GatewayExecutor {
|
|
|
1606
1685
|
historyResult.result?.messages ??
|
|
1607
1686
|
[];
|
|
1608
1687
|
const msgArray = messages;
|
|
1609
|
-
// Check 1:
|
|
1610
|
-
|
|
1611
|
-
if (msgArray.length > 0) {
|
|
1612
|
-
const lastMsg = msgArray[msgArray.length - 1];
|
|
1613
|
-
if (lastMsg.role === 'assistant' &&
|
|
1614
|
-
lastMsg.stopReason &&
|
|
1615
|
-
lastMsg.stopReason !== 'toolUse' &&
|
|
1616
|
-
lastMsg.stopReason !== 'error') {
|
|
1617
|
-
historyDone = true;
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1688
|
+
// Check 1: history-based timeout detection (stop reason)
|
|
1689
|
+
const { timedOut: historyTimedOut } = GatewayExecutor.checkHistoryCompletion(msgArray);
|
|
1620
1690
|
// Check 2: session completion status via sessions_list
|
|
1691
|
+
// Gate on session completion only — history stop reasons can
|
|
1692
|
+
// false-positive on sessions_yield artifacts (#200).
|
|
1621
1693
|
const sessionInfo = await this.getSessionInfo(sessionKey);
|
|
1622
|
-
|
|
1694
|
+
const timedOut = sessionInfo.timedOut || historyTimedOut;
|
|
1695
|
+
if (sessionInfo.completed) {
|
|
1623
1696
|
const tokens = sessionInfo.tokens;
|
|
1624
|
-
//
|
|
1625
|
-
if (
|
|
1626
|
-
|
|
1627
|
-
|
|
1697
|
+
// Gateway-side timeout detected — check staging file for recovery
|
|
1698
|
+
if (timedOut) {
|
|
1699
|
+
const output = this.readStagingFile(outputPath);
|
|
1700
|
+
if (output !== undefined)
|
|
1628
1701
|
return { output, tokens };
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
try {
|
|
1632
|
-
unlinkSync(outputPath);
|
|
1633
|
-
}
|
|
1634
|
-
catch {
|
|
1635
|
-
/* cleanup best-effort */
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1702
|
+
// No output or partial output — throw for _state recovery (§3.16.6)
|
|
1703
|
+
throw new SpawnTimeoutError('Gateway-side timeout detected (session status: timeout)', outputPath);
|
|
1638
1704
|
}
|
|
1705
|
+
// Normal completion — read output from file
|
|
1706
|
+
const output = this.readStagingFile(outputPath);
|
|
1707
|
+
if (output !== undefined)
|
|
1708
|
+
return { output, tokens };
|
|
1639
1709
|
// Fallback: extract from message content if file wasn't written.
|
|
1640
1710
|
// Skip ANNOUNCE_SKIP sentinel messages — the real output is in
|
|
1641
1711
|
// a preceding assistant message (the file write).
|
|
1642
1712
|
for (let i = msgArray.length - 1; i >= 0; i--) {
|
|
1643
1713
|
const msg = msgArray[i];
|
|
1644
|
-
if (msg.role === 'assistant'
|
|
1645
|
-
const text =
|
|
1646
|
-
|
|
1647
|
-
: Array.isArray(msg.content)
|
|
1648
|
-
? msg.content
|
|
1649
|
-
.filter((b) => b.type === 'text' && b.text)
|
|
1650
|
-
.map((b) => b.text)
|
|
1651
|
-
.join('\n')
|
|
1652
|
-
: '';
|
|
1653
|
-
if (text && text.trim() !== 'ANNOUNCE_SKIP')
|
|
1714
|
+
if (msg.role === 'assistant') {
|
|
1715
|
+
const text = GatewayExecutor.extractMessageText(msg.content);
|
|
1716
|
+
if (text !== undefined)
|
|
1654
1717
|
return { output: text, tokens };
|
|
1655
1718
|
}
|
|
1656
1719
|
}
|
|
1657
1720
|
return { output: '', tokens };
|
|
1658
1721
|
}
|
|
1659
1722
|
}
|
|
1660
|
-
catch {
|
|
1723
|
+
catch (err) {
|
|
1724
|
+
// Re-throw SpawnTimeoutError and SpawnAbortedError — only swallow transient poll failures
|
|
1725
|
+
if (err instanceof SpawnTimeoutError ||
|
|
1726
|
+
err instanceof SpawnAbortedError) {
|
|
1727
|
+
throw err;
|
|
1728
|
+
}
|
|
1661
1729
|
// Transient poll failure — keep trying
|
|
1662
1730
|
}
|
|
1663
1731
|
await sleepAsync(this.pollIntervalMs);
|
|
1664
1732
|
}
|
|
1665
|
-
throw new SpawnTimeoutError('Synthesis subprocess timed out after ' + timeoutMs.toString() + 'ms', outputPath);
|
|
1666
1733
|
}
|
|
1667
1734
|
}
|
|
1668
1735
|
|
|
@@ -2884,7 +2951,6 @@ async function runArchitect(node, currentMeta, phaseState, config, executor, wat
|
|
|
2884
2951
|
const architectTask = buildArchitectTask(ctx, currentMeta, config);
|
|
2885
2952
|
const result = await executor.spawn(architectTask, {
|
|
2886
2953
|
thinking: config.thinking,
|
|
2887
|
-
timeout: currentMeta._architectTimeout ?? config.architectTimeout,
|
|
2888
2954
|
label: 'meta-architect',
|
|
2889
2955
|
});
|
|
2890
2956
|
const builderBrief = parseArchitectOutput(result.output);
|
|
@@ -2936,7 +3002,6 @@ async function runBuilder(node, currentMeta, phaseState, config, executor, watch
|
|
|
2936
3002
|
const builderTask = buildBuilderTask(ctx, currentMeta, config);
|
|
2937
3003
|
const result = await executor.spawn(builderTask, {
|
|
2938
3004
|
thinking: config.thinking,
|
|
2939
|
-
timeout: currentMeta._builderTimeout ?? config.builderTimeout,
|
|
2940
3005
|
label: 'meta-builder',
|
|
2941
3006
|
});
|
|
2942
3007
|
const rawOutput = result.output;
|
|
@@ -3031,7 +3096,6 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
|
|
|
3031
3096
|
const criticTask = buildCriticTask(ctx, metaForCritic, config);
|
|
3032
3097
|
const result = await executor.spawn(criticTask, {
|
|
3033
3098
|
thinking: config.thinking,
|
|
3034
|
-
timeout: currentMeta._criticTimeout ?? config.criticTimeout,
|
|
3035
3099
|
label: 'meta-critic',
|
|
3036
3100
|
});
|
|
3037
3101
|
const feedback = parseCriticOutput(result.output);
|
|
@@ -3367,36 +3431,33 @@ class ProgressReporter {
|
|
|
3367
3431
|
}
|
|
3368
3432
|
|
|
3369
3433
|
/**
|
|
3370
|
-
*
|
|
3434
|
+
* Synthesis queue.
|
|
3371
3435
|
*
|
|
3372
3436
|
* Layer 1: Current — the single item currently executing (at most one).
|
|
3373
|
-
* Layer 2:
|
|
3374
|
-
*
|
|
3375
|
-
* Layer 3: Automatic — computed on read, not persisted. All
|
|
3376
|
-
* pending phase, ranked by scheduler priority.
|
|
3377
|
-
*
|
|
3378
|
-
* Legacy: `pending` array is the union of overrides + automatic.
|
|
3437
|
+
* Layer 2: Pending — items enqueued via POST /synthesize (targeted) or
|
|
3438
|
+
* scheduler tick (automatic). FIFO, ahead of automatic candidates.
|
|
3439
|
+
* Layer 3: Automatic — computed on read (GET /queue), not persisted. All
|
|
3440
|
+
* metas with a pending phase, ranked by scheduler priority.
|
|
3379
3441
|
*
|
|
3380
3442
|
* @module queue
|
|
3381
3443
|
*/
|
|
3382
3444
|
const DEPTH_WARNING_THRESHOLD = 3;
|
|
3445
|
+
/** Strip trailing .meta suffix for consistent path comparison. */
|
|
3446
|
+
function normQueuePath(p) {
|
|
3447
|
+
return p.endsWith('.meta') ? p.slice(0, -5).replace(/[/\\]$/, '') : p;
|
|
3448
|
+
}
|
|
3383
3449
|
/**
|
|
3384
|
-
*
|
|
3450
|
+
* Synthesis queue.
|
|
3385
3451
|
*
|
|
3386
|
-
* Only one synthesis runs at a time.
|
|
3387
|
-
* take priority over automatic candidates.
|
|
3452
|
+
* Only one synthesis runs at a time. Explicitly enqueued items
|
|
3453
|
+
* take priority over automatic (computed-on-read) candidates.
|
|
3388
3454
|
*/
|
|
3389
3455
|
class SynthesisQueue {
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
currentItem = null;
|
|
3456
|
+
entries = [];
|
|
3457
|
+
currentPhaseItem = null;
|
|
3393
3458
|
processing = false;
|
|
3394
3459
|
logger;
|
|
3395
3460
|
onEnqueueCallback = null;
|
|
3396
|
-
/** Explicit override entries (3-layer model). */
|
|
3397
|
-
overrideEntries = [];
|
|
3398
|
-
/** Currently executing item with phase info (3-layer model). */
|
|
3399
|
-
currentPhaseItem = null;
|
|
3400
3461
|
constructor(logger) {
|
|
3401
3462
|
this.logger = logger;
|
|
3402
3463
|
}
|
|
@@ -3404,52 +3465,67 @@ class SynthesisQueue {
|
|
|
3404
3465
|
onEnqueue(callback) {
|
|
3405
3466
|
this.onEnqueueCallback = callback;
|
|
3406
3467
|
}
|
|
3407
|
-
// ──
|
|
3468
|
+
// ── Enqueue / dequeue ──────────────────────────────────────────────
|
|
3408
3469
|
/**
|
|
3409
|
-
* Add an
|
|
3470
|
+
* Add an entry to the queue.
|
|
3410
3471
|
* Deduped by path. Returns position and whether already queued.
|
|
3411
3472
|
*/
|
|
3412
|
-
|
|
3473
|
+
enqueue(path) {
|
|
3474
|
+
const norm = normQueuePath(path);
|
|
3413
3475
|
// Check if currently executing
|
|
3414
|
-
if (this.currentPhaseItem
|
|
3415
|
-
this.
|
|
3476
|
+
if (this.currentPhaseItem &&
|
|
3477
|
+
normQueuePath(this.currentPhaseItem.path) === norm) {
|
|
3416
3478
|
return { position: 0, alreadyQueued: true };
|
|
3417
3479
|
}
|
|
3418
|
-
// Check if already in
|
|
3419
|
-
const existing = this.
|
|
3480
|
+
// Check if already in queue
|
|
3481
|
+
const existing = this.entries.findIndex((e) => normQueuePath(e.path) === norm);
|
|
3420
3482
|
if (existing !== -1) {
|
|
3421
3483
|
return { position: existing, alreadyQueued: true };
|
|
3422
3484
|
}
|
|
3423
|
-
this.
|
|
3485
|
+
this.entries.push({
|
|
3424
3486
|
path,
|
|
3425
3487
|
enqueuedAt: new Date().toISOString(),
|
|
3426
3488
|
});
|
|
3427
|
-
const position = this.
|
|
3428
|
-
if (this.
|
|
3429
|
-
this.logger.warn({ depth: this.
|
|
3489
|
+
const position = this.entries.length - 1;
|
|
3490
|
+
if (this.entries.length > DEPTH_WARNING_THRESHOLD) {
|
|
3491
|
+
this.logger.warn({ depth: this.entries.length }, 'Queue depth exceeds threshold');
|
|
3430
3492
|
}
|
|
3431
3493
|
this.onEnqueueCallback?.();
|
|
3432
3494
|
return { position, alreadyQueued: false };
|
|
3433
3495
|
}
|
|
3434
|
-
/** Dequeue the next
|
|
3435
|
-
|
|
3436
|
-
return this.
|
|
3496
|
+
/** Dequeue the next entry, or undefined if empty. */
|
|
3497
|
+
dequeue() {
|
|
3498
|
+
return this.entries.shift();
|
|
3437
3499
|
}
|
|
3438
|
-
/** Get all
|
|
3439
|
-
get
|
|
3440
|
-
return [...this.
|
|
3500
|
+
/** Get all queued entries (shallow copy). */
|
|
3501
|
+
get items() {
|
|
3502
|
+
return [...this.entries];
|
|
3441
3503
|
}
|
|
3442
|
-
/**
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3504
|
+
/** Number of items waiting in the queue (excludes current). */
|
|
3505
|
+
get depth() {
|
|
3506
|
+
return this.entries.length;
|
|
3507
|
+
}
|
|
3508
|
+
/**
|
|
3509
|
+
* Remove all pending items from the queue.
|
|
3510
|
+
* Does not affect the currently-running item.
|
|
3511
|
+
*
|
|
3512
|
+
* @returns The number of items removed.
|
|
3513
|
+
*/
|
|
3514
|
+
clear() {
|
|
3515
|
+
const count = this.entries.length;
|
|
3516
|
+
this.entries = [];
|
|
3446
3517
|
return count;
|
|
3447
3518
|
}
|
|
3448
|
-
/** Check
|
|
3449
|
-
|
|
3450
|
-
|
|
3519
|
+
/** Check whether a path is in the queue or currently being synthesized. */
|
|
3520
|
+
has(path) {
|
|
3521
|
+
const norm = normQueuePath(path);
|
|
3522
|
+
if (this.currentPhaseItem &&
|
|
3523
|
+
normQueuePath(this.currentPhaseItem.path) === norm) {
|
|
3524
|
+
return true;
|
|
3525
|
+
}
|
|
3526
|
+
return this.entries.some((e) => normQueuePath(e.path) === norm);
|
|
3451
3527
|
}
|
|
3452
|
-
// ── Current-item tracking
|
|
3528
|
+
// ── Current-item tracking ──────────────────────────────────────────
|
|
3453
3529
|
/** Set the currently executing phase item. */
|
|
3454
3530
|
setCurrentPhase(path, phase) {
|
|
3455
3531
|
this.currentPhaseItem = {
|
|
@@ -3466,129 +3542,21 @@ class SynthesisQueue {
|
|
|
3466
3542
|
get currentPhase() {
|
|
3467
3543
|
return this.currentPhaseItem;
|
|
3468
3544
|
}
|
|
3469
|
-
// ──
|
|
3470
|
-
/**
|
|
3471
|
-
* Add a path to the synthesis queue.
|
|
3472
|
-
*
|
|
3473
|
-
* @param path - Meta path to synthesize.
|
|
3474
|
-
* @param priority - If true, insert at front of queue.
|
|
3475
|
-
* @returns Position and whether the path was already queued.
|
|
3476
|
-
*/
|
|
3477
|
-
enqueue(path, priority = false) {
|
|
3478
|
-
if (this.currentItem?.path === path) {
|
|
3479
|
-
return { position: 0, alreadyQueued: true };
|
|
3480
|
-
}
|
|
3481
|
-
const existingIndex = this.queue.findIndex((item) => item.path === path);
|
|
3482
|
-
if (existingIndex !== -1) {
|
|
3483
|
-
return { position: existingIndex, alreadyQueued: true };
|
|
3484
|
-
}
|
|
3485
|
-
const item = {
|
|
3486
|
-
path,
|
|
3487
|
-
priority,
|
|
3488
|
-
enqueuedAt: new Date().toISOString(),
|
|
3489
|
-
};
|
|
3490
|
-
if (priority) {
|
|
3491
|
-
this.queue.unshift(item);
|
|
3492
|
-
}
|
|
3493
|
-
else {
|
|
3494
|
-
this.queue.push(item);
|
|
3495
|
-
}
|
|
3496
|
-
if (this.queue.length > DEPTH_WARNING_THRESHOLD) {
|
|
3497
|
-
this.logger.warn({ depth: this.queue.length }, 'Queue depth exceeds threshold');
|
|
3498
|
-
}
|
|
3499
|
-
const position = this.queue.findIndex((i) => i.path === path);
|
|
3500
|
-
this.onEnqueueCallback?.();
|
|
3501
|
-
return { position, alreadyQueued: false };
|
|
3502
|
-
}
|
|
3503
|
-
/** Remove and return the next item from the queue. */
|
|
3504
|
-
dequeue() {
|
|
3505
|
-
const item = this.queue.shift();
|
|
3506
|
-
if (item) {
|
|
3507
|
-
this.currentItem = item;
|
|
3508
|
-
}
|
|
3509
|
-
return item;
|
|
3510
|
-
}
|
|
3511
|
-
/** Mark the currently-running synthesis as complete. */
|
|
3512
|
-
complete() {
|
|
3513
|
-
this.currentItem = null;
|
|
3514
|
-
}
|
|
3515
|
-
/** Number of items waiting in the queue (excludes current). */
|
|
3516
|
-
get depth() {
|
|
3517
|
-
return this.queue.length;
|
|
3518
|
-
}
|
|
3519
|
-
/** The item currently being synthesized, or null. */
|
|
3520
|
-
get current() {
|
|
3521
|
-
return this.currentItem;
|
|
3522
|
-
}
|
|
3523
|
-
/** A shallow copy of the queued items. */
|
|
3524
|
-
get items() {
|
|
3525
|
-
return [...this.queue];
|
|
3526
|
-
}
|
|
3527
|
-
/** A shallow copy of the pending items (alias for items). */
|
|
3528
|
-
get pending() {
|
|
3529
|
-
return [...this.queue];
|
|
3530
|
-
}
|
|
3531
|
-
/**
|
|
3532
|
-
* Remove all pending items from the queue.
|
|
3533
|
-
* Does not affect the currently-running item.
|
|
3534
|
-
*
|
|
3535
|
-
* @returns The number of items removed.
|
|
3536
|
-
*/
|
|
3537
|
-
clear() {
|
|
3538
|
-
const count = this.queue.length;
|
|
3539
|
-
this.queue = [];
|
|
3540
|
-
return count;
|
|
3541
|
-
}
|
|
3542
|
-
/** Check whether a path is in the queue or currently being synthesized. */
|
|
3543
|
-
has(path) {
|
|
3544
|
-
if (this.currentItem?.path === path)
|
|
3545
|
-
return true;
|
|
3546
|
-
if (this.currentPhaseItem?.path === path)
|
|
3547
|
-
return true;
|
|
3548
|
-
return (this.queue.some((item) => item.path === path) ||
|
|
3549
|
-
this.overrideEntries.some((e) => e.path === path));
|
|
3550
|
-
}
|
|
3551
|
-
/** Get the 0-indexed position of a path in the queue. */
|
|
3552
|
-
getPosition(path) {
|
|
3553
|
-
// Check overrides first
|
|
3554
|
-
const overrideIdx = this.overrideEntries.findIndex((e) => e.path === path);
|
|
3555
|
-
if (overrideIdx !== -1)
|
|
3556
|
-
return overrideIdx;
|
|
3557
|
-
const index = this.queue.findIndex((item) => item.path === path);
|
|
3558
|
-
return index === -1 ? null : index;
|
|
3559
|
-
}
|
|
3560
|
-
/** Dequeue the next item: overrides first, then legacy queue. */
|
|
3561
|
-
nextItem() {
|
|
3562
|
-
const override = this.dequeueOverride();
|
|
3563
|
-
if (override)
|
|
3564
|
-
return { path: override.path, source: 'override' };
|
|
3565
|
-
const item = this.dequeue();
|
|
3566
|
-
if (item)
|
|
3567
|
-
return { path: item.path, source: 'legacy' };
|
|
3568
|
-
return undefined;
|
|
3569
|
-
}
|
|
3545
|
+
// ── Queue state ────────────────────────────────────────────────────
|
|
3570
3546
|
/** Return a snapshot of queue state for the /status endpoint. */
|
|
3571
3547
|
getState() {
|
|
3572
3548
|
return {
|
|
3573
|
-
depth: this.
|
|
3574
|
-
items:
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
enqueuedAt: e.enqueuedAt,
|
|
3579
|
-
})),
|
|
3580
|
-
...this.queue.map((item) => ({
|
|
3581
|
-
path: item.path,
|
|
3582
|
-
priority: item.priority,
|
|
3583
|
-
enqueuedAt: item.enqueuedAt,
|
|
3584
|
-
})),
|
|
3585
|
-
],
|
|
3549
|
+
depth: this.entries.length,
|
|
3550
|
+
items: this.entries.map((e) => ({
|
|
3551
|
+
path: e.path,
|
|
3552
|
+
enqueuedAt: e.enqueuedAt,
|
|
3553
|
+
})),
|
|
3586
3554
|
};
|
|
3587
3555
|
}
|
|
3556
|
+
// ── Processing ─────────────────────────────────────────────────────
|
|
3588
3557
|
/**
|
|
3589
|
-
* Process queued items one at a time until
|
|
3558
|
+
* Process queued items one at a time until the queue is empty.
|
|
3590
3559
|
*
|
|
3591
|
-
* Override entries are processed first (FIFO), then legacy queue items.
|
|
3592
3560
|
* Re-entry is prevented: if already processing, the call returns
|
|
3593
3561
|
* immediately. Errors are logged and do not block subsequent items.
|
|
3594
3562
|
*
|
|
@@ -3599,7 +3567,7 @@ class SynthesisQueue {
|
|
|
3599
3567
|
return;
|
|
3600
3568
|
this.processing = true;
|
|
3601
3569
|
try {
|
|
3602
|
-
let next = this.
|
|
3570
|
+
let next = this.dequeue();
|
|
3603
3571
|
while (next) {
|
|
3604
3572
|
try {
|
|
3605
3573
|
await synthesizeFn(next.path);
|
|
@@ -3608,9 +3576,7 @@ class SynthesisQueue {
|
|
|
3608
3576
|
this.logger.error({ path: next.path, err }, 'Synthesis failed');
|
|
3609
3577
|
}
|
|
3610
3578
|
this.clearCurrentPhase();
|
|
3611
|
-
|
|
3612
|
-
this.complete();
|
|
3613
|
-
next = this.nextItem();
|
|
3579
|
+
next = this.dequeue();
|
|
3614
3580
|
}
|
|
3615
3581
|
}
|
|
3616
3582
|
finally {
|
|
@@ -3944,12 +3910,6 @@ async function createMeta(ownerPath, options) {
|
|
|
3944
3910
|
metaJson._crossRefs = options.crossRefs;
|
|
3945
3911
|
if (options?.steer !== undefined)
|
|
3946
3912
|
metaJson._steer = options.steer;
|
|
3947
|
-
if (options?.architectTimeout !== undefined)
|
|
3948
|
-
metaJson._architectTimeout = options.architectTimeout;
|
|
3949
|
-
if (options?.builderTimeout !== undefined)
|
|
3950
|
-
metaJson._builderTimeout = options.builderTimeout;
|
|
3951
|
-
if (options?.criticTimeout !== undefined)
|
|
3952
|
-
metaJson._criticTimeout = options.criticTimeout;
|
|
3953
3913
|
const metaJsonPath = join(metaDir, 'meta.json');
|
|
3954
3914
|
await writeFile(metaJsonPath, JSON.stringify(metaJson, null, 2) + '\n');
|
|
3955
3915
|
return { metaDir, _id };
|
|
@@ -4025,9 +3985,6 @@ async function autoSeedPass(rules, watcher, logger) {
|
|
|
4025
3985
|
candidates.set(dir, {
|
|
4026
3986
|
steer: rule.steer,
|
|
4027
3987
|
crossRefs: rule.crossRefs,
|
|
4028
|
-
architectTimeout: rule.architectTimeout,
|
|
4029
|
-
builderTimeout: rule.builderTimeout,
|
|
4030
|
-
criticTimeout: rule.criticTimeout,
|
|
4031
3988
|
});
|
|
4032
3989
|
}
|
|
4033
3990
|
}
|
|
@@ -4173,7 +4130,6 @@ class Scheduler {
|
|
|
4173
4130
|
this.logger.debug({ backoffMultiplier: this.backoffMultiplier }, 'No ready phases found, increasing backoff');
|
|
4174
4131
|
return;
|
|
4175
4132
|
}
|
|
4176
|
-
// Enqueue using the legacy queue path (backward compat with processQueue)
|
|
4177
4133
|
this.queue.enqueue(candidate.path);
|
|
4178
4134
|
this.logger.info({ path: candidate.path, phase: candidate.phase, band: candidate.band }, 'Enqueued phase candidate');
|
|
4179
4135
|
// Opportunistic watcher restart detection
|
|
@@ -4597,7 +4553,7 @@ function registerMetasRoutes(app, deps) {
|
|
|
4597
4553
|
/**
|
|
4598
4554
|
* PATCH /metas/:path — update user-settable reserved properties on a meta.
|
|
4599
4555
|
*
|
|
4600
|
-
* Supported fields: _steer, _emphasis, _depth, _crossRefs, _disabled
|
|
4556
|
+
* Supported fields: _steer, _emphasis, _depth, _crossRefs, _disabled.
|
|
4601
4557
|
* Set a field to null to remove it. Unknown keys are rejected.
|
|
4602
4558
|
*
|
|
4603
4559
|
* @module routes/metasUpdate
|
|
@@ -4609,9 +4565,6 @@ const updateBodySchema = z
|
|
|
4609
4565
|
_depth: z.union([z.number(), z.null()]).optional(),
|
|
4610
4566
|
_crossRefs: z.union([z.array(z.string()), z.null()]).optional(),
|
|
4611
4567
|
_disabled: z.union([z.boolean(), z.null()]).optional(),
|
|
4612
|
-
_architectTimeout: z.union([z.number().int().min(30), z.null()]).optional(),
|
|
4613
|
-
_builderTimeout: z.union([z.number().int().min(30), z.null()]).optional(),
|
|
4614
|
-
_criticTimeout: z.union([z.number().int().min(30), z.null()]).optional(),
|
|
4615
4568
|
})
|
|
4616
4569
|
.strict();
|
|
4617
4570
|
function registerMetasUpdateRoute(app, deps) {
|
|
@@ -4775,8 +4728,8 @@ function registerPreviewRoute(app, deps) {
|
|
|
4775
4728
|
/**
|
|
4776
4729
|
* Queue management and abort routes.
|
|
4777
4730
|
*
|
|
4778
|
-
* - GET /queue — 3-layer queue model (current,
|
|
4779
|
-
* - POST /queue/clear — remove
|
|
4731
|
+
* - GET /queue — 3-layer queue model (current, pending, automatic)
|
|
4732
|
+
* - POST /queue/clear — remove pending entries
|
|
4780
4733
|
* - POST /synthesize/abort — abort the current synthesis
|
|
4781
4734
|
*
|
|
4782
4735
|
* @module routes/queue
|
|
@@ -4786,24 +4739,24 @@ function registerQueueRoutes(app, deps) {
|
|
|
4786
4739
|
const { queue } = deps;
|
|
4787
4740
|
app.get(getEndpoint('queue').path, async () => {
|
|
4788
4741
|
const currentPhase = queue.currentPhase;
|
|
4789
|
-
const
|
|
4790
|
-
// Compute owedPhase for each
|
|
4791
|
-
const
|
|
4742
|
+
const pending = queue.items;
|
|
4743
|
+
// Compute owedPhase for each pending entry by reading meta state
|
|
4744
|
+
const enrichedPending = await Promise.all(pending.map(async (entry) => {
|
|
4792
4745
|
try {
|
|
4793
|
-
const metaDir = resolveMetaDir(
|
|
4746
|
+
const metaDir = resolveMetaDir(entry.path);
|
|
4794
4747
|
const meta = await readMetaJson(metaDir);
|
|
4795
4748
|
const ps = derivePhaseState(meta);
|
|
4796
4749
|
return {
|
|
4797
|
-
path:
|
|
4750
|
+
path: entry.path,
|
|
4798
4751
|
owedPhase: getOwedPhase(ps),
|
|
4799
|
-
enqueuedAt:
|
|
4752
|
+
enqueuedAt: entry.enqueuedAt,
|
|
4800
4753
|
};
|
|
4801
4754
|
}
|
|
4802
4755
|
catch {
|
|
4803
4756
|
return {
|
|
4804
|
-
path:
|
|
4757
|
+
path: entry.path,
|
|
4805
4758
|
owedPhase: null,
|
|
4806
|
-
enqueuedAt:
|
|
4759
|
+
enqueuedAt: entry.enqueuedAt,
|
|
4807
4760
|
};
|
|
4808
4761
|
}
|
|
4809
4762
|
}));
|
|
@@ -4824,21 +4777,6 @@ function registerQueueRoutes(app, deps) {
|
|
|
4824
4777
|
catch {
|
|
4825
4778
|
// If listing fails, automatic stays empty
|
|
4826
4779
|
}
|
|
4827
|
-
// Legacy: pending is the union of overrides + automatic + legacy queue items
|
|
4828
|
-
const pendingItems = [
|
|
4829
|
-
...enrichedOverrides.map((o) => ({
|
|
4830
|
-
path: o.path,
|
|
4831
|
-
owedPhase: o.owedPhase,
|
|
4832
|
-
})),
|
|
4833
|
-
...automatic.map((a) => ({
|
|
4834
|
-
path: a.path,
|
|
4835
|
-
owedPhase: a.owedPhase,
|
|
4836
|
-
})),
|
|
4837
|
-
...queue.pending.map((item) => ({
|
|
4838
|
-
path: item.path,
|
|
4839
|
-
owedPhase: null,
|
|
4840
|
-
})),
|
|
4841
|
-
];
|
|
4842
4780
|
return {
|
|
4843
4781
|
current: currentPhase
|
|
4844
4782
|
? {
|
|
@@ -4846,58 +4784,45 @@ function registerQueueRoutes(app, deps) {
|
|
|
4846
4784
|
phase: currentPhase.phase,
|
|
4847
4785
|
startedAt: currentPhase.startedAt,
|
|
4848
4786
|
}
|
|
4849
|
-
:
|
|
4850
|
-
|
|
4851
|
-
path: queue.current.path,
|
|
4852
|
-
phase: null,
|
|
4853
|
-
startedAt: queue.current.enqueuedAt,
|
|
4854
|
-
}
|
|
4855
|
-
: null,
|
|
4856
|
-
overrides: enrichedOverrides,
|
|
4787
|
+
: null,
|
|
4788
|
+
pending: enrichedPending,
|
|
4857
4789
|
automatic,
|
|
4858
|
-
pending: pendingItems,
|
|
4859
|
-
// Legacy state
|
|
4860
|
-
state: queue.getState(),
|
|
4861
4790
|
};
|
|
4862
4791
|
});
|
|
4863
4792
|
app.post(getEndpoint('queueClear').path, () => {
|
|
4864
|
-
const removed = queue.
|
|
4793
|
+
const removed = queue.clear();
|
|
4865
4794
|
return { cleared: removed };
|
|
4866
4795
|
});
|
|
4867
4796
|
app.post(getEndpoint('abort').path, async (_request, reply) => {
|
|
4868
|
-
// Check 3-layer current first
|
|
4869
4797
|
const currentPhase = queue.currentPhase;
|
|
4870
|
-
|
|
4871
|
-
if (!current) {
|
|
4798
|
+
if (!currentPhase) {
|
|
4872
4799
|
return reply.status(200).send({ status: 'idle' });
|
|
4873
4800
|
}
|
|
4874
4801
|
// Abort the executor
|
|
4875
4802
|
deps.executor?.abort();
|
|
4876
|
-
const metaDir = resolveMetaDir(
|
|
4877
|
-
const phase = currentPhase
|
|
4803
|
+
const metaDir = resolveMetaDir(currentPhase.path);
|
|
4804
|
+
const { phase } = currentPhase;
|
|
4878
4805
|
// Transition running phase to failed and write _error to meta.json
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
// Best-effort — meta may be unreadable
|
|
4900
|
-
}
|
|
4806
|
+
try {
|
|
4807
|
+
const meta = await readMetaJson(metaDir);
|
|
4808
|
+
let ps = derivePhaseState(meta);
|
|
4809
|
+
ps = phaseFailed(ps, phase);
|
|
4810
|
+
const updated = {
|
|
4811
|
+
...meta,
|
|
4812
|
+
_phaseState: ps,
|
|
4813
|
+
_error: {
|
|
4814
|
+
step: phase,
|
|
4815
|
+
code: 'ABORT',
|
|
4816
|
+
message: 'Aborted by operator',
|
|
4817
|
+
},
|
|
4818
|
+
};
|
|
4819
|
+
const lockPath = join(metaDir, '.lock');
|
|
4820
|
+
const metaJsonPath = join(metaDir, 'meta.json');
|
|
4821
|
+
await writeFile(lockPath, JSON.stringify(updated, null, 2) + '\n');
|
|
4822
|
+
await copyFile(lockPath, metaJsonPath);
|
|
4823
|
+
}
|
|
4824
|
+
catch {
|
|
4825
|
+
// Best-effort — meta may be unreadable
|
|
4901
4826
|
}
|
|
4902
4827
|
// Release the lock for the current meta path
|
|
4903
4828
|
try {
|
|
@@ -4906,11 +4831,11 @@ function registerQueueRoutes(app, deps) {
|
|
|
4906
4831
|
catch {
|
|
4907
4832
|
// Lock may already be released
|
|
4908
4833
|
}
|
|
4909
|
-
deps.logger.info({ path:
|
|
4834
|
+
deps.logger.info({ path: currentPhase.path }, 'Synthesis aborted');
|
|
4910
4835
|
return {
|
|
4911
4836
|
status: 'aborted',
|
|
4912
|
-
path:
|
|
4913
|
-
|
|
4837
|
+
path: currentPhase.path,
|
|
4838
|
+
phase,
|
|
4914
4839
|
};
|
|
4915
4840
|
});
|
|
4916
4841
|
}
|
|
@@ -4993,9 +4918,9 @@ async function checkWatcher(url) {
|
|
|
4993
4918
|
function deriveServiceState(deps) {
|
|
4994
4919
|
if (deps.shuttingDown)
|
|
4995
4920
|
return 'stopping';
|
|
4996
|
-
if (deps.queue.
|
|
4921
|
+
if (deps.queue.currentPhase)
|
|
4997
4922
|
return 'synthesizing';
|
|
4998
|
-
if (deps.queue.depth > 0
|
|
4923
|
+
if (deps.queue.depth > 0)
|
|
4999
4924
|
return 'waiting';
|
|
5000
4925
|
return 'idle';
|
|
5001
4926
|
}
|
|
@@ -5047,7 +4972,7 @@ function registerStatusRoute(app, deps) {
|
|
|
5047
4972
|
}
|
|
5048
4973
|
return {
|
|
5049
4974
|
serviceState: deriveServiceState(deps),
|
|
5050
|
-
currentTarget: queue.
|
|
4975
|
+
currentTarget: queue.currentPhase?.path ?? null,
|
|
5051
4976
|
queue: queue.getState(),
|
|
5052
4977
|
stats: {
|
|
5053
4978
|
totalSyntheses: stats.totalSyntheses,
|
|
@@ -5135,7 +5060,7 @@ function registerSynthesizeRoute(app, deps) {
|
|
|
5135
5060
|
alreadyQueued: false,
|
|
5136
5061
|
});
|
|
5137
5062
|
}
|
|
5138
|
-
const result = queue.
|
|
5063
|
+
const result = queue.enqueue(targetPath);
|
|
5139
5064
|
return reply.code(202).send({
|
|
5140
5065
|
status: 'queued',
|
|
5141
5066
|
path: targetPath,
|
|
@@ -5166,8 +5091,9 @@ function registerSynthesizeRoute(app, deps) {
|
|
|
5166
5091
|
const stalest = winner.node.metaPath;
|
|
5167
5092
|
const enqueueResult = queue.enqueue(stalest);
|
|
5168
5093
|
return reply.code(202).send({
|
|
5169
|
-
status: '
|
|
5094
|
+
status: 'queued',
|
|
5170
5095
|
path: stalest,
|
|
5096
|
+
owedPhase: winner.owedPhase,
|
|
5171
5097
|
queuePosition: enqueueResult.position,
|
|
5172
5098
|
alreadyQueued: enqueueResult.alreadyQueued,
|
|
5173
5099
|
});
|
|
@@ -5316,26 +5242,14 @@ function registerShutdownHandlers(deps) {
|
|
|
5316
5242
|
deps.logger.info('Scheduler stopped');
|
|
5317
5243
|
}
|
|
5318
5244
|
// 2. Release lock for in-progress synthesis
|
|
5319
|
-
const current = deps.queue.current;
|
|
5320
|
-
if (current) {
|
|
5321
|
-
try {
|
|
5322
|
-
releaseLock(current.path);
|
|
5323
|
-
deps.logger.info({ path: current.path }, 'Released lock for in-progress synthesis');
|
|
5324
|
-
}
|
|
5325
|
-
catch {
|
|
5326
|
-
deps.logger.warn({ path: current.path }, 'Failed to release lock during shutdown');
|
|
5327
|
-
}
|
|
5328
|
-
}
|
|
5329
|
-
// Release lock for in-progress override synthesis (only when it
|
|
5330
|
-
// differs from the legacy current item to avoid double-release)
|
|
5331
5245
|
const currentPhase = deps.queue.currentPhase;
|
|
5332
|
-
if (currentPhase
|
|
5246
|
+
if (currentPhase) {
|
|
5333
5247
|
try {
|
|
5334
5248
|
releaseLock(resolveMetaDir(currentPhase.path));
|
|
5335
|
-
deps.logger.info({ path: currentPhase.path }, 'Released lock for in-progress
|
|
5249
|
+
deps.logger.info({ path: currentPhase.path }, 'Released lock for in-progress synthesis');
|
|
5336
5250
|
}
|
|
5337
5251
|
catch {
|
|
5338
|
-
deps.logger.warn({ path: currentPhase.path }, 'Failed to release
|
|
5252
|
+
deps.logger.warn({ path: currentPhase.path }, 'Failed to release lock during shutdown');
|
|
5339
5253
|
}
|
|
5340
5254
|
}
|
|
5341
5255
|
// 3. Close server
|
|
@@ -5817,12 +5731,6 @@ const metaJsonSchema = z
|
|
|
5817
5731
|
_error: metaErrorSchema.optional(),
|
|
5818
5732
|
/** When true, this meta is skipped during staleness scheduling. Manual trigger still works. */
|
|
5819
5733
|
_disabled: z.boolean().optional(),
|
|
5820
|
-
/** Per-entity timeout override for the architect phase (seconds, min 30). */
|
|
5821
|
-
_architectTimeout: z.number().int().min(30).optional(),
|
|
5822
|
-
/** Per-entity timeout override for the builder phase (seconds, min 30). */
|
|
5823
|
-
_builderTimeout: z.number().int().min(30).optional(),
|
|
5824
|
-
/** Per-entity timeout override for the critic phase (seconds, min 30). */
|
|
5825
|
-
_criticTimeout: z.number().int().min(30).optional(),
|
|
5826
5734
|
/**
|
|
5827
5735
|
* SHA-256 hash of ancestor _builder text at last synthesis.
|
|
5828
5736
|
* Observability only — no invalidation cascade.
|