@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
|
@@ -806,6 +806,14 @@ const serviceConfigSchema = metaConfigSchema.extend({
|
|
|
806
806
|
* Rules are evaluated in order; last match wins for steer/crossRefs.
|
|
807
807
|
*/
|
|
808
808
|
autoSeed: z.array(autoSeedRuleSchema).optional().default([]),
|
|
809
|
+
/** Optional workspace directory for synthesis staging files. Defaults to `os.tmpdir() + '/jeeves-meta'`. */
|
|
810
|
+
workspaceDir: z.string().optional(),
|
|
811
|
+
/** Max retries when staging file not yet visible after session completion. Default: 10. */
|
|
812
|
+
stagingRetries: z.number().int().min(0).default(10),
|
|
813
|
+
/** Delay between staging file retries in ms. Default: 250. */
|
|
814
|
+
stagingRetryDelayMs: z.number().int().min(0).default(250),
|
|
815
|
+
/** Maximum number of delta files returned in /preview response. Default: 50. */
|
|
816
|
+
previewDeltaFilesCap: z.number().int().min(1).default(50),
|
|
809
817
|
});
|
|
810
818
|
|
|
811
819
|
/**
|
|
@@ -933,12 +941,16 @@ class GatewayExecutor {
|
|
|
933
941
|
apiKey;
|
|
934
942
|
pollIntervalMs;
|
|
935
943
|
workspaceDir;
|
|
944
|
+
stagingRetries;
|
|
945
|
+
stagingRetryDelayMs;
|
|
936
946
|
controller = new AbortController();
|
|
937
947
|
constructor(options = {}) {
|
|
938
948
|
this.gatewayUrl = (options.gatewayUrl ?? 'http://127.0.0.1:18789').replace(/\/+$/, '');
|
|
939
949
|
this.apiKey = options.apiKey;
|
|
940
950
|
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
941
951
|
this.workspaceDir = options.workspaceDir ?? join(tmpdir(), 'jeeves-meta');
|
|
952
|
+
this.stagingRetries = options.stagingRetries ?? 10;
|
|
953
|
+
this.stagingRetryDelayMs = options.stagingRetryDelayMs ?? 250;
|
|
942
954
|
}
|
|
943
955
|
/** Remove a temp output file if it exists. */
|
|
944
956
|
cleanupOutputFile(outputPath) {
|
|
@@ -975,16 +987,20 @@ class GatewayExecutor {
|
|
|
975
987
|
: '';
|
|
976
988
|
return text && text.trim() !== 'ANNOUNCE_SKIP' ? text : undefined;
|
|
977
989
|
}
|
|
978
|
-
/**
|
|
990
|
+
/**
|
|
991
|
+
* Check history messages for timeout detection.
|
|
992
|
+
*
|
|
993
|
+
* Does NOT determine completion — session completion is authoritative
|
|
994
|
+
* (via sessions_list). History stop reasons can false-positive on
|
|
995
|
+
* sessions_yield artifacts (#200).
|
|
996
|
+
*/
|
|
979
997
|
static checkHistoryCompletion(messages) {
|
|
980
998
|
if (messages.length === 0)
|
|
981
|
-
return {
|
|
999
|
+
return { timedOut: false };
|
|
982
1000
|
const last = messages[messages.length - 1];
|
|
983
1001
|
if (last.role !== 'assistant' || !last.stopReason)
|
|
984
|
-
return {
|
|
985
|
-
|
|
986
|
-
return { done: false, timedOut: false };
|
|
987
|
-
return { done: true, timedOut: last.stopReason === 'timeout' };
|
|
1002
|
+
return { timedOut: false };
|
|
1003
|
+
return { timedOut: last.stopReason === 'timeout' };
|
|
988
1004
|
}
|
|
989
1005
|
/** Invoke a gateway tool via the /tools/invoke HTTP endpoint. */
|
|
990
1006
|
async invoke(tool, args, sessionKey) {
|
|
@@ -1147,12 +1163,14 @@ class GatewayExecutor {
|
|
|
1147
1163
|
historyResult.result?.messages ??
|
|
1148
1164
|
[];
|
|
1149
1165
|
const msgArray = messages;
|
|
1150
|
-
// Check 1:
|
|
1151
|
-
const {
|
|
1166
|
+
// Check 1: history-based timeout detection (stop reason)
|
|
1167
|
+
const { timedOut: historyTimedOut } = GatewayExecutor.checkHistoryCompletion(msgArray);
|
|
1152
1168
|
// Check 2: session completion status via sessions_list
|
|
1169
|
+
// Gate on session completion only — history stop reasons can
|
|
1170
|
+
// false-positive on sessions_yield artifacts (#200).
|
|
1153
1171
|
const sessionInfo = await this.getSessionInfo(sessionKey);
|
|
1154
1172
|
const timedOut = sessionInfo.timedOut || historyTimedOut;
|
|
1155
|
-
if (
|
|
1173
|
+
if (sessionInfo.completed) {
|
|
1156
1174
|
const tokens = sessionInfo.tokens;
|
|
1157
1175
|
// Gateway-side timeout detected — check staging file for recovery
|
|
1158
1176
|
if (timedOut) {
|
|
@@ -1162,10 +1180,23 @@ class GatewayExecutor {
|
|
|
1162
1180
|
// No output or partial output — throw for _state recovery (§3.16.6)
|
|
1163
1181
|
throw new SpawnTimeoutError('Gateway-side timeout detected (session status: timeout)', outputPath);
|
|
1164
1182
|
}
|
|
1165
|
-
// Normal completion — read
|
|
1183
|
+
// Normal completion — read staging file with retry for delayed visibility
|
|
1166
1184
|
const output = this.readStagingFile(outputPath);
|
|
1167
1185
|
if (output !== undefined)
|
|
1168
1186
|
return { output, tokens };
|
|
1187
|
+
// Staging file not yet visible — retry with bounded grace window
|
|
1188
|
+
for (let i = 0; i < this.stagingRetries; i++) {
|
|
1189
|
+
await sleepAsync(this.stagingRetryDelayMs);
|
|
1190
|
+
// Check abort after yield point — signal may have been
|
|
1191
|
+
// set externally (e.g. operator abort) during the sleep.
|
|
1192
|
+
if (this.aborted) {
|
|
1193
|
+
this.cleanupOutputFile(outputPath);
|
|
1194
|
+
throw new SpawnAbortedError();
|
|
1195
|
+
}
|
|
1196
|
+
const retryOutput = this.readStagingFile(outputPath);
|
|
1197
|
+
if (retryOutput !== undefined)
|
|
1198
|
+
return { output: retryOutput, tokens };
|
|
1199
|
+
}
|
|
1169
1200
|
// Fallback: extract from message content if file wasn't written.
|
|
1170
1201
|
// Skip ANNOUNCE_SKIP sentinel messages — the real output is in
|
|
1171
1202
|
// a preceding assistant message (the file write).
|
|
@@ -3167,36 +3198,33 @@ class ProgressReporter {
|
|
|
3167
3198
|
}
|
|
3168
3199
|
|
|
3169
3200
|
/**
|
|
3170
|
-
*
|
|
3201
|
+
* Synthesis queue.
|
|
3171
3202
|
*
|
|
3172
3203
|
* Layer 1: Current — the single item currently executing (at most one).
|
|
3173
|
-
* Layer 2:
|
|
3174
|
-
*
|
|
3175
|
-
* Layer 3: Automatic — computed on read, not persisted. All
|
|
3176
|
-
* pending phase, ranked by scheduler priority.
|
|
3177
|
-
*
|
|
3178
|
-
* Legacy: `pending` array is the union of overrides + automatic.
|
|
3204
|
+
* Layer 2: Pending — items enqueued via POST /synthesize (targeted) or
|
|
3205
|
+
* scheduler tick (automatic). FIFO, ahead of automatic candidates.
|
|
3206
|
+
* Layer 3: Automatic — computed on read (GET /queue), not persisted. All
|
|
3207
|
+
* metas with a pending phase, ranked by scheduler priority.
|
|
3179
3208
|
*
|
|
3180
3209
|
* @module queue
|
|
3181
3210
|
*/
|
|
3182
3211
|
const DEPTH_WARNING_THRESHOLD = 3;
|
|
3212
|
+
/** Strip trailing .meta suffix for consistent path comparison. */
|
|
3213
|
+
function normQueuePath(p) {
|
|
3214
|
+
return p.endsWith('.meta') ? p.slice(0, -5).replace(/[/\\]$/, '') : p;
|
|
3215
|
+
}
|
|
3183
3216
|
/**
|
|
3184
|
-
*
|
|
3217
|
+
* Synthesis queue.
|
|
3185
3218
|
*
|
|
3186
|
-
* Only one synthesis runs at a time.
|
|
3187
|
-
* take priority over automatic candidates.
|
|
3219
|
+
* Only one synthesis runs at a time. Explicitly enqueued items
|
|
3220
|
+
* take priority over automatic (computed-on-read) candidates.
|
|
3188
3221
|
*/
|
|
3189
3222
|
class SynthesisQueue {
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
currentItem = null;
|
|
3223
|
+
entries = [];
|
|
3224
|
+
currentPhaseItem = null;
|
|
3193
3225
|
processing = false;
|
|
3194
3226
|
logger;
|
|
3195
3227
|
onEnqueueCallback = null;
|
|
3196
|
-
/** Explicit override entries (3-layer model). */
|
|
3197
|
-
overrideEntries = [];
|
|
3198
|
-
/** Currently executing item with phase info (3-layer model). */
|
|
3199
|
-
currentPhaseItem = null;
|
|
3200
3228
|
constructor(logger) {
|
|
3201
3229
|
this.logger = logger;
|
|
3202
3230
|
}
|
|
@@ -3204,52 +3232,67 @@ class SynthesisQueue {
|
|
|
3204
3232
|
onEnqueue(callback) {
|
|
3205
3233
|
this.onEnqueueCallback = callback;
|
|
3206
3234
|
}
|
|
3207
|
-
// ──
|
|
3235
|
+
// ── Enqueue / dequeue ──────────────────────────────────────────────
|
|
3208
3236
|
/**
|
|
3209
|
-
* Add an
|
|
3237
|
+
* Add an entry to the queue.
|
|
3210
3238
|
* Deduped by path. Returns position and whether already queued.
|
|
3211
3239
|
*/
|
|
3212
|
-
|
|
3240
|
+
enqueue(path) {
|
|
3241
|
+
const norm = normQueuePath(path);
|
|
3213
3242
|
// Check if currently executing
|
|
3214
|
-
if (this.currentPhaseItem
|
|
3215
|
-
this.
|
|
3243
|
+
if (this.currentPhaseItem &&
|
|
3244
|
+
normQueuePath(this.currentPhaseItem.path) === norm) {
|
|
3216
3245
|
return { position: 0, alreadyQueued: true };
|
|
3217
3246
|
}
|
|
3218
|
-
// Check if already in
|
|
3219
|
-
const existing = this.
|
|
3247
|
+
// Check if already in queue
|
|
3248
|
+
const existing = this.entries.findIndex((e) => normQueuePath(e.path) === norm);
|
|
3220
3249
|
if (existing !== -1) {
|
|
3221
3250
|
return { position: existing, alreadyQueued: true };
|
|
3222
3251
|
}
|
|
3223
|
-
this.
|
|
3252
|
+
this.entries.push({
|
|
3224
3253
|
path,
|
|
3225
3254
|
enqueuedAt: new Date().toISOString(),
|
|
3226
3255
|
});
|
|
3227
|
-
const position = this.
|
|
3228
|
-
if (this.
|
|
3229
|
-
this.logger.warn({ depth: this.
|
|
3256
|
+
const position = this.entries.length - 1;
|
|
3257
|
+
if (this.entries.length > DEPTH_WARNING_THRESHOLD) {
|
|
3258
|
+
this.logger.warn({ depth: this.entries.length }, 'Queue depth exceeds threshold');
|
|
3230
3259
|
}
|
|
3231
3260
|
this.onEnqueueCallback?.();
|
|
3232
3261
|
return { position, alreadyQueued: false };
|
|
3233
3262
|
}
|
|
3234
|
-
/** Dequeue the next
|
|
3235
|
-
|
|
3236
|
-
return this.
|
|
3263
|
+
/** Dequeue the next entry, or undefined if empty. */
|
|
3264
|
+
dequeue() {
|
|
3265
|
+
return this.entries.shift();
|
|
3266
|
+
}
|
|
3267
|
+
/** Get all queued entries (shallow copy). */
|
|
3268
|
+
get items() {
|
|
3269
|
+
return [...this.entries];
|
|
3237
3270
|
}
|
|
3238
|
-
/**
|
|
3239
|
-
get
|
|
3240
|
-
return
|
|
3271
|
+
/** Number of items waiting in the queue (excludes current). */
|
|
3272
|
+
get depth() {
|
|
3273
|
+
return this.entries.length;
|
|
3241
3274
|
}
|
|
3242
|
-
/**
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3275
|
+
/**
|
|
3276
|
+
* Remove all pending items from the queue.
|
|
3277
|
+
* Does not affect the currently-running item.
|
|
3278
|
+
*
|
|
3279
|
+
* @returns The number of items removed.
|
|
3280
|
+
*/
|
|
3281
|
+
clear() {
|
|
3282
|
+
const count = this.entries.length;
|
|
3283
|
+
this.entries = [];
|
|
3246
3284
|
return count;
|
|
3247
3285
|
}
|
|
3248
|
-
/** Check
|
|
3249
|
-
|
|
3250
|
-
|
|
3286
|
+
/** Check whether a path is in the queue or currently being synthesized. */
|
|
3287
|
+
has(path) {
|
|
3288
|
+
const norm = normQueuePath(path);
|
|
3289
|
+
if (this.currentPhaseItem &&
|
|
3290
|
+
normQueuePath(this.currentPhaseItem.path) === norm) {
|
|
3291
|
+
return true;
|
|
3292
|
+
}
|
|
3293
|
+
return this.entries.some((e) => normQueuePath(e.path) === norm);
|
|
3251
3294
|
}
|
|
3252
|
-
// ── Current-item tracking
|
|
3295
|
+
// ── Current-item tracking ──────────────────────────────────────────
|
|
3253
3296
|
/** Set the currently executing phase item. */
|
|
3254
3297
|
setCurrentPhase(path, phase) {
|
|
3255
3298
|
this.currentPhaseItem = {
|
|
@@ -3266,129 +3309,21 @@ class SynthesisQueue {
|
|
|
3266
3309
|
get currentPhase() {
|
|
3267
3310
|
return this.currentPhaseItem;
|
|
3268
3311
|
}
|
|
3269
|
-
// ──
|
|
3270
|
-
/**
|
|
3271
|
-
* Add a path to the synthesis queue.
|
|
3272
|
-
*
|
|
3273
|
-
* @param path - Meta path to synthesize.
|
|
3274
|
-
* @param priority - If true, insert at front of queue.
|
|
3275
|
-
* @returns Position and whether the path was already queued.
|
|
3276
|
-
*/
|
|
3277
|
-
enqueue(path, priority = false) {
|
|
3278
|
-
if (this.currentItem?.path === path) {
|
|
3279
|
-
return { position: 0, alreadyQueued: true };
|
|
3280
|
-
}
|
|
3281
|
-
const existingIndex = this.queue.findIndex((item) => item.path === path);
|
|
3282
|
-
if (existingIndex !== -1) {
|
|
3283
|
-
return { position: existingIndex, alreadyQueued: true };
|
|
3284
|
-
}
|
|
3285
|
-
const item = {
|
|
3286
|
-
path,
|
|
3287
|
-
priority,
|
|
3288
|
-
enqueuedAt: new Date().toISOString(),
|
|
3289
|
-
};
|
|
3290
|
-
if (priority) {
|
|
3291
|
-
this.queue.unshift(item);
|
|
3292
|
-
}
|
|
3293
|
-
else {
|
|
3294
|
-
this.queue.push(item);
|
|
3295
|
-
}
|
|
3296
|
-
if (this.queue.length > DEPTH_WARNING_THRESHOLD) {
|
|
3297
|
-
this.logger.warn({ depth: this.queue.length }, 'Queue depth exceeds threshold');
|
|
3298
|
-
}
|
|
3299
|
-
const position = this.queue.findIndex((i) => i.path === path);
|
|
3300
|
-
this.onEnqueueCallback?.();
|
|
3301
|
-
return { position, alreadyQueued: false };
|
|
3302
|
-
}
|
|
3303
|
-
/** Remove and return the next item from the queue. */
|
|
3304
|
-
dequeue() {
|
|
3305
|
-
const item = this.queue.shift();
|
|
3306
|
-
if (item) {
|
|
3307
|
-
this.currentItem = item;
|
|
3308
|
-
}
|
|
3309
|
-
return item;
|
|
3310
|
-
}
|
|
3311
|
-
/** Mark the currently-running synthesis as complete. */
|
|
3312
|
-
complete() {
|
|
3313
|
-
this.currentItem = null;
|
|
3314
|
-
}
|
|
3315
|
-
/** Number of items waiting in the queue (excludes current). */
|
|
3316
|
-
get depth() {
|
|
3317
|
-
return this.queue.length;
|
|
3318
|
-
}
|
|
3319
|
-
/** The item currently being synthesized, or null. */
|
|
3320
|
-
get current() {
|
|
3321
|
-
return this.currentItem;
|
|
3322
|
-
}
|
|
3323
|
-
/** A shallow copy of the queued items. */
|
|
3324
|
-
get items() {
|
|
3325
|
-
return [...this.queue];
|
|
3326
|
-
}
|
|
3327
|
-
/** A shallow copy of the pending items (alias for items). */
|
|
3328
|
-
get pending() {
|
|
3329
|
-
return [...this.queue];
|
|
3330
|
-
}
|
|
3331
|
-
/**
|
|
3332
|
-
* Remove all pending items from the queue.
|
|
3333
|
-
* Does not affect the currently-running item.
|
|
3334
|
-
*
|
|
3335
|
-
* @returns The number of items removed.
|
|
3336
|
-
*/
|
|
3337
|
-
clear() {
|
|
3338
|
-
const count = this.queue.length;
|
|
3339
|
-
this.queue = [];
|
|
3340
|
-
return count;
|
|
3341
|
-
}
|
|
3342
|
-
/** Check whether a path is in the queue or currently being synthesized. */
|
|
3343
|
-
has(path) {
|
|
3344
|
-
if (this.currentItem?.path === path)
|
|
3345
|
-
return true;
|
|
3346
|
-
if (this.currentPhaseItem?.path === path)
|
|
3347
|
-
return true;
|
|
3348
|
-
return (this.queue.some((item) => item.path === path) ||
|
|
3349
|
-
this.overrideEntries.some((e) => e.path === path));
|
|
3350
|
-
}
|
|
3351
|
-
/** Get the 0-indexed position of a path in the queue. */
|
|
3352
|
-
getPosition(path) {
|
|
3353
|
-
// Check overrides first
|
|
3354
|
-
const overrideIdx = this.overrideEntries.findIndex((e) => e.path === path);
|
|
3355
|
-
if (overrideIdx !== -1)
|
|
3356
|
-
return overrideIdx;
|
|
3357
|
-
const index = this.queue.findIndex((item) => item.path === path);
|
|
3358
|
-
return index === -1 ? null : index;
|
|
3359
|
-
}
|
|
3360
|
-
/** Dequeue the next item: overrides first, then legacy queue. */
|
|
3361
|
-
nextItem() {
|
|
3362
|
-
const override = this.dequeueOverride();
|
|
3363
|
-
if (override)
|
|
3364
|
-
return { path: override.path, source: 'override' };
|
|
3365
|
-
const item = this.dequeue();
|
|
3366
|
-
if (item)
|
|
3367
|
-
return { path: item.path, source: 'legacy' };
|
|
3368
|
-
return undefined;
|
|
3369
|
-
}
|
|
3312
|
+
// ── Queue state ────────────────────────────────────────────────────
|
|
3370
3313
|
/** Return a snapshot of queue state for the /status endpoint. */
|
|
3371
3314
|
getState() {
|
|
3372
3315
|
return {
|
|
3373
|
-
depth: this.
|
|
3374
|
-
items:
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
enqueuedAt: e.enqueuedAt,
|
|
3379
|
-
})),
|
|
3380
|
-
...this.queue.map((item) => ({
|
|
3381
|
-
path: item.path,
|
|
3382
|
-
priority: item.priority,
|
|
3383
|
-
enqueuedAt: item.enqueuedAt,
|
|
3384
|
-
})),
|
|
3385
|
-
],
|
|
3316
|
+
depth: this.entries.length,
|
|
3317
|
+
items: this.entries.map((e) => ({
|
|
3318
|
+
path: e.path,
|
|
3319
|
+
enqueuedAt: e.enqueuedAt,
|
|
3320
|
+
})),
|
|
3386
3321
|
};
|
|
3387
3322
|
}
|
|
3323
|
+
// ── Processing ─────────────────────────────────────────────────────
|
|
3388
3324
|
/**
|
|
3389
|
-
* Process queued items one at a time until
|
|
3325
|
+
* Process queued items one at a time until the queue is empty.
|
|
3390
3326
|
*
|
|
3391
|
-
* Override entries are processed first (FIFO), then legacy queue items.
|
|
3392
3327
|
* Re-entry is prevented: if already processing, the call returns
|
|
3393
3328
|
* immediately. Errors are logged and do not block subsequent items.
|
|
3394
3329
|
*
|
|
@@ -3399,7 +3334,7 @@ class SynthesisQueue {
|
|
|
3399
3334
|
return;
|
|
3400
3335
|
this.processing = true;
|
|
3401
3336
|
try {
|
|
3402
|
-
let next = this.
|
|
3337
|
+
let next = this.dequeue();
|
|
3403
3338
|
while (next) {
|
|
3404
3339
|
try {
|
|
3405
3340
|
await synthesizeFn(next.path);
|
|
@@ -3408,9 +3343,7 @@ class SynthesisQueue {
|
|
|
3408
3343
|
this.logger.error({ path: next.path, err }, 'Synthesis failed');
|
|
3409
3344
|
}
|
|
3410
3345
|
this.clearCurrentPhase();
|
|
3411
|
-
|
|
3412
|
-
this.complete();
|
|
3413
|
-
next = this.nextItem();
|
|
3346
|
+
next = this.dequeue();
|
|
3414
3347
|
}
|
|
3415
3348
|
}
|
|
3416
3349
|
finally {
|
|
@@ -3964,7 +3897,6 @@ class Scheduler {
|
|
|
3964
3897
|
this.logger.debug({ backoffMultiplier: this.backoffMultiplier }, 'No ready phases found, increasing backoff');
|
|
3965
3898
|
return;
|
|
3966
3899
|
}
|
|
3967
|
-
// Enqueue using the legacy queue path (backward compat with processQueue)
|
|
3968
3900
|
this.queue.enqueue(candidate.path);
|
|
3969
3901
|
this.logger.info({ path: candidate.path, phase: candidate.phase, band: candidate.band }, 'Enqueued phase candidate');
|
|
3970
3902
|
// Opportunistic watcher restart detection
|
|
@@ -4545,9 +4477,10 @@ function registerPreviewRoute(app, deps) {
|
|
|
4545
4477
|
ownedFiles: scopeFiles.length,
|
|
4546
4478
|
childMetas: targetNode.children.length,
|
|
4547
4479
|
deltaFiles: deltaFiles
|
|
4548
|
-
.slice(0,
|
|
4480
|
+
.slice(0, config.previewDeltaFilesCap)
|
|
4549
4481
|
.map((f) => ({ path: f, action: 'modified' })),
|
|
4550
4482
|
deltaCount: deltaFiles.length,
|
|
4483
|
+
deltaFilesTruncated: deltaFiles.length > config.previewDeltaFilesCap,
|
|
4551
4484
|
},
|
|
4552
4485
|
estimatedTokens,
|
|
4553
4486
|
// New phase-state fields (additive)
|
|
@@ -4563,8 +4496,8 @@ function registerPreviewRoute(app, deps) {
|
|
|
4563
4496
|
/**
|
|
4564
4497
|
* Queue management and abort routes.
|
|
4565
4498
|
*
|
|
4566
|
-
* - GET /queue — 3-layer queue model (current,
|
|
4567
|
-
* - POST /queue/clear — remove
|
|
4499
|
+
* - GET /queue — 3-layer queue model (current, pending, automatic)
|
|
4500
|
+
* - POST /queue/clear — remove pending entries
|
|
4568
4501
|
* - POST /synthesize/abort — abort the current synthesis
|
|
4569
4502
|
*
|
|
4570
4503
|
* @module routes/queue
|
|
@@ -4574,24 +4507,24 @@ function registerQueueRoutes(app, deps) {
|
|
|
4574
4507
|
const { queue } = deps;
|
|
4575
4508
|
app.get(getEndpoint('queue').path, async () => {
|
|
4576
4509
|
const currentPhase = queue.currentPhase;
|
|
4577
|
-
const
|
|
4578
|
-
// Compute owedPhase for each
|
|
4579
|
-
const
|
|
4510
|
+
const pending = queue.items;
|
|
4511
|
+
// Compute owedPhase for each pending entry by reading meta state
|
|
4512
|
+
const enrichedPending = await Promise.all(pending.map(async (entry) => {
|
|
4580
4513
|
try {
|
|
4581
|
-
const metaDir = resolveMetaDir(
|
|
4514
|
+
const metaDir = resolveMetaDir(entry.path);
|
|
4582
4515
|
const meta = await readMetaJson(metaDir);
|
|
4583
4516
|
const ps = derivePhaseState(meta);
|
|
4584
4517
|
return {
|
|
4585
|
-
path:
|
|
4518
|
+
path: entry.path,
|
|
4586
4519
|
owedPhase: getOwedPhase(ps),
|
|
4587
|
-
enqueuedAt:
|
|
4520
|
+
enqueuedAt: entry.enqueuedAt,
|
|
4588
4521
|
};
|
|
4589
4522
|
}
|
|
4590
4523
|
catch {
|
|
4591
4524
|
return {
|
|
4592
|
-
path:
|
|
4525
|
+
path: entry.path,
|
|
4593
4526
|
owedPhase: null,
|
|
4594
|
-
enqueuedAt:
|
|
4527
|
+
enqueuedAt: entry.enqueuedAt,
|
|
4595
4528
|
};
|
|
4596
4529
|
}
|
|
4597
4530
|
}));
|
|
@@ -4612,21 +4545,6 @@ function registerQueueRoutes(app, deps) {
|
|
|
4612
4545
|
catch {
|
|
4613
4546
|
// If listing fails, automatic stays empty
|
|
4614
4547
|
}
|
|
4615
|
-
// Legacy: pending is the union of overrides + automatic + legacy queue items
|
|
4616
|
-
const pendingItems = [
|
|
4617
|
-
...enrichedOverrides.map((o) => ({
|
|
4618
|
-
path: o.path,
|
|
4619
|
-
owedPhase: o.owedPhase,
|
|
4620
|
-
})),
|
|
4621
|
-
...automatic.map((a) => ({
|
|
4622
|
-
path: a.path,
|
|
4623
|
-
owedPhase: a.owedPhase,
|
|
4624
|
-
})),
|
|
4625
|
-
...queue.pending.map((item) => ({
|
|
4626
|
-
path: item.path,
|
|
4627
|
-
owedPhase: null,
|
|
4628
|
-
})),
|
|
4629
|
-
];
|
|
4630
4548
|
return {
|
|
4631
4549
|
current: currentPhase
|
|
4632
4550
|
? {
|
|
@@ -4634,58 +4552,45 @@ function registerQueueRoutes(app, deps) {
|
|
|
4634
4552
|
phase: currentPhase.phase,
|
|
4635
4553
|
startedAt: currentPhase.startedAt,
|
|
4636
4554
|
}
|
|
4637
|
-
:
|
|
4638
|
-
|
|
4639
|
-
path: queue.current.path,
|
|
4640
|
-
phase: null,
|
|
4641
|
-
startedAt: queue.current.enqueuedAt,
|
|
4642
|
-
}
|
|
4643
|
-
: null,
|
|
4644
|
-
overrides: enrichedOverrides,
|
|
4555
|
+
: null,
|
|
4556
|
+
pending: enrichedPending,
|
|
4645
4557
|
automatic,
|
|
4646
|
-
pending: pendingItems,
|
|
4647
|
-
// Legacy state
|
|
4648
|
-
state: queue.getState(),
|
|
4649
4558
|
};
|
|
4650
4559
|
});
|
|
4651
4560
|
app.post(getEndpoint('queueClear').path, () => {
|
|
4652
|
-
const removed = queue.
|
|
4561
|
+
const removed = queue.clear();
|
|
4653
4562
|
return { cleared: removed };
|
|
4654
4563
|
});
|
|
4655
4564
|
app.post(getEndpoint('abort').path, async (_request, reply) => {
|
|
4656
|
-
// Check 3-layer current first
|
|
4657
4565
|
const currentPhase = queue.currentPhase;
|
|
4658
|
-
|
|
4659
|
-
if (!current) {
|
|
4566
|
+
if (!currentPhase) {
|
|
4660
4567
|
return reply.status(200).send({ status: 'idle' });
|
|
4661
4568
|
}
|
|
4662
4569
|
// Abort the executor
|
|
4663
4570
|
deps.executor?.abort();
|
|
4664
|
-
const metaDir = resolveMetaDir(
|
|
4665
|
-
const phase = currentPhase
|
|
4571
|
+
const metaDir = resolveMetaDir(currentPhase.path);
|
|
4572
|
+
const { phase } = currentPhase;
|
|
4666
4573
|
// Transition running phase to failed and write _error to meta.json
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
// Best-effort — meta may be unreadable
|
|
4688
|
-
}
|
|
4574
|
+
try {
|
|
4575
|
+
const meta = await readMetaJson(metaDir);
|
|
4576
|
+
let ps = derivePhaseState(meta);
|
|
4577
|
+
ps = phaseFailed(ps, phase);
|
|
4578
|
+
const updated = {
|
|
4579
|
+
...meta,
|
|
4580
|
+
_phaseState: ps,
|
|
4581
|
+
_error: {
|
|
4582
|
+
step: phase,
|
|
4583
|
+
code: 'ABORT',
|
|
4584
|
+
message: 'Aborted by operator',
|
|
4585
|
+
},
|
|
4586
|
+
};
|
|
4587
|
+
const lockPath = join(metaDir, '.lock');
|
|
4588
|
+
const metaJsonPath = join(metaDir, 'meta.json');
|
|
4589
|
+
await writeFile(lockPath, JSON.stringify(updated, null, 2) + '\n');
|
|
4590
|
+
await copyFile(lockPath, metaJsonPath);
|
|
4591
|
+
}
|
|
4592
|
+
catch {
|
|
4593
|
+
// Best-effort — meta may be unreadable
|
|
4689
4594
|
}
|
|
4690
4595
|
// Release the lock for the current meta path
|
|
4691
4596
|
try {
|
|
@@ -4694,11 +4599,11 @@ function registerQueueRoutes(app, deps) {
|
|
|
4694
4599
|
catch {
|
|
4695
4600
|
// Lock may already be released
|
|
4696
4601
|
}
|
|
4697
|
-
deps.logger.info({ path:
|
|
4602
|
+
deps.logger.info({ path: currentPhase.path }, 'Synthesis aborted');
|
|
4698
4603
|
return {
|
|
4699
4604
|
status: 'aborted',
|
|
4700
|
-
path:
|
|
4701
|
-
|
|
4605
|
+
path: currentPhase.path,
|
|
4606
|
+
phase,
|
|
4702
4607
|
};
|
|
4703
4608
|
});
|
|
4704
4609
|
}
|
|
@@ -4809,9 +4714,9 @@ async function checkWatcher(url) {
|
|
|
4809
4714
|
function deriveServiceState(deps) {
|
|
4810
4715
|
if (deps.shuttingDown)
|
|
4811
4716
|
return 'stopping';
|
|
4812
|
-
if (deps.queue.
|
|
4717
|
+
if (deps.queue.currentPhase)
|
|
4813
4718
|
return 'synthesizing';
|
|
4814
|
-
if (deps.queue.depth > 0
|
|
4719
|
+
if (deps.queue.depth > 0)
|
|
4815
4720
|
return 'waiting';
|
|
4816
4721
|
return 'idle';
|
|
4817
4722
|
}
|
|
@@ -4863,7 +4768,7 @@ function registerStatusRoute(app, deps) {
|
|
|
4863
4768
|
}
|
|
4864
4769
|
return {
|
|
4865
4770
|
serviceState: deriveServiceState(deps),
|
|
4866
|
-
currentTarget: queue.
|
|
4771
|
+
currentTarget: queue.currentPhase?.path ?? null,
|
|
4867
4772
|
queue: queue.getState(),
|
|
4868
4773
|
stats: {
|
|
4869
4774
|
totalSyntheses: stats.totalSyntheses,
|
|
@@ -4951,7 +4856,7 @@ function registerSynthesizeRoute(app, deps) {
|
|
|
4951
4856
|
alreadyQueued: false,
|
|
4952
4857
|
});
|
|
4953
4858
|
}
|
|
4954
|
-
const result = queue.
|
|
4859
|
+
const result = queue.enqueue(targetPath);
|
|
4955
4860
|
return reply.code(202).send({
|
|
4956
4861
|
status: 'queued',
|
|
4957
4862
|
path: targetPath,
|
|
@@ -4982,8 +4887,9 @@ function registerSynthesizeRoute(app, deps) {
|
|
|
4982
4887
|
const stalest = winner.node.metaPath;
|
|
4983
4888
|
const enqueueResult = queue.enqueue(stalest);
|
|
4984
4889
|
return reply.code(202).send({
|
|
4985
|
-
status: '
|
|
4890
|
+
status: 'queued',
|
|
4986
4891
|
path: stalest,
|
|
4892
|
+
owedPhase: winner.owedPhase,
|
|
4987
4893
|
queuePosition: enqueueResult.position,
|
|
4988
4894
|
alreadyQueued: enqueueResult.alreadyQueued,
|
|
4989
4895
|
});
|
|
@@ -5132,26 +5038,14 @@ function registerShutdownHandlers(deps) {
|
|
|
5132
5038
|
deps.logger.info('Scheduler stopped');
|
|
5133
5039
|
}
|
|
5134
5040
|
// 2. Release lock for in-progress synthesis
|
|
5135
|
-
const current = deps.queue.current;
|
|
5136
|
-
if (current) {
|
|
5137
|
-
try {
|
|
5138
|
-
releaseLock(current.path);
|
|
5139
|
-
deps.logger.info({ path: current.path }, 'Released lock for in-progress synthesis');
|
|
5140
|
-
}
|
|
5141
|
-
catch {
|
|
5142
|
-
deps.logger.warn({ path: current.path }, 'Failed to release lock during shutdown');
|
|
5143
|
-
}
|
|
5144
|
-
}
|
|
5145
|
-
// Release lock for in-progress override synthesis (only when it
|
|
5146
|
-
// differs from the legacy current item to avoid double-release)
|
|
5147
5041
|
const currentPhase = deps.queue.currentPhase;
|
|
5148
|
-
if (currentPhase
|
|
5042
|
+
if (currentPhase) {
|
|
5149
5043
|
try {
|
|
5150
5044
|
releaseLock(resolveMetaDir(currentPhase.path));
|
|
5151
|
-
deps.logger.info({ path: currentPhase.path }, 'Released lock for in-progress
|
|
5045
|
+
deps.logger.info({ path: currentPhase.path }, 'Released lock for in-progress synthesis');
|
|
5152
5046
|
}
|
|
5153
5047
|
catch {
|
|
5154
|
-
deps.logger.warn({ path: currentPhase.path }, 'Failed to release
|
|
5048
|
+
deps.logger.warn({ path: currentPhase.path }, 'Failed to release lock during shutdown');
|
|
5155
5049
|
}
|
|
5156
5050
|
}
|
|
5157
5051
|
// 3. Close server
|
|
@@ -5288,6 +5182,9 @@ async function startService(config, configPath) {
|
|
|
5288
5182
|
const executor = new GatewayExecutor({
|
|
5289
5183
|
gatewayUrl: config.gatewayUrl,
|
|
5290
5184
|
apiKey: config.gatewayApiKey,
|
|
5185
|
+
workspaceDir: config.workspaceDir,
|
|
5186
|
+
stagingRetries: config.stagingRetries,
|
|
5187
|
+
stagingRetryDelayMs: config.stagingRetryDelayMs,
|
|
5291
5188
|
});
|
|
5292
5189
|
// Runtime stats (mutable, shared with routes)
|
|
5293
5190
|
const stats = {
|