@songsid/agend 2.1.5 → 2.1.6-beta.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.
@@ -181,6 +181,10 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
181
181
  private instanceIdleWaiters;
182
182
  private lastInboundUser;
183
183
  private cancelButtons;
184
+ /** Latest publication generation per instance. Late results from older
185
+ * generations are retired without touching the current button. */
186
+ private cancelButtonPublications;
187
+ private nextCancelButtonPublicationGeneration;
184
188
  /** Pending idle-edge retirement, one timer per instance. */
185
189
  private cancelButtonIdleRetireTimers;
186
190
  /** Duplicate-reply suppression across both the MCP and HTTP reply paths. */
@@ -1056,6 +1060,10 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
1056
1060
  */
1057
1061
  private handleCancelClick;
1058
1062
  private hasCancelButton;
1063
+ private beginCancelButtonPublication;
1064
+ /** Remember a clear/cancel/idle decision that arrived before notifyAlert
1065
+ * returned a message id. `expected` fences an idle timer to its generation. */
1066
+ private markCancelButtonPublicationForRetirement;
1059
1067
  sendCancelButton(instanceName: string, correlationId?: string, preserveProgress?: boolean): Promise<void>;
1060
1068
  /**
1061
1069
  * The cancel button's text for a given elapsed time.
@@ -383,6 +383,10 @@ export class FleetManager {
383
383
  // reply, on cancel, or when a newer button supersedes it for the same
384
384
  // instance. Per-button tracking means a failed delete never strands a button.
385
385
  cancelButtons = new Map();
386
+ /** Latest publication generation per instance. Late results from older
387
+ * generations are retired without touching the current button. */
388
+ cancelButtonPublications = new Map();
389
+ nextCancelButtonPublicationGeneration = 0;
386
390
  /** Pending idle-edge retirement, one timer per instance. */
387
391
  cancelButtonIdleRetireTimers = new Map();
388
392
  /** Duplicate-reply suppression across both the MCP and HTTP reply paths. */
@@ -1181,12 +1185,17 @@ export class FleetManager {
1181
1185
  }
1182
1186
  scheduleIdleButtonRetirement(name) {
1183
1187
  this.cancelIdleButtonRetirement(name);
1188
+ // Bind this edge to the publication that was current when idle was
1189
+ // observed. A later inbound may start a new generation before this timer
1190
+ // fires; the old edge must not mark that newer button for retirement.
1191
+ const publication = this.cancelButtonPublications.get(name);
1184
1192
  const timer = setTimeout(() => {
1185
1193
  // Ignore a superseded timer even if it was already queued to run.
1186
1194
  if (this.cancelButtonIdleRetireTimers.get(name) !== timer)
1187
1195
  return;
1188
1196
  this.cancelButtonIdleRetireTimers.delete(name);
1189
1197
  if (this.getInstanceExecutionState(name) === "idle") {
1198
+ this.markCancelButtonPublicationForRetirement(name, publication);
1190
1199
  this.retireInstanceButtons(name);
1191
1200
  }
1192
1201
  }, CANCEL_BTN_IDLE_RETIRE_GRACE_MS);
@@ -7098,6 +7107,30 @@ export class FleetManager {
7098
7107
  }
7099
7108
  return false;
7100
7109
  }
7110
+ beginCancelButtonPublication(instanceName, correlationId) {
7111
+ // A new handoff supersedes any idle timer left by the previous turn. If the
7112
+ // instance is still idle after this publication, the post-await level check
7113
+ // below starts a fresh grace period for this generation.
7114
+ this.cancelIdleButtonRetirement(instanceName);
7115
+ const publication = {
7116
+ generation: ++this.nextCancelButtonPublicationGeneration,
7117
+ correlationId,
7118
+ inFlight: true,
7119
+ retirePending: false,
7120
+ };
7121
+ this.cancelButtonPublications.set(instanceName, publication);
7122
+ return publication;
7123
+ }
7124
+ /** Remember a clear/cancel/idle decision that arrived before notifyAlert
7125
+ * returned a message id. `expected` fences an idle timer to its generation. */
7126
+ markCancelButtonPublicationForRetirement(instanceName, expected) {
7127
+ const publication = this.cancelButtonPublications.get(instanceName);
7128
+ if (!publication?.inFlight)
7129
+ return;
7130
+ if (expected && publication !== expected)
7131
+ return;
7132
+ publication.retirePending = true;
7133
+ }
7101
7134
  async sendCancelButton(instanceName, correlationId, preserveProgress = false) {
7102
7135
  // Post first, retire after (see the tail of this method). Retiring up front
7103
7136
  // meant that from the delete until the new message came back — a chat API
@@ -7136,6 +7169,7 @@ export class FleetManager {
7136
7169
  this.logger.warn({ instanceName, topicId, groupId }, "Cannot address cancel button (no chat id resolved)");
7137
7170
  return;
7138
7171
  }
7172
+ const publication = this.beginCancelButtonPublication(instanceName, correlationId);
7139
7173
  try {
7140
7174
  const sent = await adapter.notifyAlert(chatId, {
7141
7175
  type: "cancel",
@@ -7143,6 +7177,7 @@ export class FleetManager {
7143
7177
  message: "👀 處理中…",
7144
7178
  choices: [{ id: `cancel:${instanceName}`, label: t("cancel.button") }],
7145
7179
  }, threadId ? { threadId } : undefined);
7180
+ publication.inFlight = false;
7146
7181
  const entry = {
7147
7182
  instanceName,
7148
7183
  adapterId,
@@ -7164,6 +7199,16 @@ export class FleetManager {
7164
7199
  // empty even if the daemon's reset broadcast is still in flight.
7165
7200
  toolProgress: preserveProgress ? this.instanceProgress.get(instanceName) : undefined,
7166
7201
  };
7202
+ // A newer send started while this API call was pending. Track this late
7203
+ // message just long enough for the normal bounded delete/retry machinery
7204
+ // to remove it; critically, do not sweep the newer generation.
7205
+ if (this.cancelButtonPublications.get(instanceName) !== publication) {
7206
+ this.cancelButtons.set(sent.messageId, entry);
7207
+ this.persistCancelButtons();
7208
+ this.logger.debug({ instanceName, messageId: sent.messageId, generation: publication.generation }, "Retiring superseded cancel-button publication");
7209
+ this.retireButton(entry);
7210
+ return;
7211
+ }
7167
7212
  this.startProgressTicker(entry);
7168
7213
  // Idle-check backstop: every 5min, if the instance is idle, retire the
7169
7214
  // button. Covers turns that end without hitting a clear trigger (reply /
@@ -7200,8 +7245,30 @@ export class FleetManager {
7200
7245
  }
7201
7246
  this.persistCancelButtons();
7202
7247
  this.logger.info({ instanceName, messageId: sent.messageId }, "Cancel button sent");
7248
+ // Retirement is normally edge-triggered, but the edge (or an explicit
7249
+ // reply/cancel clear) may have happened while notifyAlert was in flight.
7250
+ // Reconcile the level after publication so a late button cannot resurrect
7251
+ // on an already-idle instance. Preserve the existing two-second grace:
7252
+ // a working transition cancels this timer just as it does for a normal
7253
+ // idle edge.
7254
+ if (publication.retirePending) {
7255
+ this.retireButton(entry);
7256
+ }
7257
+ else if (this.getInstanceExecutionState(instanceName) === "idle") {
7258
+ this.scheduleIdleButtonRetirement(instanceName);
7259
+ }
7203
7260
  }
7204
7261
  catch (e) {
7262
+ if (this.cancelButtonPublications.get(instanceName) === publication) {
7263
+ this.cancelButtonPublications.delete(instanceName);
7264
+ // beginCancelButtonPublication cancelled the previous turn's idle
7265
+ // timer so it could not act on this generation. If the provider POST
7266
+ // failed while the instance remained idle, restore that retirement
7267
+ // opportunity for any older button still on screen.
7268
+ if (this.getInstanceExecutionState(instanceName) === "idle") {
7269
+ this.scheduleIdleButtonRetirement(instanceName);
7270
+ }
7271
+ }
7205
7272
  this.logger.warn({ err: e.message, instanceName }, "Failed to send cancel button");
7206
7273
  }
7207
7274
  }
@@ -7554,6 +7621,7 @@ export class FleetManager {
7554
7621
  }
7555
7622
  /** Retire all cancel buttons for an instance — on reply or cancel. */
7556
7623
  clearCancelButton(instanceName) {
7624
+ this.markCancelButtonPublicationForRetirement(instanceName);
7557
7625
  this.retireInstanceButtons(instanceName);
7558
7626
  }
7559
7627
  /** Retire the cross-instance button matching a delegate→report correlation id.
@@ -7562,6 +7630,11 @@ export class FleetManager {
7562
7630
  clearCancelButtonByCorrelation(correlationId) {
7563
7631
  if (!correlationId)
7564
7632
  return;
7633
+ for (const [instanceName, publication] of this.cancelButtonPublications) {
7634
+ if (publication.inFlight && publication.correlationId === correlationId) {
7635
+ this.markCancelButtonPublicationForRetirement(instanceName, publication);
7636
+ }
7637
+ }
7565
7638
  for (const e of [...this.cancelButtons.values()]) {
7566
7639
  if (e.correlationId === correlationId)
7567
7640
  this.retireButton(e);
@@ -10299,6 +10372,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
10299
10372
  clearInterval(entry.progressTimer);
10300
10373
  }
10301
10374
  this.cancelButtons.clear();
10375
+ this.cancelButtonPublications.clear();
10302
10376
  for (const timer of this.cancelButtonIdleRetireTimers.values())
10303
10377
  clearTimeout(timer);
10304
10378
  this.cancelButtonIdleRetireTimers.clear();