@songsid/agend 2.0.12 → 2.1.0-beta.10

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.
Files changed (50) hide show
  1. package/dist/backend/antigravity.js +3 -1
  2. package/dist/backend/antigravity.js.map +1 -1
  3. package/dist/backend/codex.js +3 -1
  4. package/dist/backend/codex.js.map +1 -1
  5. package/dist/backend/kiro.js +3 -2
  6. package/dist/backend/kiro.js.map +1 -1
  7. package/dist/backend/types.d.ts +12 -0
  8. package/dist/backend/types.js.map +1 -1
  9. package/dist/cli.js +43 -6
  10. package/dist/cli.js.map +1 -1
  11. package/dist/config.js +1 -0
  12. package/dist/config.js.map +1 -1
  13. package/dist/context-guardian.d.ts +2 -0
  14. package/dist/context-guardian.js +10 -2
  15. package/dist/context-guardian.js.map +1 -1
  16. package/dist/daemon-entry.js +2 -1
  17. package/dist/daemon-entry.js.map +1 -1
  18. package/dist/daemon.d.ts +78 -3
  19. package/dist/daemon.js +496 -27
  20. package/dist/daemon.js.map +1 -1
  21. package/dist/fleet-context.d.ts +7 -1
  22. package/dist/fleet-manager.d.ts +23 -2
  23. package/dist/fleet-manager.js +207 -65
  24. package/dist/fleet-manager.js.map +1 -1
  25. package/dist/instance-lifecycle.d.ts +6 -1
  26. package/dist/instance-lifecycle.js +45 -3
  27. package/dist/instance-lifecycle.js.map +1 -1
  28. package/dist/logger.js +6 -3
  29. package/dist/logger.js.map +1 -1
  30. package/dist/outbound-handlers.d.ts +5 -0
  31. package/dist/outbound-handlers.js +68 -16
  32. package/dist/outbound-handlers.js.map +1 -1
  33. package/dist/statusline-watcher.d.ts +1 -1
  34. package/dist/statusline-watcher.js +3 -2
  35. package/dist/statusline-watcher.js.map +1 -1
  36. package/dist/tmux-manager.d.ts +2 -0
  37. package/dist/tmux-manager.js +9 -0
  38. package/dist/tmux-manager.js.map +1 -1
  39. package/dist/topic-archiver.d.ts +1 -1
  40. package/dist/topic-commands.d.ts +5 -1
  41. package/dist/topic-commands.js +72 -12
  42. package/dist/topic-commands.js.map +1 -1
  43. package/dist/transcript-monitor.js +2 -0
  44. package/dist/transcript-monitor.js.map +1 -1
  45. package/dist/types.d.ts +2 -0
  46. package/dist/view-api.d.ts +1 -1
  47. package/dist/web-api.d.ts +2 -1
  48. package/dist/web-api.js +21 -14
  49. package/dist/web-api.js.map +1 -1
  50. package/package.json +1 -1
@@ -93,6 +93,8 @@ export class FleetManager {
93
93
  // Topic icon + auto-archive state
94
94
  topicIcons = {};
95
95
  lastActivity = new Map();
96
+ /** Latest pane-derived execution snapshot reported by each daemon. */
97
+ instanceStateCache = new Map();
96
98
  lastInboundUser = new Map(); // instanceName → last username
97
99
  // Active "🛑 Cancel" buttons, tracked per button (keyed by messageId) rather
98
100
  // than one-per-instance. A button is retired (deleted, with bounded retry) on
@@ -110,6 +112,8 @@ export class FleetManager {
110
112
  ipcStoppingInstances = new Set();
111
113
  // Adapter restart: prevents re-entrant restart attempts
112
114
  adapterRestarting = new Set();
115
+ // Adapter isolation: track state per adapter for retry + visibility
116
+ adapterState = new Map();
113
117
  collabInstances = new Set();
114
118
  // Health endpoint
115
119
  healthServer = null;
@@ -158,6 +162,7 @@ export class FleetManager {
158
162
  const instances = Object.keys(this.fleetConfig?.instances ?? {}).map(name => ({
159
163
  name,
160
164
  status: this.getInstanceStatus(name),
165
+ state: this.getInstanceExecutionState(name),
161
166
  ipc: this.instanceIpcClients.has(name),
162
167
  costCents: this.costGuard?.getDailyCostCents(name) ?? 0,
163
168
  rateLimits: this.statuslineWatcher.getRateLimits(name) ?? null,
@@ -263,17 +268,24 @@ export class FleetManager {
263
268
  */
264
269
  bindInstanceAdapter(name, adapterId, fromInbound = false) {
265
270
  const cfg = this.fleetConfig?.instances[name];
266
- if (fromInbound && (cfg?.general_topic || cfg?.channel_id))
267
- return;
268
- // A classic instance is bound authoritatively at /start. Don't let an inbound
269
- // (seen by every same-guild bot) override an existing binding — but if there
270
- // is none yet (e.g. after a restart, before v2.1 persistence), allow inbound
271
- // to re-establish it so replies don't fall back to the primary bot forever.
272
- if (fromInbound && this.classicChannels?.getChannelIdByInstance(name) !== undefined && this.instanceWorldBinding.has(name))
273
- return;
271
+ if (fromInbound) {
272
+ // Skip inbound-derived binding for any instance that doesn't have an
273
+ // explicit channel_id those default to primary adapter deterministically.
274
+ // This prevents a non-deterministic race where whichever adapter delivers
275
+ // first after restart wins the binding.
276
+ if (cfg?.general_topic || cfg?.channel_id)
277
+ return;
278
+ if (cfg && !cfg.channel_id)
279
+ return; // fleet instance without explicit binding → use primary
280
+ // Classic instance: don't override an existing binding (authoritative from /start)
281
+ if (this.classicChannels?.getChannelIdByInstance(name) !== undefined && this.instanceWorldBinding.has(name))
282
+ return;
283
+ }
274
284
  this.instanceWorldBinding.set(name, adapterId);
275
285
  }
276
286
  getInstanceStatus(name) {
287
+ if (this.lifecycle.isPaused(name))
288
+ return "paused";
277
289
  const pidPath = join(this.getInstanceDir(name), "daemon.pid");
278
290
  if (!existsSync(pidPath))
279
291
  return "stopped";
@@ -286,6 +298,35 @@ export class FleetManager {
286
298
  return "crashed";
287
299
  }
288
300
  }
301
+ getInstanceExecutionState(name) {
302
+ if (this.lifecycle.isPaused(name))
303
+ return null;
304
+ return this.instanceStateCache.get(name)?.state ?? null;
305
+ }
306
+ cacheInstanceExecutionState(name, msg) {
307
+ const state = msg.state;
308
+ if (state !== "idle" && state !== "working" && state !== "stuck")
309
+ return;
310
+ const previous = this.instanceStateCache.get(name);
311
+ const now = Date.now();
312
+ const numberOr = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
313
+ this.instanceStateCache.set(name, {
314
+ state,
315
+ unchangedForMs: numberOr(msg.unchangedForMs, previous?.unchangedForMs ?? 0),
316
+ observedAt: numberOr(msg.observedAt, now),
317
+ stateChangedAt: numberOr(msg.stateChangedAt, previous?.state === state ? previous.stateChangedAt : now),
318
+ });
319
+ }
320
+ /** Single delivery facade: wake an auto-paused CLI before sending to daemon IPC. */
321
+ async deliverToInstance(instanceName, payload) {
322
+ if (this.lifecycle.isPaused(instanceName)) {
323
+ await this.lifecycle.wake(instanceName, 30_000);
324
+ }
325
+ const ipc = this.instanceIpcClients.get(instanceName);
326
+ if (!ipc?.connected)
327
+ throw new Error(`Instance '${instanceName}' IPC is unavailable`);
328
+ ipc.send(payload);
329
+ }
289
330
  async startInstance(name, config, topicMode) {
290
331
  if (config.general_topic) {
291
332
  this.ensureGeneralInstructions(config.working_directory, config.backend);
@@ -353,6 +394,7 @@ export class FleetManager {
353
394
  }
354
395
  async stopInstance(name) {
355
396
  this.failoverActive.delete(name);
397
+ this.instanceStateCache.delete(name);
356
398
  return this.lifecycle.stop(name);
357
399
  }
358
400
  /** Restart a single instance, reloading fleet.yaml first to pick up config changes. */
@@ -836,17 +878,81 @@ export class FleetManager {
836
878
  const channelConfigs = fleet.channels ?? (fleet.channel ? [fleet.channel] : []);
837
879
  if (channelConfigs.length === 0)
838
880
  return;
839
- // Start primary adapter (first channel)this.adapter for backward compat.
840
- await this.startSingleAdapter(fleet, channelConfigs[0]);
841
- // Start additional adapters. Every bot registers its own slash commands —
842
- // Discord slash commands are per-application, so a same-guild secondary bot's
843
- // /start etc. are distinct entries (labelled with the bot name) and the
844
- // interaction only reaches the invoked bot. This is how ClassicBot supports
845
- // multiple bots in one guild: the user picks which bot from autocomplete.
846
- for (let i = 1; i < channelConfigs.length; i++) {
847
- await this.startAdditionalAdapter(channelConfigs[i]);
881
+ // Start ALL adapters in parallelany single failure doesn't block others.
882
+ const results = await Promise.allSettled(channelConfigs.map((cfg, i) => i === 0
883
+ ? this.startSingleAdapter(fleet, cfg)
884
+ : this.startAdditionalAdapter(cfg)));
885
+ // Track state + schedule background retry for failures.
886
+ for (let i = 0; i < channelConfigs.length; i++) {
887
+ const adapterId = channelConfigs[i].id ?? channelConfigs[i].type;
888
+ if (results[i].status === "fulfilled") {
889
+ this.adapterState.set(adapterId, { status: "connected", retryCount: 0 });
890
+ }
891
+ else {
892
+ const err = results[i].reason;
893
+ this.logger.error({ adapterId, err: err?.message ?? err }, "Adapter startup failed — scheduling background retry");
894
+ this.adapterState.set(adapterId, { status: "retrying", retryCount: 0, lastError: err?.message ?? String(err) });
895
+ this.scheduleAdapterRetry(adapterId, channelConfigs[i], i === 0 ? fleet : undefined);
896
+ // Notify admin via whichever adapter is already up
897
+ this.notifyAdapterFailure(adapterId, err?.message ?? String(err));
898
+ }
848
899
  }
849
900
  }
901
+ /** Exponential backoff retry for a single failed adapter (background, non-blocking). */
902
+ scheduleAdapterRetry(adapterId, channelConfig, fleet) {
903
+ const MAX_RETRIES = 10;
904
+ const INITIAL_DELAY_MS = 5_000;
905
+ const MAX_DELAY_MS = 5 * 60_000;
906
+ const state = this.adapterState.get(adapterId);
907
+ if (!state || state.retryCount >= MAX_RETRIES) {
908
+ if (state) {
909
+ state.status = "failed";
910
+ this.logger.error({ adapterId, retries: state.retryCount }, "Adapter retry exhausted — giving up");
911
+ this.notifyAdapterFailure(adapterId, `Retry exhausted after ${state.retryCount} attempts. Check token/network and restart fleet.`);
912
+ }
913
+ return;
914
+ }
915
+ const delay = Math.min(INITIAL_DELAY_MS * Math.pow(2, state.retryCount), MAX_DELAY_MS);
916
+ this.logger.info({ adapterId, attempt: state.retryCount + 1, delay_ms: delay }, "Scheduling adapter retry");
917
+ state.retryTimer = setTimeout(async () => {
918
+ state.retryCount++;
919
+ try {
920
+ if (fleet) {
921
+ await this.startSingleAdapter(fleet, channelConfig);
922
+ }
923
+ else {
924
+ await this.startAdditionalAdapter(channelConfig);
925
+ }
926
+ state.status = "connected";
927
+ state.lastError = undefined;
928
+ this.logger.info({ adapterId, attempts: state.retryCount }, "Adapter reconnected on retry");
929
+ this.notifyAdapterRecovery(adapterId, state.retryCount);
930
+ }
931
+ catch (err) {
932
+ state.lastError = err?.message ?? String(err);
933
+ this.logger.warn({ adapterId, attempt: state.retryCount, err: state.lastError }, "Adapter retry failed");
934
+ this.scheduleAdapterRetry(adapterId, channelConfig, fleet);
935
+ }
936
+ }, delay);
937
+ }
938
+ /** Notify admin about adapter failure (uses any available adapter). */
939
+ notifyAdapterFailure(adapterId, error) {
940
+ const generalId = this.findGeneralInstance();
941
+ if (generalId) {
942
+ this.notifyInstanceTopic(generalId, `⚠️ Adapter "${adapterId}" failed to start: ${error}\nRetrying in background. Other adapters unaffected.`);
943
+ }
944
+ }
945
+ /** Notify admin that a retried adapter reconnected. */
946
+ notifyAdapterRecovery(adapterId, attempts) {
947
+ const generalId = this.findGeneralInstance();
948
+ if (generalId) {
949
+ this.notifyInstanceTopic(generalId, `✅ Adapter "${adapterId}" reconnected (after ${attempts} ${attempts === 1 ? "retry" : "retries"}).`);
950
+ }
951
+ }
952
+ /** Get adapter states for /status visibility. */
953
+ getAdapterStates() {
954
+ return this.adapterState;
955
+ }
850
956
  /** Start the primary adapter (backward-compatible, sets this.adapter) */
851
957
  async startSingleAdapter(fleet, channelConfig) {
852
958
  const botToken = process.env[channelConfig.bot_token_env];
@@ -1454,10 +1560,16 @@ export class FleetManager {
1454
1560
  else if (msg.type === "fleet_set_description") {
1455
1561
  this.handleSetDescription(name, msg);
1456
1562
  }
1563
+ else if (msg.type === "instance_state" || msg.type === "instance_state_response") {
1564
+ this.cacheInstanceExecutionState(name, msg);
1565
+ }
1457
1566
  }, this.logger, `ipc.message[${name}]`));
1458
1567
  // Ask daemon for any sessions that registered before we connected
1459
1568
  // (fixes race condition where mcp_ready was broadcast before fleet manager connected)
1460
1569
  ipc.send({ type: "query_sessions" });
1570
+ // The initial state transition may have happened before FleetManager
1571
+ // connected, so seed the cache instead of waiting for another transition.
1572
+ ipc.send({ type: "query_instance_state", requestId: `fleet-state-${Date.now()}` });
1461
1573
  this.logger.debug({ name }, "Connected to instance IPC");
1462
1574
  if (!this.statuslineWatcher.has(name)) {
1463
1575
  this.statuslineWatcher.watch(name);
@@ -1860,9 +1972,8 @@ export class FleetManager {
1860
1972
  }
1861
1973
  this.warnIfRateLimited(generalInstance, msg);
1862
1974
  const { text, extraMeta } = await processAttachments(msg, inboundAdapter, this.logger, generalInstance);
1863
- const ipc = this.instanceIpcClients.get(generalInstance);
1864
- if (ipc) {
1865
- ipc.send({
1975
+ try {
1976
+ await this.deliverToInstance(generalInstance, {
1866
1977
  type: "fleet_inbound",
1867
1978
  content: text,
1868
1979
  targetSession: generalInstance,
@@ -1888,6 +1999,9 @@ export class FleetManager {
1888
1999
  this.trackInboundMsg(generalInstance, msg);
1889
2000
  void this.sendCancelButton(generalInstance);
1890
2001
  }
2002
+ catch (err) {
2003
+ this.logger.warn({ err: err.message, instanceName: generalInstance }, "General wake/delivery failed");
2004
+ }
1891
2005
  }
1892
2006
  return;
1893
2007
  }
@@ -1950,27 +2064,32 @@ export class FleetManager {
1950
2064
  this.setTopicIcon(instanceName, "blue");
1951
2065
  this.warnIfRateLimited(instanceName, msg);
1952
2066
  const { text, extraMeta } = await processAttachments(msg, inboundAdapter, this.logger, instanceName);
1953
- const ipc = this.instanceIpcClients.get(instanceName);
1954
- if (!ipc) {
1955
- this.logger.warn({ instanceName }, "No IPC connection to instance");
2067
+ try {
2068
+ await this.deliverToInstance(instanceName, {
2069
+ type: "fleet_inbound",
2070
+ content: text,
2071
+ targetSession: instanceName, // Channel messages → instance's own session
2072
+ meta: {
2073
+ chat_id: msg.chatId,
2074
+ message_id: msg.messageId,
2075
+ user: msg.username,
2076
+ user_id: msg.userId,
2077
+ ts: msg.timestamp.toISOString(),
2078
+ thread_id: msg.threadId ?? "",
2079
+ source: msg.source,
2080
+ ...(msg.replyToText ? { reply_to_text: msg.replyToText } : {}),
2081
+ ...extraMeta,
2082
+ },
2083
+ });
2084
+ }
2085
+ catch (err) {
2086
+ this.logger.warn({ err: err.message, instanceName }, "Wake/delivery failed");
2087
+ if (msg.chatId && msg.messageId) {
2088
+ const reactAdapter = this.getAdapterForInstance(instanceName) ?? inboundAdapter;
2089
+ reactAdapter.react(this.reactTarget(msg), msg.messageId, "❌").catch(() => { });
2090
+ }
1956
2091
  return;
1957
2092
  }
1958
- ipc.send({
1959
- type: "fleet_inbound",
1960
- content: text,
1961
- targetSession: instanceName, // Channel messages → instance's own session
1962
- meta: {
1963
- chat_id: msg.chatId,
1964
- message_id: msg.messageId,
1965
- user: msg.username,
1966
- user_id: msg.userId,
1967
- ts: msg.timestamp.toISOString(),
1968
- thread_id: msg.threadId ?? "",
1969
- source: msg.source,
1970
- ...(msg.replyToText ? { reply_to_text: msg.replyToText } : {}),
1971
- ...extraMeta,
1972
- },
1973
- });
1974
2093
  this.lastInboundUser.set(instanceName, msg.username);
1975
2094
  this.logger.info(`${msg.username} → ${instanceName}: ${(text ?? "").slice(0, 100)}`);
1976
2095
  this.eventLog?.logActivity("message", msg.username, (text ?? "").slice(0, 200), instanceName);
@@ -2131,20 +2250,23 @@ export class FleetManager {
2131
2250
  const schedulerDefaults = this.fleetConfig?.defaults.scheduler;
2132
2251
  const retryCount = schedulerDefaults?.retry_count ?? 3;
2133
2252
  const retryInterval = schedulerDefaults?.retry_interval_ms ?? 30_000;
2134
- const deliver = () => {
2135
- const ipc = this.instanceIpcClients.get(target);
2136
- if (!ipc?.connected)
2253
+ const deliver = async () => {
2254
+ try {
2255
+ await this.deliverToInstance(target, {
2256
+ type: "fleet_schedule_trigger",
2257
+ payload: { schedule_id: id, message: `[Scheduled] ${message}`, label },
2258
+ meta: { chat_id: reply_chat_id, thread_id: reply_thread_id, user: "scheduler" },
2259
+ });
2260
+ // A scheduled trigger also puts the instance to work — show a cancel button.
2261
+ void this.sendCancelButton(target);
2262
+ return true;
2263
+ }
2264
+ catch (err) {
2265
+ this.logger.warn({ err: err.message, target }, "Scheduled wake/delivery attempt failed");
2137
2266
  return false;
2138
- ipc.send({
2139
- type: "fleet_schedule_trigger",
2140
- payload: { schedule_id: id, message: `[Scheduled] ${message}`, label },
2141
- meta: { chat_id: reply_chat_id, thread_id: reply_thread_id, user: "scheduler" },
2142
- });
2143
- // A scheduled trigger also puts the instance to work — show a cancel button.
2144
- void this.sendCancelButton(target);
2145
- return true;
2267
+ }
2146
2268
  };
2147
- if (deliver()) {
2269
+ if (await deliver()) {
2148
2270
  this.scheduler.recordRun(id, "delivered");
2149
2271
  if (source !== target)
2150
2272
  this.notifySourceTopic(schedule);
@@ -2152,7 +2274,7 @@ export class FleetManager {
2152
2274
  }
2153
2275
  for (let i = 0; i < retryCount; i++) {
2154
2276
  await new Promise((r) => setTimeout(r, retryInterval));
2155
- if (deliver()) {
2277
+ if (await deliver()) {
2156
2278
  this.scheduler.recordRun(id, "delivered");
2157
2279
  if (source !== target)
2158
2280
  this.notifySourceTopic(schedule);
@@ -2685,6 +2807,10 @@ export class FleetManager {
2685
2807
  startStatuslineWatcher(name) {
2686
2808
  this.statuslineWatcher.watch(name);
2687
2809
  }
2810
+ stopStatuslineWatcher(name) {
2811
+ // Pausing stops I/O but retains the last observed limits for status views.
2812
+ this.statuslineWatcher.unwatch(name, true);
2813
+ }
2688
2814
  reactMessageStatus(instanceName, chatId, messageId, emoji) {
2689
2815
  // React via the adapter BOUND to this instance — NOT the first discord world.
2690
2816
  // Otherwise, in a same-channel/same-guild multi-bot setup, the inbound 👀
@@ -3035,7 +3161,7 @@ export class FleetManager {
3035
3161
  return [];
3036
3162
  }
3037
3163
  }
3038
- async sendHangNotification(instanceName) {
3164
+ async sendHangNotification(instanceName, unchangedForMs) {
3039
3165
  const adapter = this.getAdapterForInstance(instanceName) ?? this.adapter;
3040
3166
  if (!adapter)
3041
3167
  return;
@@ -3044,11 +3170,18 @@ export class FleetManager {
3044
3170
  if (!groupId)
3045
3171
  return;
3046
3172
  const threadId = this.fleetConfig?.instances[instanceName]?.topic_id;
3173
+ const instanceHangConfig = this.fleetConfig?.instances[instanceName]?.hang_detector;
3174
+ const configuredMinutes = instanceHangConfig?.timeout_minutes
3175
+ ?? this.fleetConfig?.defaults?.hang_detector?.timeout_minutes
3176
+ ?? 15;
3177
+ const unchangedMinutes = unchangedForMs == null
3178
+ ? configuredMinutes
3179
+ : Math.max(1, Math.floor(unchangedForMs / 60_000));
3047
3180
  this.setTopicIcon(instanceName, "red");
3048
3181
  await adapter.notifyAlert(String(groupId), {
3049
3182
  type: "hang",
3050
3183
  instanceName,
3051
- message: `⚠️ ${instanceName} appears hung (no activity for 15+ minutes)`,
3184
+ message: `⚠️ ${instanceName} may be stuck pane unchanged for ${unchangedMinutes}min, ready prompt not recognized`,
3052
3185
  choices: [
3053
3186
  { id: `hang:restart:${instanceName}`, label: "🔄 Force restart" },
3054
3187
  { id: `hang:wait:${instanceName}`, label: "⏳ Keep waiting" },
@@ -3549,11 +3682,6 @@ When users create specialized instances, suggest these configurations:
3549
3682
  const fullText = logContext
3550
3683
  ? `[Chat log for context]\n${logContext}\n\n[User message]\n${text}`
3551
3684
  : text;
3552
- const ipc = this.instanceIpcClients.get(instanceName);
3553
- if (!ipc) {
3554
- this.logger.warn({ instanceName }, "Classic channel instance IPC not connected");
3555
- return;
3556
- }
3557
3685
  const meta = {
3558
3686
  chat_id: msg.chatId,
3559
3687
  message_id: msg.messageId,
@@ -3575,12 +3703,18 @@ When users create specialized instances, suggest these configurations:
3575
3703
  meta.image_path = saves[saves.length - 1][1].split(",")[0].trim();
3576
3704
  }
3577
3705
  }
3578
- ipc.send({
3579
- type: "fleet_inbound",
3580
- content: fullText,
3581
- targetSession: instanceName,
3582
- meta,
3583
- });
3706
+ try {
3707
+ await this.deliverToInstance(instanceName, {
3708
+ type: "fleet_inbound",
3709
+ content: fullText,
3710
+ targetSession: instanceName,
3711
+ meta,
3712
+ });
3713
+ }
3714
+ catch (err) {
3715
+ this.logger.warn({ err: err.message, instanceName }, "Classic wake/delivery failed");
3716
+ return;
3717
+ }
3584
3718
  this.lastInboundUser.set(instanceName, msg.username);
3585
3719
  this.logger.info(`${msg.username} → ${instanceName} (classic): ${text.slice(0, 100)}`);
3586
3720
  this.trackInboundMsg(instanceName, msg);
@@ -3683,6 +3817,13 @@ When users create specialized instances, suggest these configurations:
3683
3817
  clearInterval(this.watchdogTimer);
3684
3818
  this.watchdogTimer = null;
3685
3819
  }
3820
+ // Cancel adapter retry timers
3821
+ for (const state of this.adapterState.values()) {
3822
+ if (state.retryTimer) {
3823
+ clearTimeout(state.retryTimer);
3824
+ state.retryTimer = undefined;
3825
+ }
3826
+ }
3686
3827
  this.clearStatuslineWatchers();
3687
3828
  this.costGuard?.stop();
3688
3829
  this.dailySummary?.stop();
@@ -4253,6 +4394,7 @@ When users create specialized instances, suggest these configurations:
4253
4394
  lastActivity: this.lastActivityMs(inst.name) || null,
4254
4395
  currentTask,
4255
4396
  idle: this.getInstanceIdle(inst.name),
4397
+ state: this.getInstanceExecutionState(inst.name),
4256
4398
  };
4257
4399
  });
4258
4400
  res.setHeader("Access-Control-Allow-Origin", "*");