@wrongstack/core 0.10.0 → 0.10.3

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 (42) hide show
  1. package/dist/{agent-bridge-DBBGP5O4.d.ts → agent-bridge-D_XcS2HL.d.ts} +3 -1
  2. package/dist/{agent-subagent-runner-KwufTWwn.d.ts → agent-subagent-runner-DuRtZmhl.d.ts} +13 -3
  3. package/dist/coordination/index.d.ts +8 -8
  4. package/dist/coordination/index.js +83 -21
  5. package/dist/coordination/index.js.map +1 -1
  6. package/dist/defaults/index.d.ts +11 -11
  7. package/dist/defaults/index.js +89 -23
  8. package/dist/defaults/index.js.map +1 -1
  9. package/dist/{events-Bw1K5E6K.d.ts → events-BrQiweXN.d.ts} +1 -1
  10. package/dist/execution/index.d.ts +6 -6
  11. package/dist/execution/index.js +32 -10
  12. package/dist/execution/index.js.map +1 -1
  13. package/dist/extension/index.d.ts +2 -2
  14. package/dist/{index-DgQWnZxP.d.ts → index-DAHEM_II.d.ts} +1 -1
  15. package/dist/{index-DIt5TIaF.d.ts → index-DnBHrz2y.d.ts} +2 -2
  16. package/dist/index.d.ts +17 -17
  17. package/dist/index.js +226 -101
  18. package/dist/index.js.map +1 -1
  19. package/dist/infrastructure/index.d.ts +2 -2
  20. package/dist/kernel/index.d.ts +3 -3
  21. package/dist/kernel/index.js.map +1 -1
  22. package/dist/{multi-agent-C1O8EFzb.d.ts → multi-agent-C8Z1i__e.d.ts} +2 -2
  23. package/dist/{multi-agent-coordinator-DoVSBFNx.d.ts → multi-agent-coordinator-CjG-FZKj.d.ts} +8 -3
  24. package/dist/{null-fleet-bus-T-s3JaOt.d.ts → null-fleet-bus-B3WdVY9i.d.ts} +28 -23
  25. package/dist/observability/index.d.ts +1 -1
  26. package/dist/{path-resolver-BfyK-I7w.d.ts → path-resolver-DumKAi0n.d.ts} +1 -1
  27. package/dist/{plan-templates-BWs6Je7j.d.ts → plan-templates-BUP4o-ds.d.ts} +2 -2
  28. package/dist/{provider-runner-BmPe2RuU.d.ts → provider-runner-Dlv8Fvw9.d.ts} +1 -1
  29. package/dist/sdd/index.d.ts +4 -4
  30. package/dist/sdd/index.js +30 -8
  31. package/dist/sdd/index.js.map +1 -1
  32. package/dist/security/index.js +3 -3
  33. package/dist/security/index.js.map +1 -1
  34. package/dist/{session-reader-CCSBfQmd.d.ts → session-reader-bfgsy2a0.d.ts} +7 -3
  35. package/dist/storage/index.d.ts +29 -10
  36. package/dist/storage/index.js +131 -65
  37. package/dist/storage/index.js.map +1 -1
  38. package/dist/{tool-executor-D7RxLtg4.d.ts → tool-executor-D87-_5Of.d.ts} +1 -1
  39. package/dist/types/index.d.ts +8 -8
  40. package/dist/types/index.js +25 -5
  41. package/dist/types/index.js.map +1 -1
  42. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -56,8 +56,8 @@ async function atomicWrite(targetPath, content, opts = {}) {
56
56
  }
57
57
  let mode;
58
58
  try {
59
- const stat8 = await fsp2.stat(targetPath);
60
- mode = stat8.mode & 511;
59
+ const stat9 = await fsp2.stat(targetPath);
60
+ mode = stat9.mode & 511;
61
61
  } catch {
62
62
  mode = opts.mode;
63
63
  }
@@ -1302,7 +1302,7 @@ var isColorTty = () => {
1302
1302
  return Boolean(process.stdout?.isTTY);
1303
1303
  };
1304
1304
  var COLOR = isColorTty();
1305
- var wrap = (open3, close) => (s) => COLOR ? `\x1B[${open3}m${s}\x1B[${close}m` : s;
1305
+ var wrap = (open5, close) => (s) => COLOR ? `\x1B[${open5}m${s}\x1B[${close}m` : s;
1306
1306
  var color = {
1307
1307
  reset: wrap("0", "0"),
1308
1308
  bold: wrap("1", "22"),
@@ -2879,12 +2879,14 @@ var InMemoryAgentBridge = class {
2879
2879
  timeoutMs;
2880
2880
  /** Guards request() so concurrent calls on the same id can't silently overwrite. */
2881
2881
  inflightGuards = /* @__PURE__ */ new Set();
2882
+ /** Stores the transport unsubscribe function so it can be called on stop(). */
2883
+ _transportUnsubscribe;
2882
2884
  constructor(config, transport) {
2883
2885
  this.agentId = config.agentId;
2884
2886
  this.coordinatorId = config.coordinatorId;
2885
2887
  this.transport = transport;
2886
2888
  this.timeoutMs = config.timeoutMs ?? 3e4;
2887
- this.transport.subscribe(this.agentId, (msg) => {
2889
+ this._transportUnsubscribe = this.transport.subscribe(this.agentId, (msg) => {
2888
2890
  if (msg.type === "heartbeat") return;
2889
2891
  const pending = this.pendingRequests.get(msg.id);
2890
2892
  if (pending) {
@@ -2959,6 +2961,8 @@ var InMemoryAgentBridge = class {
2959
2961
  this.pendingRequests.clear();
2960
2962
  this.inflightGuards.clear();
2961
2963
  this.subscriptions.clear();
2964
+ this._transportUnsubscribe?.();
2965
+ this._transportUnsubscribe = void 0;
2962
2966
  await this.transport.close(this.agentId);
2963
2967
  }
2964
2968
  };
@@ -3110,13 +3114,13 @@ var DANGEROUS_FOR_SUBAGENTS = [
3110
3114
  function hasDangerousCapabilityForSubagents(toolOrCaps) {
3111
3115
  if (!toolOrCaps) return false;
3112
3116
  const input = toolOrCaps;
3113
- const caps = Array.isArray(input) ? input : input?.capabilities ?? [];
3117
+ const caps = Array.isArray(toolOrCaps) ? toolOrCaps : input.capabilities ?? [];
3114
3118
  return caps.some((c) => DANGEROUS_FOR_SUBAGENTS.includes(c));
3115
3119
  }
3116
3120
  function getDangerousCapabilities(toolOrCaps) {
3117
3121
  if (!toolOrCaps) return [];
3118
3122
  const input = toolOrCaps;
3119
- const caps = Array.isArray(input) ? input : input?.capabilities ?? [];
3123
+ const caps = Array.isArray(toolOrCaps) ? toolOrCaps : input.capabilities ?? [];
3120
3124
  return caps.filter(
3121
3125
  (c) => DANGEROUS_FOR_SUBAGENTS.includes(c)
3122
3126
  );
@@ -3683,11 +3687,27 @@ var DefaultSessionReader = class {
3683
3687
  const data = await this.store.load(sessionId);
3684
3688
  for (const e of data.events) yield e;
3685
3689
  }
3686
- async search(q, sessionId) {
3690
+ async search(q, sessionId, sessionQuery) {
3687
3691
  const limit = q.limit ?? 100;
3688
3692
  const matcher = buildMatcher(q);
3689
3693
  const allowedTypes = q.types ? new Set(q.types) : null;
3690
- const ids = sessionId ? [sessionId] : (await this.store.list(1e3)).map((s) => s.id);
3694
+ let ids;
3695
+ if (sessionId) {
3696
+ ids = [sessionId];
3697
+ } else {
3698
+ const sessions = await this.store.list(1e3);
3699
+ const titleNeedle = sessionQuery?.titleContains?.toLowerCase();
3700
+ const filtered = sessions.filter((s) => {
3701
+ if (sessionQuery?.since && s.startedAt < sessionQuery.since) return false;
3702
+ if (sessionQuery?.until && s.startedAt > sessionQuery.until) return false;
3703
+ if (sessionQuery?.provider && s.provider !== sessionQuery.provider) return false;
3704
+ if (sessionQuery?.model && s.model !== sessionQuery.model) return false;
3705
+ if (sessionQuery?.minTokens !== void 0 && s.tokenTotal < sessionQuery.minTokens) return false;
3706
+ if (titleNeedle && !s.title.toLowerCase().includes(titleNeedle)) return false;
3707
+ return true;
3708
+ });
3709
+ ids = filtered.map((s) => s.id);
3710
+ }
3691
3711
  const hits = [];
3692
3712
  for (const id of ids) {
3693
3713
  let data;
@@ -4643,8 +4663,8 @@ var DefaultSessionStore = class {
4643
4663
  return JSON.parse(raw);
4644
4664
  } catch {
4645
4665
  const full = this.sessionPath(id, ".jsonl");
4646
- const stat8 = await fsp2.stat(full);
4647
- const summary = await this.summarize(id, stat8.mtime.toISOString());
4666
+ const stat9 = await fsp2.stat(full);
4667
+ const summary = await this.summarize(id, stat9.mtime.toISOString());
4648
4668
  await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
4649
4669
  console.warn(
4650
4670
  `[session-store] Failed to write manifest for "${id}":`,
@@ -9204,8 +9224,9 @@ var SubagentBudget = class _SubagentBudget {
9204
9224
  if (elapsedMs !== void 0) {
9205
9225
  const idle = this.idleMs();
9206
9226
  if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
9207
- exceeded.push({ kind: "timeout", used: idle, limit: this.limits.idleTimeoutMs });
9208
- } else if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs) {
9227
+ exceeded.push({ kind: "idle_timeout", used: idle, limit: this.limits.idleTimeoutMs });
9228
+ }
9229
+ if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs) {
9209
9230
  exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
9210
9231
  }
9211
9232
  }
@@ -12418,10 +12439,6 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
12418
12439
  this.drainPendingAsAborted("Coordinator stopAll() drained the pending queue");
12419
12440
  await Promise.allSettled([...this.subagents.keys()].map((id) => this.stop(id)));
12420
12441
  }
12421
- async remove(subagentId) {
12422
- await this.stop(subagentId);
12423
- this.subagents.delete(subagentId);
12424
- }
12425
12442
  /**
12426
12443
  * Get current coordinator stats for monitoring/debugging.
12427
12444
  */
@@ -12687,7 +12704,7 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
12687
12704
  durationMs: Date.now() - startTime
12688
12705
  };
12689
12706
  } catch (err) {
12690
- const status = err instanceof BudgetExceededError && err.kind === "timeout" ? "timeout" : subagent.abortController.signal.aborted ? "stopped" : "failed";
12707
+ const status = err instanceof BudgetExceededError && (err.kind === "timeout" || err.kind === "idle_timeout") ? "timeout" : subagent.abortController.signal.aborted ? "stopped" : "failed";
12691
12708
  const usage = budget.usage();
12692
12709
  result = {
12693
12710
  subagentId,
@@ -12841,6 +12858,30 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
12841
12858
  });
12842
12859
  }
12843
12860
  }
12861
+ /**
12862
+ * Stop a subagent and remove it from the coordinator. Releases all
12863
+ * associated resources (AbortController, context, budget state).
12864
+ * The subagent entry is deleted so the id can be reused in a future spawn.
12865
+ */
12866
+ async remove(subagentId) {
12867
+ const subagent = this.subagents.get(subagentId);
12868
+ if (!subagent) return;
12869
+ if (subagent.status === "running" || subagent.status === "idle") {
12870
+ this.terminating.add(subagentId);
12871
+ subagent.abortController.abort();
12872
+ subagent.status = "stopped";
12873
+ }
12874
+ this.subagents.delete(subagentId);
12875
+ this.terminating.delete(subagentId);
12876
+ this.pendingTasks = this.pendingTasks.filter((t2) => t2.subagentId !== subagentId);
12877
+ this.fleetBus?.emit({
12878
+ subagentId,
12879
+ ts: Date.now(),
12880
+ type: "subagent.removed",
12881
+ payload: { subagentId }
12882
+ });
12883
+ this.emitCoordinatorStats();
12884
+ }
12844
12885
  isDone() {
12845
12886
  if (this.config.doneCondition.type === "all_tasks_done") {
12846
12887
  return this.pendingTasks.length === 0 && this.inFlight === 0;
@@ -12864,7 +12905,8 @@ function classifySubagentError(err, hints = {}) {
12864
12905
  tool_calls: "budget_tool_calls",
12865
12906
  tokens: "budget_tokens",
12866
12907
  cost: "budget_cost",
12867
- timeout: "budget_timeout"
12908
+ timeout: "budget_timeout",
12909
+ idle_timeout: "budget_timeout"
12868
12910
  };
12869
12911
  return {
12870
12912
  kind: map[err.kind],
@@ -13626,14 +13668,36 @@ var FleetUsageAggregator = class {
13626
13668
  constructor(bus, priceLookup, metaLookup) {
13627
13669
  this.priceLookup = priceLookup;
13628
13670
  this.metaLookup = metaLookup;
13629
- bus.filter("provider.response", (e) => this.onProviderResponse(e));
13630
- bus.filter("tool.executed", (e) => this.onToolExecuted(e));
13631
- bus.filter("iteration.started", (e) => this.onIterationStarted(e));
13671
+ this.unsub.push(bus.filter("provider.response", (e) => this.onProviderResponse(e)));
13672
+ this.unsub.push(bus.filter("tool.executed", (e) => this.onToolExecuted(e)));
13673
+ this.unsub.push(bus.filter("iteration.started", (e) => this.onIterationStarted(e)));
13632
13674
  }
13633
13675
  priceLookup;
13634
13676
  metaLookup;
13635
13677
  perSubagent = /* @__PURE__ */ new Map();
13636
13678
  total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
13679
+ unsub = new Array();
13680
+ /**
13681
+ * Remove a terminated subagent's data from the aggregator and subtract its
13682
+ * contribution from the running totals. Call this when a subagent is removed
13683
+ * from the fleet so the aggregator doesn't accumulate unbounded data for
13684
+ * entities that will never emit events again.
13685
+ */
13686
+ removeSubagent(subagentId) {
13687
+ const snap = this.perSubagent.get(subagentId);
13688
+ if (!snap) return;
13689
+ this.perSubagent.delete(subagentId);
13690
+ this.total.input -= snap.input;
13691
+ this.total.output -= snap.output;
13692
+ this.total.cacheRead -= snap.cacheRead;
13693
+ this.total.cacheWrite -= snap.cacheWrite;
13694
+ this.total.cost -= snap.cost;
13695
+ }
13696
+ /** Disposes all fleet-bus subscriptions. Call when the aggregator is no longer needed. */
13697
+ dispose() {
13698
+ for (const off of this.unsub) off();
13699
+ this.unsub.length = 0;
13700
+ }
13637
13701
  /** Live snapshot — safe to call from a tool's execute() body. */
13638
13702
  snapshot() {
13639
13703
  return {
@@ -14386,7 +14450,7 @@ Emit each evaluation immediately. Do not wait until you have read all reports.`;
14386
14450
  this.cancel(`Director cancelled: ${role} ${payload.kind} threshold`);
14387
14451
  return;
14388
14452
  }
14389
- if (payload.kind === "timeout") {
14453
+ if (payload.kind === "timeout" || payload.kind === "idle_timeout") {
14390
14454
  const progress = this.progressBySubagent.get(e.subagentId) ?? 0;
14391
14455
  const lastProgress = this.lastTimeoutProgress.get(e.subagentId) ?? -1;
14392
14456
  if (progress <= lastProgress) {
@@ -15124,7 +15188,7 @@ var Director = class {
15124
15188
  if (needsNickname) {
15125
15189
  const role = config.role ?? "subagent";
15126
15190
  if (this.fleetManager) {
15127
- this.fleetManager.assignNicknameAndRecord("", config, priceLookup);
15191
+ this.fleetManager.assignNicknameAndRecord(config);
15128
15192
  } else {
15129
15193
  config.name = assignNickname(role, this._usedNicknames);
15130
15194
  this._usedNicknames.add(config.name.split(" ")[0].toLowerCase().replace(/[^a-z0-9-]/g, "-"));
@@ -15132,9 +15196,7 @@ var Director = class {
15132
15196
  }
15133
15197
  result = await this.coordinator.spawn(config);
15134
15198
  if (this.fleetManager) {
15135
- if (!needsNickname) {
15136
- this.fleetManager.recordSpawn(result.subagentId, config, priceLookup);
15137
- }
15199
+ this.fleetManager.recordSpawn(result.subagentId, config, priceLookup);
15138
15200
  } else {
15139
15201
  this.spawnCount += 1;
15140
15202
  this.subagentMeta.set(result.subagentId, {
@@ -15422,6 +15484,7 @@ var Director = class {
15422
15484
  }
15423
15485
  async remove(subagentId) {
15424
15486
  await this.coordinator.remove(subagentId);
15487
+ this.usage.removeSubagent(subagentId);
15425
15488
  }
15426
15489
  status() {
15427
15490
  const base = this.coordinator.getStatus();
@@ -15757,6 +15820,7 @@ function createDelegateTool(opts) {
15757
15820
  if (timer) clearTimeout(timer);
15758
15821
  offTool();
15759
15822
  offIter();
15823
+ offProgress();
15760
15824
  resolve9(value);
15761
15825
  };
15762
15826
  const arm = () => {
@@ -15768,6 +15832,7 @@ function createDelegateTool(opts) {
15768
15832
  };
15769
15833
  const offTool = dir.fleet.filter("tool.executed", bump);
15770
15834
  const offIter = dir.fleet.filter("iteration.started", bump);
15835
+ const offProgress = dir.fleet.filter("tool.progress", bump);
15771
15836
  arm();
15772
15837
  dir.awaitTasks([taskId]).then((r) => finish(r[0] ?? { __timeout: true })).catch(() => finish({ __timeout: true }));
15773
15838
  });
@@ -15987,7 +16052,8 @@ var FIELD_BY_KIND = {
15987
16052
  tool_calls: "maxToolCalls",
15988
16053
  tokens: "maxTokens",
15989
16054
  cost: "maxCostUsd",
15990
- timeout: "timeoutMs"
16055
+ timeout: "timeoutMs",
16056
+ idle_timeout: "timeoutMs"
15991
16057
  };
15992
16058
  function attachAutoExtend(events, policy = {}) {
15993
16059
  const factor = policy.factor ?? 0.5;
@@ -16005,7 +16071,7 @@ function attachAutoExtend(events, policy = {}) {
16005
16071
  }),
16006
16072
  events.on("budget.threshold_reached", (e) => {
16007
16073
  const { kind, limit, extend, deny } = e;
16008
- if (kind === "timeout") {
16074
+ if (kind === "timeout" || kind === "idle_timeout") {
16009
16075
  if (progress > lastTimeoutProgress) {
16010
16076
  lastTimeoutProgress = progress;
16011
16077
  const next2 = Math.min(Math.ceil(limit * (1 + factor)), ceiling.timeoutMs);
@@ -16110,8 +16176,8 @@ async function loadProjectModes(modesDir) {
16110
16176
  for (const entry of entries) {
16111
16177
  if (!entry.endsWith(".md") && !entry.endsWith(".txt")) continue;
16112
16178
  const filePath = path6.join(modesDir, entry);
16113
- const stat8 = await fsp2.stat(filePath);
16114
- if (!stat8.isFile()) continue;
16179
+ const stat9 = await fsp2.stat(filePath);
16180
+ if (!stat9.isFile()) continue;
16115
16181
  const content = await fsp2.readFile(filePath, "utf8");
16116
16182
  const id = path6.basename(entry, path6.extname(entry));
16117
16183
  modes.push({
@@ -20040,10 +20106,10 @@ var SkillInstaller = class {
20040
20106
  if (!resolved.startsWith(path6.resolve(destDir))) {
20041
20107
  throw new Error(`Path traversal detected in skill file: ${file}`);
20042
20108
  }
20043
- const stat8 = await fsp2.stat(srcPath);
20044
- if (stat8.size > MAX_SKILL_FILE_SIZE) {
20109
+ const stat9 = await fsp2.stat(srcPath);
20110
+ if (stat9.size > MAX_SKILL_FILE_SIZE) {
20045
20111
  throw new Error(
20046
- `Skill file "${file}" is too large (${(stat8.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`
20112
+ `Skill file "${file}" is too large (${(stat9.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`
20047
20113
  );
20048
20114
  }
20049
20115
  await fsp2.mkdir(path6.dirname(destPath), { recursive: true });
@@ -20473,6 +20539,8 @@ var ReplayLogStore = class {
20473
20539
  writeChains = /* @__PURE__ */ new Map();
20474
20540
  /** Per-session hash → entry index, kept in memory after the first load. */
20475
20541
  cache = /* @__PURE__ */ new Map();
20542
+ /** Per-session entry count on disk, to detect when compaction is needed. */
20543
+ diskCount = /* @__PURE__ */ new Map();
20476
20544
  maxEntries;
20477
20545
  constructor(opts) {
20478
20546
  this.dir = opts.dir;
@@ -20495,17 +20563,30 @@ var ReplayLogStore = class {
20495
20563
  request: input.request,
20496
20564
  response: input.response
20497
20565
  };
20498
- let all = [...cache.values(), entry];
20499
- if (all.length > this.maxEntries) {
20500
- const evictCount = all.length - this.maxEntries;
20501
- all = all.slice(evictCount);
20502
- }
20503
- await this.writeAll(input.sessionId, all);
20504
- cache.clear();
20505
- for (const e of all) cache.set(e.hash, e);
20566
+ cache.set(hash, entry);
20567
+ const count = (this.diskCount.get(input.sessionId) ?? 0) + 1;
20568
+ if (count > this.maxEntries) {
20569
+ await this.compact(input.sessionId, cache);
20570
+ } else {
20571
+ await fsp2.appendFile(this.filePath(input.sessionId), JSON.stringify(entry) + "\n", "utf8");
20572
+ this.diskCount.set(input.sessionId, count);
20573
+ }
20506
20574
  });
20507
20575
  return hash;
20508
20576
  }
20577
+ /**
20578
+ * Compact the replay log to keep only the most recent maxEntries.
20579
+ * Called when entry count exceeds the cap. Rewrites the entire file
20580
+ * but only happens O(n / maxEntries) times per session.
20581
+ */
20582
+ async compact(sessionId, cache) {
20583
+ const all = [...cache.values()];
20584
+ const keep = all.slice(-this.maxEntries);
20585
+ await this.writeAll(sessionId, keep);
20586
+ cache.clear();
20587
+ for (const e of keep) cache.set(e.hash, e);
20588
+ this.diskCount.set(sessionId, keep.length);
20589
+ }
20509
20590
  // ── Reads ───────────────────────────────────────────────────────────────
20510
20591
  /**
20511
20592
  * Look up an entry by hash. Returns `null` when the request has
@@ -20589,6 +20670,7 @@ var ReplayLogStore = class {
20589
20670
  cache = /* @__PURE__ */ new Map();
20590
20671
  for (const e of all) cache.set(e.hash, e);
20591
20672
  this.cache.set(sessionId, cache);
20673
+ this.diskCount.set(sessionId, all.length);
20592
20674
  return cache;
20593
20675
  }
20594
20676
  enqueue(sessionId, fn) {
@@ -20618,14 +20700,50 @@ var SessionRecovery = class {
20618
20700
  */
20619
20701
  async detectStale(sessionId) {
20620
20702
  const fp = this.filePath(sessionId);
20621
- let raw;
20703
+ const TAIL_SIZE = 8192;
20704
+ let stat9;
20622
20705
  try {
20623
- raw = await fsp2.readFile(fp, "utf8");
20706
+ stat9 = await fsp2.stat(fp);
20624
20707
  } catch (err) {
20625
20708
  if (err.code === "ENOENT") return null;
20626
20709
  return null;
20627
20710
  }
20628
- return this.parseForStale(sessionId, fp, raw);
20711
+ if (stat9.size === 0) return null;
20712
+ const position = Math.max(0, stat9.size - TAIL_SIZE);
20713
+ const buf = Buffer.alloc(TAIL_SIZE);
20714
+ let fh;
20715
+ try {
20716
+ fh = await fsp2.open(fp, "r");
20717
+ const { bytesRead } = await fh.read(buf, 0, TAIL_SIZE, position);
20718
+ let eventCount = 0;
20719
+ const raw = buf.subarray(0, bytesRead).toString("utf8");
20720
+ for (const line of raw.split("\n")) {
20721
+ if (line.trim()) eventCount++;
20722
+ }
20723
+ const lines = raw.split("\n").filter((l) => l.trim());
20724
+ for (let i = lines.length - 1; i >= 0; i--) {
20725
+ try {
20726
+ const ev = JSON.parse(lines[i]);
20727
+ if (ev.type === "in_flight_start") {
20728
+ return {
20729
+ sessionId,
20730
+ path: fp,
20731
+ lastEventTs: ev.ts,
20732
+ context: ev.context,
20733
+ eventCount
20734
+ };
20735
+ }
20736
+ return null;
20737
+ } catch {
20738
+ continue;
20739
+ }
20740
+ }
20741
+ return null;
20742
+ } catch {
20743
+ return null;
20744
+ } finally {
20745
+ if (fh) await fh.close();
20746
+ }
20629
20747
  }
20630
20748
  /**
20631
20749
  * Generate a recovery plan for a session. The plan describes
@@ -20706,49 +20824,22 @@ var SessionRecovery = class {
20706
20824
  }
20707
20825
  return path6.join(this.dir, `${sessionId}.jsonl`);
20708
20826
  }
20709
- /**
20710
- * Stream-parse the last few lines of a JSONL log. We do NOT load
20711
- * the whole file into memory — for long-running sessions the log
20712
- * can be megabytes. Instead we read tail-ward and find the last
20713
- * `in_flight_start` / `in_flight_end` pair.
20714
- */
20715
- async parseForStale(sessionId, fp, raw) {
20716
- const lines = raw.split("\n");
20717
- let lastEvent = null;
20718
- let eventCount = 0;
20719
- for (const line of lines) {
20720
- if (!line.trim()) continue;
20721
- try {
20722
- const ev = JSON.parse(line);
20723
- lastEvent = ev;
20724
- eventCount++;
20725
- } catch {
20726
- }
20727
- }
20728
- if (!lastEvent) return null;
20729
- if (lastEvent.type === "in_flight_start") {
20730
- return {
20731
- sessionId,
20732
- path: fp,
20733
- lastEventTs: lastEvent.ts,
20734
- context: lastEvent.context,
20735
- eventCount
20736
- };
20737
- }
20738
- return null;
20739
- }
20740
20827
  };
20741
-
20742
- // src/storage/tool-audit-log.ts
20743
- init_atomic_write();
20744
20828
  var GENESIS_PREV = "0".repeat(64);
20829
+ var DEFAULT_FSYNC_EVERY = 100;
20745
20830
  var ToolAuditLog = class {
20746
20831
  dir;
20747
20832
  /** In-memory cache of the last entry's hash (per session), to compute chains efficiently. */
20748
20833
  tailHash = /* @__PURE__ */ new Map();
20834
+ /** In-memory counter for entry indices — avoids re-reading the file on every write. */
20835
+ tailIndex = /* @__PURE__ */ new Map();
20836
+ /** Tracks writes since last fsync, per session. */
20837
+ unSyncedWrites = /* @__PURE__ */ new Map();
20749
20838
  writeChains = /* @__PURE__ */ new Map();
20839
+ fsyncEvery;
20750
20840
  constructor(opts) {
20751
20841
  this.dir = opts.dir;
20842
+ this.fsyncEvery = opts.fsyncEvery ?? DEFAULT_FSYNC_EVERY;
20752
20843
  }
20753
20844
  /**
20754
20845
  * Append a tool call/result pair to the chain. Returns the
@@ -20760,6 +20851,7 @@ var ToolAuditLog = class {
20760
20851
  let entry = null;
20761
20852
  await this.enqueue(input.sessionId, async () => {
20762
20853
  const prevHash = this.tailHash.get(input.sessionId) ?? GENESIS_PREV;
20854
+ const index = this.tailIndex.get(input.sessionId) ?? 0;
20763
20855
  const id = randomUUID();
20764
20856
  const ts = (/* @__PURE__ */ new Date()).toISOString();
20765
20857
  const content = {
@@ -20770,12 +20862,11 @@ var ToolAuditLog = class {
20770
20862
  toolUseId: input.toolUseId,
20771
20863
  input: input.input,
20772
20864
  output: input.output,
20773
- isError: input.isError
20865
+ isError: input.isError,
20866
+ index
20774
20867
  };
20775
20868
  const hash = createHash("sha256").update(stableStringify2(content), "utf8").digest("hex");
20776
20869
  entry = {
20777
- index: this.tailHash.has(input.sessionId) ? -1 : 0,
20778
- // placeholder; recomputed below
20779
20870
  id,
20780
20871
  ts,
20781
20872
  prevHash,
@@ -20784,15 +20875,12 @@ var ToolAuditLog = class {
20784
20875
  toolUseId: input.toolUseId,
20785
20876
  input: input.input,
20786
20877
  output: input.output,
20787
- isError: input.isError
20878
+ isError: input.isError,
20879
+ index
20788
20880
  };
20789
- const existing = await this.readAll(input.sessionId);
20790
- const index = existing.length;
20791
- const finalContent = { ...content, index };
20792
- const finalHash = createHash("sha256").update(stableStringify2(finalContent), "utf8").digest("hex");
20793
- entry = { ...entry, index, hash: finalHash };
20794
20881
  await this.appendLine(input.sessionId, entry);
20795
- this.tailHash.set(input.sessionId, finalHash);
20882
+ this.tailHash.set(input.sessionId, hash);
20883
+ this.tailIndex.set(input.sessionId, index + 1);
20796
20884
  });
20797
20885
  return entry;
20798
20886
  }
@@ -20874,9 +20962,34 @@ var ToolAuditLog = class {
20874
20962
  }
20875
20963
  async appendLine(sessionId, entry) {
20876
20964
  const fp = this.filePath(sessionId);
20877
- const existing = await this.readAll(sessionId);
20878
- const all = [...existing, entry];
20879
- await atomicWrite(fp, all.map((e) => JSON.stringify(e)).join("\n") + (all.length ? "\n" : ""));
20965
+ const line = JSON.stringify(entry) + "\n";
20966
+ await fsp2.appendFile(fp, line, "utf8");
20967
+ const count = (this.unSyncedWrites.get(sessionId) ?? 0) + 1;
20968
+ this.unSyncedWrites.set(sessionId, count);
20969
+ if (this.fsyncEvery !== Infinity && count % this.fsyncEvery === 0) {
20970
+ await this.sync(sessionId, fp);
20971
+ }
20972
+ }
20973
+ /**
20974
+ * Explicitly sync the file to disk. Called automatically every
20975
+ * `fsyncEvery` writes, and available for callers who want to
20976
+ * force a sync before closing or during graceful shutdown.
20977
+ */
20978
+ async flush(sessionId) {
20979
+ await this.sync(sessionId, this.filePath(sessionId));
20980
+ }
20981
+ async sync(sessionId, fp) {
20982
+ try {
20983
+ const fh = await fsp2.open(fp, "r+");
20984
+ try {
20985
+ await fh.sync();
20986
+ } finally {
20987
+ await fh.close();
20988
+ }
20989
+ } catch {
20990
+ } finally {
20991
+ this.unSyncedWrites.set(sessionId, 0);
20992
+ }
20880
20993
  }
20881
20994
  enqueue(sessionId, fn) {
20882
20995
  const prev = this.writeChains.get(sessionId) ?? Promise.resolve();
@@ -21353,8 +21466,8 @@ var CloudSync = class {
21353
21466
  const localPath = this.categoryToPath(cat);
21354
21467
  if (!localPath) continue;
21355
21468
  try {
21356
- const stat8 = await fsp2.stat(localPath);
21357
- if (stat8.isDirectory()) {
21469
+ const stat9 = await fsp2.stat(localPath);
21470
+ if (stat9.isDirectory()) {
21358
21471
  const files = await this.walkDir(localPath, localPath);
21359
21472
  for (const file of files) {
21360
21473
  const content = await fsp2.readFile(file, "utf8");
@@ -21379,8 +21492,8 @@ var CloudSync = class {
21379
21492
  const localPath = this.categoryToPath(cat);
21380
21493
  if (!localPath) continue;
21381
21494
  try {
21382
- const stat8 = await fsp2.stat(localPath);
21383
- if (stat8.isDirectory()) {
21495
+ const stat9 = await fsp2.stat(localPath);
21496
+ if (stat9.isDirectory()) {
21384
21497
  const files = await this.walkDir(localPath, localPath);
21385
21498
  for (const file of files) {
21386
21499
  const content = await fsp2.readFile(file);
@@ -23434,14 +23547,18 @@ var FleetManager = class {
23434
23547
  * Call this INSTEAD of `recordSpawn` when you want automatic nicknames.
23435
23548
  * The nickname is written back to `config.name` BEFORE the coordinator
23436
23549
  * sees the config, so the manifest, logs, and fleet UI all show it.
23550
+ *
23551
+ * NOTE: This method ONLY assigns the nickname and marks it used.
23552
+ * The caller MUST call `recordSpawn(subagentId, config, priceLookup)` AFTER
23553
+ * `coordinator.spawn()` returns with the real subagentId. This is because
23554
+ * the subagentId is not known until after the coordinator creates the subagent.
23437
23555
  */
23438
- assignNicknameAndRecord(subagentId, config, priceLookup) {
23556
+ assignNicknameAndRecord(config) {
23439
23557
  const role = config.role ?? "subagent";
23440
23558
  const nickname = assignNickname(role, this._usedNicknames);
23441
23559
  const baseKey = nickname.split(" ")[0].toLowerCase().replace(/[^a-z0-9-]/g, "-");
23442
23560
  this._usedNicknames.add(baseKey);
23443
23561
  config.name = nickname;
23444
- this.recordSpawn(subagentId, config, priceLookup);
23445
23562
  return nickname;
23446
23563
  }
23447
23564
  /**
@@ -23619,6 +23736,14 @@ var FleetManager = class {
23619
23736
  }));
23620
23737
  return { pending, live: [] };
23621
23738
  }
23739
+ /** Release all resources: clear the manifest debounce timer and dispose the usage aggregator. */
23740
+ dispose() {
23741
+ if (this.manifestTimer) {
23742
+ clearTimeout(this.manifestTimer);
23743
+ this.manifestTimer = null;
23744
+ }
23745
+ this.usage.dispose();
23746
+ }
23622
23747
  };
23623
23748
  function createMcpControlTool(opts) {
23624
23749
  const { getConfig, configPath, registry } = opts;
@@ -25311,8 +25436,8 @@ var DefaultSystemPromptBuilder = class {
25311
25436
  return "";
25312
25437
  }
25313
25438
  if (!Array.isArray(parsed.items) || parsed.items.length === 0) return "";
25314
- const open3 = parsed.items.filter((i) => i?.status !== "done");
25315
- if (open3.length === 0) return "";
25439
+ const open5 = parsed.items.filter((i) => i?.status !== "done");
25440
+ if (open5.length === 0) return "";
25316
25441
  const lines = ["## Active plan"];
25317
25442
  if (parsed.title) lines.push(`*${parsed.title}*`, "");
25318
25443
  parsed.items.forEach((it, idx) => {
@@ -25528,8 +25653,8 @@ ${mem}`);
25528
25653
  }
25529
25654
  async dirExists(p) {
25530
25655
  try {
25531
- const stat8 = await fsp2.stat(p);
25532
- return stat8.isDirectory();
25656
+ const stat9 = await fsp2.stat(p);
25657
+ return stat9.isDirectory();
25533
25658
  } catch {
25534
25659
  return false;
25535
25660
  }