@songsid/agend 2.1.12-beta.5 → 2.1.12-beta.7

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 (70) hide show
  1. package/dist/agent-cli.js +13 -2
  2. package/dist/agent-cli.js.map +1 -1
  3. package/dist/backend/antigravity.d.ts +51 -10
  4. package/dist/backend/antigravity.js +142 -18
  5. package/dist/backend/antigravity.js.map +1 -1
  6. package/dist/backend/codex-metadata.js +4 -0
  7. package/dist/backend/codex-metadata.js.map +1 -1
  8. package/dist/backend/effort-metadata.d.ts +9 -0
  9. package/dist/backend/effort-metadata.js +29 -0
  10. package/dist/backend/effort-metadata.js.map +1 -1
  11. package/dist/backend/kiro.js +6 -2
  12. package/dist/backend/kiro.js.map +1 -1
  13. package/dist/backend/muse.d.ts +5 -0
  14. package/dist/backend/muse.js +29 -0
  15. package/dist/backend/muse.js.map +1 -1
  16. package/dist/backend/types.d.ts +8 -0
  17. package/dist/backend/types.js +9 -3
  18. package/dist/backend/types.js.map +1 -1
  19. package/dist/channel/adapters/discord.d.ts +13 -0
  20. package/dist/channel/adapters/discord.js +39 -0
  21. package/dist/channel/adapters/discord.js.map +1 -1
  22. package/dist/channel/mcp-tools.js +1 -1
  23. package/dist/channel/mcp-tools.js.map +1 -1
  24. package/dist/cli.js +12 -1
  25. package/dist/cli.js.map +1 -1
  26. package/dist/config-validator.js +4 -0
  27. package/dist/config-validator.js.map +1 -1
  28. package/dist/cpu-profile.d.ts +32 -0
  29. package/dist/cpu-profile.js +221 -0
  30. package/dist/cpu-profile.js.map +1 -0
  31. package/dist/delivery-outbox.d.ts +24 -0
  32. package/dist/delivery-outbox.js +118 -0
  33. package/dist/delivery-outbox.js.map +1 -1
  34. package/dist/event-log.d.ts +1 -0
  35. package/dist/event-log.js +5 -1
  36. package/dist/event-log.js.map +1 -1
  37. package/dist/event-loop-watch.d.ts +18 -0
  38. package/dist/event-loop-watch.js +32 -3
  39. package/dist/event-loop-watch.js.map +1 -1
  40. package/dist/fleet-manager.d.ts +37 -0
  41. package/dist/fleet-manager.js +219 -61
  42. package/dist/fleet-manager.js.map +1 -1
  43. package/dist/instance-lifecycle.js +7 -0
  44. package/dist/instance-lifecycle.js.map +1 -1
  45. package/dist/locale.js +2 -0
  46. package/dist/locale.js.map +1 -1
  47. package/dist/login-flows.js +5 -1
  48. package/dist/login-flows.js.map +1 -1
  49. package/dist/outbound-handlers.d.ts +2 -0
  50. package/dist/outbound-handlers.js +8 -0
  51. package/dist/outbound-handlers.js.map +1 -1
  52. package/dist/outbound-schemas.d.ts +15 -1
  53. package/dist/outbound-schemas.js +9 -3
  54. package/dist/outbound-schemas.js.map +1 -1
  55. package/dist/scheduler/db.d.ts +29 -4
  56. package/dist/scheduler/db.js +96 -6
  57. package/dist/scheduler/db.js.map +1 -1
  58. package/dist/scheduler/types.d.ts +33 -0
  59. package/dist/statusline-watcher.js +2 -1
  60. package/dist/statusline-watcher.js.map +1 -1
  61. package/dist/sync-work-attribution.d.ts +9 -0
  62. package/dist/sync-work-attribution.js +25 -0
  63. package/dist/sync-work-attribution.js.map +1 -0
  64. package/dist/topic-commands.js +10 -3
  65. package/dist/topic-commands.js.map +1 -1
  66. package/dist/transcript-sources.d.ts +1 -27
  67. package/dist/transcript-sources.js +9 -0
  68. package/dist/transcript-sources.js.map +1 -1
  69. package/dist/types.d.ts +4 -0
  70. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { measureSyncWork } from "./sync-work-attribution.js";
1
2
  import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, rmSync, readdirSync, renameSync, copyFileSync, chmodSync, statSync, accessSync, constants as fsConstants } from "node:fs";
2
3
  import { createHash, randomBytes, randomUUID } from "node:crypto";
3
4
  import { spawnSync } from "node:child_process";
@@ -34,7 +35,7 @@ import { createAdapter } from "./channel/factory.js";
34
35
  import { TelegramAdapter } from "./channel/adapters/telegram.js";
35
36
  import { createBackend } from "./backend/factory.js";
36
37
  import { readEffortMetadata } from "./backend/effort-metadata.js";
37
- import { isModelCompatible, SYSINFO_BACKEND_IDS, UnsupportedCliError } from "./backend/types.js";
38
+ import { CLI_ENV_TTL_MS, isModelCompatible, SYSINFO_BACKEND_IDS, UnsupportedCliError } from "./backend/types.js";
38
39
  import { createLogger, rotateLogIfNeeded } from "./logger.js";
39
40
  import { processAttachments } from "./channel/attachment-handler.js";
40
41
  import { routeToolCall } from "./channel/tool-router.js";
@@ -56,6 +57,51 @@ import { DeliveryOutbox } from "./delivery-outbox.js";
56
57
  // active manager/target generation pair owns the lane until the daemon reports
57
58
  // a state transition or that generation is replaced.
58
59
  export const DURABLE_DELIVERY_LANE_ALERT_MS = 35 * 60_000;
60
+ /**
61
+ * #1335: Cap an unfiltered task list at 100 rows (most recently updated first).
62
+ * Exported so the production branch can be tested directly without starting a fleet.
63
+ * Filtered calls pass-through unchanged. Empty strings count as "not set" (P3).
64
+ * #1336: generic over the row shape so it works on both full Task and compact rows.
65
+ */
66
+ export const TASK_LIST_CAP = 100;
67
+ /**
68
+ * #1336: the non-terminal statuses a bare `list` returns by default. `done`
69
+ * and `cancelled` are excluded unless the caller passes `filter_status`.
70
+ */
71
+ export const LIVE_TASK_STATUSES = ["open", "claimed", "blocked"];
72
+ /**
73
+ * #1336 P2: shared normalization for filter_status across both task handlers
74
+ * and the cap helper. Trims whitespace before dropping empties, so
75
+ * " \t " and [" ", "\t"] both normalize to undefined (not treated as explicit
76
+ * filters). The same result drives the live-only default and the cap decision.
77
+ */
78
+ export function normalizeStatusFilter(v) {
79
+ if (typeof v === "string") {
80
+ const s = v.trim();
81
+ return s || undefined;
82
+ }
83
+ if (Array.isArray(v) && v.every(x => typeof x === "string")) {
84
+ const arr = [...new Set(v.map(s => s.trim()).filter(s => s.length > 0))];
85
+ return arr.length > 0 ? arr : undefined;
86
+ }
87
+ return undefined;
88
+ }
89
+ export function applyTaskListCap(tasks, filterAssignee, filterStatus) {
90
+ // Use the shared normalizer so " \t " is treated identically to undefined.
91
+ const normalized = normalizeStatusFilter(filterStatus);
92
+ const hasStatusFilter = Array.isArray(normalized) ? normalized.length > 0 : !!normalized;
93
+ const isFiltered = !!filterAssignee || hasStatusFilter;
94
+ if (!isFiltered && tasks.length > TASK_LIST_CAP) {
95
+ const omitted = tasks.length - TASK_LIST_CAP;
96
+ tasks.sort((a, b) => b.updated_at.localeCompare(a.updated_at));
97
+ return {
98
+ tasks: tasks.slice(0, TASK_LIST_CAP),
99
+ omitted,
100
+ hint: `${omitted} older task(s) omitted — use filter_assignee or filter_status to narrow results`,
101
+ };
102
+ }
103
+ return tasks;
104
+ }
59
105
  import { handleWebRequest, broadcastSseEvent } from "./web-api.js";
60
106
  import { handleViewRequest, isViewPath } from "./view-api.js";
61
107
  import { filterUsageProviders, formatDiscordUsageActivity, getUsageSnapshot, handleUsageRequest, isUsagePath, usageProviderIdForBackend } from "./usage/usage-api.js";
@@ -263,7 +309,6 @@ const NONCE_BUTTON_TIMEOUT_MS = 15 * 60_000;
263
309
  const TIP_BUTTON_TIMEOUT_MS = 24 * 60 * 60_000;
264
310
  /** How long shutdown will spend retiring still-armed button prompts. */
265
311
  const NONCE_RETIRE_BUDGET_MS = 5_000;
266
- const CLI_ENV_TTL_MS = 24 * 60 * 60 * 1000; // hard validity bound for the cached CLI env
267
312
  /**
268
313
  * How old the cached CLI env may be before `/model` re-probes it live.
269
314
  *
@@ -571,6 +616,7 @@ export class FleetManager {
571
616
  /** Injectable only to keep the chat→CLI reload hand-off deterministic in tests. */
572
617
  fullRestartLauncher = launchFullRestartHelper;
573
618
  eventLogPruneTimer = null;
619
+ outboxPruneTimer = null;
574
620
  logRotateTimer = null;
575
621
  discordPresenceTimer = null;
576
622
  discordPresenceEagerTimer = null;
@@ -2766,7 +2812,7 @@ export class FleetManager {
2766
2812
  oldToolProgress.set(ch.instanceName, this.classicChannels.getToolProgress(ch.channelId, ch.adapterId, this.fleetConfig?.defaults?.tool_progress));
2767
2813
  oldReplyGuard.set(ch.instanceName, this.classicChannels.getReplyCompletionGuard(ch.channelId, ch.adapterId, this.fleetConfig?.defaults?.reply_completion_guard));
2768
2814
  }
2769
- if (!this.classicChannels.checkReload())
2815
+ if (!measureSyncWork("fleet.classicConfigReload", () => this.classicChannels.checkReload()))
2770
2816
  return;
2771
2817
  // A reload can introduce a bad id (hand edit) or clear one; the
2772
2818
  // throttle keeps a repeated report from flooding the topic.
@@ -2870,7 +2916,7 @@ export class FleetManager {
2870
2916
  // managed independently from fleet-topic workers.
2871
2917
  try {
2872
2918
  const skillsWorkDir = this.resolveKnowledgeWorkDir(config.working_directory, backend, name);
2873
- this.syncRoleSkills(skillsWorkDir, backend, "worker");
2919
+ measureSyncWork("fleet.workerSkills", () => this.syncRoleSkills(skillsWorkDir, backend, "worker"));
2874
2920
  }
2875
2921
  catch (err) {
2876
2922
  // Skill publishing is additive. A read-only or temporarily unavailable
@@ -4116,6 +4162,11 @@ export class FleetManager {
4116
4162
  this.pruneEventLog();
4117
4163
  this.eventLogPruneTimer = setInterval(() => this.pruneEventLog(), 24 * 60 * 60_000);
4118
4164
  this.eventLogPruneTimer.unref?.();
4165
+ // #1335: delivery-outbox.db and Task Board retention. Same once-at-startup
4166
+ // + daily-timer pattern; chunked DELETEs so one run cannot hold the loop.
4167
+ void this.pruneOutboxAndTasks();
4168
+ this.outboxPruneTimer = setInterval(() => { void this.pruneOutboxAndTasks(); }, 24 * 60 * 60_000);
4169
+ this.outboxPruneTimer.unref?.();
4119
4170
  // Same shape for logs, and for the same reason: the only sweep that
4120
4171
  // covered them lived inside the daily-summary callback, so it did not run at
4121
4172
  // all when summaries were off.
@@ -6636,45 +6687,74 @@ export class FleetManager {
6636
6687
  const asStatus = (v) => {
6637
6688
  return (v === "open" || v === "claimed" || v === "done" || v === "blocked" || v === "cancelled") ? v : undefined;
6638
6689
  };
6639
- switch (action) {
6640
- case "create": {
6641
- const title = asStr(args.title);
6642
- if (!title)
6643
- return { error: "title is required" };
6644
- return db.createTask({
6645
- title,
6646
- description: asStr(args.description),
6647
- priority: asPriority(args.priority),
6648
- assignee: asStr(args.assignee),
6649
- depends_on: asStrArr(args.depends_on),
6650
- created_by: instance,
6651
- });
6652
- }
6653
- case "list": return db.listTasks({ assignee: asStr(args.filter_assignee), status: asStr(args.filter_status) });
6654
- case "claim": {
6655
- const id = asStr(args.id);
6656
- if (!id)
6657
- return { error: "id is required" };
6658
- return db.claimTask(id, instance);
6659
- }
6660
- case "done": {
6661
- const id = asStr(args.id);
6662
- if (!id)
6663
- return { error: "id is required" };
6664
- return db.completeTask(id, asStr(args.result));
6665
- }
6666
- case "update": {
6667
- const id = asStr(args.id);
6668
- if (!id)
6669
- return { error: "id is required" };
6670
- return db.updateTask(id, {
6671
- status: asStatus(args.status),
6672
- assignee: asStr(args.assignee),
6673
- result: asStr(args.result),
6674
- priority: asPriority(args.priority),
6675
- });
6690
+ // #1336 P2: use the shared normalizer — trims whitespace AND drops empties,
6691
+ // so " \t " / [" ","\t"] are treated identically to undefined.
6692
+ const asStatusFilter = normalizeStatusFilter;
6693
+ // #1336: small write ack — identifying fields only.
6694
+ const ack = (t) => ({ id: t.id, status: t.status, updated_at: t.updated_at });
6695
+ try {
6696
+ switch (action) {
6697
+ case "create": {
6698
+ const title = asStr(args.title);
6699
+ if (!title)
6700
+ return { error: "title is required" };
6701
+ return ack(db.createTask({
6702
+ title,
6703
+ description: asStr(args.description),
6704
+ priority: asPriority(args.priority),
6705
+ assignee: asStr(args.assignee),
6706
+ depends_on: asStrArr(args.depends_on),
6707
+ created_by: instance,
6708
+ }));
6709
+ }
6710
+ case "list": {
6711
+ const filterAssignee = asStr(args.filter_assignee) || undefined;
6712
+ const explicitStatus = asStatusFilter(args.filter_status);
6713
+ // #1336 item 1: default to live-only (no done/cancelled) unless the
6714
+ // caller explicitly asked for a status.
6715
+ const effectiveStatus = explicitStatus ?? LIVE_TASK_STATUSES;
6716
+ const verbose = args.verbose === true;
6717
+ const tasks = verbose
6718
+ ? db.listTasks({ assignee: filterAssignee, status: effectiveStatus, verbose: true })
6719
+ : db.listTasks({ assignee: filterAssignee, status: effectiveStatus });
6720
+ // #1335: cap unfiltered list. The default live-only filter does not
6721
+ // count as an explicit filter for the cap decision.
6722
+ return applyTaskListCap(tasks, filterAssignee, explicitStatus);
6723
+ }
6724
+ case "get": {
6725
+ const id = asStr(args.id);
6726
+ if (!id)
6727
+ return { error: "id is required" };
6728
+ return db.getTaskByPrefix(id);
6729
+ }
6730
+ case "claim": {
6731
+ const id = asStr(args.id);
6732
+ if (!id)
6733
+ return { error: "id is required" };
6734
+ return ack(db.claimTask(db.getTaskByPrefix(id).id, instance));
6735
+ }
6736
+ case "done": {
6737
+ const id = asStr(args.id);
6738
+ if (!id)
6739
+ return { error: "id is required" };
6740
+ return ack(db.completeTask(db.getTaskByPrefix(id).id, asStr(args.result)));
6741
+ }
6742
+ case "update": {
6743
+ const id = asStr(args.id);
6744
+ if (!id)
6745
+ return { error: "id is required" };
6746
+ return ack(db.updateTask(db.getTaskByPrefix(id).id, {
6747
+ status: asStatus(args.status),
6748
+ assignee: asStr(args.assignee),
6749
+ result: asStr(args.result),
6750
+ priority: asPriority(args.priority),
6751
+ }));
6752
+ }
6753
+ default: return { error: `Unknown task action: ${action}` };
6676
6754
  }
6677
- default: return { error: `Unknown task action: ${action}` };
6755
+ }
6756
+ catch (err) {
6757
+ return { error: err.message };
6678
6758
  }
6679
6759
  }
6680
6760
  async handleSetDisplayNameHttp(instance, name) {
@@ -6745,11 +6825,17 @@ export class FleetManager {
6745
6825
  }
6746
6826
  const db = this.scheduler.db;
6747
6827
  const action = payload.action;
6828
+ // #1336 P2: use the shared normalizer — trims whitespace AND drops empties,
6829
+ // so " \t " / [" ","\t"] are treated identically to undefined.
6830
+ const asStatusFilter = normalizeStatusFilter;
6831
+ const ack = (t) => ({ id: t.id, status: t.status, updated_at: t.updated_at });
6748
6832
  try {
6749
6833
  let result;
6834
+ // Full task kept for activity logging; the IPC reply uses the compact ack.
6835
+ let logTask;
6750
6836
  switch (action) {
6751
6837
  case "create":
6752
- result = db.createTask({
6838
+ logTask = db.createTask({
6753
6839
  title: payload.title,
6754
6840
  description: payload.description,
6755
6841
  priority: payload.priority,
@@ -6757,43 +6843,57 @@ export class FleetManager {
6757
6843
  depends_on: payload.depends_on,
6758
6844
  created_by: meta.instance_name || instanceName,
6759
6845
  });
6846
+ result = ack(logTask);
6760
6847
  break;
6761
- case "list":
6762
- result = db.listTasks({
6763
- assignee: payload.filter_assignee,
6764
- status: payload.filter_status,
6765
- });
6848
+ case "list": {
6849
+ // P3: normalize empty strings to undefined — SchedulerDb.listTasks
6850
+ // ignores them but the cap logic must treat them as "not filtered".
6851
+ const filterAssignee = payload.filter_assignee || undefined;
6852
+ const explicitStatus = asStatusFilter(payload.filter_status);
6853
+ // #1336 item 1: default to live-only (no done/cancelled).
6854
+ const effectiveStatus = explicitStatus ?? LIVE_TASK_STATUSES;
6855
+ const verbose = payload.verbose === true;
6856
+ const tasks = verbose
6857
+ ? db.listTasks({ assignee: filterAssignee, status: effectiveStatus, verbose: true })
6858
+ : db.listTasks({ assignee: filterAssignee, status: effectiveStatus });
6859
+ // #1335: cap unfiltered list — the default live-only filter does not
6860
+ // count as an explicit filter.
6861
+ result = applyTaskListCap(tasks, filterAssignee, explicitStatus);
6862
+ break;
6863
+ }
6864
+ case "get":
6865
+ result = db.getTaskByPrefix(payload.id);
6766
6866
  break;
6767
6867
  case "claim":
6768
- result = db.claimTask(payload.id, meta.instance_name || instanceName);
6868
+ logTask = db.claimTask(db.getTaskByPrefix(payload.id).id, meta.instance_name || instanceName);
6869
+ result = ack(logTask);
6769
6870
  break;
6770
6871
  case "done":
6771
- result = db.completeTask(payload.id, payload.result);
6872
+ logTask = db.completeTask(db.getTaskByPrefix(payload.id).id, payload.result);
6873
+ result = ack(logTask);
6772
6874
  break;
6773
6875
  case "update":
6774
- result = db.updateTask(payload.id, {
6876
+ logTask = db.updateTask(db.getTaskByPrefix(payload.id).id, {
6775
6877
  status: payload.status,
6776
6878
  assignee: payload.assignee,
6777
6879
  result: payload.result,
6778
6880
  priority: payload.priority,
6779
6881
  });
6882
+ result = ack(logTask);
6780
6883
  break;
6781
6884
  default:
6782
6885
  throw new Error(`Unknown task action: ${action}`);
6783
6886
  }
6784
6887
  ipc.send({ type: "fleet_task_response", fleetRequestId, result });
6785
6888
  // Activity log for task lifecycle events
6786
- if (action === "create") {
6787
- const t = result;
6788
- this.eventLog?.logActivity("task_update", instanceName, `created task: ${t.title}`, t.assignee ?? undefined);
6889
+ if (action === "create" && logTask) {
6890
+ this.eventLog?.logActivity("task_update", instanceName, `created task: ${logTask.title}`, logTask.assignee ?? undefined);
6789
6891
  }
6790
- else if (action === "claim") {
6791
- const t = result;
6792
- this.eventLog?.logActivity("task_update", instanceName, `claimed: ${t.title}`);
6892
+ else if (action === "claim" && logTask) {
6893
+ this.eventLog?.logActivity("task_update", instanceName, `claimed: ${logTask.title}`);
6793
6894
  }
6794
- else if (action === "done") {
6795
- const t = result;
6796
- this.eventLog?.logActivity("task_update", instanceName, `completed: ${t.title}`, undefined, t.result ?? undefined);
6895
+ else if (action === "done" && logTask) {
6896
+ this.eventLog?.logActivity("task_update", instanceName, `completed: ${logTask.title}`, undefined, logTask.result ?? undefined);
6797
6897
  }
6798
6898
  }
6799
6899
  catch (err) {
@@ -8165,6 +8265,42 @@ export class FleetManager {
8165
8265
  this.logger.warn({ err }, "Event log prune failed");
8166
8266
  }
8167
8267
  }
8268
+ /** #1335: Prune delivery-outbox.db and Task Board in chunked async passes. */
8269
+ async pruneOutboxAndTasks() {
8270
+ const days = this.fleetConfig?.defaults?.retention_days ?? 30;
8271
+ // Outbox prune
8272
+ const outbox = this.deliveryOutbox;
8273
+ if (outbox?.isOpen) {
8274
+ try {
8275
+ const { pruned, durationMs } = await outbox.prune(days);
8276
+ if (pruned > 0) {
8277
+ this.logger.info({ pruned, durationMs: Math.round(durationMs) }, "Delivery outbox pruned");
8278
+ }
8279
+ }
8280
+ catch (err) {
8281
+ this.logger.warn({ err }, "Delivery outbox prune failed");
8282
+ }
8283
+ }
8284
+ // Task Board prune
8285
+ if (this.scheduler?.db) {
8286
+ try {
8287
+ const pruned = await this.scheduler.db.pruneOldTasks(days);
8288
+ if (pruned > 0) {
8289
+ this.logger.info({ pruned }, "Task board pruned");
8290
+ }
8291
+ }
8292
+ catch (err) {
8293
+ this.logger.warn({ err }, "Task board prune failed");
8294
+ }
8295
+ }
8296
+ }
8297
+ /**
8298
+ * #1335: True when the delivery_id was in the outbox, was pruned by
8299
+ * retention, and the caller is the original source or target (#1340 P2 🔒).
8300
+ */
8301
+ wasDeliveryIdPrunedForCaller(deliveryId, callerInstance) {
8302
+ return this.deliveryOutbox?.wasDeliveryIdPrunedForCaller(deliveryId, callerInstance) ?? false;
8303
+ }
8168
8304
  openEventLog() {
8169
8305
  const dbPath = join(this.dataDir, "events.db");
8170
8306
  try {
@@ -10419,8 +10555,11 @@ export class FleetManager {
10419
10555
  * definition or a function name, neither of which a spawn could run.
10420
10556
  */
10421
10557
  locateBinaryOnLoginShell(binary) {
10558
+ return measureSyncWork("fleet.installLookup", () => this.locateBinaryOnLoginShellSync(binary));
10559
+ }
10560
+ locateBinaryOnLoginShellSync(binary) {
10422
10561
  try {
10423
- const result = spawnSync("bash", ["-lc", `command -v ${binary}`], { timeout: 10_000, stdio: "pipe", encoding: "utf8" });
10562
+ const result = measureSyncWork("fleet.installLoginShell", () => spawnSync("bash", ["-lc", `command -v ${binary}`], { timeout: 10_000, stdio: "pipe", encoding: "utf8" }));
10424
10563
  if (result.status !== 0)
10425
10564
  return null;
10426
10565
  const path = String(result.stdout ?? "").trim().split("\n").pop()?.trim() ?? "";
@@ -10640,6 +10779,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
10640
10779
  `;
10641
10780
  /** Ensure the general instance has its project instructions file + knowledge */
10642
10781
  ensureGeneralInstructions(workDir, backendName, instanceName) {
10782
+ measureSyncWork("fleet.generalInstructions", () => this.ensureGeneralInstructionsSync(workDir, backendName, instanceName));
10783
+ }
10784
+ ensureGeneralInstructionsSync(workDir, backendName, instanceName) {
10643
10785
  const backend = backendName ?? "claude-code";
10644
10786
  workDir = this.resolveKnowledgeWorkDir(workDir, backend, instanceName);
10645
10787
  const filename = FleetManager.INSTRUCTIONS_FILENAME[backend] ?? "CLAUDE.md";
@@ -11460,6 +11602,15 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
11460
11602
  if (previous?.apiModels?.length)
11461
11603
  env.apiModels = previous.apiModels;
11462
11604
  }
11605
+ // Effort levels read from --help (#1328) are a capability of one binary. A help that could not be read (absent)
11606
+ // keeps the cached levels only for that same binary: both versions known and equal, cache still valid. A help
11607
+ // that was read and lists none ([]) is an answer and is written as is, so the fallback applies.
11608
+ if (env.effortLevels === undefined) {
11609
+ const previous = this.readCliEnv(backend);
11610
+ if (previous?.effortLevels && previous.version && env.version && previous.version === env.version) {
11611
+ env.effortLevels = previous.effortLevels;
11612
+ }
11613
+ }
11463
11614
  const path = this.cliEnvPath(backend);
11464
11615
  mkdirSync(dirname(path), { recursive: true });
11465
11616
  writeFileSync(path, JSON.stringify(env, null, 2));
@@ -12589,6 +12740,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
12589
12740
  clearInterval(this.eventLogPruneTimer);
12590
12741
  this.eventLogPruneTimer = null;
12591
12742
  }
12743
+ if (this.outboxPruneTimer) {
12744
+ clearInterval(this.outboxPruneTimer);
12745
+ this.outboxPruneTimer = null;
12746
+ }
12592
12747
  if (this.replyObligationTimer) {
12593
12748
  clearInterval(this.replyObligationTimer);
12594
12749
  this.replyObligationTimer = null;
@@ -14804,6 +14959,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
14804
14959
  this.healthServer.listen(port, "127.0.0.1", () => markListening());
14805
14960
  }
14806
14961
  getUiStatus() {
14962
+ return measureSyncWork("fleet.uiStatus", () => this.getUiStatusSync());
14963
+ }
14964
+ getUiStatusSync() {
14807
14965
  const fleetNames = Object.keys(this.fleetConfig?.instances ?? {});
14808
14966
  // Classic rooms live only in classicBot.yaml — /api/profiles merges them into
14809
14967
  // the View roster, but previously getUiStatus skipped them so context_pct was