@karmaniverous/jeeves-meta 0.16.0 → 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.
@@ -975,16 +975,20 @@ class GatewayExecutor {
975
975
  : '';
976
976
  return text && text.trim() !== 'ANNOUNCE_SKIP' ? text : undefined;
977
977
  }
978
- /** Check history messages for terminal completion. */
978
+ /**
979
+ * Check history messages for timeout detection.
980
+ *
981
+ * Does NOT determine completion — session completion is authoritative
982
+ * (via sessions_list). History stop reasons can false-positive on
983
+ * sessions_yield artifacts (#200).
984
+ */
979
985
  static checkHistoryCompletion(messages) {
980
986
  if (messages.length === 0)
981
- return { done: false, timedOut: false };
987
+ return { timedOut: false };
982
988
  const last = messages[messages.length - 1];
983
989
  if (last.role !== 'assistant' || !last.stopReason)
984
- return { done: false, timedOut: false };
985
- if (last.stopReason === 'toolUse' || last.stopReason === 'error')
986
- return { done: false, timedOut: false };
987
- return { done: true, timedOut: last.stopReason === 'timeout' };
990
+ return { timedOut: false };
991
+ return { timedOut: last.stopReason === 'timeout' };
988
992
  }
989
993
  /** Invoke a gateway tool via the /tools/invoke HTTP endpoint. */
990
994
  async invoke(tool, args, sessionKey) {
@@ -1147,12 +1151,14 @@ class GatewayExecutor {
1147
1151
  historyResult.result?.messages ??
1148
1152
  [];
1149
1153
  const msgArray = messages;
1150
- // Check 1: terminal stop reason in history
1151
- const { done: historyDone, timedOut: historyTimedOut } = GatewayExecutor.checkHistoryCompletion(msgArray);
1154
+ // Check 1: history-based timeout detection (stop reason)
1155
+ const { timedOut: historyTimedOut } = GatewayExecutor.checkHistoryCompletion(msgArray);
1152
1156
  // Check 2: session completion status via sessions_list
1157
+ // Gate on session completion only — history stop reasons can
1158
+ // false-positive on sessions_yield artifacts (#200).
1153
1159
  const sessionInfo = await this.getSessionInfo(sessionKey);
1154
1160
  const timedOut = sessionInfo.timedOut || historyTimedOut;
1155
- if (historyDone || sessionInfo.completed) {
1161
+ if (sessionInfo.completed) {
1156
1162
  const tokens = sessionInfo.tokens;
1157
1163
  // Gateway-side timeout detected — check staging file for recovery
1158
1164
  if (timedOut) {
@@ -3167,36 +3173,33 @@ class ProgressReporter {
3167
3173
  }
3168
3174
 
3169
3175
  /**
3170
- * Hybrid 3-layer synthesis queue.
3176
+ * Synthesis queue.
3171
3177
  *
3172
3178
  * Layer 1: Current — the single item currently executing (at most one).
3173
- * Layer 2: Overrides — items manually enqueued via POST /synthesize with path.
3174
- * FIFO among overrides, ahead of automatic candidates.
3175
- * Layer 3: Automatic — computed on read, not persisted. All metas with a
3176
- * pending phase, ranked by scheduler priority.
3177
- *
3178
- * Legacy: `pending` array is the union of overrides + automatic.
3179
+ * Layer 2: Pending — items enqueued via POST /synthesize (targeted) or
3180
+ * scheduler tick (automatic). FIFO, ahead of automatic candidates.
3181
+ * Layer 3: Automatic — computed on read (GET /queue), not persisted. All
3182
+ * metas with a pending phase, ranked by scheduler priority.
3179
3183
  *
3180
3184
  * @module queue
3181
3185
  */
3182
3186
  const DEPTH_WARNING_THRESHOLD = 3;
3187
+ /** Strip trailing .meta suffix for consistent path comparison. */
3188
+ function normQueuePath(p) {
3189
+ return p.endsWith('.meta') ? p.slice(0, -5).replace(/[/\\]$/, '') : p;
3190
+ }
3183
3191
  /**
3184
- * Hybrid 3-layer synthesis queue.
3192
+ * Synthesis queue.
3185
3193
  *
3186
- * Only one synthesis runs at a time. Override items (explicit triggers)
3187
- * take priority over automatic candidates.
3194
+ * Only one synthesis runs at a time. Explicitly enqueued items
3195
+ * take priority over automatic (computed-on-read) candidates.
3188
3196
  */
3189
3197
  class SynthesisQueue {
3190
- /** Legacy queue (used by processQueue for backward compat). */
3191
- queue = [];
3192
- currentItem = null;
3198
+ entries = [];
3199
+ currentPhaseItem = null;
3193
3200
  processing = false;
3194
3201
  logger;
3195
3202
  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
3203
  constructor(logger) {
3201
3204
  this.logger = logger;
3202
3205
  }
@@ -3204,52 +3207,67 @@ class SynthesisQueue {
3204
3207
  onEnqueue(callback) {
3205
3208
  this.onEnqueueCallback = callback;
3206
3209
  }
3207
- // ── Override layer (3-layer model) ─────────────────────────────────
3210
+ // ── Enqueue / dequeue ──────────────────────────────────────────────
3208
3211
  /**
3209
- * Add an explicit override entry (from POST /synthesize with path).
3212
+ * Add an entry to the queue.
3210
3213
  * Deduped by path. Returns position and whether already queued.
3211
3214
  */
3212
- enqueueOverride(path) {
3215
+ enqueue(path) {
3216
+ const norm = normQueuePath(path);
3213
3217
  // Check if currently executing
3214
- if (this.currentPhaseItem?.path === path ||
3215
- this.currentItem?.path === path) {
3218
+ if (this.currentPhaseItem &&
3219
+ normQueuePath(this.currentPhaseItem.path) === norm) {
3216
3220
  return { position: 0, alreadyQueued: true };
3217
3221
  }
3218
- // Check if already in overrides
3219
- const existing = this.overrideEntries.findIndex((e) => e.path === path);
3222
+ // Check if already in queue
3223
+ const existing = this.entries.findIndex((e) => normQueuePath(e.path) === norm);
3220
3224
  if (existing !== -1) {
3221
3225
  return { position: existing, alreadyQueued: true };
3222
3226
  }
3223
- this.overrideEntries.push({
3227
+ this.entries.push({
3224
3228
  path,
3225
3229
  enqueuedAt: new Date().toISOString(),
3226
3230
  });
3227
- const position = this.overrideEntries.length - 1;
3228
- if (this.overrideEntries.length > DEPTH_WARNING_THRESHOLD) {
3229
- this.logger.warn({ depth: this.overrideEntries.length }, 'Override queue depth exceeds threshold');
3231
+ const position = this.entries.length - 1;
3232
+ if (this.entries.length > DEPTH_WARNING_THRESHOLD) {
3233
+ this.logger.warn({ depth: this.entries.length }, 'Queue depth exceeds threshold');
3230
3234
  }
3231
3235
  this.onEnqueueCallback?.();
3232
3236
  return { position, alreadyQueued: false };
3233
3237
  }
3234
- /** Dequeue the next override entry, or undefined if empty. */
3235
- dequeueOverride() {
3236
- return this.overrideEntries.shift();
3238
+ /** Dequeue the next entry, or undefined if empty. */
3239
+ dequeue() {
3240
+ return this.entries.shift();
3241
+ }
3242
+ /** Get all queued entries (shallow copy). */
3243
+ get items() {
3244
+ return [...this.entries];
3237
3245
  }
3238
- /** Get all override entries (shallow copy). */
3239
- get overrides() {
3240
- return [...this.overrideEntries];
3246
+ /** Number of items waiting in the queue (excludes current). */
3247
+ get depth() {
3248
+ return this.entries.length;
3241
3249
  }
3242
- /** Clear all override entries. Returns count removed. */
3243
- clearOverrides() {
3244
- const count = this.overrideEntries.length;
3245
- this.overrideEntries = [];
3250
+ /**
3251
+ * Remove all pending items from the queue.
3252
+ * Does not affect the currently-running item.
3253
+ *
3254
+ * @returns The number of items removed.
3255
+ */
3256
+ clear() {
3257
+ const count = this.entries.length;
3258
+ this.entries = [];
3246
3259
  return count;
3247
3260
  }
3248
- /** Check if a path is in the override layer. */
3249
- hasOverride(path) {
3250
- return this.overrideEntries.some((e) => e.path === path);
3261
+ /** Check whether a path is in the queue or currently being synthesized. */
3262
+ has(path) {
3263
+ const norm = normQueuePath(path);
3264
+ if (this.currentPhaseItem &&
3265
+ normQueuePath(this.currentPhaseItem.path) === norm) {
3266
+ return true;
3267
+ }
3268
+ return this.entries.some((e) => normQueuePath(e.path) === norm);
3251
3269
  }
3252
- // ── Current-item tracking (3-layer model) ──────────────────────────
3270
+ // ── Current-item tracking ──────────────────────────────────────────
3253
3271
  /** Set the currently executing phase item. */
3254
3272
  setCurrentPhase(path, phase) {
3255
3273
  this.currentPhaseItem = {
@@ -3266,129 +3284,21 @@ class SynthesisQueue {
3266
3284
  get currentPhase() {
3267
3285
  return this.currentPhaseItem;
3268
3286
  }
3269
- // ── Legacy queue interface (preserved for backward compat) ─────────
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
- }
3287
+ // ── Queue state ────────────────────────────────────────────────────
3370
3288
  /** Return a snapshot of queue state for the /status endpoint. */
3371
3289
  getState() {
3372
3290
  return {
3373
- depth: this.queue.length + this.overrideEntries.length,
3374
- items: [
3375
- ...this.overrideEntries.map((e) => ({
3376
- path: e.path,
3377
- priority: true,
3378
- enqueuedAt: e.enqueuedAt,
3379
- })),
3380
- ...this.queue.map((item) => ({
3381
- path: item.path,
3382
- priority: item.priority,
3383
- enqueuedAt: item.enqueuedAt,
3384
- })),
3385
- ],
3291
+ depth: this.entries.length,
3292
+ items: this.entries.map((e) => ({
3293
+ path: e.path,
3294
+ enqueuedAt: e.enqueuedAt,
3295
+ })),
3386
3296
  };
3387
3297
  }
3298
+ // ── Processing ─────────────────────────────────────────────────────
3388
3299
  /**
3389
- * Process queued items one at a time until all queues are empty.
3300
+ * Process queued items one at a time until the queue is empty.
3390
3301
  *
3391
- * Override entries are processed first (FIFO), then legacy queue items.
3392
3302
  * Re-entry is prevented: if already processing, the call returns
3393
3303
  * immediately. Errors are logged and do not block subsequent items.
3394
3304
  *
@@ -3399,7 +3309,7 @@ class SynthesisQueue {
3399
3309
  return;
3400
3310
  this.processing = true;
3401
3311
  try {
3402
- let next = this.nextItem();
3312
+ let next = this.dequeue();
3403
3313
  while (next) {
3404
3314
  try {
3405
3315
  await synthesizeFn(next.path);
@@ -3408,9 +3318,7 @@ class SynthesisQueue {
3408
3318
  this.logger.error({ path: next.path, err }, 'Synthesis failed');
3409
3319
  }
3410
3320
  this.clearCurrentPhase();
3411
- if (next.source === 'legacy')
3412
- this.complete();
3413
- next = this.nextItem();
3321
+ next = this.dequeue();
3414
3322
  }
3415
3323
  }
3416
3324
  finally {
@@ -3964,7 +3872,6 @@ class Scheduler {
3964
3872
  this.logger.debug({ backoffMultiplier: this.backoffMultiplier }, 'No ready phases found, increasing backoff');
3965
3873
  return;
3966
3874
  }
3967
- // Enqueue using the legacy queue path (backward compat with processQueue)
3968
3875
  this.queue.enqueue(candidate.path);
3969
3876
  this.logger.info({ path: candidate.path, phase: candidate.phase, band: candidate.band }, 'Enqueued phase candidate');
3970
3877
  // Opportunistic watcher restart detection
@@ -4563,8 +4470,8 @@ function registerPreviewRoute(app, deps) {
4563
4470
  /**
4564
4471
  * Queue management and abort routes.
4565
4472
  *
4566
- * - GET /queue — 3-layer queue model (current, overrides, automatic, pending)
4567
- * - POST /queue/clear — remove override entries only
4473
+ * - GET /queue — 3-layer queue model (current, pending, automatic)
4474
+ * - POST /queue/clear — remove pending entries
4568
4475
  * - POST /synthesize/abort — abort the current synthesis
4569
4476
  *
4570
4477
  * @module routes/queue
@@ -4574,24 +4481,24 @@ function registerQueueRoutes(app, deps) {
4574
4481
  const { queue } = deps;
4575
4482
  app.get(getEndpoint('queue').path, async () => {
4576
4483
  const currentPhase = queue.currentPhase;
4577
- const overrides = queue.overrides;
4578
- // Compute owedPhase for each override entry by reading meta state
4579
- const enrichedOverrides = await Promise.all(overrides.map(async (o) => {
4484
+ const pending = queue.items;
4485
+ // Compute owedPhase for each pending entry by reading meta state
4486
+ const enrichedPending = await Promise.all(pending.map(async (entry) => {
4580
4487
  try {
4581
- const metaDir = resolveMetaDir(o.path);
4488
+ const metaDir = resolveMetaDir(entry.path);
4582
4489
  const meta = await readMetaJson(metaDir);
4583
4490
  const ps = derivePhaseState(meta);
4584
4491
  return {
4585
- path: o.path,
4492
+ path: entry.path,
4586
4493
  owedPhase: getOwedPhase(ps),
4587
- enqueuedAt: o.enqueuedAt,
4494
+ enqueuedAt: entry.enqueuedAt,
4588
4495
  };
4589
4496
  }
4590
4497
  catch {
4591
4498
  return {
4592
- path: o.path,
4499
+ path: entry.path,
4593
4500
  owedPhase: null,
4594
- enqueuedAt: o.enqueuedAt,
4501
+ enqueuedAt: entry.enqueuedAt,
4595
4502
  };
4596
4503
  }
4597
4504
  }));
@@ -4612,21 +4519,6 @@ function registerQueueRoutes(app, deps) {
4612
4519
  catch {
4613
4520
  // If listing fails, automatic stays empty
4614
4521
  }
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
4522
  return {
4631
4523
  current: currentPhase
4632
4524
  ? {
@@ -4634,58 +4526,45 @@ function registerQueueRoutes(app, deps) {
4634
4526
  phase: currentPhase.phase,
4635
4527
  startedAt: currentPhase.startedAt,
4636
4528
  }
4637
- : queue.current
4638
- ? {
4639
- path: queue.current.path,
4640
- phase: null,
4641
- startedAt: queue.current.enqueuedAt,
4642
- }
4643
- : null,
4644
- overrides: enrichedOverrides,
4529
+ : null,
4530
+ pending: enrichedPending,
4645
4531
  automatic,
4646
- pending: pendingItems,
4647
- // Legacy state
4648
- state: queue.getState(),
4649
4532
  };
4650
4533
  });
4651
4534
  app.post(getEndpoint('queueClear').path, () => {
4652
- const removed = queue.clearOverrides();
4535
+ const removed = queue.clear();
4653
4536
  return { cleared: removed };
4654
4537
  });
4655
4538
  app.post(getEndpoint('abort').path, async (_request, reply) => {
4656
- // Check 3-layer current first
4657
4539
  const currentPhase = queue.currentPhase;
4658
- const current = currentPhase ?? queue.current;
4659
- if (!current) {
4540
+ if (!currentPhase) {
4660
4541
  return reply.status(200).send({ status: 'idle' });
4661
4542
  }
4662
4543
  // Abort the executor
4663
4544
  deps.executor?.abort();
4664
- const metaDir = resolveMetaDir(current.path);
4665
- const phase = currentPhase?.phase ?? null;
4545
+ const metaDir = resolveMetaDir(currentPhase.path);
4546
+ const { phase } = currentPhase;
4666
4547
  // Transition running phase to failed and write _error to meta.json
4667
- if (phase) {
4668
- try {
4669
- const meta = await readMetaJson(metaDir);
4670
- let ps = derivePhaseState(meta);
4671
- ps = phaseFailed(ps, phase);
4672
- const updated = {
4673
- ...meta,
4674
- _phaseState: ps,
4675
- _error: {
4676
- step: phase,
4677
- code: 'ABORT',
4678
- message: 'Aborted by operator',
4679
- },
4680
- };
4681
- const lockPath = join(metaDir, '.lock');
4682
- const metaJsonPath = join(metaDir, 'meta.json');
4683
- await writeFile(lockPath, JSON.stringify(updated, null, 2) + '\n');
4684
- await copyFile(lockPath, metaJsonPath);
4685
- }
4686
- catch {
4687
- // Best-effort — meta may be unreadable
4688
- }
4548
+ try {
4549
+ const meta = await readMetaJson(metaDir);
4550
+ let ps = derivePhaseState(meta);
4551
+ ps = phaseFailed(ps, phase);
4552
+ const updated = {
4553
+ ...meta,
4554
+ _phaseState: ps,
4555
+ _error: {
4556
+ step: phase,
4557
+ code: 'ABORT',
4558
+ message: 'Aborted by operator',
4559
+ },
4560
+ };
4561
+ const lockPath = join(metaDir, '.lock');
4562
+ const metaJsonPath = join(metaDir, 'meta.json');
4563
+ await writeFile(lockPath, JSON.stringify(updated, null, 2) + '\n');
4564
+ await copyFile(lockPath, metaJsonPath);
4565
+ }
4566
+ catch {
4567
+ // Best-effort — meta may be unreadable
4689
4568
  }
4690
4569
  // Release the lock for the current meta path
4691
4570
  try {
@@ -4694,11 +4573,11 @@ function registerQueueRoutes(app, deps) {
4694
4573
  catch {
4695
4574
  // Lock may already be released
4696
4575
  }
4697
- deps.logger.info({ path: current.path }, 'Synthesis aborted');
4576
+ deps.logger.info({ path: currentPhase.path }, 'Synthesis aborted');
4698
4577
  return {
4699
4578
  status: 'aborted',
4700
- path: current.path,
4701
- ...(phase ? { phase } : {}),
4579
+ path: currentPhase.path,
4580
+ phase,
4702
4581
  };
4703
4582
  });
4704
4583
  }
@@ -4809,9 +4688,9 @@ async function checkWatcher(url) {
4809
4688
  function deriveServiceState(deps) {
4810
4689
  if (deps.shuttingDown)
4811
4690
  return 'stopping';
4812
- if (deps.queue.current || deps.queue.currentPhase)
4691
+ if (deps.queue.currentPhase)
4813
4692
  return 'synthesizing';
4814
- if (deps.queue.depth > 0 || deps.queue.overrides.length > 0)
4693
+ if (deps.queue.depth > 0)
4815
4694
  return 'waiting';
4816
4695
  return 'idle';
4817
4696
  }
@@ -4863,7 +4742,7 @@ function registerStatusRoute(app, deps) {
4863
4742
  }
4864
4743
  return {
4865
4744
  serviceState: deriveServiceState(deps),
4866
- currentTarget: queue.current?.path ?? queue.currentPhase?.path ?? null,
4745
+ currentTarget: queue.currentPhase?.path ?? null,
4867
4746
  queue: queue.getState(),
4868
4747
  stats: {
4869
4748
  totalSyntheses: stats.totalSyntheses,
@@ -4951,7 +4830,7 @@ function registerSynthesizeRoute(app, deps) {
4951
4830
  alreadyQueued: false,
4952
4831
  });
4953
4832
  }
4954
- const result = queue.enqueueOverride(targetPath);
4833
+ const result = queue.enqueue(targetPath);
4955
4834
  return reply.code(202).send({
4956
4835
  status: 'queued',
4957
4836
  path: targetPath,
@@ -4982,8 +4861,9 @@ function registerSynthesizeRoute(app, deps) {
4982
4861
  const stalest = winner.node.metaPath;
4983
4862
  const enqueueResult = queue.enqueue(stalest);
4984
4863
  return reply.code(202).send({
4985
- status: 'accepted',
4864
+ status: 'queued',
4986
4865
  path: stalest,
4866
+ owedPhase: winner.owedPhase,
4987
4867
  queuePosition: enqueueResult.position,
4988
4868
  alreadyQueued: enqueueResult.alreadyQueued,
4989
4869
  });
@@ -5132,26 +5012,14 @@ function registerShutdownHandlers(deps) {
5132
5012
  deps.logger.info('Scheduler stopped');
5133
5013
  }
5134
5014
  // 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
5015
  const currentPhase = deps.queue.currentPhase;
5148
- if (currentPhase && currentPhase.path !== current?.path) {
5016
+ if (currentPhase) {
5149
5017
  try {
5150
5018
  releaseLock(resolveMetaDir(currentPhase.path));
5151
- deps.logger.info({ path: currentPhase.path }, 'Released lock for in-progress override synthesis');
5019
+ deps.logger.info({ path: currentPhase.path }, 'Released lock for in-progress synthesis');
5152
5020
  }
5153
5021
  catch {
5154
- deps.logger.warn({ path: currentPhase.path }, 'Failed to release override lock during shutdown');
5022
+ deps.logger.warn({ path: currentPhase.path }, 'Failed to release lock during shutdown');
5155
5023
  }
5156
5024
  }
5157
5025
  // 3. Close server
@@ -40,7 +40,13 @@ export declare class GatewayExecutor implements MetaExecutor {
40
40
  private readStagingFile;
41
41
  /** Extract plain text from a message content field, skipping ANNOUNCE_SKIP sentinels. */
42
42
  private static extractMessageText;
43
- /** Check history messages for terminal completion. */
43
+ /**
44
+ * Check history messages for timeout detection.
45
+ *
46
+ * Does NOT determine completion — session completion is authoritative
47
+ * (via sessions_list). History stop reasons can false-positive on
48
+ * sessions_yield artifacts (#200).
49
+ */
44
50
  private static checkHistoryCompletion;
45
51
  /** Invoke a gateway tool via the /tools/invoke HTTP endpoint. */
46
52
  private invoke;
package/dist/index.d.ts CHANGED
@@ -23,7 +23,7 @@ export { formatProgressEvent, type ProgressEvent, type ProgressPhase, ProgressRe
23
23
  export { actualStaleness, computeEffectiveStaleness, hasSteerChanged, isArchitectTriggered, isStale, MAX_STALENESS_SECONDS, type StalenessCandidate, } from './scheduling/index.js';
24
24
  export { type MetaConfig, metaConfigSchema, type MetaError, metaErrorSchema, type MetaJson, metaJsonSchema, type ServiceConfig, serviceConfigSchema, } from './schema/index.js';
25
25
  export { Scheduler } from './scheduler/index.js';
26
- export { type EnqueueResult, type QueueItem, type QueueState, SynthesisQueue, } from './queue/index.js';
26
+ export { type CurrentItem, type EnqueueResult, type QueueEntry, type QueueState, SynthesisQueue, } from './queue/index.js';
27
27
  export { registerRoutes, type RouteDeps, type ServiceStats, } from './routes/index.js';
28
28
  export { RuleRegistrar } from './rules/index.js';
29
29
  export { verifyRuleApplication } from './rules/verify.js';