@karmaniverous/jeeves-meta 0.16.0 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/cli/jeeves-meta/index.js +158 -261
- package/dist/executor/GatewayExecutor.d.ts +13 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +158 -261
- package/dist/queue/index.d.ts +25 -66
- package/dist/routes/queue.d.ts +2 -2
- package/dist/schema/config.d.ts +4 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1295,6 +1295,14 @@ const serviceConfigSchema = metaConfigSchema.extend({
|
|
|
1295
1295
|
* Rules are evaluated in order; last match wins for steer/crossRefs.
|
|
1296
1296
|
*/
|
|
1297
1297
|
autoSeed: z.array(autoSeedRuleSchema).optional().default([]),
|
|
1298
|
+
/** Optional workspace directory for synthesis staging files. Defaults to `os.tmpdir() + '/jeeves-meta'`. */
|
|
1299
|
+
workspaceDir: z.string().optional(),
|
|
1300
|
+
/** Max retries when staging file not yet visible after session completion. Default: 10. */
|
|
1301
|
+
stagingRetries: z.number().int().min(0).default(10),
|
|
1302
|
+
/** Delay between staging file retries in ms. Default: 250. */
|
|
1303
|
+
stagingRetryDelayMs: z.number().int().min(0).default(250),
|
|
1304
|
+
/** Maximum number of delta files returned in /preview response. Default: 50. */
|
|
1305
|
+
previewDeltaFilesCap: z.number().int().min(1).default(50),
|
|
1298
1306
|
});
|
|
1299
1307
|
|
|
1300
1308
|
/**
|
|
@@ -1467,12 +1475,16 @@ class GatewayExecutor {
|
|
|
1467
1475
|
apiKey;
|
|
1468
1476
|
pollIntervalMs;
|
|
1469
1477
|
workspaceDir;
|
|
1478
|
+
stagingRetries;
|
|
1479
|
+
stagingRetryDelayMs;
|
|
1470
1480
|
controller = new AbortController();
|
|
1471
1481
|
constructor(options = {}) {
|
|
1472
1482
|
this.gatewayUrl = (options.gatewayUrl ?? 'http://127.0.0.1:18789').replace(/\/+$/, '');
|
|
1473
1483
|
this.apiKey = options.apiKey;
|
|
1474
1484
|
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
1475
1485
|
this.workspaceDir = options.workspaceDir ?? join(tmpdir(), 'jeeves-meta');
|
|
1486
|
+
this.stagingRetries = options.stagingRetries ?? 10;
|
|
1487
|
+
this.stagingRetryDelayMs = options.stagingRetryDelayMs ?? 250;
|
|
1476
1488
|
}
|
|
1477
1489
|
/** Remove a temp output file if it exists. */
|
|
1478
1490
|
cleanupOutputFile(outputPath) {
|
|
@@ -1509,16 +1521,20 @@ class GatewayExecutor {
|
|
|
1509
1521
|
: '';
|
|
1510
1522
|
return text && text.trim() !== 'ANNOUNCE_SKIP' ? text : undefined;
|
|
1511
1523
|
}
|
|
1512
|
-
/**
|
|
1524
|
+
/**
|
|
1525
|
+
* Check history messages for timeout detection.
|
|
1526
|
+
*
|
|
1527
|
+
* Does NOT determine completion — session completion is authoritative
|
|
1528
|
+
* (via sessions_list). History stop reasons can false-positive on
|
|
1529
|
+
* sessions_yield artifacts (#200).
|
|
1530
|
+
*/
|
|
1513
1531
|
static checkHistoryCompletion(messages) {
|
|
1514
1532
|
if (messages.length === 0)
|
|
1515
|
-
return {
|
|
1533
|
+
return { timedOut: false };
|
|
1516
1534
|
const last = messages[messages.length - 1];
|
|
1517
1535
|
if (last.role !== 'assistant' || !last.stopReason)
|
|
1518
|
-
return {
|
|
1519
|
-
|
|
1520
|
-
return { done: false, timedOut: false };
|
|
1521
|
-
return { done: true, timedOut: last.stopReason === 'timeout' };
|
|
1536
|
+
return { timedOut: false };
|
|
1537
|
+
return { timedOut: last.stopReason === 'timeout' };
|
|
1522
1538
|
}
|
|
1523
1539
|
/** Invoke a gateway tool via the /tools/invoke HTTP endpoint. */
|
|
1524
1540
|
async invoke(tool, args, sessionKey) {
|
|
@@ -1681,12 +1697,14 @@ class GatewayExecutor {
|
|
|
1681
1697
|
historyResult.result?.messages ??
|
|
1682
1698
|
[];
|
|
1683
1699
|
const msgArray = messages;
|
|
1684
|
-
// Check 1:
|
|
1685
|
-
const {
|
|
1700
|
+
// Check 1: history-based timeout detection (stop reason)
|
|
1701
|
+
const { timedOut: historyTimedOut } = GatewayExecutor.checkHistoryCompletion(msgArray);
|
|
1686
1702
|
// Check 2: session completion status via sessions_list
|
|
1703
|
+
// Gate on session completion only — history stop reasons can
|
|
1704
|
+
// false-positive on sessions_yield artifacts (#200).
|
|
1687
1705
|
const sessionInfo = await this.getSessionInfo(sessionKey);
|
|
1688
1706
|
const timedOut = sessionInfo.timedOut || historyTimedOut;
|
|
1689
|
-
if (
|
|
1707
|
+
if (sessionInfo.completed) {
|
|
1690
1708
|
const tokens = sessionInfo.tokens;
|
|
1691
1709
|
// Gateway-side timeout detected — check staging file for recovery
|
|
1692
1710
|
if (timedOut) {
|
|
@@ -1696,10 +1714,23 @@ class GatewayExecutor {
|
|
|
1696
1714
|
// No output or partial output — throw for _state recovery (§3.16.6)
|
|
1697
1715
|
throw new SpawnTimeoutError('Gateway-side timeout detected (session status: timeout)', outputPath);
|
|
1698
1716
|
}
|
|
1699
|
-
// Normal completion — read
|
|
1717
|
+
// Normal completion — read staging file with retry for delayed visibility
|
|
1700
1718
|
const output = this.readStagingFile(outputPath);
|
|
1701
1719
|
if (output !== undefined)
|
|
1702
1720
|
return { output, tokens };
|
|
1721
|
+
// Staging file not yet visible — retry with bounded grace window
|
|
1722
|
+
for (let i = 0; i < this.stagingRetries; i++) {
|
|
1723
|
+
await sleepAsync(this.stagingRetryDelayMs);
|
|
1724
|
+
// Check abort after yield point — signal may have been
|
|
1725
|
+
// set externally (e.g. operator abort) during the sleep.
|
|
1726
|
+
if (this.aborted) {
|
|
1727
|
+
this.cleanupOutputFile(outputPath);
|
|
1728
|
+
throw new SpawnAbortedError();
|
|
1729
|
+
}
|
|
1730
|
+
const retryOutput = this.readStagingFile(outputPath);
|
|
1731
|
+
if (retryOutput !== undefined)
|
|
1732
|
+
return { output: retryOutput, tokens };
|
|
1733
|
+
}
|
|
1703
1734
|
// Fallback: extract from message content if file wasn't written.
|
|
1704
1735
|
// Skip ANNOUNCE_SKIP sentinel messages — the real output is in
|
|
1705
1736
|
// a preceding assistant message (the file write).
|
|
@@ -3425,36 +3456,33 @@ class ProgressReporter {
|
|
|
3425
3456
|
}
|
|
3426
3457
|
|
|
3427
3458
|
/**
|
|
3428
|
-
*
|
|
3459
|
+
* Synthesis queue.
|
|
3429
3460
|
*
|
|
3430
3461
|
* Layer 1: Current — the single item currently executing (at most one).
|
|
3431
|
-
* Layer 2:
|
|
3432
|
-
*
|
|
3433
|
-
* Layer 3: Automatic — computed on read, not persisted. All
|
|
3434
|
-
* pending phase, ranked by scheduler priority.
|
|
3435
|
-
*
|
|
3436
|
-
* Legacy: `pending` array is the union of overrides + automatic.
|
|
3462
|
+
* Layer 2: Pending — items enqueued via POST /synthesize (targeted) or
|
|
3463
|
+
* scheduler tick (automatic). FIFO, ahead of automatic candidates.
|
|
3464
|
+
* Layer 3: Automatic — computed on read (GET /queue), not persisted. All
|
|
3465
|
+
* metas with a pending phase, ranked by scheduler priority.
|
|
3437
3466
|
*
|
|
3438
3467
|
* @module queue
|
|
3439
3468
|
*/
|
|
3440
3469
|
const DEPTH_WARNING_THRESHOLD = 3;
|
|
3470
|
+
/** Strip trailing .meta suffix for consistent path comparison. */
|
|
3471
|
+
function normQueuePath(p) {
|
|
3472
|
+
return p.endsWith('.meta') ? p.slice(0, -5).replace(/[/\\]$/, '') : p;
|
|
3473
|
+
}
|
|
3441
3474
|
/**
|
|
3442
|
-
*
|
|
3475
|
+
* Synthesis queue.
|
|
3443
3476
|
*
|
|
3444
|
-
* Only one synthesis runs at a time.
|
|
3445
|
-
* take priority over automatic candidates.
|
|
3477
|
+
* Only one synthesis runs at a time. Explicitly enqueued items
|
|
3478
|
+
* take priority over automatic (computed-on-read) candidates.
|
|
3446
3479
|
*/
|
|
3447
3480
|
class SynthesisQueue {
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
currentItem = null;
|
|
3481
|
+
entries = [];
|
|
3482
|
+
currentPhaseItem = null;
|
|
3451
3483
|
processing = false;
|
|
3452
3484
|
logger;
|
|
3453
3485
|
onEnqueueCallback = null;
|
|
3454
|
-
/** Explicit override entries (3-layer model). */
|
|
3455
|
-
overrideEntries = [];
|
|
3456
|
-
/** Currently executing item with phase info (3-layer model). */
|
|
3457
|
-
currentPhaseItem = null;
|
|
3458
3486
|
constructor(logger) {
|
|
3459
3487
|
this.logger = logger;
|
|
3460
3488
|
}
|
|
@@ -3462,52 +3490,67 @@ class SynthesisQueue {
|
|
|
3462
3490
|
onEnqueue(callback) {
|
|
3463
3491
|
this.onEnqueueCallback = callback;
|
|
3464
3492
|
}
|
|
3465
|
-
// ──
|
|
3493
|
+
// ── Enqueue / dequeue ──────────────────────────────────────────────
|
|
3466
3494
|
/**
|
|
3467
|
-
* Add an
|
|
3495
|
+
* Add an entry to the queue.
|
|
3468
3496
|
* Deduped by path. Returns position and whether already queued.
|
|
3469
3497
|
*/
|
|
3470
|
-
|
|
3498
|
+
enqueue(path) {
|
|
3499
|
+
const norm = normQueuePath(path);
|
|
3471
3500
|
// Check if currently executing
|
|
3472
|
-
if (this.currentPhaseItem
|
|
3473
|
-
this.
|
|
3501
|
+
if (this.currentPhaseItem &&
|
|
3502
|
+
normQueuePath(this.currentPhaseItem.path) === norm) {
|
|
3474
3503
|
return { position: 0, alreadyQueued: true };
|
|
3475
3504
|
}
|
|
3476
|
-
// Check if already in
|
|
3477
|
-
const existing = this.
|
|
3505
|
+
// Check if already in queue
|
|
3506
|
+
const existing = this.entries.findIndex((e) => normQueuePath(e.path) === norm);
|
|
3478
3507
|
if (existing !== -1) {
|
|
3479
3508
|
return { position: existing, alreadyQueued: true };
|
|
3480
3509
|
}
|
|
3481
|
-
this.
|
|
3510
|
+
this.entries.push({
|
|
3482
3511
|
path,
|
|
3483
3512
|
enqueuedAt: new Date().toISOString(),
|
|
3484
3513
|
});
|
|
3485
|
-
const position = this.
|
|
3486
|
-
if (this.
|
|
3487
|
-
this.logger.warn({ depth: this.
|
|
3514
|
+
const position = this.entries.length - 1;
|
|
3515
|
+
if (this.entries.length > DEPTH_WARNING_THRESHOLD) {
|
|
3516
|
+
this.logger.warn({ depth: this.entries.length }, 'Queue depth exceeds threshold');
|
|
3488
3517
|
}
|
|
3489
3518
|
this.onEnqueueCallback?.();
|
|
3490
3519
|
return { position, alreadyQueued: false };
|
|
3491
3520
|
}
|
|
3492
|
-
/** Dequeue the next
|
|
3493
|
-
|
|
3494
|
-
return this.
|
|
3521
|
+
/** Dequeue the next entry, or undefined if empty. */
|
|
3522
|
+
dequeue() {
|
|
3523
|
+
return this.entries.shift();
|
|
3524
|
+
}
|
|
3525
|
+
/** Get all queued entries (shallow copy). */
|
|
3526
|
+
get items() {
|
|
3527
|
+
return [...this.entries];
|
|
3495
3528
|
}
|
|
3496
|
-
/**
|
|
3497
|
-
get
|
|
3498
|
-
return
|
|
3529
|
+
/** Number of items waiting in the queue (excludes current). */
|
|
3530
|
+
get depth() {
|
|
3531
|
+
return this.entries.length;
|
|
3499
3532
|
}
|
|
3500
|
-
/**
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3533
|
+
/**
|
|
3534
|
+
* Remove all pending items from the queue.
|
|
3535
|
+
* Does not affect the currently-running item.
|
|
3536
|
+
*
|
|
3537
|
+
* @returns The number of items removed.
|
|
3538
|
+
*/
|
|
3539
|
+
clear() {
|
|
3540
|
+
const count = this.entries.length;
|
|
3541
|
+
this.entries = [];
|
|
3504
3542
|
return count;
|
|
3505
3543
|
}
|
|
3506
|
-
/** Check
|
|
3507
|
-
|
|
3508
|
-
|
|
3544
|
+
/** Check whether a path is in the queue or currently being synthesized. */
|
|
3545
|
+
has(path) {
|
|
3546
|
+
const norm = normQueuePath(path);
|
|
3547
|
+
if (this.currentPhaseItem &&
|
|
3548
|
+
normQueuePath(this.currentPhaseItem.path) === norm) {
|
|
3549
|
+
return true;
|
|
3550
|
+
}
|
|
3551
|
+
return this.entries.some((e) => normQueuePath(e.path) === norm);
|
|
3509
3552
|
}
|
|
3510
|
-
// ── Current-item tracking
|
|
3553
|
+
// ── Current-item tracking ──────────────────────────────────────────
|
|
3511
3554
|
/** Set the currently executing phase item. */
|
|
3512
3555
|
setCurrentPhase(path, phase) {
|
|
3513
3556
|
this.currentPhaseItem = {
|
|
@@ -3524,129 +3567,21 @@ class SynthesisQueue {
|
|
|
3524
3567
|
get currentPhase() {
|
|
3525
3568
|
return this.currentPhaseItem;
|
|
3526
3569
|
}
|
|
3527
|
-
// ──
|
|
3528
|
-
/**
|
|
3529
|
-
* Add a path to the synthesis queue.
|
|
3530
|
-
*
|
|
3531
|
-
* @param path - Meta path to synthesize.
|
|
3532
|
-
* @param priority - If true, insert at front of queue.
|
|
3533
|
-
* @returns Position and whether the path was already queued.
|
|
3534
|
-
*/
|
|
3535
|
-
enqueue(path, priority = false) {
|
|
3536
|
-
if (this.currentItem?.path === path) {
|
|
3537
|
-
return { position: 0, alreadyQueued: true };
|
|
3538
|
-
}
|
|
3539
|
-
const existingIndex = this.queue.findIndex((item) => item.path === path);
|
|
3540
|
-
if (existingIndex !== -1) {
|
|
3541
|
-
return { position: existingIndex, alreadyQueued: true };
|
|
3542
|
-
}
|
|
3543
|
-
const item = {
|
|
3544
|
-
path,
|
|
3545
|
-
priority,
|
|
3546
|
-
enqueuedAt: new Date().toISOString(),
|
|
3547
|
-
};
|
|
3548
|
-
if (priority) {
|
|
3549
|
-
this.queue.unshift(item);
|
|
3550
|
-
}
|
|
3551
|
-
else {
|
|
3552
|
-
this.queue.push(item);
|
|
3553
|
-
}
|
|
3554
|
-
if (this.queue.length > DEPTH_WARNING_THRESHOLD) {
|
|
3555
|
-
this.logger.warn({ depth: this.queue.length }, 'Queue depth exceeds threshold');
|
|
3556
|
-
}
|
|
3557
|
-
const position = this.queue.findIndex((i) => i.path === path);
|
|
3558
|
-
this.onEnqueueCallback?.();
|
|
3559
|
-
return { position, alreadyQueued: false };
|
|
3560
|
-
}
|
|
3561
|
-
/** Remove and return the next item from the queue. */
|
|
3562
|
-
dequeue() {
|
|
3563
|
-
const item = this.queue.shift();
|
|
3564
|
-
if (item) {
|
|
3565
|
-
this.currentItem = item;
|
|
3566
|
-
}
|
|
3567
|
-
return item;
|
|
3568
|
-
}
|
|
3569
|
-
/** Mark the currently-running synthesis as complete. */
|
|
3570
|
-
complete() {
|
|
3571
|
-
this.currentItem = null;
|
|
3572
|
-
}
|
|
3573
|
-
/** Number of items waiting in the queue (excludes current). */
|
|
3574
|
-
get depth() {
|
|
3575
|
-
return this.queue.length;
|
|
3576
|
-
}
|
|
3577
|
-
/** The item currently being synthesized, or null. */
|
|
3578
|
-
get current() {
|
|
3579
|
-
return this.currentItem;
|
|
3580
|
-
}
|
|
3581
|
-
/** A shallow copy of the queued items. */
|
|
3582
|
-
get items() {
|
|
3583
|
-
return [...this.queue];
|
|
3584
|
-
}
|
|
3585
|
-
/** A shallow copy of the pending items (alias for items). */
|
|
3586
|
-
get pending() {
|
|
3587
|
-
return [...this.queue];
|
|
3588
|
-
}
|
|
3589
|
-
/**
|
|
3590
|
-
* Remove all pending items from the queue.
|
|
3591
|
-
* Does not affect the currently-running item.
|
|
3592
|
-
*
|
|
3593
|
-
* @returns The number of items removed.
|
|
3594
|
-
*/
|
|
3595
|
-
clear() {
|
|
3596
|
-
const count = this.queue.length;
|
|
3597
|
-
this.queue = [];
|
|
3598
|
-
return count;
|
|
3599
|
-
}
|
|
3600
|
-
/** Check whether a path is in the queue or currently being synthesized. */
|
|
3601
|
-
has(path) {
|
|
3602
|
-
if (this.currentItem?.path === path)
|
|
3603
|
-
return true;
|
|
3604
|
-
if (this.currentPhaseItem?.path === path)
|
|
3605
|
-
return true;
|
|
3606
|
-
return (this.queue.some((item) => item.path === path) ||
|
|
3607
|
-
this.overrideEntries.some((e) => e.path === path));
|
|
3608
|
-
}
|
|
3609
|
-
/** Get the 0-indexed position of a path in the queue. */
|
|
3610
|
-
getPosition(path) {
|
|
3611
|
-
// Check overrides first
|
|
3612
|
-
const overrideIdx = this.overrideEntries.findIndex((e) => e.path === path);
|
|
3613
|
-
if (overrideIdx !== -1)
|
|
3614
|
-
return overrideIdx;
|
|
3615
|
-
const index = this.queue.findIndex((item) => item.path === path);
|
|
3616
|
-
return index === -1 ? null : index;
|
|
3617
|
-
}
|
|
3618
|
-
/** Dequeue the next item: overrides first, then legacy queue. */
|
|
3619
|
-
nextItem() {
|
|
3620
|
-
const override = this.dequeueOverride();
|
|
3621
|
-
if (override)
|
|
3622
|
-
return { path: override.path, source: 'override' };
|
|
3623
|
-
const item = this.dequeue();
|
|
3624
|
-
if (item)
|
|
3625
|
-
return { path: item.path, source: 'legacy' };
|
|
3626
|
-
return undefined;
|
|
3627
|
-
}
|
|
3570
|
+
// ── Queue state ────────────────────────────────────────────────────
|
|
3628
3571
|
/** Return a snapshot of queue state for the /status endpoint. */
|
|
3629
3572
|
getState() {
|
|
3630
3573
|
return {
|
|
3631
|
-
depth: this.
|
|
3632
|
-
items:
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
enqueuedAt: e.enqueuedAt,
|
|
3637
|
-
})),
|
|
3638
|
-
...this.queue.map((item) => ({
|
|
3639
|
-
path: item.path,
|
|
3640
|
-
priority: item.priority,
|
|
3641
|
-
enqueuedAt: item.enqueuedAt,
|
|
3642
|
-
})),
|
|
3643
|
-
],
|
|
3574
|
+
depth: this.entries.length,
|
|
3575
|
+
items: this.entries.map((e) => ({
|
|
3576
|
+
path: e.path,
|
|
3577
|
+
enqueuedAt: e.enqueuedAt,
|
|
3578
|
+
})),
|
|
3644
3579
|
};
|
|
3645
3580
|
}
|
|
3581
|
+
// ── Processing ─────────────────────────────────────────────────────
|
|
3646
3582
|
/**
|
|
3647
|
-
* Process queued items one at a time until
|
|
3583
|
+
* Process queued items one at a time until the queue is empty.
|
|
3648
3584
|
*
|
|
3649
|
-
* Override entries are processed first (FIFO), then legacy queue items.
|
|
3650
3585
|
* Re-entry is prevented: if already processing, the call returns
|
|
3651
3586
|
* immediately. Errors are logged and do not block subsequent items.
|
|
3652
3587
|
*
|
|
@@ -3657,7 +3592,7 @@ class SynthesisQueue {
|
|
|
3657
3592
|
return;
|
|
3658
3593
|
this.processing = true;
|
|
3659
3594
|
try {
|
|
3660
|
-
let next = this.
|
|
3595
|
+
let next = this.dequeue();
|
|
3661
3596
|
while (next) {
|
|
3662
3597
|
try {
|
|
3663
3598
|
await synthesizeFn(next.path);
|
|
@@ -3666,9 +3601,7 @@ class SynthesisQueue {
|
|
|
3666
3601
|
this.logger.error({ path: next.path, err }, 'Synthesis failed');
|
|
3667
3602
|
}
|
|
3668
3603
|
this.clearCurrentPhase();
|
|
3669
|
-
|
|
3670
|
-
this.complete();
|
|
3671
|
-
next = this.nextItem();
|
|
3604
|
+
next = this.dequeue();
|
|
3672
3605
|
}
|
|
3673
3606
|
}
|
|
3674
3607
|
finally {
|
|
@@ -4222,7 +4155,6 @@ class Scheduler {
|
|
|
4222
4155
|
this.logger.debug({ backoffMultiplier: this.backoffMultiplier }, 'No ready phases found, increasing backoff');
|
|
4223
4156
|
return;
|
|
4224
4157
|
}
|
|
4225
|
-
// Enqueue using the legacy queue path (backward compat with processQueue)
|
|
4226
4158
|
this.queue.enqueue(candidate.path);
|
|
4227
4159
|
this.logger.info({ path: candidate.path, phase: candidate.phase, band: candidate.band }, 'Enqueued phase candidate');
|
|
4228
4160
|
// Opportunistic watcher restart detection
|
|
@@ -4803,9 +4735,10 @@ function registerPreviewRoute(app, deps) {
|
|
|
4803
4735
|
ownedFiles: scopeFiles.length,
|
|
4804
4736
|
childMetas: targetNode.children.length,
|
|
4805
4737
|
deltaFiles: deltaFiles
|
|
4806
|
-
.slice(0,
|
|
4738
|
+
.slice(0, config.previewDeltaFilesCap)
|
|
4807
4739
|
.map((f) => ({ path: f, action: 'modified' })),
|
|
4808
4740
|
deltaCount: deltaFiles.length,
|
|
4741
|
+
deltaFilesTruncated: deltaFiles.length > config.previewDeltaFilesCap,
|
|
4809
4742
|
},
|
|
4810
4743
|
estimatedTokens,
|
|
4811
4744
|
// New phase-state fields (additive)
|
|
@@ -4821,8 +4754,8 @@ function registerPreviewRoute(app, deps) {
|
|
|
4821
4754
|
/**
|
|
4822
4755
|
* Queue management and abort routes.
|
|
4823
4756
|
*
|
|
4824
|
-
* - GET /queue — 3-layer queue model (current,
|
|
4825
|
-
* - POST /queue/clear — remove
|
|
4757
|
+
* - GET /queue — 3-layer queue model (current, pending, automatic)
|
|
4758
|
+
* - POST /queue/clear — remove pending entries
|
|
4826
4759
|
* - POST /synthesize/abort — abort the current synthesis
|
|
4827
4760
|
*
|
|
4828
4761
|
* @module routes/queue
|
|
@@ -4832,24 +4765,24 @@ function registerQueueRoutes(app, deps) {
|
|
|
4832
4765
|
const { queue } = deps;
|
|
4833
4766
|
app.get(getEndpoint('queue').path, async () => {
|
|
4834
4767
|
const currentPhase = queue.currentPhase;
|
|
4835
|
-
const
|
|
4836
|
-
// Compute owedPhase for each
|
|
4837
|
-
const
|
|
4768
|
+
const pending = queue.items;
|
|
4769
|
+
// Compute owedPhase for each pending entry by reading meta state
|
|
4770
|
+
const enrichedPending = await Promise.all(pending.map(async (entry) => {
|
|
4838
4771
|
try {
|
|
4839
|
-
const metaDir = resolveMetaDir(
|
|
4772
|
+
const metaDir = resolveMetaDir(entry.path);
|
|
4840
4773
|
const meta = await readMetaJson(metaDir);
|
|
4841
4774
|
const ps = derivePhaseState(meta);
|
|
4842
4775
|
return {
|
|
4843
|
-
path:
|
|
4776
|
+
path: entry.path,
|
|
4844
4777
|
owedPhase: getOwedPhase(ps),
|
|
4845
|
-
enqueuedAt:
|
|
4778
|
+
enqueuedAt: entry.enqueuedAt,
|
|
4846
4779
|
};
|
|
4847
4780
|
}
|
|
4848
4781
|
catch {
|
|
4849
4782
|
return {
|
|
4850
|
-
path:
|
|
4783
|
+
path: entry.path,
|
|
4851
4784
|
owedPhase: null,
|
|
4852
|
-
enqueuedAt:
|
|
4785
|
+
enqueuedAt: entry.enqueuedAt,
|
|
4853
4786
|
};
|
|
4854
4787
|
}
|
|
4855
4788
|
}));
|
|
@@ -4870,21 +4803,6 @@ function registerQueueRoutes(app, deps) {
|
|
|
4870
4803
|
catch {
|
|
4871
4804
|
// If listing fails, automatic stays empty
|
|
4872
4805
|
}
|
|
4873
|
-
// Legacy: pending is the union of overrides + automatic + legacy queue items
|
|
4874
|
-
const pendingItems = [
|
|
4875
|
-
...enrichedOverrides.map((o) => ({
|
|
4876
|
-
path: o.path,
|
|
4877
|
-
owedPhase: o.owedPhase,
|
|
4878
|
-
})),
|
|
4879
|
-
...automatic.map((a) => ({
|
|
4880
|
-
path: a.path,
|
|
4881
|
-
owedPhase: a.owedPhase,
|
|
4882
|
-
})),
|
|
4883
|
-
...queue.pending.map((item) => ({
|
|
4884
|
-
path: item.path,
|
|
4885
|
-
owedPhase: null,
|
|
4886
|
-
})),
|
|
4887
|
-
];
|
|
4888
4806
|
return {
|
|
4889
4807
|
current: currentPhase
|
|
4890
4808
|
? {
|
|
@@ -4892,58 +4810,45 @@ function registerQueueRoutes(app, deps) {
|
|
|
4892
4810
|
phase: currentPhase.phase,
|
|
4893
4811
|
startedAt: currentPhase.startedAt,
|
|
4894
4812
|
}
|
|
4895
|
-
:
|
|
4896
|
-
|
|
4897
|
-
path: queue.current.path,
|
|
4898
|
-
phase: null,
|
|
4899
|
-
startedAt: queue.current.enqueuedAt,
|
|
4900
|
-
}
|
|
4901
|
-
: null,
|
|
4902
|
-
overrides: enrichedOverrides,
|
|
4813
|
+
: null,
|
|
4814
|
+
pending: enrichedPending,
|
|
4903
4815
|
automatic,
|
|
4904
|
-
pending: pendingItems,
|
|
4905
|
-
// Legacy state
|
|
4906
|
-
state: queue.getState(),
|
|
4907
4816
|
};
|
|
4908
4817
|
});
|
|
4909
4818
|
app.post(getEndpoint('queueClear').path, () => {
|
|
4910
|
-
const removed = queue.
|
|
4819
|
+
const removed = queue.clear();
|
|
4911
4820
|
return { cleared: removed };
|
|
4912
4821
|
});
|
|
4913
4822
|
app.post(getEndpoint('abort').path, async (_request, reply) => {
|
|
4914
|
-
// Check 3-layer current first
|
|
4915
4823
|
const currentPhase = queue.currentPhase;
|
|
4916
|
-
|
|
4917
|
-
if (!current) {
|
|
4824
|
+
if (!currentPhase) {
|
|
4918
4825
|
return reply.status(200).send({ status: 'idle' });
|
|
4919
4826
|
}
|
|
4920
4827
|
// Abort the executor
|
|
4921
4828
|
deps.executor?.abort();
|
|
4922
|
-
const metaDir = resolveMetaDir(
|
|
4923
|
-
const phase = currentPhase
|
|
4829
|
+
const metaDir = resolveMetaDir(currentPhase.path);
|
|
4830
|
+
const { phase } = currentPhase;
|
|
4924
4831
|
// Transition running phase to failed and write _error to meta.json
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
// Best-effort — meta may be unreadable
|
|
4946
|
-
}
|
|
4832
|
+
try {
|
|
4833
|
+
const meta = await readMetaJson(metaDir);
|
|
4834
|
+
let ps = derivePhaseState(meta);
|
|
4835
|
+
ps = phaseFailed(ps, phase);
|
|
4836
|
+
const updated = {
|
|
4837
|
+
...meta,
|
|
4838
|
+
_phaseState: ps,
|
|
4839
|
+
_error: {
|
|
4840
|
+
step: phase,
|
|
4841
|
+
code: 'ABORT',
|
|
4842
|
+
message: 'Aborted by operator',
|
|
4843
|
+
},
|
|
4844
|
+
};
|
|
4845
|
+
const lockPath = join(metaDir, '.lock');
|
|
4846
|
+
const metaJsonPath = join(metaDir, 'meta.json');
|
|
4847
|
+
await writeFile(lockPath, JSON.stringify(updated, null, 2) + '\n');
|
|
4848
|
+
await copyFile(lockPath, metaJsonPath);
|
|
4849
|
+
}
|
|
4850
|
+
catch {
|
|
4851
|
+
// Best-effort — meta may be unreadable
|
|
4947
4852
|
}
|
|
4948
4853
|
// Release the lock for the current meta path
|
|
4949
4854
|
try {
|
|
@@ -4952,11 +4857,11 @@ function registerQueueRoutes(app, deps) {
|
|
|
4952
4857
|
catch {
|
|
4953
4858
|
// Lock may already be released
|
|
4954
4859
|
}
|
|
4955
|
-
deps.logger.info({ path:
|
|
4860
|
+
deps.logger.info({ path: currentPhase.path }, 'Synthesis aborted');
|
|
4956
4861
|
return {
|
|
4957
4862
|
status: 'aborted',
|
|
4958
|
-
path:
|
|
4959
|
-
|
|
4863
|
+
path: currentPhase.path,
|
|
4864
|
+
phase,
|
|
4960
4865
|
};
|
|
4961
4866
|
});
|
|
4962
4867
|
}
|
|
@@ -5039,9 +4944,9 @@ async function checkWatcher(url) {
|
|
|
5039
4944
|
function deriveServiceState(deps) {
|
|
5040
4945
|
if (deps.shuttingDown)
|
|
5041
4946
|
return 'stopping';
|
|
5042
|
-
if (deps.queue.
|
|
4947
|
+
if (deps.queue.currentPhase)
|
|
5043
4948
|
return 'synthesizing';
|
|
5044
|
-
if (deps.queue.depth > 0
|
|
4949
|
+
if (deps.queue.depth > 0)
|
|
5045
4950
|
return 'waiting';
|
|
5046
4951
|
return 'idle';
|
|
5047
4952
|
}
|
|
@@ -5093,7 +4998,7 @@ function registerStatusRoute(app, deps) {
|
|
|
5093
4998
|
}
|
|
5094
4999
|
return {
|
|
5095
5000
|
serviceState: deriveServiceState(deps),
|
|
5096
|
-
currentTarget: queue.
|
|
5001
|
+
currentTarget: queue.currentPhase?.path ?? null,
|
|
5097
5002
|
queue: queue.getState(),
|
|
5098
5003
|
stats: {
|
|
5099
5004
|
totalSyntheses: stats.totalSyntheses,
|
|
@@ -5181,7 +5086,7 @@ function registerSynthesizeRoute(app, deps) {
|
|
|
5181
5086
|
alreadyQueued: false,
|
|
5182
5087
|
});
|
|
5183
5088
|
}
|
|
5184
|
-
const result = queue.
|
|
5089
|
+
const result = queue.enqueue(targetPath);
|
|
5185
5090
|
return reply.code(202).send({
|
|
5186
5091
|
status: 'queued',
|
|
5187
5092
|
path: targetPath,
|
|
@@ -5212,8 +5117,9 @@ function registerSynthesizeRoute(app, deps) {
|
|
|
5212
5117
|
const stalest = winner.node.metaPath;
|
|
5213
5118
|
const enqueueResult = queue.enqueue(stalest);
|
|
5214
5119
|
return reply.code(202).send({
|
|
5215
|
-
status: '
|
|
5120
|
+
status: 'queued',
|
|
5216
5121
|
path: stalest,
|
|
5122
|
+
owedPhase: winner.owedPhase,
|
|
5217
5123
|
queuePosition: enqueueResult.position,
|
|
5218
5124
|
alreadyQueued: enqueueResult.alreadyQueued,
|
|
5219
5125
|
});
|
|
@@ -5362,26 +5268,14 @@ function registerShutdownHandlers(deps) {
|
|
|
5362
5268
|
deps.logger.info('Scheduler stopped');
|
|
5363
5269
|
}
|
|
5364
5270
|
// 2. Release lock for in-progress synthesis
|
|
5365
|
-
const current = deps.queue.current;
|
|
5366
|
-
if (current) {
|
|
5367
|
-
try {
|
|
5368
|
-
releaseLock(current.path);
|
|
5369
|
-
deps.logger.info({ path: current.path }, 'Released lock for in-progress synthesis');
|
|
5370
|
-
}
|
|
5371
|
-
catch {
|
|
5372
|
-
deps.logger.warn({ path: current.path }, 'Failed to release lock during shutdown');
|
|
5373
|
-
}
|
|
5374
|
-
}
|
|
5375
|
-
// Release lock for in-progress override synthesis (only when it
|
|
5376
|
-
// differs from the legacy current item to avoid double-release)
|
|
5377
5271
|
const currentPhase = deps.queue.currentPhase;
|
|
5378
|
-
if (currentPhase
|
|
5272
|
+
if (currentPhase) {
|
|
5379
5273
|
try {
|
|
5380
5274
|
releaseLock(resolveMetaDir(currentPhase.path));
|
|
5381
|
-
deps.logger.info({ path: currentPhase.path }, 'Released lock for in-progress
|
|
5275
|
+
deps.logger.info({ path: currentPhase.path }, 'Released lock for in-progress synthesis');
|
|
5382
5276
|
}
|
|
5383
5277
|
catch {
|
|
5384
|
-
deps.logger.warn({ path: currentPhase.path }, 'Failed to release
|
|
5278
|
+
deps.logger.warn({ path: currentPhase.path }, 'Failed to release lock during shutdown');
|
|
5385
5279
|
}
|
|
5386
5280
|
}
|
|
5387
5281
|
// 3. Close server
|
|
@@ -5518,6 +5412,9 @@ async function startService(config, configPath) {
|
|
|
5518
5412
|
const executor = new GatewayExecutor({
|
|
5519
5413
|
gatewayUrl: config.gatewayUrl,
|
|
5520
5414
|
apiKey: config.gatewayApiKey,
|
|
5415
|
+
workspaceDir: config.workspaceDir,
|
|
5416
|
+
stagingRetries: config.stagingRetries,
|
|
5417
|
+
stagingRetryDelayMs: config.stagingRetryDelayMs,
|
|
5521
5418
|
});
|
|
5522
5419
|
// Runtime stats (mutable, shared with routes)
|
|
5523
5420
|
const stats = {
|