@songsid/agend 2.1.2-beta.31 → 2.1.2-beta.32

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.
@@ -100,6 +100,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
100
100
  private instanceIdleWaiters;
101
101
  private lastInboundUser;
102
102
  private cancelButtons;
103
+ /** Pending idle-edge retirement, one timer per instance. */
104
+ private cancelButtonIdleRetireTimers;
103
105
  /** Duplicate-reply suppression across both the MCP and HTTP reply paths. */
104
106
  readonly replyDeduper: ReplyDeduper;
105
107
  /** instanceName → what it is doing right now, when the backend can tell us. */
@@ -231,6 +233,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
231
233
  getInstanceExecutionState(name: string): InstanceState | null;
232
234
  isClassicInstance(name: string): boolean;
233
235
  private cacheInstanceExecutionState;
236
+ private cancelIdleButtonRetirement;
237
+ private scheduleIdleButtonRetirement;
234
238
  private cacheInstanceProcessStatus;
235
239
  /**
236
240
  * Fleet-wide warm cap: if more than `defaults.warm_cap` instances are running,
@@ -87,6 +87,12 @@ const CANCEL_BTN_MAX_RETRIES = 3;
87
87
  * buttons no clear trigger reached (e.g. a scheduled/HTTP turn that never called
88
88
  * reply). 5min (not the old 2s idle-watch) so Thinking isn't misread as idle. */
89
89
  const CANCEL_BTN_IDLE_CHECK_INTERVAL_MS = 5 * 60_000;
90
+ /**
91
+ * A queued turn can produce a very short idle edge while the CLI hands off to
92
+ * the next message. Do not retire the cancel button until that edge remains
93
+ * idle for this long; a working/stuck report during the grace cancels it.
94
+ */
95
+ const CANCEL_BTN_IDLE_RETIRE_GRACE_MS = 2_000;
90
96
  /**
91
97
  * How long after a reply an instance gets to resume working before its cancel
92
98
  * button is retired. A short turn ends with a reply and never works again → the
@@ -220,6 +226,8 @@ export class FleetManager {
220
226
  // reply, on cancel, or when a newer button supersedes it for the same
221
227
  // instance. Per-button tracking means a failed delete never strands a button.
222
228
  cancelButtons = new Map();
229
+ /** Pending idle-edge retirement, one timer per instance. */
230
+ cancelButtonIdleRetireTimers = new Map();
223
231
  /** Duplicate-reply suppression across both the MCP and HTTP reply paths. */
224
232
  replyDeduper = new ReplyDeduper();
225
233
  /** instanceName → what it is doing right now, when the backend can tell us. */
@@ -624,11 +632,35 @@ export class FleetManager {
624
632
  // edge into idle, not on every idle heartbeat.
625
633
  if (state === "idle" && previous?.state !== "idle") {
626
634
  this.enforceWarmCap();
627
- // The turn is genuinely over retire the cancel/progress button now rather
628
- // than waiting for the 5-minute idle backstop to notice.
629
- this.retireInstanceButtons(name);
635
+ // A queued message may turn this edge back into working almost
636
+ // immediately. Give that handoff a short grace before retiring the button.
637
+ this.scheduleIdleButtonRetirement(name);
638
+ }
639
+ else if (state !== "idle") {
640
+ this.cancelIdleButtonRetirement(name);
630
641
  }
631
642
  }
643
+ cancelIdleButtonRetirement(name) {
644
+ const timer = this.cancelButtonIdleRetireTimers.get(name);
645
+ if (!timer)
646
+ return;
647
+ clearTimeout(timer);
648
+ this.cancelButtonIdleRetireTimers.delete(name);
649
+ }
650
+ scheduleIdleButtonRetirement(name) {
651
+ this.cancelIdleButtonRetirement(name);
652
+ const timer = setTimeout(() => {
653
+ // Ignore a superseded timer even if it was already queued to run.
654
+ if (this.cancelButtonIdleRetireTimers.get(name) !== timer)
655
+ return;
656
+ this.cancelButtonIdleRetireTimers.delete(name);
657
+ if (this.getInstanceExecutionState(name) === "idle") {
658
+ this.retireInstanceButtons(name);
659
+ }
660
+ }, CANCEL_BTN_IDLE_RETIRE_GRACE_MS);
661
+ timer.unref?.();
662
+ this.cancelButtonIdleRetireTimers.set(name, timer);
663
+ }
632
664
  cacheInstanceProcessStatus(name, status) {
633
665
  if (status === "running") {
634
666
  this.instanceProcessStatus.delete(name);
@@ -636,6 +668,7 @@ export class FleetManager {
636
668
  }
637
669
  if (status !== "crashed" && status !== "stopped")
638
670
  return;
671
+ this.cancelIdleButtonRetirement(name);
639
672
  this.instanceProcessStatus.set(name, status);
640
673
  // Never display the last ready prompt as current execution state after its
641
674
  // owning CLI process has exited.
@@ -980,6 +1013,7 @@ export class FleetManager {
980
1013
  }
981
1014
  async stopInstance(name) {
982
1015
  this.failoverActive.delete(name);
1016
+ this.cancelIdleButtonRetirement(name);
983
1017
  this.instanceStateCache.delete(name);
984
1018
  this.instanceProcessStatus.delete(name);
985
1019
  this.lastDeliveryAt.delete(name);
@@ -6095,6 +6129,9 @@ When users create specialized instances, suggest these configurations:
6095
6129
  clearInterval(entry.progressTimer);
6096
6130
  }
6097
6131
  this.cancelButtons.clear();
6132
+ for (const timer of this.cancelButtonIdleRetireTimers.values())
6133
+ clearTimeout(timer);
6134
+ this.cancelButtonIdleRetireTimers.clear();
6098
6135
  if (this.topicCleanupTimer) {
6099
6136
  clearInterval(this.topicCleanupTimer);
6100
6137
  this.topicCleanupTimer = null;