@wrongstack/core 0.309.1 → 0.310.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.
Files changed (120) hide show
  1. package/dist/chronicle/index.js +113 -41
  2. package/dist/chronicle/metrics-ingest.d.ts +7 -0
  3. package/dist/chronicle/metrics-schema.d.ts +4 -1
  4. package/dist/chronicle/project-server.js +94 -38
  5. package/dist/chronicle/query.d.ts +1 -0
  6. package/dist/chronicle/sqlite-journal-schema.d.ts +2 -1
  7. package/dist/chronicle/types.d.ts +2 -0
  8. package/dist/{agent-status-helpers.d.ts → coordination/agent-status-helpers.d.ts} +1 -1
  9. package/dist/{agent-status-tracker.d.ts → coordination/agent-status-tracker.d.ts} +2 -2
  10. package/dist/{middleware → coordination}/collab-pause.d.ts +1 -1
  11. package/dist/coordination/director-kanban-queue-helpers.d.ts +1 -1
  12. package/dist/coordination/fleet-status-tool.d.ts +1 -1
  13. package/dist/coordination/index.d.ts +5 -1
  14. package/dist/coordination/index.js +2061 -346
  15. package/dist/coordination/kanban-dispatch-port.d.ts +30 -0
  16. package/dist/coordination/kanban-ops-port.d.ts +21 -0
  17. package/dist/coordination/mailbox-hooks.d.ts +1 -1
  18. package/dist/coordination/mailbox-project-server.js +14 -9
  19. package/dist/core/context.d.ts +35 -44
  20. package/dist/core/conversation-state.d.ts +16 -52
  21. package/dist/core/index.js +53 -16
  22. package/dist/core/provider-runner.d.ts +2 -2
  23. package/dist/core/run-env.d.ts +7 -28
  24. package/dist/core/streaming-response-builder.d.ts +2 -2
  25. package/dist/execution/index.js +111 -153
  26. package/dist/goal/index.js +100 -22
  27. package/dist/hq/auth-store.d.ts +9 -0
  28. package/dist/hq/cost-bridge.d.ts +1 -1
  29. package/dist/hq/index.js +24 -2
  30. package/dist/index.d.ts +12 -5
  31. package/dist/index.js +27808 -33937
  32. package/dist/infrastructure/index.js +210 -132
  33. package/dist/infrastructure/provider-cache-ledger.d.ts +1 -1
  34. package/dist/infrastructure/token-counter.d.ts +5 -1
  35. package/dist/kernel/events/file-events.d.ts +6 -0
  36. package/dist/kernel/events/provider-events.d.ts +10 -7
  37. package/dist/kernel/events/session-events.d.ts +4 -4
  38. package/dist/kernel/events/tool-events.d.ts +12 -2
  39. package/dist/plugin/index.js +105 -35
  40. package/dist/security/index.d.ts +1 -0
  41. package/dist/security/index.js +47 -18
  42. package/dist/security/kanban-boundary.d.ts +1 -1
  43. package/dist/security/kanban-governance-port.d.ts +28 -0
  44. package/dist/session-catalog/index.js +2 -3
  45. package/dist/session-catalog/project-server.js +2 -3
  46. package/dist/session-catalog/protocol.d.ts +1 -1
  47. package/dist/session-catalog/registry.d.ts +1 -1
  48. package/dist/session-catalog/store-schema.d.ts +1 -1
  49. package/dist/session-catalog/store.d.ts +1 -1
  50. package/dist/storage/annotations-store.d.ts +1 -1
  51. package/dist/storage/board-store-port.d.ts +45 -0
  52. package/dist/storage/completed-work-checkpoint.d.ts +1 -1
  53. package/dist/storage/config-loader/types.d.ts +1 -1
  54. package/dist/storage/event-bus-port.d.ts +27 -0
  55. package/dist/storage/file-session-writer.d.ts +47 -5
  56. package/dist/storage/goal-coordination.d.ts +1 -1
  57. package/dist/storage/goal-store.d.ts +1 -1
  58. package/dist/storage/index.d.ts +3 -4
  59. package/dist/storage/index.js +1063 -1459
  60. package/dist/storage/plan-store.d.ts +1 -1
  61. package/dist/storage/queue-store.d.ts +1 -1
  62. package/dist/storage/replay-log-store.d.ts +1 -1
  63. package/dist/storage/session-recovery.d.ts +10 -0
  64. package/dist/storage/session-resume-validation.d.ts +13 -1
  65. package/dist/storage/session-store/events.d.ts +1 -1
  66. package/dist/storage/session-store/load-session-data.d.ts +1 -1
  67. package/dist/storage/session-store/rename-session.d.ts +1 -1
  68. package/dist/storage/session-store/resume-session.d.ts +3 -1
  69. package/dist/storage/session-store/session-store-index.d.ts +2 -1
  70. package/dist/storage/session-store/types.d.ts +1 -1
  71. package/dist/storage/session-store.d.ts +70 -0
  72. package/dist/storage/session-summary-tracker.d.ts +8 -0
  73. package/dist/storage/session-write-buffer.d.ts +31 -2
  74. package/dist/storage/task-store.d.ts +1 -1
  75. package/dist/storage/todos-checkpoint.d.ts +1 -1
  76. package/dist/storage/tool-audit-log.d.ts +1 -1
  77. package/dist/tasking/index.js +100 -22
  78. package/dist/tasking/task-tracker.d.ts +24 -1
  79. package/dist/types/compactor.d.ts +2 -2
  80. package/dist/types/config/runtime.d.ts +7 -0
  81. package/dist/types/config/tools.d.ts +30 -0
  82. package/dist/types/context.d.ts +212 -0
  83. package/dist/types/conversation-state.d.ts +109 -0
  84. package/dist/types/error-handler.d.ts +2 -2
  85. package/dist/types/file-event-record.d.ts +6 -0
  86. package/dist/types/index.d.ts +4 -4
  87. package/dist/types/index.js +19 -1
  88. package/dist/types/permission.d.ts +3 -3
  89. package/dist/types/plugin.d.ts +3 -3
  90. package/dist/types/provider-runner.d.ts +2 -2
  91. package/dist/types/provider.d.ts +10 -0
  92. package/dist/types/run-env.d.ts +32 -0
  93. package/dist/types/session.d.ts +3 -0
  94. package/dist/types/slash-command.d.ts +2 -2
  95. package/dist/types/token-counter.d.ts +30 -1
  96. package/dist/types/tool-executor.d.ts +2 -2
  97. package/dist/types/tool.d.ts +5 -5
  98. package/dist/utils/context-breakdown.d.ts +2 -2
  99. package/dist/utils/context-evidence.d.ts +12 -12
  100. package/dist/utils/crash-shield.d.ts +9 -0
  101. package/dist/utils/heap-watchdog.js +11 -3
  102. package/dist/utils/index.d.ts +1 -1
  103. package/dist/utils/index.js +72 -156
  104. package/dist/utils/regex-guard.d.ts +7 -30
  105. package/dist/utils/todos-format.d.ts +1 -1
  106. package/dist/utils/tree-kill.d.ts +2 -0
  107. package/dist/utils/tree-kill.js +1 -0
  108. package/dist/wiring/proxy-rewrite.d.ts +76 -0
  109. package/dist/wiring/proxy-rewrite.js +70 -0
  110. package/instructions/coordination/subagent-baseline.md +10 -0
  111. package/instructions/system-lite.md +2 -0
  112. package/instructions/system-pro.md +40 -0
  113. package/instructions/system.md +15 -0
  114. package/package.json +10 -9
  115. package/dist/defaults/index.d.ts +0 -69
  116. package/dist/defaults/index.js +0 -38133
  117. /package/dist/{fleet-notifier.d.ts → coordination/fleet-notifier.d.ts} +0 -0
  118. /package/dist/{session-registry-atomic-file.d.ts → session-catalog/session-registry-atomic-file.d.ts} +0 -0
  119. /package/dist/{session-registry-types.d.ts → session-catalog/session-registry-types.d.ts} +0 -0
  120. /package/dist/{session-registry.d.ts → session-catalog/session-registry.d.ts} +0 -0
@@ -224,7 +224,7 @@ var InMemoryAgentBridge = class {
224
224
  });
225
225
  }
226
226
  this.inflightGuards.add(correlationId);
227
- return new Promise((resolve16, reject) => {
227
+ return new Promise((resolve17, reject) => {
228
228
  const timer = setTimeout(() => {
229
229
  this.inflightGuards.delete(correlationId);
230
230
  this.pendingRequests.delete(correlationId);
@@ -243,7 +243,7 @@ var InMemoryAgentBridge = class {
243
243
  return;
244
244
  }
245
245
  this.pendingRequests.set(correlationId, {
246
- resolve: resolve16,
246
+ resolve: resolve17,
247
247
  reject,
248
248
  timer
249
249
  });
@@ -714,13 +714,13 @@ var SubagentBudget = class _SubagentBudget {
714
714
  if (!bus?.hasListenerFor("budget.threshold_reached")) {
715
715
  return Promise.resolve("stop");
716
716
  }
717
- return new Promise((resolve16) => {
717
+ return new Promise((resolve17) => {
718
718
  let resolved = false;
719
719
  const respond = (d) => {
720
720
  if (resolved) return;
721
721
  resolved = true;
722
722
  clearTimeout(fallback);
723
- resolve16(d);
723
+ resolve17(d);
724
724
  };
725
725
  const fallback = setTimeout(() => respond("stop"), _SubagentBudget.DECISION_TIMEOUT_MS);
726
726
  const sessionId = this.currentSessionId();
@@ -7649,13 +7649,13 @@ var BrainDecisionQueue = class {
7649
7649
  options: request.options,
7650
7650
  rationale: "Decision escalated to human authority."
7651
7651
  };
7652
- const pending = new Promise((resolve16) => {
7653
- const entry = { request, resolve: resolve16 };
7652
+ const pending = new Promise((resolve17) => {
7653
+ const entry = { request, resolve: resolve17 };
7654
7654
  if (this.opts.timeoutMs && this.opts.timeoutMs > 0) {
7655
7655
  entry.timer = setTimeout(() => {
7656
7656
  this.pending.delete(request.id);
7657
7657
  markDecisionTier(request, "terminal");
7658
- resolve16(
7658
+ resolve17(
7659
7659
  this.opts.onTimeout?.(request) ?? {
7660
7660
  type: "deny",
7661
7661
  reason: "Brain human decision timed out."
@@ -8439,26 +8439,26 @@ var BrainMonitor = class {
8439
8439
  trackFileChurn(toolName, ok, input) {
8440
8440
  if (!this.signals.fileChurn) return;
8441
8441
  if (!ok || !this.fileEditTools.has(toolName.toLowerCase())) return;
8442
- const path42 = editedPath(input);
8443
- if (!path42) return;
8442
+ const path44 = editedPath(input);
8443
+ if (!path44) return;
8444
8444
  const now = Date.now();
8445
- const stamps = (this.editTimestamps.get(path42) ?? []).filter(
8445
+ const stamps = (this.editTimestamps.get(path44) ?? []).filter(
8446
8446
  (t) => now - t <= this.fileChurnWindowMs
8447
8447
  );
8448
8448
  stamps.push(now);
8449
8449
  if (stamps.length >= this.fileChurnThreshold) {
8450
- this.editTimestamps.delete(path42);
8450
+ this.editTimestamps.delete(path44);
8451
8451
  void this.engage("file_churn", {
8452
- question: `The file "${path42}" has been edited ${stamps.length} times within ${Math.round(this.fileChurnWindowMs / 6e4)} minutes \u2014 the agent may be oscillating (edit/revert loop) instead of converging. Should it be steered?`,
8452
+ question: `The file "${path44}" has been edited ${stamps.length} times within ${Math.round(this.fileChurnWindowMs / 6e4)} minutes \u2014 the agent may be oscillating (edit/revert loop) instead of converging. Should it be steered?`,
8453
8453
  context: [
8454
- `File: ${path42}`,
8454
+ `File: ${path44}`,
8455
8455
  `Edits in window: ${stamps.length}`,
8456
8456
  `Window: ${Math.round(this.fileChurnWindowMs / 1e3)}s`
8457
8457
  ].join("\n")
8458
8458
  });
8459
8459
  return;
8460
8460
  }
8461
- if (this.editTimestamps.size >= 500 && !this.editTimestamps.has(path42)) {
8461
+ if (this.editTimestamps.size >= 500 && !this.editTimestamps.has(path44)) {
8462
8462
  for (const [p, times] of this.editTimestamps) {
8463
8463
  if (times.every((t) => now - t > this.fileChurnWindowMs)) {
8464
8464
  this.editTimestamps.delete(p);
@@ -8469,7 +8469,7 @@ var BrainMonitor = class {
8469
8469
  if (oldest !== void 0) this.editTimestamps.delete(oldest);
8470
8470
  }
8471
8471
  }
8472
- this.editTimestamps.set(path42, stamps);
8472
+ this.editTimestamps.set(path44, stamps);
8473
8473
  }
8474
8474
  async engage(kind, input) {
8475
8475
  const last = this.lastEngagedAt.get(kind) ?? 0;
@@ -9289,49 +9289,57 @@ async function expandGlob(pattern) {
9289
9289
  async function walk(dir, pat) {
9290
9290
  let entries;
9291
9291
  try {
9292
- entries = await fsp.readdir(dir);
9292
+ entries = await fsp.readdir(dir, { withFileTypes: true });
9293
9293
  } catch {
9294
9294
  return;
9295
9295
  }
9296
- const firstGlob = pat.search(/[*?[[]/);
9297
- if (firstGlob < 0) {
9298
- const re = globToRegex(pat);
9296
+ if (pat.startsWith("**/")) {
9297
+ const rest = pat.slice(3);
9298
+ await walk(dir, rest);
9299
9299
  for (const e of entries) {
9300
- if (re.test(e)) {
9301
- const full = `${dir}${SEP}${e}`;
9302
- results.add(abs ? resolve4(full) : full);
9300
+ if (e.isDirectory()) {
9301
+ const subDir = `${dir}${SEP}${e.name}`;
9302
+ await walk(subDir, pat);
9303
9303
  }
9304
9304
  }
9305
9305
  return;
9306
9306
  }
9307
- const before = pat.slice(0, firstGlob);
9308
- const rest = pat.slice(firstGlob);
9309
- if (before.endsWith("**")) {
9310
- await walk(dir, rest);
9307
+ if (pat === "**") {
9311
9308
  for (const e of entries) {
9312
- const full = `${dir}${SEP}${e}`;
9313
- try {
9314
- const stat13 = await fsp.stat(full);
9315
- if (stat13.isDirectory()) await walk(full, rest);
9316
- } catch {
9309
+ const full = `${dir}${SEP}${e.name}`;
9310
+ results.add(abs ? resolve4(full) : full);
9311
+ if (e.isDirectory()) {
9312
+ await walk(full, "**");
9317
9313
  }
9318
9314
  }
9319
- } else if (before === "") {
9320
- const re = globToRegex(rest);
9315
+ return;
9316
+ }
9317
+ const firstSlash = pat.indexOf("/");
9318
+ if (firstSlash < 0) {
9319
+ const re = globToRegex(pat);
9321
9320
  for (const e of entries) {
9322
- if (re.test(e)) {
9323
- const full = `${dir}${SEP}${e}`;
9321
+ if (re.test(e.name)) {
9322
+ const full = `${dir}${SEP}${e.name}`;
9324
9323
  results.add(abs ? resolve4(full) : full);
9325
9324
  }
9326
9325
  }
9326
+ return;
9327
+ }
9328
+ const currentSeg = pat.slice(0, firstSlash);
9329
+ const remainingPat = pat.slice(firstSlash + 1);
9330
+ if (isGlob(currentSeg)) {
9331
+ const re = globToRegex(currentSeg);
9332
+ for (const e of entries) {
9333
+ if (e.isDirectory() && re.test(e.name)) {
9334
+ const subDir = `${dir}${SEP}${e.name}`;
9335
+ await walk(subDir, remainingPat);
9336
+ }
9337
+ }
9327
9338
  } else {
9328
- const seg = before.replace(/[*?[\]]/g, "").replace(/\/$/, "");
9329
- if (entries.includes(seg)) {
9330
- const full = `${dir}${SEP}${seg}`;
9331
- try {
9332
- const stat13 = await fsp.stat(full);
9333
- if (stat13.isDirectory()) await walk(full, rest);
9334
- } catch {
9339
+ for (const e of entries) {
9340
+ if (e.isDirectory() && e.name === currentSeg) {
9341
+ const subDir = `${dir}${SEP}${e.name}`;
9342
+ await walk(subDir, remainingPat);
9335
9343
  }
9336
9344
  }
9337
9345
  }
@@ -10466,7 +10474,7 @@ async function gitOtherWorktrees(cwd, signal) {
10466
10474
  return branches.slice(1);
10467
10475
  }
10468
10476
  function runGit(args, cwd, signal) {
10469
- return new Promise((resolve16, reject) => {
10477
+ return new Promise((resolve17, reject) => {
10470
10478
  let stdout = "";
10471
10479
  let stderr = "";
10472
10480
  const child = spawn("git", args, {
@@ -10485,7 +10493,7 @@ function runGit(args, cwd, signal) {
10485
10493
  });
10486
10494
  child.on("error", (err) => reject(err));
10487
10495
  child.on("close", (code) => {
10488
- if (code === 0) resolve16(stdout);
10496
+ if (code === 0) resolve17(stdout);
10489
10497
  else reject(new Error(stderr || `git ${args[0]} exited ${code}`));
10490
10498
  });
10491
10499
  });
@@ -11163,7 +11171,7 @@ function createDelegateTool(opts) {
11163
11171
  };
11164
11172
  }
11165
11173
  async function awaitDelegateAttempt(director, subagentId, taskId, timeoutMs, abortSignal) {
11166
- return new Promise((resolve16) => {
11174
+ return new Promise((resolve17) => {
11167
11175
  let settled = false;
11168
11176
  let timer;
11169
11177
  let offAbort = () => {
@@ -11176,7 +11184,7 @@ async function awaitDelegateAttempt(director, subagentId, taskId, timeoutMs, abo
11176
11184
  offIter();
11177
11185
  offProgress();
11178
11186
  offAbort();
11179
- resolve16(value);
11187
+ resolve17(value);
11180
11188
  };
11181
11189
  const arm = () => {
11182
11190
  if (timer) clearTimeout(timer);
@@ -11797,31 +11805,31 @@ var ExploreCompanion = class {
11797
11805
  // ── signal handlers ──────────────────────────────────────────────────────
11798
11806
  trackToolExecuted(e) {
11799
11807
  const tool = e.name.toLowerCase();
11800
- const path42 = extractedPath(e.input);
11801
- if (e.ok && this.cfg.signals.editUnreadFile && this.cfg.fileEditTools.has(tool) && path42) {
11802
- if (!this.readSet.has(path42)) {
11808
+ const path44 = extractedPath(e.input);
11809
+ if (e.ok && this.cfg.signals.editUnreadFile && this.cfg.fileEditTools.has(tool) && path44) {
11810
+ if (!this.readSet.has(path44)) {
11803
11811
  this.engage({
11804
11812
  id: randomUUID6(),
11805
- probe: `Map file ${path42}: role, exports, dependencies, and callers \u2014 the leader is about to edit it.`,
11806
- hint: { file: path42 },
11807
- context: `Leader edited ${path42} without reading it first.`,
11813
+ probe: `Map file ${path44}: role, exports, dependencies, and callers \u2014 the leader is about to edit it.`,
11814
+ hint: { file: path44 },
11815
+ context: `Leader edited ${path44} without reading it first.`,
11808
11816
  source: "edit_unread_file",
11809
- subject: `file:${path42}`,
11817
+ subject: `file:${path44}`,
11810
11818
  createdAt: this.now()
11811
11819
  });
11812
11820
  }
11813
11821
  return;
11814
11822
  }
11815
- if (e.ok && this.cfg.signals.unfamiliarRead && tool === "read" && path42) {
11816
- if (!this.readSet.has(path42)) {
11817
- this.readSet.add(path42);
11823
+ if (e.ok && this.cfg.signals.unfamiliarRead && tool === "read" && path44) {
11824
+ if (!this.readSet.has(path44)) {
11825
+ this.readSet.add(path44);
11818
11826
  this.engage({
11819
11827
  id: randomUUID6(),
11820
- probe: `Skeleton + callers + dependents of ${path42}: what it exports, who imports it, and how it fits the feature flow.`,
11821
- hint: { file: path42 },
11822
- context: `Leader read unfamiliar file ${path42}.`,
11828
+ probe: `Skeleton + callers + dependents of ${path44}: what it exports, who imports it, and how it fits the feature flow.`,
11829
+ hint: { file: path44 },
11830
+ context: `Leader read unfamiliar file ${path44}.`,
11823
11831
  source: "unfamiliar_read",
11824
- subject: `file:${path42}`,
11832
+ subject: `file:${path44}`,
11825
11833
  createdAt: this.now()
11826
11834
  });
11827
11835
  }
@@ -12010,13 +12018,13 @@ function makeDependencyWatcherConfig(opts) {
12010
12018
  const globPatterns = patterns.filter((p) => p.includes("*"));
12011
12019
  const plainPatterns = patterns.filter((p) => !p.includes("*"));
12012
12020
  function matchesPattern(filePath) {
12013
- const basename6 = filePath.split("/").pop()?.split("\\").pop() ?? "";
12014
- if (plainPatterns.includes(basename6)) return true;
12021
+ const basename7 = filePath.split("/").pop()?.split("\\").pop() ?? "";
12022
+ if (plainPatterns.includes(basename7)) return true;
12015
12023
  for (const gp of globPatterns) {
12016
12024
  const regex = new RegExp(
12017
12025
  "^" + gp.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$"
12018
12026
  );
12019
- if (regex.test(basename6)) return true;
12027
+ if (regex.test(basename7)) return true;
12020
12028
  }
12021
12029
  return false;
12022
12030
  }
@@ -12988,10 +12996,10 @@ var DirectorTaskRegistry = class _DirectorTaskRegistry {
12988
12996
  pending: taskIds.filter((id) => !done.has(id))
12989
12997
  });
12990
12998
  }
12991
- return new Promise((resolve16) => {
12999
+ return new Promise((resolve17) => {
12992
13000
  const entry = {
12993
13001
  ids: new Set(taskIds),
12994
- resolve: (result) => resolve16({
13002
+ resolve: (result) => resolve17({
12995
13003
  completed: [result],
12996
13004
  pending: taskIds.filter((id) => id !== result.taskId)
12997
13005
  })
@@ -12999,7 +13007,7 @@ var DirectorTaskRegistry = class _DirectorTaskRegistry {
12999
13007
  if (opts?.timeoutMs !== void 0) {
13000
13008
  entry.timer = setTimeout(() => {
13001
13009
  this.anyWaiters.delete(entry);
13002
- resolve16({ completed: [], pending: [...taskIds], timedOut: true });
13010
+ resolve17({ completed: [], pending: [...taskIds], timedOut: true });
13003
13011
  }, opts.timeoutMs);
13004
13012
  }
13005
13013
  this.anyWaiters.add(entry);
@@ -13117,11 +13125,11 @@ ${JSON.stringify(result.result, null, 2)}
13117
13125
  this.makeStoppedResult(taskId, "director", `Unknown task id "${taskId}" \u2014 never assigned`)
13118
13126
  );
13119
13127
  }
13120
- let resolve16;
13128
+ let resolve17;
13121
13129
  const promise = new Promise((done) => {
13122
- resolve16 = done;
13130
+ resolve17 = done;
13123
13131
  });
13124
- this.taskWaiters.set(taskId, { promise, resolve: resolve16 });
13132
+ this.taskWaiters.set(taskId, { promise, resolve: resolve17 });
13125
13133
  return promise;
13126
13134
  }
13127
13135
  recordAssignment(task) {
@@ -13182,16 +13190,21 @@ ${JSON.stringify(result.result, null, 2)}
13182
13190
 
13183
13191
  // src/coordination/director-tools.ts
13184
13192
  import { randomUUID as randomUUID12 } from "node:crypto";
13185
- import {
13186
- completeKanbanDispatch,
13187
- failKanbanDispatch,
13188
- getBoard,
13189
- heartbeatTaskAssignment,
13190
- listReadyTasks,
13191
- reserveKanbanDispatch,
13192
- startKanbanDispatch,
13193
- updateTaskAssignment
13194
- } from "@wrongstack/kanban";
13193
+
13194
+ // src/coordination/kanban-dispatch-port.ts
13195
+ var notWired = () => {
13196
+ throw new Error(
13197
+ "Kanban dispatch port is not wired \u2014 register the implementation at the CLI composition root (see setKanbanDispatch)."
13198
+ );
13199
+ };
13200
+ var port = void 0;
13201
+ function setKanbanDispatch(impl) {
13202
+ port = impl;
13203
+ }
13204
+ function kanbanDispatch() {
13205
+ if (!port) notWired();
13206
+ return port;
13207
+ }
13195
13208
 
13196
13209
  // src/coordination/director-input-helpers.ts
13197
13210
  import { randomUUID as randomUUID8 } from "node:crypto";
@@ -13212,8 +13225,21 @@ function instantiateRosterConfig2(role, base) {
13212
13225
  };
13213
13226
  }
13214
13227
 
13228
+ // src/coordination/kanban-ops-port.ts
13229
+ var notWired2 = () => {
13230
+ throw new Error(
13231
+ "KanbanBoundaryOpsPort is not wired \u2014 register the implementation at the CLI composition root (see setKanbanBoundaryOps)."
13232
+ );
13233
+ };
13234
+ var port2 = void 0;
13235
+ function setKanbanBoundaryOps(impl) {
13236
+ port2 = impl;
13237
+ }
13238
+ function kanbanBoundaryOps() {
13239
+ return port2 ?? notWired2();
13240
+ }
13241
+
13215
13242
  // src/coordination/director-kanban-queue-helpers.ts
13216
- import { describeKanbanBoundary } from "@wrongstack/kanban";
13217
13243
  function normalizeKanbanQueueInput(input) {
13218
13244
  const raw = input ?? {};
13219
13245
  return {
@@ -13297,8 +13323,8 @@ function buildKanbanFleetTaskPrompt(board, task, lease) {
13297
13323
  task.origin.taskId ? `sourceTaskId: ${task.origin.taskId}` : ""
13298
13324
  ].filter(Boolean).join("\n") : "";
13299
13325
  const boundaries = [
13300
- board.boundary?.enabled ? `board: ${describeKanbanBoundary(board.boundary)}` : "",
13301
- task.boundary?.enabled ? `task: ${describeKanbanBoundary(task.boundary)}` : "",
13326
+ board.boundary?.enabled ? `board: ${kanbanBoundaryOps().describeKanbanBoundary(board.boundary)}` : "",
13327
+ task.boundary?.enabled ? `task: ${kanbanBoundaryOps().describeKanbanBoundary(task.boundary)}` : "",
13302
13328
  ...(board.boundary?.allow ?? []).map(
13303
13329
  (selector) => `board allow ${selector.access} ${selector.kind}:${selector.path}`
13304
13330
  ),
@@ -15231,7 +15257,7 @@ function makeKanbanQueueTool(director, roster) {
15231
15257
  i.heartbeatIntervalMs ?? Math.floor(leaseTtlMs / 2)
15232
15258
  );
15233
15259
  const effectiveLeaseTtlMs = Math.max(leaseTtlMs, heartbeatIntervalMs * 2);
15234
- const candidateTaskIds = i.taskId !== void 0 ? [i.taskId] : i.query ? (await listReadyTasks(projectRoot, {
15260
+ const candidateTaskIds = i.taskId !== void 0 ? [i.taskId] : i.query ? (await kanbanDispatch().listReadyTasks(projectRoot, {
15235
15261
  ...i.boardId !== void 0 ? { boardId: i.boardId } : {}
15236
15262
  })).filter((candidate) => matchesKanbanQueueQuery(candidate.task, i.query ?? "")).slice(0, maxTasks).map((candidate) => candidate.task.id) : void 0;
15237
15263
  const dispatches = [];
@@ -15242,7 +15268,7 @@ function makeKanbanQueueTool(director, roster) {
15242
15268
  const candidateTaskId = candidateTaskIds?.[index];
15243
15269
  if (candidateTaskIds && !candidateTaskId) break;
15244
15270
  if (candidateTaskId && budgetRejectedTaskIds.has(candidateTaskId)) continue;
15245
- const reserved = await reserveKanbanDispatch(projectRoot, {
15271
+ const reserved = await kanbanDispatch().reserveKanbanDispatch(projectRoot, {
15246
15272
  ...i.boardId !== void 0 ? { boardId: i.boardId } : {},
15247
15273
  ...candidateTaskId !== void 0 ? { taskId: candidateTaskId } : {},
15248
15274
  routing: {
@@ -15272,7 +15298,7 @@ function makeKanbanQueueTool(director, roster) {
15272
15298
  if (remaining !== void 0 && remaining < costCeiling) {
15273
15299
  budgetRejectedTaskIds.add(claim.task.id);
15274
15300
  const budgetError = `Cost ceiling ${costCeiling} exceeds remaining budget ${remaining.toFixed(4)}`;
15275
- await updateTaskAssignment(
15301
+ await kanbanDispatch().updateTaskAssignment(
15276
15302
  projectRoot,
15277
15303
  claim.board.id,
15278
15304
  claim.task.id,
@@ -15323,7 +15349,7 @@ function makeKanbanQueueTool(director, roster) {
15323
15349
  }
15324
15350
  }
15325
15351
  };
15326
- const started = await startKanbanDispatch(projectRoot, {
15352
+ const started = await kanbanDispatch().startKanbanDispatch(projectRoot, {
15327
15353
  boardId: claim.board.id,
15328
15354
  taskId: claim.task.id,
15329
15355
  leaseId: ourLeaseId,
@@ -15368,7 +15394,7 @@ function makeKanbanQueueTool(director, roster) {
15368
15394
  message = `${message}; cleanup failed for spawned subagent ${subagentId}: ${toErrorMessage(cleanupErr)}`;
15369
15395
  }
15370
15396
  }
15371
- await failKanbanDispatch(projectRoot, {
15397
+ await kanbanDispatch().failKanbanDispatch(projectRoot, {
15372
15398
  boardId: claim.board.id,
15373
15399
  taskId: claim.task.id,
15374
15400
  leaseId: ourLeaseId,
@@ -15431,7 +15457,7 @@ function makeKanbanQueueTool(director, roster) {
15431
15457
  for (const dispatch of dispatches) {
15432
15458
  if (!runTaskIds.has(dispatch.runTaskId)) continue;
15433
15459
  try {
15434
- const board = await getBoard(projectRoot, dispatch.boardId);
15460
+ const board = await kanbanDispatch().getBoard(projectRoot, dispatch.boardId);
15435
15461
  const liveTask = board?.tasks.find((t) => t.id === dispatch.taskId);
15436
15462
  const liveLeaseId = liveTask?.assignment?.leaseId;
15437
15463
  if (liveLeaseId !== void 0 && liveLeaseId !== dispatch.leaseId) {
@@ -15443,10 +15469,15 @@ function makeKanbanQueueTool(director, roster) {
15443
15469
  } catch {
15444
15470
  }
15445
15471
  try {
15446
- await heartbeatTaskAssignment(projectRoot, dispatch.boardId, dispatch.taskId, {
15447
- leaseExpiresAt: refreshedExpiry,
15448
- expectedLeaseId: dispatch.leaseId
15449
- });
15472
+ await kanbanDispatch().heartbeatTaskAssignment(
15473
+ projectRoot,
15474
+ dispatch.boardId,
15475
+ dispatch.taskId,
15476
+ {
15477
+ leaseExpiresAt: refreshedExpiry,
15478
+ expectedLeaseId: dispatch.leaseId
15479
+ }
15480
+ );
15450
15481
  } catch {
15451
15482
  }
15452
15483
  }
@@ -15469,7 +15500,7 @@ function makeKanbanQueueTool(director, roster) {
15469
15500
  if (!dispatch) continue;
15470
15501
  runTaskIds.delete(dispatch.runTaskId);
15471
15502
  if (result.status === "success") {
15472
- const completed = await completeKanbanDispatch(projectRoot, {
15503
+ const completed = await kanbanDispatch().completeKanbanDispatch(projectRoot, {
15473
15504
  boardId: dispatch.boardId,
15474
15505
  taskId: dispatch.taskId,
15475
15506
  leaseId: dispatch.leaseId,
@@ -15485,7 +15516,7 @@ function makeKanbanQueueTool(director, roster) {
15485
15516
  });
15486
15517
  }
15487
15518
  } else {
15488
- const failed = await failKanbanDispatch(projectRoot, {
15519
+ const failed = await kanbanDispatch().failKanbanDispatch(projectRoot, {
15489
15520
  boardId: dispatch.boardId,
15490
15521
  taskId: dispatch.taskId,
15491
15522
  leaseId: dispatch.leaseId,
@@ -15537,7 +15568,7 @@ async function renewAndRevokeLease(opts) {
15537
15568
  } = opts;
15538
15569
  let revoked = false;
15539
15570
  try {
15540
- const board = await getBoard(projectRoot, boardId);
15571
+ const board = await kanbanDispatch().getBoard(projectRoot, boardId);
15541
15572
  const liveTask = board?.tasks.find((t) => t.id === taskId);
15542
15573
  const liveLeaseId = liveTask?.assignment?.leaseId;
15543
15574
  if (liveLeaseId !== void 0 && liveLeaseId !== ourLeaseId) {
@@ -15550,7 +15581,7 @@ async function renewAndRevokeLease(opts) {
15550
15581
  } catch {
15551
15582
  }
15552
15583
  if (!revoked) {
15553
- await heartbeatTaskAssignment(projectRoot, boardId, taskId, {
15584
+ await kanbanDispatch().heartbeatTaskAssignment(projectRoot, boardId, taskId, {
15554
15585
  leaseExpiresAt: refreshedExpiry,
15555
15586
  expectedLeaseId: ourLeaseId
15556
15587
  }).catch(() => {
@@ -15653,12 +15684,12 @@ function rosterSummaryFromConfigs(roster) {
15653
15684
 
15654
15685
  // src/coordination/director-session.ts
15655
15686
  import * as fsp24 from "node:fs/promises";
15656
- import * as path32 from "node:path";
15687
+ import * as path33 from "node:path";
15657
15688
 
15658
15689
  // src/storage/session-store.ts
15659
15690
  import { randomUUID as randomUUID14 } from "node:crypto";
15660
15691
  import * as fsp23 from "node:fs/promises";
15661
- import * as path31 from "node:path";
15692
+ import * as path32 from "node:path";
15662
15693
 
15663
15694
  // src/session-catalog/client.ts
15664
15695
  import { spawn as spawn2 } from "node:child_process";
@@ -15736,7 +15767,7 @@ function normalize2(value) {
15736
15767
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
15737
15768
  }
15738
15769
  function delay(ms) {
15739
- return new Promise((resolve16) => setTimeout(resolve16, ms));
15770
+ return new Promise((resolve17) => setTimeout(resolve17, ms));
15740
15771
  }
15741
15772
  var SessionCatalogProjectClient = class {
15742
15773
  constructor(options) {
@@ -15826,8 +15857,8 @@ var SessionCatalogProjectClient = class {
15826
15857
  this.socket = null;
15827
15858
  this.info = null;
15828
15859
  if (socket && !socket.destroyed)
15829
- await new Promise((resolve16) => {
15830
- socket.once("close", resolve16);
15860
+ await new Promise((resolve17) => {
15861
+ socket.once("close", resolve17);
15831
15862
  socket.end();
15832
15863
  });
15833
15864
  }
@@ -15865,7 +15896,7 @@ var SessionCatalogProjectClient = class {
15865
15896
  this.info = null;
15866
15897
  this.authToken = void 0;
15867
15898
  this.buffer = "";
15868
- return new Promise((resolve16, reject) => {
15899
+ return new Promise((resolve17, reject) => {
15869
15900
  const socket = net.createConnection(this.endpoint);
15870
15901
  this.socket = socket;
15871
15902
  socket.setEncoding("utf8");
@@ -15878,7 +15909,7 @@ var SessionCatalogProjectClient = class {
15878
15909
  clearTimeout(timer);
15879
15910
  this.connectResolve = null;
15880
15911
  this.connectReject = null;
15881
- resolve16();
15912
+ resolve17();
15882
15913
  };
15883
15914
  this.connectReject = (error) => {
15884
15915
  clearTimeout(timer);
@@ -15923,7 +15954,7 @@ var SessionCatalogProjectClient = class {
15923
15954
  });
15924
15955
  if (encoded.length > SESSION_CATALOG_MAX_FRAME_CHARS)
15925
15956
  return Promise.reject(new Error("Session Catalog request exceeded frame limit"));
15926
- return new Promise((resolve16, reject) => {
15957
+ return new Promise((resolve17, reject) => {
15927
15958
  const timer = setTimeout(() => {
15928
15959
  const pending = this.pending.get(id);
15929
15960
  if (!pending) return;
@@ -15935,7 +15966,7 @@ var SessionCatalogProjectClient = class {
15935
15966
  );
15936
15967
  }, timeoutMs);
15937
15968
  timer.unref?.();
15938
- this.pending.set(id, { resolve: resolve16, reject, timer });
15969
+ this.pending.set(id, { resolve: resolve17, reject, timer });
15939
15970
  socket.write(encoded);
15940
15971
  });
15941
15972
  }
@@ -16349,6 +16380,29 @@ var SessionSummaryTracker = class {
16349
16380
  get currentSummary() {
16350
16381
  return this.summary;
16351
16382
  }
16383
+ /**
16384
+ * Non-destructive materialization of every live counter into a summary
16385
+ * snapshot. Unlike finalize(), it stamps no endedAt, applies no final
16386
+ * outcome, and resolves no name — mid-session metadata checkpoints use it
16387
+ * so a killed process leaves accurate listing metadata behind without
16388
+ * pretending the session ended cleanly.
16389
+ */
16390
+ snapshot() {
16391
+ const { lastUserMessage: _lastUserMessage, ...rest } = this.summary;
16392
+ return {
16393
+ ...rest,
16394
+ messageCount: this.messageCount,
16395
+ ...this.lastUserMessage !== void 0 ? { lastUserMessage: this.lastUserMessage } : {},
16396
+ iterationCount: this.iterationCount,
16397
+ toolCallCount: this.toolCallCount,
16398
+ toolErrorCount: this.toolErrorCount,
16399
+ fileChangeCount: this.fileChangeCount,
16400
+ compactionCount: this.compactionCount > 0 ? this.compactionCount : void 0,
16401
+ toolBreakdown: { ...this.toolBreakdown },
16402
+ lastActivityAt: this.lastActivityAt,
16403
+ ...this.outcome !== void 0 ? { outcome: this.outcome } : {}
16404
+ };
16405
+ }
16352
16406
  get pendingToolUses() {
16353
16407
  return Array.from(this.openToolUses);
16354
16408
  }
@@ -16541,6 +16595,12 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
16541
16595
  lastAppendWarnAt = 0;
16542
16596
  writeChain = Promise.resolve();
16543
16597
  flushPromise = null;
16598
+ /**
16599
+ * Batch currently inside enqueueWrite. `flushSync` may steal it if the
16600
+ * async append has not started, so a dying process writes in-flight +
16601
+ * remaining buffer as one ordered append instead of racing a second fd.
16602
+ */
16603
+ inFlight = null;
16544
16604
  get isClosed() {
16545
16605
  return false;
16546
16606
  }
@@ -16581,14 +16641,20 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
16581
16641
  return true;
16582
16642
  }
16583
16643
  enqueueWrite(data) {
16644
+ return this.enqueueFlight({ data, stolen: false, started: false });
16645
+ }
16646
+ enqueueFlight(flight) {
16584
16647
  const write = this.writeChain.then(async () => {
16648
+ if (flight.stolen) return;
16649
+ flight.started = true;
16650
+ if (flight.stolen) return;
16585
16651
  try {
16586
- return await this.opts.getHandle().appendFile(data, "utf8");
16652
+ return await this.opts.getHandle().appendFile(flight.data, "utf8");
16587
16653
  } catch (err) {
16588
16654
  if (isClosedHandleError(err)) {
16589
16655
  const reloaded = await fsp7.open(this.opts.filePath, "a", 384);
16590
16656
  this.opts.setHandle(reloaded);
16591
- return await reloaded.appendFile(data, "utf8");
16657
+ return await reloaded.appendFile(flight.data, "utf8");
16592
16658
  }
16593
16659
  throw err;
16594
16660
  }
@@ -16613,17 +16679,29 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
16613
16679
  this.flushTimer = null;
16614
16680
  }
16615
16681
  }
16616
- async flushBuffer(isClosed = false) {
16617
- if (this.flushPromise) return this.flushPromise;
16682
+ async flushBuffer(isClosed = false, opts = {}) {
16683
+ if (this.flushPromise) {
16684
+ const joined = this.flushPromise;
16685
+ const continueIfDirty = () => {
16686
+ if (this.writeBuffer.length === 0) return Promise.resolve();
16687
+ return this.flushBuffer(isClosed, opts);
16688
+ };
16689
+ if (opts.datasync === true) {
16690
+ return joined.then(
16691
+ () => this.opts.getHandle().datasync().catch(() => void 0).then(continueIfDirty)
16692
+ );
16693
+ }
16694
+ return joined.then(continueIfDirty);
16695
+ }
16618
16696
  const flush = (async () => {
16619
- while (this.writeBuffer.length > 0) await this.flushBufferOnce(isClosed);
16697
+ while (this.writeBuffer.length > 0) await this.flushBufferOnce(isClosed, opts);
16620
16698
  })().finally(() => {
16621
16699
  if (this.flushPromise === flush) this.flushPromise = null;
16622
16700
  });
16623
16701
  this.flushPromise = flush;
16624
16702
  return flush;
16625
16703
  }
16626
- async flushBufferOnce(isClosed) {
16704
+ async flushBufferOnce(isClosed, opts) {
16627
16705
  if (this.writeBuffer.length === 0) return;
16628
16706
  const events = this.writeBuffer;
16629
16707
  const eventCount = events.length;
@@ -16631,12 +16709,21 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
16631
16709
  const batch = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
16632
16710
  this.writeBuffer = [];
16633
16711
  this.writeBufferBytes = 0;
16712
+ const flight = { data: batch, stolen: false, started: false };
16713
+ this.inFlight = flight;
16634
16714
  const t0 = Date.now();
16635
16715
  let outcome = "success";
16636
16716
  let errorMsg;
16637
16717
  try {
16638
- await this.enqueueWrite(batch);
16718
+ await this.enqueueFlight(flight);
16719
+ if (flight.stolen) {
16720
+ return;
16721
+ }
16722
+ if (opts?.datasync === true) {
16723
+ await this.opts.getHandle().datasync().catch(() => void 0);
16724
+ }
16639
16725
  } catch (err) {
16726
+ if (flight.stolen) return;
16640
16727
  outcome = "failure";
16641
16728
  errorMsg = toErrorMessage(err);
16642
16729
  const newer = this.writeBuffer;
@@ -16674,6 +16761,7 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
16674
16761
  ...eventCount !== void 0 ? { eventCount } : {},
16675
16762
  ...this.opts.getTraceId?.() ? { traceId: this.opts.getTraceId() } : {}
16676
16763
  });
16764
+ if (this.inFlight === flight) this.inFlight = null;
16677
16765
  }
16678
16766
  }
16679
16767
  async drainWriteChain() {
@@ -16682,18 +16770,56 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
16682
16770
  async drainFlushPromise() {
16683
16771
  await this.flushPromise?.catch(() => void 0);
16684
16772
  }
16773
+ /**
16774
+ * Last-gasp synchronous append (SIGKILL/SIGTERM traps, `process.on('exit')`).
16775
+ *
16776
+ * Failure contract: nothing is discarded before the write is known to have
16777
+ * landed. Buffered events stay in `writeBuffer` — it is cleared only after
16778
+ * `fsyncSync` returns — and a stolen in-flight batch is handed back to the
16779
+ * async write chain, so a survivable failure (EACCES, ENOSPC, EMFILE) loses
16780
+ * nothing. Failure is never silent: a structured `session.flush_sync_failed`
16781
+ * warning names exactly what was still pending, which is what SIGKILL-trap
16782
+ * tests assert against.
16783
+ */
16685
16784
  flushSync() {
16686
- if (this.writeBuffer.length === 0 || !this.opts.filePath) return;
16785
+ if (!this.opts.filePath) return;
16687
16786
  this.cancelTimer();
16688
- const batch = this.writeBuffer.map((e) => JSON.stringify(e)).join("\n") + "\n";
16689
- this.writeBuffer = [];
16690
- this.writeBufferBytes = 0;
16787
+ const chunks = [];
16788
+ const flight = this.inFlight;
16789
+ const stole = flight !== null && !flight.started && !flight.stolen;
16790
+ if (stole && flight) {
16791
+ flight.stolen = true;
16792
+ chunks.push(flight.data);
16793
+ }
16794
+ const events = this.writeBuffer;
16795
+ if (events.length > 0) {
16796
+ chunks.push(events.map((e) => JSON.stringify(e)).join("\n") + "\n");
16797
+ }
16798
+ if (chunks.length === 0) return;
16691
16799
  let fd;
16692
16800
  try {
16693
16801
  fd = openSync(this.opts.filePath, "a");
16694
- writeSync(fd, batch, null, "utf8");
16802
+ writeSync(fd, chunks.join(""), null, "utf8");
16695
16803
  fsyncSync(fd);
16696
- } catch {
16804
+ if (this.writeBuffer === events) {
16805
+ this.writeBuffer = [];
16806
+ this.writeBufferBytes = 0;
16807
+ }
16808
+ } catch (err) {
16809
+ if (stole && flight) flight.stolen = false;
16810
+ console.warn(
16811
+ JSON.stringify({
16812
+ level: "error",
16813
+ event: "session.flush_sync_failed",
16814
+ sessionId: this.opts.sessionId,
16815
+ filePath: this.opts.filePath,
16816
+ message: toErrorMessage(err),
16817
+ pendingEvents: events.length,
16818
+ pendingBytes: this.writeBufferBytes,
16819
+ hadInFlightBatch: stole,
16820
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
16821
+ })
16822
+ );
16697
16823
  } finally {
16698
16824
  if (fd !== void 0) {
16699
16825
  try {
@@ -16861,6 +16987,17 @@ function isClosedHandleError2(err) {
16861
16987
  const code = err?.code;
16862
16988
  return code === "EBADF" || code === "ERR_CLOSED_RESOURCE" || code === "ERR_INVALID_HANDLE";
16863
16989
  }
16990
+ var CRITICAL_EVENT_TYPES = /* @__PURE__ */ new Set([
16991
+ "user_input",
16992
+ "llm_response",
16993
+ "checkpoint",
16994
+ "in_flight_start",
16995
+ "in_flight_end"
16996
+ ]);
16997
+ function isCriticalEvent(event) {
16998
+ return CRITICAL_EVENT_TYPES.has(event.type);
16999
+ }
17000
+ var METADATA_CHECKPOINT_INTERVAL_MS = 1e4;
16864
17001
  var FileSessionWriter = class _FileSessionWriter {
16865
17002
  constructor(id, handle, startedAt, meta, events, opts = {}, traceId) {
16866
17003
  this.id = id;
@@ -16876,6 +17013,8 @@ var FileSessionWriter = class _FileSessionWriter {
16876
17013
  this._onAppend = opts.onAppend;
16877
17014
  this._onAppendBatch = opts.onAppendBatch;
16878
17015
  this.onCloseCb = opts.onClose;
17016
+ this.onMetadataCheckpointCb = opts.onMetadataCheckpoint;
17017
+ this.metadataCheckpointMs = opts.metadataCheckpointMs ?? METADATA_CHECKPOINT_INTERVAL_MS;
16879
17018
  this.summaryTracker = new SessionSummaryTracker({
16880
17019
  id,
16881
17020
  startedAt,
@@ -16943,6 +17082,15 @@ var FileSessionWriter = class _FileSessionWriter {
16943
17082
  this._onAppendBatch = cb;
16944
17083
  }
16945
17084
  onCloseCb;
17085
+ /** Mid-session metadata checkpoint throttle. 0 disables checkpointing. */
17086
+ metadataCheckpointMs;
17087
+ /** One-shot guard for the "interval set but no sink" warning below. */
17088
+ _checkpointNoSinkWarned = false;
17089
+ onMetadataCheckpointCb;
17090
+ /** Set whenever summary counters changed since the last metadata checkpoint. */
17091
+ metadataDirty = false;
17092
+ metadataTimer = null;
17093
+ metadataCheckpointInFlight = null;
16946
17094
  /** Implements SessionWriter.traceId — propagated from ContextInit.traceId. */
16947
17095
  traceId;
16948
17096
  /**
@@ -16974,18 +17122,31 @@ var FileSessionWriter = class _FileSessionWriter {
16974
17122
  void this.ensureInit();
16975
17123
  const appendEvent = scrubSessionWriterEvent(event, this.secretScrubber);
16976
17124
  this.summaryTracker.observe(appendEvent);
17125
+ this.metadataDirty = true;
17126
+ this.scheduleMetadataCheckpoint();
16977
17127
  try {
16978
17128
  this._onAppend?.(appendEvent);
16979
17129
  } catch {
16980
17130
  }
17131
+ const critical = isCriticalEvent(appendEvent);
16981
17132
  if (!this.buffer.push(appendEvent)) {
16982
17133
  this.buffer.cancelTimer();
16983
- void this.buffer.flushBuffer(this.closed).catch(() => void 0).then(() => {
16984
- this.buffer.push(appendEvent);
17134
+ void this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => void 0).then(() => {
17135
+ if (this.buffer.push(appendEvent)) {
17136
+ if (!critical) return;
17137
+ this.buffer.cancelTimer();
17138
+ void this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => void 0);
17139
+ return;
17140
+ }
17141
+ void this.buffer.drainWriteChain().then(() => this.buffer.enqueueWrite(`${JSON.stringify(appendEvent)}
17142
+ `)).then(() => {
17143
+ if (!critical) return;
17144
+ return this.handle.datasync().catch(() => void 0);
17145
+ }).catch(() => void 0);
16985
17146
  });
16986
- } else if (this.buffer.shouldFlushNow()) {
17147
+ } else if (critical || this.buffer.shouldFlushNow()) {
16987
17148
  this.buffer.cancelTimer();
16988
- void this.buffer.flushBuffer(this.closed).catch(() => {
17149
+ void this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => {
16989
17150
  });
16990
17151
  } else {
16991
17152
  this.buffer.scheduleFlush(this.closed);
@@ -17056,23 +17217,112 @@ var FileSessionWriter = class _FileSessionWriter {
17056
17217
  provider: this.meta.provider ?? "unknown"
17057
17218
  });
17058
17219
  }
17220
+ /**
17221
+ * Arm the mid-session metadata checkpoint timer if it is not already armed.
17222
+ * Called after every observed event; the timer itself is unref'd so an idle
17223
+ * session never keeps the process alive for a cosmetic sidecar refresh.
17224
+ */
17225
+ scheduleMetadataCheckpoint() {
17226
+ if (this.closed || this.metadataTimer) return;
17227
+ if (this.metadataCheckpointMs <= 0) return;
17228
+ if (!this.onMetadataCheckpointCb && !this.manifestFile) {
17229
+ if (!this._checkpointNoSinkWarned) {
17230
+ this._checkpointNoSinkWarned = true;
17231
+ console.warn(
17232
+ JSON.stringify({
17233
+ level: "warn",
17234
+ event: "session.metadata_checkpoint_no_sink",
17235
+ sessionId: this.id,
17236
+ message: "metadataCheckpointMs set but neither onMetadataCheckpoint nor manifestFile configured; mid-session checkpoints disabled.",
17237
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
17238
+ })
17239
+ );
17240
+ }
17241
+ return;
17242
+ }
17243
+ this.metadataTimer = setTimeout(() => {
17244
+ this.metadataTimer = null;
17245
+ void this.runMetadataCheckpoint();
17246
+ }, this.metadataCheckpointMs);
17247
+ this.metadataTimer.unref?.();
17248
+ }
17249
+ /**
17250
+ * Persist a mid-session summary snapshot: the `.summary.json` sidecar under
17251
+ * the manifest lock, then the store-level index row / catalog upsert via
17252
+ * `onMetadataCheckpoint`. Runs at most once per throttle interval and only
17253
+ * when summary counters changed since the last checkpoint; a failed
17254
+ * checkpoint stays dirty and retries on the next armed tick.
17255
+ */
17256
+ runMetadataCheckpoint() {
17257
+ if (this.closed || !this.metadataDirty) return Promise.resolve();
17258
+ if (this.metadataCheckpointInFlight) return this.metadataCheckpointInFlight;
17259
+ const {
17260
+ endedAt: _priorEndedAt,
17261
+ outcome: _priorOutcome,
17262
+ ...snapshot
17263
+ } = this.summaryTracker.snapshot();
17264
+ const run = (async () => {
17265
+ const t0 = Date.now();
17266
+ let outcome = "success";
17267
+ let errorMsg;
17268
+ try {
17269
+ if (this.manifestFile) {
17270
+ await withFileLock(this.manifestFile, async () => {
17271
+ await atomicWrite(this.manifestFile, JSON.stringify(snapshot), { mode: 384 });
17272
+ });
17273
+ }
17274
+ this.metadataDirty = false;
17275
+ await this.onMetadataCheckpointCb?.(snapshot);
17276
+ if (this.metadataDirty && !this.closed) this.scheduleMetadataCheckpoint();
17277
+ } catch (err) {
17278
+ outcome = "failure";
17279
+ errorMsg = toErrorMessage(err);
17280
+ this.metadataDirty = true;
17281
+ this.scheduleMetadataCheckpoint();
17282
+ } finally {
17283
+ this.metadataCheckpointInFlight = null;
17284
+ this.events?.emit("storage.write", {
17285
+ sessionId: this.id,
17286
+ store: "session",
17287
+ filePath: this.manifestFile || this.filePath,
17288
+ operation: "metadata_checkpoint",
17289
+ outcome,
17290
+ durationMs: Date.now() - t0,
17291
+ ...errorMsg !== void 0 ? { error: errorMsg } : {},
17292
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
17293
+ });
17294
+ }
17295
+ })();
17296
+ this.metadataCheckpointInFlight = run;
17297
+ return run;
17298
+ }
17059
17299
  async append(event) {
17060
17300
  if (this.closed) return;
17061
17301
  await this.ensureInit();
17062
17302
  const scrubbed = scrubSessionWriterEvent(event, this.secretScrubber);
17063
17303
  this.summaryTracker.observe(scrubbed);
17304
+ this.metadataDirty = true;
17305
+ this.scheduleMetadataCheckpoint();
17064
17306
  try {
17065
17307
  this._onAppend?.(scrubbed);
17066
17308
  } catch {
17067
17309
  }
17068
- if (!this.buffer.push(scrubbed)) {
17310
+ let pushed = this.buffer.push(scrubbed);
17311
+ if (!pushed) {
17069
17312
  this.buffer.cancelTimer();
17070
- await this.buffer.flushBuffer(this.closed).catch(() => void 0);
17071
- this.buffer.push(scrubbed);
17313
+ await this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => void 0);
17314
+ pushed = this.buffer.push(scrubbed);
17315
+ if (!pushed) {
17316
+ await this.buffer.drainWriteChain().then(() => this.buffer.enqueueWrite(`${JSON.stringify(scrubbed)}
17317
+ `)).then(() => {
17318
+ if (!isCriticalEvent(scrubbed)) return;
17319
+ return this.handle.datasync().catch(() => void 0);
17320
+ }).catch(() => void 0);
17321
+ }
17072
17322
  }
17073
- if (this.buffer.shouldFlushNow()) {
17323
+ if (isCriticalEvent(scrubbed) || this.buffer.shouldFlushNow()) {
17074
17324
  this.buffer.cancelTimer();
17075
- await this.buffer.flushBuffer(this.closed).catch(() => {
17325
+ await this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => {
17076
17326
  });
17077
17327
  } else {
17078
17328
  this.buffer.scheduleFlush(this.closed);
@@ -17089,20 +17339,33 @@ var FileSessionWriter = class _FileSessionWriter {
17089
17339
  this._onAppend?.(scrubbed);
17090
17340
  } catch {
17091
17341
  }
17092
- if (!this.buffer.push(scrubbed)) {
17342
+ let pushed = this.buffer.push(scrubbed);
17343
+ if (!pushed) {
17093
17344
  this.buffer.cancelTimer();
17094
- await this.buffer.flushBuffer(this.closed).catch(() => void 0);
17095
- this.buffer.push(scrubbed);
17345
+ await this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => void 0);
17346
+ pushed = this.buffer.push(scrubbed);
17347
+ if (!pushed) {
17348
+ await this.buffer.drainWriteChain().then(() => this.buffer.enqueueWrite(`${JSON.stringify(scrubbed)}
17349
+ `)).then(() => {
17350
+ if (!isCriticalEvent(scrubbed)) return;
17351
+ return this.handle.datasync().catch(() => void 0);
17352
+ }).catch(() => void 0);
17353
+ }
17096
17354
  }
17097
17355
  scrubbedBatch.push(scrubbed);
17098
17356
  }
17357
+ if (scrubbedBatch.length > 0) {
17358
+ this.metadataDirty = true;
17359
+ this.scheduleMetadataCheckpoint();
17360
+ }
17099
17361
  try {
17100
17362
  this._onAppendBatch?.(scrubbedBatch);
17101
17363
  } catch {
17102
17364
  }
17103
- if (this.buffer.shouldFlushNow()) {
17365
+ const hasCritical = scrubbedBatch.some(isCriticalEvent);
17366
+ if (hasCritical || this.buffer.shouldFlushNow()) {
17104
17367
  this.buffer.cancelTimer();
17105
- await this.buffer.flushBuffer(this.closed).catch(() => {
17368
+ await this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => {
17106
17369
  });
17107
17370
  } else {
17108
17371
  this.buffer.scheduleFlush(this.closed);
@@ -17110,8 +17373,10 @@ var FileSessionWriter = class _FileSessionWriter {
17110
17373
  }
17111
17374
  /**
17112
17375
  * Flush buffered events to disk immediately. Critical events
17113
- * (user_input, llm_response) call this so they survive SIGKILL/crash
17114
- * instead of sitting in the in-memory buffer for up to 500ms.
17376
+ * (user_input, llm_response, checkpoint, in_flight_*) already flush
17377
+ * themselves inside append()/appendBatch(), so calling this matters for
17378
+ * non-critical tails that would otherwise sit in the in-memory buffer
17379
+ * for up to 500ms.
17115
17380
  *
17116
17381
  * Idempotent — cancels any pending timer, writes whatever has accumulated,
17117
17382
  * then asks the OS to synchronize the file data before resolving. Even an
@@ -17120,7 +17385,7 @@ var FileSessionWriter = class _FileSessionWriter {
17120
17385
  async flush() {
17121
17386
  if (this.closed) return;
17122
17387
  this.buffer.cancelTimer();
17123
- await this.buffer.flushBuffer(this.closed);
17388
+ await this.buffer.flushBuffer(this.closed, { datasync: true });
17124
17389
  await this.buffer.drainWriteChain();
17125
17390
  try {
17126
17391
  await this.handle.datasync();
@@ -17136,23 +17401,35 @@ var FileSessionWriter = class _FileSessionWriter {
17136
17401
  * Last-gasp synchronous drain for hard-exit paths (process.exit after
17137
17402
  * rapid Ctrl+C). The async write chain cannot be awaited when the process
17138
17403
  * is about to die, but whatever still sits in the in-memory buffer CAN be
17139
- * saved with a blocking append. Best-effort: an in-flight async write may
17140
- * be cut off by the exit regardless; errors here are swallowed.
17404
+ * saved with a blocking append. A failed sync append leaves the buffer
17405
+ * intact so a subsequent close()/flush() can retry. An in-flight async
17406
+ * write may still be cut off by a hard exit.
17141
17407
  */
17142
17408
  flushSync() {
17143
17409
  this.buffer.flushSync();
17144
17410
  }
17145
17411
  async close() {
17146
17412
  if (this.closePromise) return this.closePromise;
17147
- this.closePromise = this.doClose().catch((err) => {
17413
+ this.closePromise = this.doClose().catch(async (err) => {
17414
+ if (this.metadataTimer) {
17415
+ clearTimeout(this.metadataTimer);
17416
+ this.metadataTimer = null;
17417
+ }
17418
+ await this.metadataCheckpointInFlight?.catch(() => void 0);
17148
17419
  this.closed = false;
17149
17420
  this.closePromise = null;
17150
17421
  if (this.buffer.length > 0) this.buffer.scheduleFlush(this.closed);
17422
+ if (this.metadataDirty) this.scheduleMetadataCheckpoint();
17151
17423
  throw err;
17152
17424
  });
17153
17425
  return this.closePromise;
17154
17426
  }
17155
17427
  async doClose() {
17428
+ if (this.metadataTimer) {
17429
+ clearTimeout(this.metadataTimer);
17430
+ this.metadataTimer = null;
17431
+ }
17432
+ await this.metadataCheckpointInFlight?.catch(() => void 0);
17156
17433
  await this.ensureInit();
17157
17434
  if (this.pendingFileSnapshots.length > 0) {
17158
17435
  await this.writeFileSnapshot(this.activePromptIndex ?? 0, [...this.pendingFileSnapshots]);
@@ -17160,8 +17437,13 @@ var FileSessionWriter = class _FileSessionWriter {
17160
17437
  this.pendingFileSnapshotBytes = 0;
17161
17438
  }
17162
17439
  this.closed = true;
17440
+ if (this.metadataTimer) {
17441
+ clearTimeout(this.metadataTimer);
17442
+ this.metadataTimer = null;
17443
+ }
17444
+ await this.metadataCheckpointInFlight?.catch(() => void 0);
17163
17445
  this.buffer.cancelTimer();
17164
- await this.buffer.flushBuffer(this.closed);
17446
+ await this.buffer.flushBuffer(this.closed, { datasync: true });
17165
17447
  await this.buffer.drainWriteChain();
17166
17448
  try {
17167
17449
  await this.handle.datasync();
@@ -17276,10 +17558,23 @@ var FileSessionWriter = class _FileSessionWriter {
17276
17558
  async truncateToCheckpoint(targetPromptIndex, revertedFiles = []) {
17277
17559
  if (!this.filePath) return 0;
17278
17560
  this.buffer.cancelTimer();
17279
- await this.buffer.flushBuffer(this.closed);
17561
+ await this.buffer.flushBuffer(this.closed, { datasync: true });
17280
17562
  await this.buffer.drainWriteChain();
17281
- const plan = await findSessionCheckpointTruncatePlan(this.filePath, targetPromptIndex);
17282
- if (!plan) return 0;
17563
+ if (this.metadataTimer) {
17564
+ clearTimeout(this.metadataTimer);
17565
+ this.metadataTimer = null;
17566
+ }
17567
+ await this.metadataCheckpointInFlight?.catch(() => void 0);
17568
+ const plan = await findSessionCheckpointTruncatePlan(this.filePath, targetPromptIndex).catch(
17569
+ (err) => {
17570
+ this.scheduleMetadataCheckpoint();
17571
+ throw err;
17572
+ }
17573
+ );
17574
+ if (!plan) {
17575
+ this.scheduleMetadataCheckpoint();
17576
+ return 0;
17577
+ }
17283
17578
  await this.buffer.drainWriteChain();
17284
17579
  try {
17285
17580
  await this.handle.close();
@@ -17290,6 +17585,7 @@ var FileSessionWriter = class _FileSessionWriter {
17290
17585
  this.handle = await fsp9.open(this.filePath, "a", 384);
17291
17586
  } catch (err) {
17292
17587
  this.handle = await fsp9.open(this.filePath, "a", 384).catch(() => this.handle);
17588
+ this.scheduleMetadataCheckpoint();
17293
17589
  throw err;
17294
17590
  }
17295
17591
  await this.summaryTracker.recomputeFromDisk(this.filePath);
@@ -17315,6 +17611,11 @@ var FileSessionWriter = class _FileSessionWriter {
17315
17611
  await this.buffer.drainFlushPromise();
17316
17612
  this.buffer.clear();
17317
17613
  await this.buffer.drainWriteChain();
17614
+ if (this.metadataTimer) {
17615
+ clearTimeout(this.metadataTimer);
17616
+ this.metadataTimer = null;
17617
+ }
17618
+ await this.metadataCheckpointInFlight?.catch(() => void 0);
17318
17619
  const resetAt = (/* @__PURE__ */ new Date()).toISOString();
17319
17620
  const record = `${JSON.stringify({
17320
17621
  type: "session_start",
@@ -17325,8 +17626,10 @@ var FileSessionWriter = class _FileSessionWriter {
17325
17626
  })}
17326
17627
  `;
17327
17628
  await this.handle.close();
17328
- await fsp9.writeFile(this.filePath, record, "utf8");
17629
+ await atomicWrite(this.filePath, record, { mode: 384 });
17329
17630
  this.summaryTracker.reset(resetAt);
17631
+ this.metadataDirty = true;
17632
+ this.scheduleMetadataCheckpoint();
17330
17633
  this.activePromptIndex = null;
17331
17634
  this.pendingFileSnapshots = [];
17332
17635
  this.pendingFileSnapshotBytes = 0;
@@ -17717,7 +18020,7 @@ var SessionCheckpointCas = class {
17717
18020
  }
17718
18021
  };
17719
18022
  function defaultRunGit(args, cwd) {
17720
- return new Promise((resolve16) => {
18023
+ return new Promise((resolve17) => {
17721
18024
  const stdoutChunks = [];
17722
18025
  const stderrChunks = [];
17723
18026
  let stdoutBytes = 0;
@@ -17760,8 +18063,8 @@ function defaultRunGit(args, cwd) {
17760
18063
  stdoutTruncated,
17761
18064
  stderrTruncated
17762
18065
  });
17763
- child.on("error", (err) => resolve16(result(1, err.message)));
17764
- child.on("close", (code) => resolve16(result(code ?? 1)));
18066
+ child.on("error", (err) => resolve17(result(1, err.message)));
18067
+ child.on("close", (code) => resolve17(result(code ?? 1)));
17765
18068
  });
17766
18069
  }
17767
18070
 
@@ -18575,7 +18878,7 @@ async function executeRenameSession(params) {
18575
18878
 
18576
18879
  // src/storage/session-store/resume-session.ts
18577
18880
  import * as fsp16 from "node:fs/promises";
18578
- import * as path30 from "node:path";
18881
+ import * as path31 from "node:path";
18579
18882
 
18580
18883
  // src/storage/session-resume-validation.ts
18581
18884
  import { createHash as createHash5 } from "node:crypto";
@@ -18671,8 +18974,22 @@ async function validateResumeFileObservations(events, projectRoot) {
18671
18974
  }
18672
18975
  var RESUME_NOTICE_HEADERS = [
18673
18976
  "[SESSION RESUME FILE VALIDATION]",
18674
- "[SESSION RESUME INTERRUPTED WORK]"
18977
+ "[SESSION RESUME INTERRUPTED WORK]",
18978
+ "[SESSION RESUME CRASH RECOVERY]"
18675
18979
  ];
18980
+ function formatCrashRecoveryNotice(interruptedTools, lastContext) {
18981
+ if (interruptedTools.length === 0) return null;
18982
+ const plural = interruptedTools.length === 1 ? "call was" : "calls were";
18983
+ const lines = [
18984
+ "[SESSION RESUME CRASH RECOVERY]",
18985
+ `The previous run stopped mid-iteration${lastContext ? ` while: ${lastContext}` : ""} \u2014 ${interruptedTools.length} tool ${plural} left without a recorded result.`,
18986
+ "Those interrupted tool calls were removed from the restored conversation and were NOT re-executed; their workspace side effects were NOT rolled back."
18987
+ ];
18988
+ for (const tool of interruptedTools.slice(0, NOTICE_PATH_LIMIT)) {
18989
+ lines.push(`- ${tool.name}${tool.argsSummary ? ` (${tool.argsSummary})` : ""}`);
18990
+ }
18991
+ return lines.join("\n");
18992
+ }
18676
18993
  function isResumeNoticeMessage(message) {
18677
18994
  if (message.role !== "system" || typeof message.content !== "string") return false;
18678
18995
  return RESUME_NOTICE_HEADERS.some((header) => message.content === header || message.content.startsWith(`${header}
@@ -18705,9 +19022,345 @@ function formatInterruptedToolNotice(pendingToolUseCount) {
18705
19022
  ].join("\n");
18706
19023
  }
18707
19024
 
18708
- // src/storage/session-store/summary-builder.ts
19025
+ // src/storage/session-recovery.ts
18709
19026
  import { createReadStream as createReadStream4 } from "node:fs";
19027
+ import * as fs4 from "node:fs/promises";
19028
+ import * as path30 from "node:path";
18710
19029
  import { createInterface as createInterface4 } from "node:readline";
19030
+ function extractInterruptedTools(plan) {
19031
+ const tools = [];
19032
+ const openCalls = /* @__PURE__ */ new Map();
19033
+ let anonymousSeq = 0;
19034
+ for (const ev of plan.pendingEvents) {
19035
+ if ((ev.type === "tool_use" || ev.type === "tool_call_start") && typeof ev.name === "string") {
19036
+ const toolName = ev.name;
19037
+ const rawId = ev.id;
19038
+ const callId = rawId ?? toolName;
19039
+ openCalls.set(callId, {
19040
+ id: rawId,
19041
+ name: toolName,
19042
+ args: ev.input ?? ev.args,
19043
+ ts: ev.ts
19044
+ });
19045
+ } else if (ev.type === "tool_result" || ev.type === "tool_call_end") {
19046
+ const callId = ev.id ?? ev.toolUseId ?? ev.name;
19047
+ if (callId) openCalls.delete(callId);
19048
+ } else if (ev.type === "llm_response" && Array.isArray(ev.content)) {
19049
+ for (const block of ev.content) {
19050
+ if (block && block.type === "tool_use" && typeof block.name === "string") {
19051
+ const rawId = block.id;
19052
+ const callId = rawId ?? `${block.name}#${++anonymousSeq}`;
19053
+ openCalls.set(callId, {
19054
+ id: rawId,
19055
+ name: block.name,
19056
+ args: block.input,
19057
+ ts: ev.ts
19058
+ });
19059
+ }
19060
+ }
19061
+ } else if (ev.type === "message_appended" && ev.message) {
19062
+ const msg = ev.message;
19063
+ if (Array.isArray(msg.content)) {
19064
+ for (const block of msg.content) {
19065
+ if (block && block.type === "tool_use" && typeof block.name === "string") {
19066
+ const rawId = block.id;
19067
+ const callId = rawId ?? `${block.name}#${++anonymousSeq}`;
19068
+ openCalls.set(callId, {
19069
+ id: rawId,
19070
+ name: block.name,
19071
+ args: block.input,
19072
+ ts: ev.ts
19073
+ });
19074
+ } else if (block && block.type === "tool_result") {
19075
+ const callId = block.tool_use_id ?? block.id;
19076
+ if (callId) openCalls.delete(callId);
19077
+ }
19078
+ }
19079
+ }
19080
+ }
19081
+ }
19082
+ for (const call of openCalls.values()) {
19083
+ let argsSummary;
19084
+ if (call.args && typeof call.args === "object") {
19085
+ try {
19086
+ const str = JSON.stringify(call.args);
19087
+ argsSummary = str.length > 80 ? `${str.slice(0, 77)}...` : str;
19088
+ } catch {
19089
+ }
19090
+ }
19091
+ tools.push({
19092
+ id: call.id,
19093
+ name: call.name,
19094
+ argsSummary,
19095
+ ts: call.ts
19096
+ });
19097
+ }
19098
+ return tools;
19099
+ }
19100
+ var SessionRecovery = class _SessionRecovery {
19101
+ constructor(dir) {
19102
+ this.dir = dir;
19103
+ }
19104
+ dir;
19105
+ static MAX_PENDING_EVENTS = 1e4;
19106
+ static MAX_PENDING_BYTES = 16 * 1024 * 1024;
19107
+ /**
19108
+ * Build a recovery plan from ALREADY-LOADED events without touching disk.
19109
+ * executeResumeSession uses this because load() has already paid for the
19110
+ * transcript read; recover()'s file scan would duplicate it.
19111
+ */
19112
+ static buildRecoveryPlan(events, sessionId) {
19113
+ const pendingEvents = [];
19114
+ const pendingSizes = [];
19115
+ let pendingBytes = 0;
19116
+ let lastCheckpoint = null;
19117
+ let latestBoundary = null;
19118
+ for (const event of events) {
19119
+ if (!event || typeof event !== "object" || typeof event.type !== "string") continue;
19120
+ if (event.type === "checkpoint") {
19121
+ lastCheckpoint = event;
19122
+ pendingEvents.length = 0;
19123
+ pendingSizes.length = 0;
19124
+ pendingBytes = 0;
19125
+ continue;
19126
+ }
19127
+ if (isLifecycleBoundary(event)) latestBoundary = event;
19128
+ const bytes = Buffer.byteLength(JSON.stringify(event), "utf8");
19129
+ if (bytes > _SessionRecovery.MAX_PENDING_BYTES) continue;
19130
+ while (pendingEvents.length >= _SessionRecovery.MAX_PENDING_EVENTS || pendingBytes + bytes > _SessionRecovery.MAX_PENDING_BYTES) {
19131
+ pendingBytes -= pendingSizes.shift();
19132
+ pendingEvents.shift();
19133
+ }
19134
+ pendingEvents.push(event);
19135
+ pendingSizes.push(bytes);
19136
+ pendingBytes += bytes;
19137
+ }
19138
+ const inFlightStart = latestBoundary?.type === "in_flight_start" ? latestBoundary : null;
19139
+ return {
19140
+ sessionId,
19141
+ stale: inFlightStart !== null,
19142
+ lastCheckpoint,
19143
+ pendingEvents,
19144
+ inFlightStart,
19145
+ context: inFlightStart?.context ?? null
19146
+ };
19147
+ }
19148
+ /**
19149
+ * Scan a session log and return a `StaleSession` if and only if the newest
19150
+ * lifecycle boundary is an `in_flight_start` without a later
19151
+ * `in_flight_end`/`session_end`. Ordinary provider/tool events after the
19152
+ * marker do not make the session clean. Returns `null` when:
19153
+ * - the log does not exist;
19154
+ * - the log is empty;
19155
+ * - the latest lifecycle boundary is `in_flight_end` or `session_end`;
19156
+ * - there is no lifecycle boundary (legacy/pre-marker log).
19157
+ *
19158
+ * The reverse scanner is chunked and line-aware. It can cross arbitrarily
19159
+ * large JSONL records (for example a large tool result) without imposing a
19160
+ * fixed tail-size correctness limit. Clean logs normally return after the
19161
+ * first chunk; stale logs continue counting lines so `eventCount` remains
19162
+ * the documented total rather than a tail-only approximation.
19163
+ */
19164
+ async resolveId(query) {
19165
+ const resolution = resolveSessionId(query, await collectSessionIds(this.dir));
19166
+ if (resolution.status === "resolved") return resolution.id;
19167
+ if (resolution.status === "missing") return resolution.query;
19168
+ throw sessionIdResolutionError(resolution);
19169
+ }
19170
+ async detectStale(sessionId) {
19171
+ const canonicalId = await this.resolveId(sessionId);
19172
+ return this.detectStaleExact(canonicalId);
19173
+ }
19174
+ async detectStaleExact(sessionId) {
19175
+ const fp = this.filePath(sessionId);
19176
+ let stat13;
19177
+ try {
19178
+ stat13 = await fs4.stat(fp);
19179
+ } catch {
19180
+ return null;
19181
+ }
19182
+ if (stat13.size === 0) return null;
19183
+ try {
19184
+ const scan = await scanLatestLifecycleBoundary(fp, stat13.size);
19185
+ if (scan?.boundary.type !== "in_flight_start") return null;
19186
+ return {
19187
+ sessionId,
19188
+ path: fp,
19189
+ lastEventTs: scan.boundary.ts,
19190
+ context: scan.boundary.context,
19191
+ eventCount: scan.eventCount
19192
+ };
19193
+ } catch {
19194
+ return null;
19195
+ }
19196
+ }
19197
+ /**
19198
+ * Generate a recovery plan for a session. The plan describes
19199
+ * the persisted tail after the last checkpoint, plus the dangling in-flight
19200
+ * marker if present. SessionStore.resume() independently reconstructs state
19201
+ * from the journal and does not re-run these events as commands.
19202
+ *
19203
+ * Returns a non-null plan for ANY session that has at least
19204
+ * one event after a checkpoint (or, for legacy sessions, at
19205
+ * least one event). Pure read; no mutation.
19206
+ */
19207
+ async recover(sessionId) {
19208
+ const canonicalId = await this.resolveId(sessionId);
19209
+ const fp = this.filePath(canonicalId);
19210
+ const pendingEvents = [];
19211
+ const pendingSizes = [];
19212
+ let pendingBytes = 0;
19213
+ let lastCheckpoint = null;
19214
+ let latestBoundary = null;
19215
+ let sawEvent = false;
19216
+ const stream = createReadStream4(fp, { encoding: "utf8" });
19217
+ const lines = createInterface4({ input: stream, crlfDelay: Infinity });
19218
+ try {
19219
+ for await (const line of lines) {
19220
+ if (!line.trim()) continue;
19221
+ let event;
19222
+ try {
19223
+ event = JSON.parse(line);
19224
+ } catch {
19225
+ continue;
19226
+ }
19227
+ if (!event || typeof event !== "object" || typeof event.type !== "string") continue;
19228
+ sawEvent = true;
19229
+ if (event.type === "checkpoint") {
19230
+ lastCheckpoint = event;
19231
+ pendingEvents.length = 0;
19232
+ pendingSizes.length = 0;
19233
+ pendingBytes = 0;
19234
+ } else {
19235
+ const bytes = Buffer.byteLength(line, "utf8");
19236
+ if (bytes <= _SessionRecovery.MAX_PENDING_BYTES) {
19237
+ while (pendingEvents.length >= _SessionRecovery.MAX_PENDING_EVENTS || pendingBytes + bytes > _SessionRecovery.MAX_PENDING_BYTES) {
19238
+ pendingEvents.shift();
19239
+ pendingBytes = Math.max(0, pendingBytes - pendingSizes.shift());
19240
+ }
19241
+ pendingEvents.push(event);
19242
+ pendingSizes.push(bytes);
19243
+ pendingBytes += bytes;
19244
+ }
19245
+ }
19246
+ if (isLifecycleBoundary(event)) latestBoundary = event;
19247
+ }
19248
+ } catch {
19249
+ return null;
19250
+ } finally {
19251
+ lines.close();
19252
+ stream.close();
19253
+ }
19254
+ if (!sawEvent) return null;
19255
+ const inFlightStart = latestBoundary?.type === "in_flight_start" ? latestBoundary : null;
19256
+ const context = inFlightStart && inFlightStart.type === "in_flight_start" ? inFlightStart.context : null;
19257
+ return {
19258
+ sessionId: canonicalId,
19259
+ stale: inFlightStart !== null,
19260
+ lastCheckpoint,
19261
+ pendingEvents,
19262
+ inFlightStart,
19263
+ context
19264
+ };
19265
+ }
19266
+ /**
19267
+ * List every stale session in a directory. Returns an array
19268
+ * (possibly empty) sorted by `lastEventTs` descending — most
19269
+ * recent crash first.
19270
+ */
19271
+ async listResumable() {
19272
+ const out = [];
19273
+ const collect = async (dir, prefix, depth) => {
19274
+ let entries;
19275
+ try {
19276
+ entries = await fs4.readdir(dir, { withFileTypes: true });
19277
+ } catch {
19278
+ return;
19279
+ }
19280
+ for (const entry of entries) {
19281
+ if (entry.name.startsWith(".")) continue;
19282
+ if (entry.name === "shared" || entry.name === "subagents" || entry.name === "attachments")
19283
+ continue;
19284
+ if (entry.isDirectory()) {
19285
+ if (depth === 0) {
19286
+ await collect(path30.join(dir, entry.name), entry.name, depth + 1);
19287
+ }
19288
+ continue;
19289
+ }
19290
+ if (!entry.isFile() || !isSessionTranscriptFileName(entry.name)) continue;
19291
+ const base = entry.name.slice(0, -".jsonl".length);
19292
+ const sessionId = prefix ? `${prefix}/${base}` : base;
19293
+ const stale = await this.detectStaleExact(sessionId);
19294
+ if (stale) out.push(stale);
19295
+ }
19296
+ };
19297
+ await collect(this.dir, "", 0);
19298
+ return out.sort((a, b) => b.lastEventTs.localeCompare(a.lastEventTs));
19299
+ }
19300
+ // ── Internals ──────────────────────────────────────────────────────────
19301
+ filePath(sessionId) {
19302
+ return sessionScopedPath(this.dir, sessionId, ".jsonl");
19303
+ }
19304
+ };
19305
+ function isLifecycleBoundary(event) {
19306
+ return event.type === "in_flight_start" || event.type === "in_flight_end" || event.type === "session_end";
19307
+ }
19308
+ function parseLifecycleBoundary(line) {
19309
+ try {
19310
+ const parsed = JSON.parse(line.toString("utf8"));
19311
+ return isLifecycleBoundary(parsed) ? parsed : null;
19312
+ } catch {
19313
+ return null;
19314
+ }
19315
+ }
19316
+ function hasNonWhitespace(line) {
19317
+ for (const byte of line) {
19318
+ if (byte !== 9 && byte !== 10 && byte !== 13 && byte !== 32) return true;
19319
+ }
19320
+ return false;
19321
+ }
19322
+ async function scanLatestLifecycleBoundary(filePath, size) {
19323
+ const CHUNK_SIZE2 = 64 * 1024;
19324
+ const handle = await fs4.open(filePath, "r");
19325
+ let position = size;
19326
+ let laterLineFragment = Buffer.alloc(0);
19327
+ let latestBoundary = null;
19328
+ let eventCount = 0;
19329
+ const observeLine = (line) => {
19330
+ if (!hasNonWhitespace(line)) return null;
19331
+ eventCount++;
19332
+ return parseLifecycleBoundary(line);
19333
+ };
19334
+ try {
19335
+ while (position > 0) {
19336
+ const length = Math.min(CHUNK_SIZE2, position);
19337
+ position -= length;
19338
+ const chunk = Buffer.allocUnsafe(length);
19339
+ const { bytesRead } = await handle.read(chunk, 0, length, position);
19340
+ const data = Buffer.concat([chunk.subarray(0, bytesRead), laterLineFragment]);
19341
+ let lineEnd = data.length;
19342
+ for (let i = data.length - 1; i >= 0; i--) {
19343
+ if (data[i] !== 10) continue;
19344
+ const boundary2 = observeLine(data.subarray(i + 1, lineEnd));
19345
+ if (!latestBoundary && boundary2) latestBoundary = boundary2;
19346
+ lineEnd = i;
19347
+ }
19348
+ laterLineFragment = data.subarray(0, lineEnd);
19349
+ if (latestBoundary && latestBoundary.type !== "in_flight_start") {
19350
+ return { boundary: latestBoundary, eventCount };
19351
+ }
19352
+ }
19353
+ const boundary = observeLine(laterLineFragment);
19354
+ if (!latestBoundary && boundary) latestBoundary = boundary;
19355
+ return latestBoundary ? { boundary: latestBoundary, eventCount } : null;
19356
+ } finally {
19357
+ await handle.close();
19358
+ }
19359
+ }
19360
+
19361
+ // src/storage/session-store/summary-builder.ts
19362
+ import { createReadStream as createReadStream5 } from "node:fs";
19363
+ import { createInterface as createInterface5 } from "node:readline";
18711
19364
  async function summarizeSessionFile(opts) {
18712
19365
  return summarizeSessionEventSequence({
18713
19366
  id: opts.id,
@@ -18841,8 +19494,8 @@ async function summarizeSessionEventSequence(opts) {
18841
19494
  }
18842
19495
  }
18843
19496
  async function* iterateSessionEvents(file, secretScrubber) {
18844
- const stream = createReadStream4(file, { encoding: "utf8" });
18845
- const lines = createInterface4({ input: stream, crlfDelay: Infinity });
19497
+ const stream = createReadStream5(file, { encoding: "utf8" });
19498
+ const lines = createInterface5({ input: stream, crlfDelay: Infinity });
18846
19499
  try {
18847
19500
  for await (const line of lines) {
18848
19501
  if (!line.trim()) continue;
@@ -18875,7 +19528,8 @@ async function executeResumeSession(params) {
18875
19528
  readSummaryManifest,
18876
19529
  searchEvents,
18877
19530
  persistCatalogSummary,
18878
- logWarn
19531
+ logWarn,
19532
+ sessionsDir
18879
19533
  } = params;
18880
19534
  const t0 = Date.now();
18881
19535
  const data = await load(canonicalId);
@@ -18896,6 +19550,24 @@ async function executeResumeSession(params) {
18896
19550
  ...derivedSummary,
18897
19551
  ...persistedSummary?.name !== void 0 ? { name: persistedSummary.name } : {}
18898
19552
  };
19553
+ let recoveryPlan = SessionRecovery.buildRecoveryPlan(data.events, canonicalId);
19554
+ if (eventsDropped > 0 && sessionsDir) {
19555
+ const fromDisk = await new SessionRecovery(sessionsDir).recover(canonicalId);
19556
+ if (fromDisk) recoveryPlan = fromDisk;
19557
+ }
19558
+ const interruptedTools = recoveryPlan.stale ? extractInterruptedTools(recoveryPlan) : [];
19559
+ const synthesizedResults = interruptedTools.flatMap(
19560
+ (tool) => typeof tool.id === "string" ? [
19561
+ {
19562
+ type: "tool_result",
19563
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
19564
+ id: tool.id,
19565
+ content: "[interrupted] No result was recorded \u2014 the previous process stopped before this call completed. Re-run it if still needed.",
19566
+ isError: true
19567
+ }
19568
+ ] : []
19569
+ );
19570
+ if (synthesizedResults.length > 0) data.events.push(...synthesizedResults);
18899
19571
  const noticeMessages = [];
18900
19572
  let resumeValidation;
18901
19573
  if (projectRoot) {
@@ -18924,7 +19596,7 @@ async function executeResumeSession(params) {
18924
19596
  );
18925
19597
  }
18926
19598
  }
18927
- const interruptedNotice = formatInterruptedToolNotice(data.pendingToolUseCount ?? 0);
19599
+ const interruptedNotice = recoveryPlan.stale ? null : formatInterruptedToolNotice(data.pendingToolUseCount ?? 0);
18928
19600
  if (interruptedNotice) {
18929
19601
  noticeMessages.push({
18930
19602
  role: "system",
@@ -18932,6 +19604,14 @@ async function executeResumeSession(params) {
18932
19604
  ts: (/* @__PURE__ */ new Date()).toISOString()
18933
19605
  });
18934
19606
  }
19607
+ const crashNotice = formatCrashRecoveryNotice(interruptedTools, recoveryPlan.context);
19608
+ if (crashNotice) {
19609
+ noticeMessages.push({
19610
+ role: "system",
19611
+ content: crashNotice,
19612
+ ts: (/* @__PURE__ */ new Date()).toISOString()
19613
+ });
19614
+ }
18935
19615
  const carriedMessages = data.messages.filter((message) => !isResumeNoticeMessage(message));
18936
19616
  const resumedData = {
18937
19617
  ...data,
@@ -18962,7 +19642,7 @@ async function executeResumeSession(params) {
18962
19642
  {
18963
19643
  resumed: true,
18964
19644
  initialSummary,
18965
- dir: path30.dirname(file),
19645
+ dir: path31.dirname(file),
18966
19646
  filePath: file,
18967
19647
  secretScrubber,
18968
19648
  checkpointCas,
@@ -18973,9 +19653,21 @@ async function executeResumeSession(params) {
18973
19653
  if (!current) return null;
18974
19654
  return current.name === void 0 ? {} : { name: sessionContentText(secretScrubber.scrub(current.name)) };
18975
19655
  },
18976
- onClose: (s) => persistCatalogSummary(s)
19656
+ onClose: (s) => persistCatalogSummary(s),
19657
+ // Resumed sessions checkpoint their index/catalog metadata mid-flight
19658
+ // too, so a kill during a resumed session still leaves fresh listing
19659
+ // state behind.
19660
+ onMetadataCheckpoint: (s) => persistCatalogSummary(s)
18977
19661
  }
18978
19662
  );
19663
+ if (synthesizedResults.length > 0) {
19664
+ await writer.appendBatch(synthesizedResults);
19665
+ await writer.flush();
19666
+ }
19667
+ if (recoveryPlan.stale) {
19668
+ await writer.clearInFlightMarker("recovered");
19669
+ await writer.flush();
19670
+ }
18979
19671
  emitSessionStoreWrite(events, canonicalId, file, "resume", "success", Date.now() - t0);
18980
19672
  return { writer, data: resumedData };
18981
19673
  } catch (err) {
@@ -19194,6 +19886,11 @@ function applySessionIndexLines(raw, byId, deleted) {
19194
19886
  byId.delete(entry.id);
19195
19887
  continue;
19196
19888
  }
19889
+ if (entry.action === "create" && entry.id) {
19890
+ deleted.delete(entry.id);
19891
+ byId.delete(entry.id);
19892
+ continue;
19893
+ }
19197
19894
  if (entry.id && !deleted.has(entry.id)) {
19198
19895
  byId.set(entry.id, entry);
19199
19896
  }
@@ -19238,6 +19935,7 @@ async function readFileRange(file, start, end) {
19238
19935
 
19239
19936
  // src/storage/session-store/session-store-index.ts
19240
19937
  var COMPACT_EVERY = 30;
19938
+ var NO_DELETED_IDS = /* @__PURE__ */ new Set();
19241
19939
  async function appendToIndexStrict(dir, indexFile, summary, invalidateShard, onAppended, compactInner) {
19242
19940
  await ensureDir(dir);
19243
19941
  let shouldCompact = false;
@@ -19270,10 +19968,14 @@ async function readIndexFile(indexFile, currentCache) {
19270
19968
  const s = await fsp20.stat(indexFile);
19271
19969
  stat13 = { mtimeMs: s.mtimeMs, size: s.size, ino: s.ino, birthtimeMs: s.birthtimeMs };
19272
19970
  } catch {
19273
- return { summaries: [], cache: null };
19971
+ return { summaries: [], deletedIds: NO_DELETED_IDS, cache: null };
19274
19972
  }
19275
19973
  if (currentCache !== null && currentCache.mtimeMs === stat13.mtimeMs && currentCache.size === stat13.size && currentCache.ino === stat13.ino && currentCache.birthtimeMs === stat13.birthtimeMs) {
19276
- return { summaries: currentCache.summaries, cache: currentCache };
19974
+ return {
19975
+ summaries: currentCache.summaries,
19976
+ deletedIds: currentCache.deleted,
19977
+ cache: currentCache
19978
+ };
19277
19979
  }
19278
19980
  const cached = currentCache;
19279
19981
  const sameFile = cached !== null && cached.ino === stat13.ino && cached.birthtimeMs === stat13.birthtimeMs;
@@ -19289,14 +19991,14 @@ async function readIndexFile(indexFile, currentCache) {
19289
19991
  byId: cached.byId,
19290
19992
  deleted: cached.deleted
19291
19993
  };
19292
- return { summaries: summaries2, cache: nextCache2 };
19994
+ return { summaries: summaries2, deletedIds: cached.deleted, cache: nextCache2 };
19293
19995
  }
19294
19996
  }
19295
19997
  let raw;
19296
19998
  try {
19297
19999
  raw = await fsp20.readFile(indexFile, "utf8");
19298
20000
  } catch {
19299
- return { summaries: [], cache: null };
20001
+ return { summaries: [], deletedIds: NO_DELETED_IDS, cache: null };
19300
20002
  }
19301
20003
  const deleted = /* @__PURE__ */ new Set();
19302
20004
  const byId = /* @__PURE__ */ new Map();
@@ -19304,7 +20006,18 @@ async function readIndexFile(indexFile, currentCache) {
19304
20006
  const summaries = Array.from(byId.values());
19305
20007
  summaries.sort(compareSessionSummaries);
19306
20008
  const nextCache = { ...stat13, summaries, byId, deleted };
19307
- return { summaries, cache: nextCache };
20009
+ return { summaries, deletedIds: deleted, cache: nextCache };
20010
+ }
20011
+ async function compactIndexInner(indexFile, entries, deletedIds) {
20012
+ const parts = entries.map((s) => JSON.stringify(s));
20013
+ if (deletedIds) {
20014
+ for (const id of deletedIds) {
20015
+ parts.push(JSON.stringify({ action: "delete", id }));
20016
+ }
20017
+ }
20018
+ if (parts.length === 0) return;
20019
+ const lines = parts.join("\n") + "\n";
20020
+ await atomicWrite(indexFile, lines, { mode: 384 });
19308
20021
  }
19309
20022
 
19310
20023
  // src/storage/session-store/shard-manifest.ts
@@ -19342,12 +20055,12 @@ async function readOrBuildShardManifestEntry(opts) {
19342
20055
  }
19343
20056
 
19344
20057
  // src/storage/session-store/strict-empty-check.ts
19345
- import { createReadStream as createReadStream5 } from "node:fs";
19346
- import { createInterface as createInterface5 } from "node:readline";
20058
+ import { createReadStream as createReadStream6 } from "node:fs";
20059
+ import { createInterface as createInterface6 } from "node:readline";
19347
20060
  var EMPTY_SESSION_EVENT_TYPES = /* @__PURE__ */ new Set(["session_start", "session_resumed", "session_end"]);
19348
20061
  async function isStrictlyEmptySessionFile(file) {
19349
- const input = createReadStream5(file, { encoding: "utf8" });
19350
- const lines = createInterface5({ input, crlfDelay: Infinity });
20062
+ const input = createReadStream6(file, { encoding: "utf8" });
20063
+ const lines = createInterface6({ input, crlfDelay: Infinity });
19351
20064
  let sawSessionStart = false;
19352
20065
  try {
19353
20066
  for await (const line of lines) {
@@ -19423,6 +20136,7 @@ function damagedSummary(id, startedAt) {
19423
20136
  }
19424
20137
 
19425
20138
  // src/storage/session-store.ts
20139
+ var SESSION_FILTER_POOL_LIMIT = 1e4;
19426
20140
  var DefaultSessionStore = class _DefaultSessionStore {
19427
20141
  dir;
19428
20142
  events;
@@ -19438,14 +20152,36 @@ var DefaultSessionStore = class _DefaultSessionStore {
19438
20152
  _loadCache = /* @__PURE__ */ new Map();
19439
20153
  loadCache = new SessionLoadCache(this._loadCache);
19440
20154
  _indexCache = null;
20155
+ /**
20156
+ * Tombstoned ids — hidden even if their JSONL remains on disk.
20157
+ * Convention: readIndex() REASSIGNS this set from the parsed index file
20158
+ * MERGED with _manualTombstones; writeTombstone() adds in-place immediately
20159
+ * so an incremental cache rebuild can never resurrect a just-deleted
20160
+ * session.
20161
+ */
20162
+ _indexDeletedIds = /* @__PURE__ */ new Set();
20163
+ /**
20164
+ * Tombstones added by THIS store between reads. Merged into every fresh
20165
+ * snapshot so a read racing writeTombstone cannot drop an in-flight
20166
+ * deletion; entries are pruned once the parsed index file itself carries
20167
+ * them.
20168
+ */
20169
+ _manualTombstones = /* @__PURE__ */ new Set();
20170
+ /**
20171
+ * File-truth tombstones from the last readIndex() parse (EXCLUDES
20172
+ * _manualTombstones additions). compactIndexInner persists THIS snapshot so
20173
+ * concurrent writeTombstones that landed after the parse are not written
20174
+ * prematurely — they persist through their own append path instead.
20175
+ */
20176
+ _indexFileDeletedIds = /* @__PURE__ */ new Set();
19441
20177
  shardManifestCache = /* @__PURE__ */ new Map();
19442
20178
  static LIST_SCAN_CONCURRENCY = 32;
19443
20179
  indexAppendCount = 0;
19444
20180
  constructor(opts) {
19445
20181
  this.dir = opts.dir;
19446
- this.projectRoot = opts.projectRoot ? path31.resolve(opts.projectRoot) : void 0;
20182
+ this.projectRoot = opts.projectRoot ? path32.resolve(opts.projectRoot) : void 0;
19447
20183
  this.checkpointCas = this.projectRoot ? new SessionCheckpointCas({
19448
- rootDir: path31.join(this.dir, "_cas"),
20184
+ rootDir: path32.join(this.dir, "_cas"),
19449
20185
  projectRoot: this.projectRoot
19450
20186
  }) : void 0;
19451
20187
  this.events = opts.events;
@@ -19456,7 +20192,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
19456
20192
  this.onAppendBatch = opts.onAppendBatch;
19457
20193
  const builtRuntime = import.meta.url.includes("/dist/");
19458
20194
  this.catalogClient = this.projectRoot && (builtRuntime || process.env["WRONGSTACK_SESSION_CATALOG_FORCE"] === "1") && resolveSessionCatalogProjectServerUrl() ? new SessionCatalogProjectClient({
19459
- projectDir: path31.dirname(this.dir),
20195
+ projectDir: path32.dirname(this.dir),
19460
20196
  projectRoot: this.projectRoot
19461
20197
  }) : void 0;
19462
20198
  }
@@ -19478,7 +20214,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
19478
20214
  this.clearLoadCache();
19479
20215
  }
19480
20216
  get indexFile() {
19481
- return path31.join(this.dir, "_index.jsonl");
20217
+ return path32.join(this.dir, "_index.jsonl");
19482
20218
  }
19483
20219
  sessionPath(id, ext) {
19484
20220
  return sessionPath(this.dir, id, ext);
@@ -19504,19 +20240,94 @@ var DefaultSessionStore = class _DefaultSessionStore {
19504
20240
  async ensureShardDir(id) {
19505
20241
  return ensureShardDir(this.dir, id);
19506
20242
  }
20243
+ /**
20244
+ * Create a fresh session writer.
20245
+ *
20246
+ * @threadSafety Failure-prone steps (manifest invalidation, sidecar
20247
+ * removal, catalog upsert, the durable `{action:'create'}` index row)
20248
+ * run BEFORE the truncating `'w'` open, so no rejection path can destroy
20249
+ * prior bytes. Ordinary index summary rows never undelete a tombstone
20250
+ * (the parser only honors `{action:'create'}`), so a swallowed create-row
20251
+ * would leave a live writer whose id stays hidden forever — that append
20252
+ * is therefore required, not best-effort.
20253
+ */
19507
20254
  async create(meta) {
19508
20255
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
19509
20256
  const id = meta.id && meta.id.length > 0 ? meta.id : generateSessionId(startedAt);
19510
20257
  const shardDir = await this.ensureShardDir(id);
19511
20258
  const file = this.sessionPath(id, ".jsonl");
20259
+ const inUseBy = this.isSessionInUse ? await this.isSessionInUse(id) : null;
20260
+ if (inUseBy) {
20261
+ throw new Error(`Refusing to create session ${id}: in use (${inUseBy}).`);
20262
+ }
19512
20263
  const t0 = Date.now();
20264
+ try {
20265
+ await this.invalidateShardManifestBySessionId(id);
20266
+ } catch (cause) {
20267
+ throw new Error(
20268
+ `Failed to invalidate stale shard manifest for ${id}: ${toErrorMessage(cause)}`,
20269
+ { cause }
20270
+ );
20271
+ }
20272
+ const sidecar = path32.join(shardDir, `${path32.basename(id)}.summary.json`);
20273
+ try {
20274
+ await fsp23.rm(sidecar, { force: true });
20275
+ } catch (cause) {
20276
+ emitSessionStoreError(this.events, id, sidecar, "create", toErrorMessage(cause), true);
20277
+ try {
20278
+ await fsp23.access(sidecar);
20279
+ throw new Error(
20280
+ `Failed to remove stale session sidecar for ${id}: ${toErrorMessage(cause)}`,
20281
+ { cause }
20282
+ );
20283
+ } catch (accessErr) {
20284
+ const code = accessErr.code;
20285
+ if (code !== "ENOENT" && code !== "ENAMETOOLONG") throw accessErr;
20286
+ }
20287
+ }
20288
+ if (this.catalogClient) {
20289
+ await this.catalogClient.call("upsert_summary", {
20290
+ summary: {
20291
+ id,
20292
+ title: meta.title ?? "",
20293
+ startedAt,
20294
+ model: meta.model ?? "",
20295
+ provider: meta.provider ?? "",
20296
+ tokenTotal: 0,
20297
+ lastActivityAt: startedAt
20298
+ },
20299
+ transcriptRelativePath: `${id}.jsonl`,
20300
+ summaryRelativePath: `${id}.summary.json`
20301
+ });
20302
+ }
20303
+ try {
20304
+ await withFileLock(this.indexFile, async () => {
20305
+ try {
20306
+ await fsp23.appendFile(this.indexFile, `${JSON.stringify({ action: "create", id })}
20307
+ `, {
20308
+ encoding: "utf8",
20309
+ mode: 384
20310
+ });
20311
+ } finally {
20312
+ this._indexCache = null;
20313
+ }
20314
+ this._manualTombstones.delete(id);
20315
+ this._indexDeletedIds.delete(id);
20316
+ });
20317
+ } catch (cause) {
20318
+ throw new Error(
20319
+ `Failed to record session create in the index for ${id}: ${toErrorMessage(cause)}`,
20320
+ { cause }
20321
+ );
20322
+ }
19513
20323
  let handle;
19514
20324
  try {
19515
- handle = await fsp23.open(file, "a", 384);
20325
+ handle = await fsp23.open(file, "w", 384);
19516
20326
  } catch (err) {
19517
20327
  emitSessionStoreError(this.events, id, file, "create", toErrorMessage(err), false);
19518
20328
  throw new Error(`Failed to open session file: ${toErrorMessage(err)}`, { cause: err });
19519
20329
  }
20330
+ await this.invalidateShardManifestBySessionId(id).catch(() => void 0);
19520
20331
  try {
19521
20332
  const writer = new FileSessionWriter(id, handle, startedAt, meta, this.events, {
19522
20333
  dir: shardDir,
@@ -19530,23 +20341,11 @@ var DefaultSessionStore = class _DefaultSessionStore {
19530
20341
  if (!current) return null;
19531
20342
  return current.name === void 0 ? {} : { name: sessionContentText(this.secretScrubber.scrub(current.name)) };
19532
20343
  },
19533
- onClose: (s) => this.persistCatalogSummary(s)
20344
+ onClose: (s) => this.persistCatalogSummary(s),
20345
+ // Mid-session metadata checkpoints reuse the same sink as close so
20346
+ // killed sessions leave accurate index rows / catalog entries behind.
20347
+ onMetadataCheckpoint: (s) => this.persistCatalogSummary(s)
19534
20348
  });
19535
- if (this.catalogClient) {
19536
- await this.catalogClient.call("upsert_summary", {
19537
- summary: {
19538
- id,
19539
- title: meta.title ?? "",
19540
- startedAt,
19541
- model: meta.model ?? "",
19542
- provider: meta.provider ?? "",
19543
- tokenTotal: 0,
19544
- lastActivityAt: startedAt
19545
- },
19546
- transcriptRelativePath: `${id}.jsonl`,
19547
- summaryRelativePath: `${id}.summary.json`
19548
- });
19549
- }
19550
20349
  emitSessionStoreWrite(this.events, id, file, "create", "success", Date.now() - t0);
19551
20350
  return writer;
19552
20351
  } catch (err) {
@@ -19608,7 +20407,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
19608
20407
  readSummaryManifest: (summaryId) => this.readSummaryManifest(summaryId),
19609
20408
  searchEvents: (searchId, pred) => this.searchEvents(searchId, pred),
19610
20409
  persistCatalogSummary: (sum) => this.persistCatalogSummary(sum),
19611
- logWarn: (msg, ctx) => this.logWarn(msg, ctx)
20410
+ logWarn: (msg, ctx) => this.logWarn(msg, ctx),
20411
+ sessionsDir: this.dir
19612
20412
  });
19613
20413
  }
19614
20414
  async load(id) {
@@ -19682,11 +20482,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
19682
20482
  return this.scrubSummaries(records);
19683
20483
  }
19684
20484
  try {
19685
- const indexed = await this.readIndex();
19686
- if (indexed.length > 0) {
19687
- return this.scrubSummaries(indexed.slice(0, limit));
19688
- }
19689
- return this.scrubSummaries(await this.listFromDirectoryScan(limit));
20485
+ const [indexed, scanned] = await Promise.all([
20486
+ this.readIndex(),
20487
+ // Wide scan bound: mergeIndexWithScan slices to `limit`, so killed
20488
+ // sessions deep in history stay visible instead of being dropped by
20489
+ // the user-facing page size before the union runs.
20490
+ this.listFromDirectoryScan(SESSION_FILTER_POOL_LIMIT).catch(() => [])
20491
+ ]);
20492
+ return this.scrubSummaries(this.mergeIndexWithScan(indexed, scanned, limit));
19690
20493
  } catch {
19691
20494
  return [];
19692
20495
  }
@@ -19701,15 +20504,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
19701
20504
  return this.scrubSummaries(records);
19702
20505
  }
19703
20506
  try {
19704
- const indexed = await this.readIndex();
19705
- if (indexed.length === 0) {
19706
- const raw = await this.list(Math.max(limit, 100));
19707
- return raw.filter((s) => matchesSessionFilter(s, criteria)).slice(0, limit);
19708
- }
19709
- const filtered = this.scrubSummaries(indexed).filter(
19710
- (s) => matchesSessionFilter(s, criteria)
19711
- );
19712
- return filtered.slice(0, limit);
20507
+ const [indexed, scanned] = await Promise.all([
20508
+ this.readIndex(),
20509
+ // Same best-effort contract as list(): scan failures enrich nothing
20510
+ // but must not blank the filtered result set.
20511
+ this.listFromDirectoryScan(SESSION_FILTER_POOL_LIMIT).catch(() => [])
20512
+ ]);
20513
+ const pool = this.mergeIndexWithScan(indexed, scanned, SESSION_FILTER_POOL_LIMIT);
20514
+ return this.scrubSummaries(pool).filter((s) => matchesSessionFilter(s, criteria)).slice(0, limit);
19713
20515
  } catch {
19714
20516
  return [];
19715
20517
  }
@@ -19751,6 +20553,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
19751
20553
  id,
19752
20554
  (sid) => this.invalidateShardManifestBySessionId(sid),
19753
20555
  () => {
20556
+ this._manualTombstones.add(id);
20557
+ this._indexDeletedIds.add(id);
19754
20558
  this._indexCache = null;
19755
20559
  this.indexAppendCount++;
19756
20560
  }
@@ -19778,34 +20582,92 @@ var DefaultSessionStore = class _DefaultSessionStore {
19778
20582
  );
19779
20583
  }
19780
20584
  }
20585
+ /**
20586
+ * Compact the local index in place.
20587
+ *
20588
+ * Contract carried into the shared compactIndexInner helper
20589
+ * (session-store-index.ts): `entries` MUST already exclude tombstoned ids,
20590
+ * and the deleted-set argument is persisted VERBATIM — neither the helper
20591
+ * nor its callers may resurrect filtered rows or invent deletions.
20592
+ * Locking: callers MUST already hold the indexFile lock (both do:
20593
+ * compactIndex() below and the appendToIndexStrict compaction hook);
20594
+ * readIndex() inside reads that same locked file, so no second lock may
20595
+ * be taken here (non-reentrant → deadlock).
20596
+ *
20597
+ * That same lock is what makes the _indexFileDeletedIds snapshot safe to
20598
+ * pass across the await below: writeTombstone() appends under the identical
20599
+ * non-reentrant indexFile lock, so no tombstone can land between our
20600
+ * readIndex() and the snapshot handed to the helper. Compaction would
20601
+ * otherwise be racing a delete it cannot see.
20602
+ */
19781
20603
  async compactIndexInner() {
19782
20604
  const entries = await this.readIndex();
19783
- if (entries.length === 0) return;
19784
- const lines = entries.map((s) => JSON.stringify(s)).join("\n") + "\n";
19785
- await atomicWrite(this.indexFile, lines, { mode: 384 });
20605
+ await compactIndexInner(this.indexFile, entries, this._indexFileDeletedIds);
19786
20606
  this._indexCache = null;
19787
20607
  }
19788
20608
  async readIndex() {
19789
- const { summaries, cache } = await readIndexFile(this.indexFile, this._indexCache);
20609
+ const { summaries, deletedIds, cache } = await readIndexFile(this.indexFile, this._indexCache);
19790
20610
  this._indexCache = cache;
20611
+ const merged = new Set(deletedIds);
20612
+ for (const manual of this._manualTombstones) {
20613
+ merged.add(manual);
20614
+ if (deletedIds.has(manual)) this._manualTombstones.delete(manual);
20615
+ }
20616
+ this._indexFileDeletedIds = deletedIds;
20617
+ this._indexDeletedIds = merged;
19791
20618
  return summaries;
19792
20619
  }
20620
+ /**
20621
+ * Merge close-time index rows with directory-scan results, keyed by id.
20622
+ * Scanned entries win — their metadata is re-derived from the transcript,
20623
+ * so it reflects mid-session activity that index rows (written on close)
20624
+ * cannot know about. Indexed-only ids fill gaps; duplicates within the
20625
+ * index resolve last-wins, matching append order.
20626
+ */
20627
+ mergeIndexWithScan(indexed, scanned, limit) {
20628
+ const byId = /* @__PURE__ */ new Map();
20629
+ for (const row of indexed) byId.set(row.id, row);
20630
+ for (const row of scanned) byId.set(row.id, row);
20631
+ return [...byId.values()].filter((row) => !this._indexDeletedIds.has(row.id)).sort(compareSessionSummaries).slice(0, limit);
20632
+ }
20633
+ /**
20634
+ * Rebuild the index from what is actually on disk.
20635
+ *
20636
+ * @returns the number of healthy, live entries in the rebuilt index. Both
20637
+ * backends report that same quantity: ids whose summary could not be derived
20638
+ * are excluded (the catalog counts them as `damaged`; the local scan drops
20639
+ * them when `summaryFor` rejects), and ids carrying a surviving tombstone are
20640
+ * excluded (the catalog rebuilds only from live files; the local branch skips
20641
+ * them explicitly). It is NOT a count of rows written to the file — tombstone
20642
+ * rows are persisted but never counted.
20643
+ */
19793
20644
  async rebuildIndex() {
19794
20645
  if (this.catalogClient) {
19795
20646
  const result = await this.catalogClient.call("rebuild_catalog", {}, { timeoutMs: 12e4 });
19796
20647
  return result.indexed;
19797
20648
  }
19798
- const ids = await this.collectSessionIds(this.dir);
19799
- const summaries = await Promise.all(
19800
- ids.map((id) => this.summaryFor(id).catch(() => null))
19801
- );
19802
- const valid = summaries.filter((s) => s !== null);
19803
- const lines = valid.map((s) => JSON.stringify(s)).join("\n") + "\n";
19804
- await withFileLock(this.indexFile, async () => {
20649
+ return withFileLock(this.indexFile, async () => {
20650
+ await this.readIndex();
20651
+ const ids = await this.collectSessionIds(this.dir);
20652
+ const summaries = await Promise.all(ids.map((id) => this.summaryFor(id).catch(() => null)));
20653
+ const valid = summaries.filter((s) => s !== null);
20654
+ const parts = [];
20655
+ let tombstoned = 0;
20656
+ for (const s of valid) {
20657
+ if (this._indexDeletedIds.has(s.id)) {
20658
+ tombstoned++;
20659
+ continue;
20660
+ }
20661
+ parts.push(JSON.stringify(s));
20662
+ }
20663
+ for (const id of this._indexDeletedIds) {
20664
+ parts.push(JSON.stringify({ action: "delete", id }));
20665
+ }
20666
+ const lines = parts.join("\n") + "\n";
19805
20667
  await atomicWrite(this.indexFile, lines, { mode: 384 });
19806
20668
  this._indexCache = null;
20669
+ return valid.length - tombstoned;
19807
20670
  });
19808
- return valid.length;
19809
20671
  }
19810
20672
  async listFromDirectoryScan(limit) {
19811
20673
  const shardKeys = await this.collectShardKeys();
@@ -19887,7 +20749,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
19887
20749
  return void 0;
19888
20750
  }
19889
20751
  async collectSessionFilesInShard(shardKey) {
19890
- const dir = shardKey ? path31.join(this.dir, shardKey) : this.dir;
20752
+ const dir = shardKey ? path32.join(this.dir, shardKey) : this.dir;
19891
20753
  const entries = await this.collectSessionFiles(dir, shardKey);
19892
20754
  return shardKey ? entries.filter((entry) => entry.id.startsWith(`${shardKey}/`)) : entries.filter((entry) => !entry.id.includes("/"));
19893
20755
  }
@@ -20063,9 +20925,9 @@ function makeDirectorSessionFactory(opts) {
20063
20925
  let dir;
20064
20926
  if (opts.store) {
20065
20927
  store = opts.store;
20066
- dir = opts.sessionsRoot ? path32.join(opts.sessionsRoot, runId) : "(caller-managed)";
20928
+ dir = opts.sessionsRoot ? path33.join(opts.sessionsRoot, runId) : "(caller-managed)";
20067
20929
  } else if (opts.sessionsRoot) {
20068
- dir = path32.join(opts.sessionsRoot, runId);
20930
+ dir = path33.join(opts.sessionsRoot, runId);
20069
20931
  store = new DefaultSessionStore({ dir });
20070
20932
  } else {
20071
20933
  throw new Error("makeDirectorSessionFactory requires either `store` or `sessionsRoot`");
@@ -20089,7 +20951,7 @@ function makeDirectorSessionFactory(opts) {
20089
20951
  }
20090
20952
  async function readDirectorSubagentSession(args) {
20091
20953
  if (!args.sessionsRoot) return null;
20092
- const filePath = path32.join(args.sessionsRoot, args.directorRunId, `${args.subagentId}.jsonl`);
20954
+ const filePath = path33.join(args.sessionsRoot, args.directorRunId, `${args.subagentId}.jsonl`);
20093
20955
  let raw;
20094
20956
  try {
20095
20957
  raw = await fsp24.readFile(filePath, "utf8");
@@ -21789,12 +22651,12 @@ async function executeSubagentWithTimeout({
21789
22651
  }
21790
22652
  return new Promise((resolveDecision) => {
21791
22653
  let settled = false;
21792
- const resolve16 = (d) => {
22654
+ const resolve17 = (d) => {
21793
22655
  if (settled) return;
21794
22656
  settled = true;
21795
22657
  resolveDecision(d);
21796
22658
  };
21797
- const fallback = setTimeout(() => resolve16("stop"), DECISION_TIMEOUT_MS);
22659
+ const fallback = setTimeout(() => resolve17("stop"), DECISION_TIMEOUT_MS);
21798
22660
  const sessionId = currentSessionId();
21799
22661
  budget._events?.emit("budget.threshold_reached", {
21800
22662
  ...sessionId ? { sessionId } : {},
@@ -21804,11 +22666,11 @@ async function executeSubagentWithTimeout({
21804
22666
  timeoutMs: DECISION_TIMEOUT_MS,
21805
22667
  extend: (extra) => {
21806
22668
  clearTimeout(fallback);
21807
- queueMicrotask(() => resolve16({ extend: extra }));
22669
+ queueMicrotask(() => resolve17({ extend: extra }));
21808
22670
  },
21809
22671
  deny: () => {
21810
22672
  clearTimeout(fallback);
21811
- resolve16("stop");
22673
+ resolve17("stop");
21812
22674
  }
21813
22675
  });
21814
22676
  });
@@ -22215,7 +23077,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
22215
23077
  taskIds.map((id) => {
22216
23078
  const cached = this.completedResults.find((r) => r.taskId === id);
22217
23079
  if (cached) return cached;
22218
- return new Promise((resolve16, reject) => {
23080
+ return new Promise((resolve17, reject) => {
22219
23081
  const timeout = setTimeout(() => {
22220
23082
  this.off("task.completed", handler);
22221
23083
  reject(new Error(`awaitTasks timed out waiting for task "${id}"`));
@@ -22224,7 +23086,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
22224
23086
  if (result.taskId === id) {
22225
23087
  clearTimeout(timeout);
22226
23088
  this.off("task.completed", handler);
22227
- resolve16(result);
23089
+ resolve17(result);
22228
23090
  }
22229
23091
  };
22230
23092
  this.on("task.completed", handler);
@@ -22249,13 +23111,13 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
22249
23111
  const done = new Set(completed.map((r) => r.taskId));
22250
23112
  return { completed, pending: taskIds.filter((id) => !done.has(id)) };
22251
23113
  }
22252
- return new Promise((resolve16) => {
23114
+ return new Promise((resolve17) => {
22253
23115
  let timer;
22254
23116
  const handler = ({ result }) => {
22255
23117
  if (!ids.has(result.taskId)) return;
22256
23118
  if (timer) clearTimeout(timer);
22257
23119
  this.off("task.completed", handler);
22258
- resolve16({
23120
+ resolve17({
22259
23121
  completed: [result],
22260
23122
  pending: taskIds.filter((id) => id !== result.taskId)
22261
23123
  });
@@ -22263,7 +23125,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
22263
23125
  if (opts?.timeoutMs !== void 0) {
22264
23126
  timer = setTimeout(() => {
22265
23127
  this.off("task.completed", handler);
22266
- resolve16({ completed: [], pending: [...taskIds], timedOut: true });
23128
+ resolve17({ completed: [], pending: [...taskIds], timedOut: true });
22267
23129
  }, opts.timeoutMs);
22268
23130
  }
22269
23131
  this.on("task.completed", handler);
@@ -23531,7 +24393,7 @@ var Director = class _Director {
23531
24393
  // src/coordination/fleet-manager.ts
23532
24394
  import { randomUUID as randomUUID17 } from "node:crypto";
23533
24395
  import * as fsp26 from "node:fs/promises";
23534
- import * as path33 from "node:path";
24396
+ import * as path34 from "node:path";
23535
24397
  var FleetManager = class {
23536
24398
  /** The fleet-wide event bus. */
23537
24399
  fleet;
@@ -23865,7 +24727,7 @@ var FleetManager = class {
23865
24727
  })),
23866
24728
  usage: this.usage.snapshot()
23867
24729
  };
23868
- await fsp26.mkdir(path33.dirname(this.manifestPath), { recursive: true });
24730
+ await fsp26.mkdir(path34.dirname(this.manifestPath), { recursive: true });
23869
24731
  await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
23870
24732
  return this.manifestPath;
23871
24733
  }
@@ -24028,7 +24890,7 @@ var FleetManager = class {
24028
24890
  };
24029
24891
 
24030
24892
  // src/coordination/remote-mailbox.ts
24031
- import * as path37 from "node:path";
24893
+ import * as path38 from "node:path";
24032
24894
 
24033
24895
  // src/coordination/mailbox-constants.ts
24034
24896
  var HQ_MAILBOX_SNAPSHOT_MIN_INTERVAL_MS = 1e4;
@@ -24038,15 +24900,15 @@ var MAILBOX_MAX_ACK_BATCH = 500;
24038
24900
 
24039
24901
  // src/coordination/mailbox-project-server-client.ts
24040
24902
  import { spawn as spawn5 } from "node:child_process";
24041
- import * as fs4 from "node:fs";
24903
+ import * as fs5 from "node:fs";
24042
24904
  import * as net2 from "node:net";
24043
- import * as path35 from "node:path";
24905
+ import * as path36 from "node:path";
24044
24906
  import { fileURLToPath as fileURLToPath4 } from "node:url";
24045
24907
 
24046
24908
  // src/coordination/mailbox-project-server-endpoint.ts
24047
24909
  import { createHash as createHash6 } from "node:crypto";
24048
24910
  import * as os3 from "node:os";
24049
- import * as path34 from "node:path";
24911
+ import * as path35 from "node:path";
24050
24912
 
24051
24913
  // src/coordination/mailbox-project-server-protocol.ts
24052
24914
  var MAILBOX_PROJECT_SERVER_PROTOCOL_VERSION = 4;
@@ -24079,7 +24941,7 @@ function encodeMailboxProjectServerMessage(message) {
24079
24941
  // src/coordination/mailbox-project-server-endpoint.ts
24080
24942
  var MAILBOX_PROJECT_SERVER_METADATA_FILE = ".mailbox-server.json";
24081
24943
  function normalizeLocalPath(value) {
24082
- const resolved = path34.resolve(value);
24944
+ const resolved = path35.resolve(value);
24083
24945
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
24084
24946
  }
24085
24947
  function mailboxProjectServerKey(projectDir) {
@@ -24090,14 +24952,14 @@ function mailboxProjectServerEndpoint(projectDir) {
24090
24952
  if (process.platform === "win32") {
24091
24953
  return `\\\\.\\pipe\\wrongstack-mailbox-v${MAILBOX_PROJECT_SERVER_PROTOCOL_VERSION}-${key}`;
24092
24954
  }
24093
- return path34.join(
24955
+ return path35.join(
24094
24956
  os3.tmpdir(),
24095
24957
  `wsmb-v${MAILBOX_PROJECT_SERVER_PROTOCOL_VERSION}`,
24096
24958
  `${key}.sock`
24097
24959
  );
24098
24960
  }
24099
24961
  function mailboxProjectServerMetadataPath(projectDir) {
24100
- return path34.join(path34.resolve(projectDir), MAILBOX_PROJECT_SERVER_METADATA_FILE);
24962
+ return path35.join(path35.resolve(projectDir), MAILBOX_PROJECT_SERVER_METADATA_FILE);
24101
24963
  }
24102
24964
 
24103
24965
  // src/coordination/mailbox-project-server-client.ts
@@ -24108,7 +24970,7 @@ var DEFAULT_HEARTBEAT_INTERVAL_MS = 1e4;
24108
24970
  var AUTH_RETRY_DELAY_MS = 150;
24109
24971
  var AUTH_RETRY_MAX_ATTEMPTS = 13;
24110
24972
  function normalizePath(value) {
24111
- const resolved = path35.resolve(value);
24973
+ const resolved = path36.resolve(value);
24112
24974
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
24113
24975
  }
24114
24976
  function resolveProjectServerUrl() {
@@ -24118,7 +24980,7 @@ function resolveProjectServerUrl() {
24118
24980
  ]) {
24119
24981
  try {
24120
24982
  const url = new URL(relative4, import.meta.url);
24121
- if (url.protocol === "file:" && fs4.existsSync(fileURLToPath4(url))) return url;
24983
+ if (url.protocol === "file:" && fs5.existsSync(fileURLToPath4(url))) return url;
24122
24984
  } catch {
24123
24985
  }
24124
24986
  }
@@ -24128,7 +24990,7 @@ function isMailboxProjectServerAvailable() {
24128
24990
  return resolveProjectServerUrl() !== null;
24129
24991
  }
24130
24992
  function delay2(ms) {
24131
- return new Promise((resolve16) => setTimeout(resolve16, ms));
24993
+ return new Promise((resolve17) => setTimeout(resolve17, ms));
24132
24994
  }
24133
24995
  function isUnauthorizedMailboxError(error) {
24134
24996
  return error instanceof Error && error.name === "UnauthorizedMailboxRequest";
@@ -24136,7 +24998,7 @@ function isUnauthorizedMailboxError(error) {
24136
24998
  var MailboxProjectServerConnection = class {
24137
24999
  constructor(projectDir) {
24138
25000
  this.projectDir = projectDir;
24139
- this.projectDir = path35.resolve(projectDir);
25001
+ this.projectDir = path36.resolve(projectDir);
24140
25002
  this.endpoint = mailboxProjectServerEndpoint(this.projectDir);
24141
25003
  this.state = {
24142
25004
  status: isMailboxProjectServerAvailable() ? "offline" : "unavailable",
@@ -24301,7 +25163,7 @@ var MailboxProjectServerConnection = class {
24301
25163
  this.info = null;
24302
25164
  this.buffer = "";
24303
25165
  this.authToken = void 0;
24304
- return new Promise((resolve16, reject) => {
25166
+ return new Promise((resolve17, reject) => {
24305
25167
  const socket = net2.createConnection(this.endpoint);
24306
25168
  this.socket = socket;
24307
25169
  socket.setEncoding("utf8");
@@ -24316,7 +25178,7 @@ var MailboxProjectServerConnection = class {
24316
25178
  this.connectReject = null;
24317
25179
  this.startHeartbeat();
24318
25180
  this.transition("connected");
24319
- resolve16();
25181
+ resolve17();
24320
25182
  void this.request({ type: "request", op: "ping", args: {} }, 3e3).catch(
24321
25183
  () => void 0
24322
25184
  );
@@ -24345,7 +25207,7 @@ var MailboxProjectServerConnection = class {
24345
25207
  currentAuthToken() {
24346
25208
  if (this.authToken === void 0) {
24347
25209
  try {
24348
- const raw = fs4.readFileSync(mailboxProjectServerMetadataPath(this.projectDir), "utf8");
25210
+ const raw = fs5.readFileSync(mailboxProjectServerMetadataPath(this.projectDir), "utf8");
24349
25211
  const parsed = JSON.parse(raw);
24350
25212
  if (typeof parsed.authToken === "string" && parsed.authToken.length > 0) {
24351
25213
  this.authToken = parsed.authToken;
@@ -24369,7 +25231,7 @@ var MailboxProjectServerConnection = class {
24369
25231
  if (encoded.length > MAILBOX_PROJECT_SERVER_MAX_FRAME_CHARS) {
24370
25232
  return Promise.reject(new Error("Mailbox project server request exceeded frame limit"));
24371
25233
  }
24372
- return new Promise((resolve16, reject) => {
25234
+ return new Promise((resolve17, reject) => {
24373
25235
  const timer = setTimeout(() => {
24374
25236
  const pending = this.pending.get(id);
24375
25237
  if (!pending) return;
@@ -24377,7 +25239,7 @@ var MailboxProjectServerConnection = class {
24377
25239
  pending.reject(new Error(`Mailbox ${message.type} exceeded its ${timeoutMs}ms timeout`));
24378
25240
  }, timeoutMs);
24379
25241
  timer.unref?.();
24380
- this.pending.set(id, { resolve: resolve16, reject, timer });
25242
+ this.pending.set(id, { resolve: resolve17, reject, timer });
24381
25243
  socket.write(encoded);
24382
25244
  });
24383
25245
  }
@@ -24492,7 +25354,7 @@ var MailboxProjectServerConnection = class {
24492
25354
  spawnDetachedServer() {
24493
25355
  const url = resolveProjectServerUrl();
24494
25356
  if (!url) throw new Error("Mailbox project server entrypoint is unavailable");
24495
- fs4.mkdirSync(this.projectDir, { recursive: true });
25357
+ fs5.mkdirSync(this.projectDir, { recursive: true });
24496
25358
  const child = spawn5(process.execPath, [fileURLToPath4(url), "--project-dir", this.projectDir], {
24497
25359
  cwd: this.projectDir,
24498
25360
  detached: process.platform !== "win32",
@@ -24593,9 +25455,9 @@ var CredentialVerifyThrottle = class {
24593
25455
  var credentialVerifyThrottle = new CredentialVerifyThrottle();
24594
25456
 
24595
25457
  // src/coordination/global-mailbox-paths.ts
24596
- import * as path36 from "node:path";
25458
+ import * as path37 from "node:path";
24597
25459
  function resolveProjectDir(projectRoot, globalRoot) {
24598
- return path36.join(globalRoot, "projects", projectSlug(projectRoot));
25460
+ return path37.join(globalRoot, "projects", projectSlug(projectRoot));
24599
25461
  }
24600
25462
 
24601
25463
  // src/coordination/sqlite-mailbox.ts
@@ -24645,8 +25507,8 @@ var RemoteMailbox = class {
24645
25507
  hqPublisher,
24646
25508
  eventEmitter
24647
25509
  } : optionsOrProjectDir;
24648
- this.projectDir = path37.resolve(options.projectDir);
24649
- this.messagePath = path37.join(this.projectDir, SQLITE_MAILBOX_FILE);
25510
+ this.projectDir = path38.resolve(options.projectDir);
25511
+ this.messagePath = path38.join(this.projectDir, SQLITE_MAILBOX_FILE);
24650
25512
  this.registryPath = this.messagePath;
24651
25513
  this.clientRegistryPath = this.messagePath;
24652
25514
  this.events = options.events;
@@ -24892,7 +25754,7 @@ var RemoteMailbox = class {
24892
25754
  this.hqEventPending.clear();
24893
25755
  return;
24894
25756
  }
24895
- const mailboxId = `${path37.basename(this.projectDir)}:mailbox`;
25757
+ const mailboxId = `${path38.basename(this.projectDir)}:mailbox`;
24896
25758
  const inFlight = this.query({ includeDeleted: true, limit: 100 }).then((messages) => {
24897
25759
  const message = messages.find((candidate) => candidate.id === event.messageId);
24898
25760
  const action = event.type === "message.sent" ? "message.sent" : "message.updated";
@@ -24915,7 +25777,7 @@ var RemoteMailbox = class {
24915
25777
  if (!publisher || this.closed || !event.startsWith("mailbox.agent_") && !event.startsWith("mailbox.client_")) {
24916
25778
  return;
24917
25779
  }
24918
- const mailboxId = `${path37.basename(this.projectDir)}:mailbox`;
25780
+ const mailboxId = `${path38.basename(this.projectDir)}:mailbox`;
24919
25781
  const record = typeof payload === "object" && payload !== null ? payload : {};
24920
25782
  const agentId = typeof record["agentId"] === "string" ? record["agentId"] : void 0;
24921
25783
  const action = event === "mailbox.agent_registered" ? "agent.registered" : event === "mailbox.agent_heartbeat" ? "agent.heartbeat" : event === "mailbox.agent_deregistered" ? "agent.deregistered" : void 0;
@@ -24948,7 +25810,7 @@ function createProjectMailbox(options) {
24948
25810
  return new RemoteMailbox(options);
24949
25811
  }
24950
25812
  function getSharedProjectMailbox(projectDir, events, hqPublisher) {
24951
- const key = path37.resolve(projectDir);
25813
+ const key = path38.resolve(projectDir);
24952
25814
  let projectCache = sharedRemoteMailboxes.get(key);
24953
25815
  if (!projectCache) {
24954
25816
  projectCache = {
@@ -27298,25 +28160,25 @@ function requiredSendCapability(type) {
27298
28160
  }
27299
28161
  return "mail.send.informational";
27300
28162
  }
27301
- function requiredCredentialCapability(method, path42) {
27302
- if (method === "POST" && path42 === "/mailbox/send") return "mail.send.informational";
27303
- if (method === "POST" && (path42 === "/mailbox/query" || path42 === "/mailbox/check")) {
28163
+ function requiredCredentialCapability(method, path44) {
28164
+ if (method === "POST" && path44 === "/mailbox/send") return "mail.send.informational";
28165
+ if (method === "POST" && (path44 === "/mailbox/query" || path44 === "/mailbox/check")) {
27304
28166
  return "mail.read.self";
27305
28167
  }
27306
- if (method === "POST" && (path42 === "/mailbox/ack" || path42 === "/mailbox/ack-many")) {
28168
+ if (method === "POST" && (path44 === "/mailbox/ack" || path44 === "/mailbox/ack-many")) {
27307
28169
  return "mail.ack.self";
27308
28170
  }
27309
- if (method === "POST" && path42 === "/mailbox/unread-count") return "mail.read.self";
27310
- if (method === "POST" && path42 === "/mailbox/agents/register") {
28171
+ if (method === "POST" && path44 === "/mailbox/unread-count") return "mail.read.self";
28172
+ if (method === "POST" && path44 === "/mailbox/agents/register") {
27311
28173
  return "mail.presence.register.self";
27312
28174
  }
27313
- if (method === "POST" && path42 === "/mailbox/agents/heartbeat") {
28175
+ if (method === "POST" && path44 === "/mailbox/agents/heartbeat") {
27314
28176
  return "mail.presence.heartbeat.self";
27315
28177
  }
27316
- if (method === "GET" && (path42 === "/mailbox/agents" || path42 === "/mailbox/agents/online")) {
28178
+ if (method === "GET" && (path44 === "/mailbox/agents" || path44 === "/mailbox/agents/online")) {
27317
28179
  return "mail.presence.read";
27318
28180
  }
27319
- if (method === "GET" && path42 === "/mailbox/events") return "mail.events.self";
28181
+ if (method === "GET" && path44 === "/mailbox/events") return "mail.events.self";
27320
28182
  return void 0;
27321
28183
  }
27322
28184
  async function checkMailbox(mailbox, input, minTimestampIso, includeReceiptState = false, eligibleRecipients, readerRole) {
@@ -27536,7 +28398,7 @@ function createMailboxHttpRouter(options) {
27536
28398
  return;
27537
28399
  }
27538
28400
  const customAccess = options.authorize ? await options.authorize(request) : void 0;
27539
- let access5 = customAccess ?? { allowed: true };
28401
+ let access6 = customAccess ?? { allowed: true };
27540
28402
  const presentedCredential = parseCredentialAuthorization(request);
27541
28403
  if (options.credentialStore !== void 0 && presentedCredential !== void 0) {
27542
28404
  const persistedAccess = await authorizePersistedMailboxCredential(
@@ -27544,30 +28406,30 @@ function createMailboxHttpRouter(options) {
27544
28406
  options.credentialStore
27545
28407
  );
27546
28408
  if (!persistedAccess.allowed) {
27547
- access5 = persistedAccess;
28409
+ access6 = persistedAccess;
27548
28410
  } else if (persistedAccess.actor === void 0) {
27549
- access5 = { allowed: false };
27550
- } else if (access5.allowed) {
27551
- if (access5.actor !== void 0 && (access5.actor.actorId !== persistedAccess.actor.actorId || access5.actor.projectId !== persistedAccess.actor.projectId)) {
27552
- access5 = { allowed: false };
28411
+ access6 = { allowed: false };
28412
+ } else if (access6.allowed) {
28413
+ if (access6.actor !== void 0 && (access6.actor.actorId !== persistedAccess.actor.actorId || access6.actor.projectId !== persistedAccess.actor.projectId)) {
28414
+ access6 = { allowed: false };
27553
28415
  } else {
27554
- access5 = {
27555
- ...access5,
28416
+ access6 = {
28417
+ ...access6,
27556
28418
  actor: persistedAccess.actor,
27557
- ...access5.rateLimitKey ?? persistedAccess.rateLimitKey ? { rateLimitKey: access5.rateLimitKey ?? persistedAccess.rateLimitKey } : {}
28419
+ ...access6.rateLimitKey ?? persistedAccess.rateLimitKey ? { rateLimitKey: access6.rateLimitKey ?? persistedAccess.rateLimitKey } : {}
27558
28420
  };
27559
28421
  }
27560
28422
  }
27561
28423
  } else if (customAccess === void 0 && options.credentialStore !== void 0) {
27562
- access5 = { allowed: false };
28424
+ access6 = { allowed: false };
27563
28425
  }
27564
- if (access5.allowed && access5.actor !== void 0 && options.projectId !== void 0 && access5.actor.projectId !== options.projectId) {
28426
+ if (access6.allowed && access6.actor !== void 0 && options.projectId !== void 0 && access6.actor.projectId !== options.projectId) {
27565
28427
  writeJson(response, 403, {
27566
28428
  error: { code: "FORBIDDEN", message: "credential is scoped to a different project" }
27567
28429
  });
27568
28430
  return;
27569
28431
  }
27570
- if (!access5.allowed) {
28432
+ if (!access6.allowed) {
27571
28433
  const forwardedFor = request.headers["x-forwarded-for"];
27572
28434
  const clientIp = (Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor)?.split(",")[0]?.trim() ?? request.socket?.remoteAddress ?? "unknown";
27573
28435
  console.warn(
@@ -27583,14 +28445,14 @@ function createMailboxHttpRouter(options) {
27583
28445
  );
27584
28446
  writeJson(
27585
28447
  response,
27586
- access5.status ?? 401,
27587
- access5.body ?? {
28448
+ access6.status ?? 401,
28449
+ access6.body ?? {
27588
28450
  error: { code: "UNAUTHORIZED", message: "invalid or missing authorization credential" }
27589
28451
  }
27590
28452
  );
27591
28453
  return;
27592
28454
  }
27593
- if (options.rateLimiter && access5.rateLimitKey !== void 0 && !options.rateLimiter.allow(access5.rateLimitKey)) {
28455
+ if (options.rateLimiter && access6.rateLimitKey !== void 0 && !options.rateLimiter.allow(access6.rateLimitKey)) {
27594
28456
  writeJson(response, 429, {
27595
28457
  error: {
27596
28458
  code: "RATE_LIMITED",
@@ -27610,7 +28472,7 @@ function createMailboxHttpRouter(options) {
27610
28472
  defaultMaxAgeMs,
27611
28473
  closeSseStreams,
27612
28474
  routePath,
27613
- access5.actor,
28475
+ access6.actor,
27614
28476
  options.credentialStore
27615
28477
  );
27616
28478
  } catch (error) {
@@ -27638,16 +28500,16 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
27638
28500
  throw validationError(`routePath must not start with '?' (got ${JSON.stringify(routePath)})`);
27639
28501
  }
27640
28502
  const queryIndex = url.indexOf("?");
27641
- const path42 = queryIndex === -1 ? url : url.slice(0, queryIndex);
28503
+ const path44 = queryIndex === -1 ? url : url.slice(0, queryIndex);
27642
28504
  if (actor !== void 0) {
27643
- const requiredCapability = requiredCredentialCapability(method, path42);
28505
+ const requiredCapability = requiredCredentialCapability(method, path44);
27644
28506
  if (requiredCapability === void 0) {
27645
28507
  writeJson(response, 403, {
27646
- error: { code: "FORBIDDEN", message: `credential access is not permitted for ${method} ${path42}` }
28508
+ error: { code: "FORBIDDEN", message: `credential access is not permitted for ${method} ${path44}` }
27647
28509
  });
27648
28510
  return;
27649
28511
  }
27650
- if (path42 !== "/mailbox/send" && !hasMailboxCapability(actor, requiredCapability)) {
28512
+ if (path44 !== "/mailbox/send" && !hasMailboxCapability(actor, requiredCapability)) {
27651
28513
  writeJson(response, 403, {
27652
28514
  error: {
27653
28515
  code: "FORBIDDEN",
@@ -27657,7 +28519,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
27657
28519
  return;
27658
28520
  }
27659
28521
  }
27660
- if (method === "POST" && path42 === "/mailbox/send") {
28522
+ if (method === "POST" && path44 === "/mailbox/send") {
27661
28523
  const input = validateSend(
27662
28524
  await readJsonBody(request, maxBodyBytes),
27663
28525
  actor?.actorId,
@@ -27679,7 +28541,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
27679
28541
  writeJson(response, 201, await mailbox.send(input));
27680
28542
  return;
27681
28543
  }
27682
- if (method === "POST" && path42 === "/mailbox/query") {
28544
+ if (method === "POST" && path44 === "/mailbox/query") {
27683
28545
  const queryContext = parseSinceMs(url, defaultMaxAgeMs);
27684
28546
  if ("error" in queryContext) {
27685
28547
  writeJson(response, 400, { error: queryContext.error });
@@ -27699,7 +28561,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
27699
28561
  writeJson(response, 200, { data: projected, count: projected.length });
27700
28562
  return;
27701
28563
  }
27702
- if (method === "POST" && path42 === "/mailbox/check") {
28564
+ if (method === "POST" && path44 === "/mailbox/check") {
27703
28565
  const queryContext = parseSinceMs(url, defaultMaxAgeMs);
27704
28566
  if ("error" in queryContext) {
27705
28567
  writeJson(response, 400, { error: queryContext.error });
@@ -27733,7 +28595,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
27733
28595
  writeJson(response, 200, { data: projected, count: projected.length });
27734
28596
  return;
27735
28597
  }
27736
- if (method === "POST" && path42 === "/mailbox/ack") {
28598
+ if (method === "POST" && path44 === "/mailbox/ack") {
27737
28599
  const input = validateAck(await readJsonBody(request, maxBodyBytes), actor?.actorId);
27738
28600
  if (actor !== void 0) {
27739
28601
  input.readerId = actor.actorId;
@@ -27747,7 +28609,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
27747
28609
  writeJson(response, 200, { updated: projectedAck });
27748
28610
  return;
27749
28611
  }
27750
- if (method === "POST" && path42 === "/mailbox/ack-many") {
28612
+ if (method === "POST" && path44 === "/mailbox/ack-many") {
27751
28613
  const input = validateAckMany(await readJsonBody(request, maxBodyBytes), actor?.actorId);
27752
28614
  if (actor !== void 0) {
27753
28615
  const requestedIds = new Set(input.acks.map((ack) => ack.messageId));
@@ -27763,54 +28625,54 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
27763
28625
  writeJson(response, 200, { updated: projectedMany, count: projectedMany.length });
27764
28626
  return;
27765
28627
  }
27766
- if (method === "POST" && path42 === "/mailbox/unread-count") {
28628
+ if (method === "POST" && path44 === "/mailbox/unread-count") {
27767
28629
  const body = await readJsonBody(request, maxBodyBytes);
27768
28630
  const count = actor === void 0 ? await mailbox.unreadCount(requireString2(body, "forAgentId")) : await unreadCountForActor(mailbox, actor);
27769
28631
  writeJson(response, 200, { count });
27770
28632
  return;
27771
28633
  }
27772
- if (method === "POST" && path42 === "/mailbox/agents/register") {
28634
+ if (method === "POST" && path44 === "/mailbox/agents/register") {
27773
28635
  const input = validateAgentRegistration(await readJsonBody(request, maxBodyBytes), actor);
27774
28636
  await mailbox.registerAgent(input);
27775
28637
  writeJson(response, 200, { ok: true });
27776
28638
  return;
27777
28639
  }
27778
- if (method === "POST" && path42 === "/mailbox/agents/heartbeat") {
28640
+ if (method === "POST" && path44 === "/mailbox/agents/heartbeat") {
27779
28641
  const input = validateAgentHeartbeat(await readJsonBody(request, maxBodyBytes), actor?.actorId);
27780
28642
  if (actor !== void 0) input.agentId = actor.actorId;
27781
28643
  await mailbox.heartbeat(input);
27782
28644
  writeJson(response, 200, { ok: true });
27783
28645
  return;
27784
28646
  }
27785
- if (method === "POST" && path42 === "/mailbox/register-client") {
28647
+ if (method === "POST" && path44 === "/mailbox/register-client") {
27786
28648
  await mailbox.registerClient(
27787
28649
  validateClientRegistration(await readJsonBody(request, maxBodyBytes))
27788
28650
  );
27789
28651
  writeJson(response, 200, { ok: true });
27790
28652
  return;
27791
28653
  }
27792
- if (method === "POST" && path42 === "/mailbox/heartbeat") {
28654
+ if (method === "POST" && path44 === "/mailbox/heartbeat") {
27793
28655
  await mailbox.clientHeartbeat(
27794
28656
  validateClientHeartbeat(await readJsonBody(request, maxBodyBytes))
27795
28657
  );
27796
28658
  writeJson(response, 200, { ok: true });
27797
28659
  return;
27798
28660
  }
27799
- if (method === "POST" && path42 === "/mailbox/purge-clients") {
28661
+ if (method === "POST" && path44 === "/mailbox/purge-clients") {
27800
28662
  writeJson(response, 200, { ok: true, purged: await mailbox.purgeClients() });
27801
28663
  return;
27802
28664
  }
27803
- if (method === "GET" && path42 === "/mailbox/agents") {
28665
+ if (method === "GET" && path44 === "/mailbox/agents") {
27804
28666
  const agents = await mailbox.getAgentStatuses();
27805
28667
  writeJson(response, 200, { data: agents, count: agents.length });
27806
28668
  return;
27807
28669
  }
27808
- if (method === "GET" && path42 === "/mailbox/agents/online") {
28670
+ if (method === "GET" && path44 === "/mailbox/agents/online") {
27809
28671
  const agents = await mailbox.getOnlineAgents();
27810
28672
  writeJson(response, 200, { data: agents, count: agents.length });
27811
28673
  return;
27812
28674
  }
27813
- if (method === "GET" && path42 === "/mailbox/events" && eventEmitter) {
28675
+ if (method === "GET" && path44 === "/mailbox/events" && eventEmitter) {
27814
28676
  const queryContext = parseSinceMs(url, defaultMaxAgeMs);
27815
28677
  if ("error" in queryContext) {
27816
28678
  writeJson(response, 400, { error: queryContext.error });
@@ -27870,16 +28732,16 @@ function writeJson(response, status, body) {
27870
28732
  var NULL_FLEET_BUS = new FleetBus();
27871
28733
 
27872
28734
  // src/coordination/package-author-tracker.ts
27873
- import * as fs5 from "node:fs/promises";
27874
- import * as path38 from "node:path";
28735
+ import * as fs6 from "node:fs/promises";
28736
+ import * as path39 from "node:path";
27875
28737
  var DEFAULT_MAX_ENTRIES2 = 1e4;
27876
28738
  var LOG_FILENAME2 = "package-authors.json";
27877
28739
  function logPath2(storageDir) {
27878
- return path38.join(storageDir, LOG_FILENAME2);
28740
+ return path39.join(storageDir, LOG_FILENAME2);
27879
28741
  }
27880
28742
  async function loadLog2(storageDir, projectRoot) {
27881
28743
  try {
27882
- const raw = await fs5.readFile(logPath2(storageDir), "utf-8");
28744
+ const raw = await fs6.readFile(logPath2(storageDir), "utf-8");
27883
28745
  const parsed = JSON.parse(raw);
27884
28746
  if (!parsed.entries || !Array.isArray(parsed.entries)) {
27885
28747
  return { projectRoot, entries: [] };
@@ -27897,7 +28759,7 @@ async function saveLog2(storageDir, log) {
27897
28759
  `);
27898
28760
  }
27899
28761
  function detectEcosystem(manifestPath) {
27900
- const name = path38.win32.basename(manifestPath).toLowerCase();
28762
+ const name = path39.win32.basename(manifestPath).toLowerCase();
27901
28763
  if (name === "package.json") return "npm";
27902
28764
  if (name === "go.mod") return "go";
27903
28765
  if (name === "cargo.toml") return "cargo";
@@ -28787,12 +29649,12 @@ import { randomBytes as randomBytes2 } from "node:crypto";
28787
29649
  import * as fsp27 from "node:fs/promises";
28788
29650
  import { execFile } from "node:child_process";
28789
29651
  import * as os4 from "node:os";
28790
- import * as path39 from "node:path";
29652
+ import * as path40 from "node:path";
28791
29653
  var MAILBOX_BRIDGE_LOCK_FILENAME = ".mailbox-bridge.lock";
28792
29654
  var MAILBOX_BRIDGE_TOKEN_FILENAME = ".mailbox.token";
28793
29655
  async function acquireOrJoin(opts) {
28794
- const lockPath = path39.join(opts.projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
28795
- const tokenPath = path39.join(opts.projectDir, MAILBOX_BRIDGE_TOKEN_FILENAME);
29656
+ const lockPath = path40.join(opts.projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
29657
+ const tokenPath = path40.join(opts.projectDir, MAILBOX_BRIDGE_TOKEN_FILENAME);
28796
29658
  const inspected = await readLockForInspection(lockPath);
28797
29659
  if (inspected.kind === "live") {
28798
29660
  const existing = inspected.lock;
@@ -28820,8 +29682,8 @@ async function acquireOrJoin(opts) {
28820
29682
  return { kind: "acquired", lock: tentative, tokenPath };
28821
29683
  }
28822
29684
  async function finalize(projectDir, tentative, boundPort) {
28823
- const lockPath = path39.join(projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
28824
- const tokenPath = path39.join(projectDir, MAILBOX_BRIDGE_TOKEN_FILENAME);
29685
+ const lockPath = path40.join(projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
29686
+ const tokenPath = path40.join(projectDir, MAILBOX_BRIDGE_TOKEN_FILENAME);
28825
29687
  const finalized = {
28826
29688
  ...tentative,
28827
29689
  port: boundPort,
@@ -28832,8 +29694,8 @@ async function finalize(projectDir, tentative, boundPort) {
28832
29694
  return finalized;
28833
29695
  }
28834
29696
  async function release(projectDir, generation) {
28835
- const lockPath = path39.join(projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
28836
- const tokenPath = path39.join(projectDir, MAILBOX_BRIDGE_TOKEN_FILENAME);
29697
+ const lockPath = path40.join(projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
29698
+ const tokenPath = path40.join(projectDir, MAILBOX_BRIDGE_TOKEN_FILENAME);
28837
29699
  try {
28838
29700
  const raw = await fsp27.readFile(lockPath, "utf-8");
28839
29701
  const parsed = JSON.parse(raw);
@@ -28867,7 +29729,7 @@ async function readLockForInspection(lockPath) {
28867
29729
  return { kind: "live", lock: parsed };
28868
29730
  }
28869
29731
  async function readLiveLock(projectDir) {
28870
- const lockPath = path39.join(projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
29732
+ const lockPath = path40.join(projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
28871
29733
  const result = await readLockForInspection(lockPath);
28872
29734
  if (result.kind === "live") {
28873
29735
  return { kind: "live", lock: result.lock };
@@ -28878,7 +29740,7 @@ async function readLiveLock(projectDir) {
28878
29740
  return { kind: "absent" };
28879
29741
  }
28880
29742
  async function atomicWriteJson(targetPath, value) {
28881
- const dir = path39.dirname(targetPath);
29743
+ const dir = path40.dirname(targetPath);
28882
29744
  await fsp27.mkdir(dir, { recursive: true });
28883
29745
  const tmp = `${targetPath}.tmp.${process.pid}.${randomBytes2(4).toString("hex")}`;
28884
29746
  const body = JSON.stringify(value, null, 2) + "\n";
@@ -28894,7 +29756,7 @@ async function isProcessAlive(pid) {
28894
29756
  if (!Number.isInteger(pid) || pid <= 0) return false;
28895
29757
  if (pid === process.pid) return true;
28896
29758
  if (os4.platform() === "win32") {
28897
- return new Promise((resolve16) => {
29759
+ return new Promise((resolve17) => {
28898
29760
  execFile(
28899
29761
  "tasklist",
28900
29762
  ["/FI", `PID eq ${pid}`, "/NH", "/FO", "CSV"],
@@ -28906,11 +29768,11 @@ async function isProcessAlive(pid) {
28906
29768
  },
28907
29769
  (error, out) => {
28908
29770
  if (error) {
28909
- resolve16(false);
29771
+ resolve17(false);
28910
29772
  return;
28911
29773
  }
28912
29774
  const match = /(?:^|\n)"[^"]*","(\d+)"/.exec(out);
28913
- resolve16(match !== null && match[1] === String(pid));
29775
+ resolve17(match !== null && match[1] === String(pid));
28914
29776
  }
28915
29777
  );
28916
29778
  });
@@ -29061,8 +29923,8 @@ function acceptManifestCandidate(candidate) {
29061
29923
  if (normalized.split("/").includes("..")) return false;
29062
29924
  return isManifestFile(normalized);
29063
29925
  }
29064
- function isManifestFile(path42) {
29065
- const name = pathBasename(path42).toLowerCase();
29926
+ function isManifestFile(path44) {
29927
+ const name = pathBasename(path44).toLowerCase();
29066
29928
  const manifests = [
29067
29929
  "package.json",
29068
29930
  "package-lock.json",
@@ -29142,7 +30004,7 @@ function buildTechStackTask(msg, manifestPath) {
29142
30004
  ].join("\n");
29143
30005
  }
29144
30006
 
29145
- // src/middleware/collab-pause.ts
30007
+ // src/coordination/collab-pause.ts
29146
30008
  function collabPauseMiddleware(bus, opts = {}) {
29147
30009
  const timeoutMs = opts.defaultTimeoutMs ?? 6e4;
29148
30010
  const logger = opts.logger;
@@ -29373,10 +30235,10 @@ var AdaptiveConcurrencyController = class {
29373
30235
  };
29374
30236
 
29375
30237
  // src/coordination/agent-monitor.ts
29376
- import { createReadStream as createReadStream6 } from "node:fs";
29377
- import * as fs6 from "node:fs/promises";
29378
- import * as path40 from "node:path";
29379
- import { createInterface as createInterface6 } from "node:readline";
30238
+ import { createReadStream as createReadStream7 } from "node:fs";
30239
+ import * as fs7 from "node:fs/promises";
30240
+ import * as path41 from "node:path";
30241
+ import { createInterface as createInterface7 } from "node:readline";
29380
30242
  var AgentMonitorService = class _AgentMonitorService {
29381
30243
  _fleetBus;
29382
30244
  _events;
@@ -29455,7 +30317,7 @@ var AgentMonitorService = class _AgentMonitorService {
29455
30317
  async loadSessionsFromDisk() {
29456
30318
  let subagentIds;
29457
30319
  try {
29458
- const dirents = await fs6.readdir(this._transcriptsDir, { withFileTypes: true });
30320
+ const dirents = await fs7.readdir(this._transcriptsDir, { withFileTypes: true });
29459
30321
  subagentIds = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
29460
30322
  } catch {
29461
30323
  return this.getAllSessions();
@@ -29479,12 +30341,12 @@ var AgentMonitorService = class _AgentMonitorService {
29479
30341
  return this.getAllSessions();
29480
30342
  }
29481
30343
  async _readTranscriptFile(subagentId) {
29482
- const file = path40.join(this._transcriptsDir, subagentId, "transcript.jsonl");
29483
- const accessible = await fs6.access(file).then(() => true).catch(() => false);
30344
+ const file = path41.join(this._transcriptsDir, subagentId, "transcript.jsonl");
30345
+ const accessible = await fs7.access(file).then(() => true).catch(() => false);
29484
30346
  if (!accessible) return [];
29485
30347
  const out = [];
29486
- const input = createReadStream6(file, { encoding: "utf8" });
29487
- const lines = createInterface6({ input, crlfDelay: Number.POSITIVE_INFINITY });
30348
+ const input = createReadStream7(file, { encoding: "utf8" });
30349
+ const lines = createInterface7({ input, crlfDelay: Number.POSITIVE_INFINITY });
29488
30350
  try {
29489
30351
  for await (const line of lines) {
29490
30352
  const trimmed = line.trim();
@@ -29725,9 +30587,9 @@ var AgentMonitorService = class _AgentMonitorService {
29725
30587
  * firehose (or, worse, only the first word).
29726
30588
  */
29727
30589
  _appendStreamDelta(subagentId, session, kind, text, iteration) {
29728
- const open9 = this._openStreams.get(subagentId);
29729
- if (open9 && open9.entry.kind === kind && open9.entry.iteration === iteration && open9.entry.content.length + text.length <= _AgentMonitorService._MAX_SEGMENT_CHARS) {
29730
- open9.entry.content += text;
30590
+ const open10 = this._openStreams.get(subagentId);
30591
+ if (open10 && open10.entry.kind === kind && open10.entry.iteration === iteration && open10.entry.content.length + text.length <= _AgentMonitorService._MAX_SEGMENT_CHARS) {
30592
+ open10.entry.content += text;
29731
30593
  return;
29732
30594
  }
29733
30595
  this._closeStream(subagentId);
@@ -29751,11 +30613,11 @@ var AgentMonitorService = class _AgentMonitorService {
29751
30613
  * announce the completed entry on the local bus + HQ callback.
29752
30614
  */
29753
30615
  _closeStream(subagentId) {
29754
- const open9 = this._openStreams.get(subagentId);
29755
- if (!open9) return;
30616
+ const open10 = this._openStreams.get(subagentId);
30617
+ if (!open10) return;
29756
30618
  this._openStreams.delete(subagentId);
29757
- this._enqueueWrite(subagentId, open9.entry);
29758
- this._emitEntry(open9.entry);
30619
+ this._enqueueWrite(subagentId, open10.entry);
30620
+ this._emitEntry(open10.entry);
29759
30621
  }
29760
30622
  _addEntry(subagentId, entry) {
29761
30623
  const session = this._sessions.get(subagentId);
@@ -29820,14 +30682,14 @@ var AgentMonitorService = class _AgentMonitorService {
29820
30682
  this._writeDrain = drain;
29821
30683
  }
29822
30684
  async _appendToFile(subagentId, line) {
29823
- const dir = path40.join(this._transcriptsDir, subagentId);
30685
+ const dir = path41.join(this._transcriptsDir, subagentId);
29824
30686
  if (!this._ensuredDirs.has(dir)) {
29825
- await fs6.mkdir(dir, { recursive: true });
30687
+ await fs7.mkdir(dir, { recursive: true });
29826
30688
  this._ensuredDirs.add(dir);
29827
30689
  }
29828
- const filePath = path40.join(dir, "transcript.jsonl");
30690
+ const filePath = path41.join(dir, "transcript.jsonl");
29829
30691
  try {
29830
- await fs6.appendFile(filePath, line, { encoding: "utf8", mode: SECRET_FILE_MODE });
30692
+ await fs7.appendFile(filePath, line, { encoding: "utf8", mode: SECRET_FILE_MODE });
29831
30693
  } catch (error) {
29832
30694
  this._ensuredDirs.delete(dir);
29833
30695
  throw error;
@@ -30218,7 +31080,7 @@ import { randomUUID as randomUUID22 } from "node:crypto";
30218
31080
  // src/coordination/knowledge-graph.ts
30219
31081
  import { randomUUID as randomUUID20 } from "node:crypto";
30220
31082
  import * as fsp28 from "node:fs/promises";
30221
- import * as path41 from "node:path";
31083
+ import * as path42 from "node:path";
30222
31084
  var DEFAULT_MAX_NODES = 2e3;
30223
31085
  var MAX_SUBSCRIPTIONS = 1e3;
30224
31086
  var MAX_PENDING_DELIVERIES_PER_SUBSCRIPTION = 1e3;
@@ -30253,8 +31115,8 @@ var KnowledgeGraph = class _KnowledgeGraph {
30253
31115
  return this.index;
30254
31116
  }
30255
31117
  constructor(sessionDir, maxNodes = DEFAULT_MAX_NODES, compactEveryWrites = Math.max(1e3, maxNodes * 4)) {
30256
- this.filePath = path41.join(sessionDir, "_knowledge_graph");
30257
- this.graphFilePath = path41.join(this.filePath, "graph.jsonl");
31118
+ this.filePath = path42.join(sessionDir, "_knowledge_graph");
31119
+ this.graphFilePath = path42.join(this.filePath, "graph.jsonl");
30258
31120
  this.maxNodes = maxNodes;
30259
31121
  this.compactEveryWrites = Math.max(1, Math.floor(compactEveryWrites));
30260
31122
  }
@@ -31358,6 +32220,11 @@ var ConsensusProtocol = class {
31358
32220
  if (change?.type !== "change") {
31359
32221
  throw new Error(`ConsensusProtocol: no change found with id "${changeId}"`);
31360
32222
  }
32223
+ if (change.status === "approved" || change.status === "rejected") {
32224
+ throw new Error(
32225
+ `ConsensusProtocol: cannot initiate vote on already resolved change "${changeId}" (status: "${change.status}")`
32226
+ );
32227
+ }
31361
32228
  await this.graph.update(changeId, { status: "proposed", votes: [] });
31362
32229
  const eligible = this._eligibleVoters(change);
31363
32230
  this._notifyVoters(change, eligible, "vote_initiated");
@@ -31371,6 +32238,11 @@ var ConsensusProtocol = class {
31371
32238
  if (change?.type !== "change") {
31372
32239
  throw new Error(`ConsensusProtocol: no change found for "${changeId}"`);
31373
32240
  }
32241
+ if (change.status !== "proposed") {
32242
+ throw new Error(
32243
+ `ConsensusProtocol: cannot cast vote on change "${changeId}" with status "${change.status}" (ballot is closed)`
32244
+ );
32245
+ }
31374
32246
  const voter = this.voters.get(voterId);
31375
32247
  if (!voter) {
31376
32248
  throw new Error(`ConsensusProtocol: unknown voter "${voterId}"`);
@@ -31445,8 +32317,8 @@ var ConsensusProtocol = class {
31445
32317
  (sum, v) => sum + (this.voters.get(v.agentId)?.weight ?? 1),
31446
32318
  0
31447
32319
  );
31448
- const totalWeight = Array.from(this.voters.values()).reduce(
31449
- (sum, v) => sum + v.weight,
32320
+ const totalWeight = eligible.reduce(
32321
+ (sum, id) => sum + (this.voters.get(id)?.weight ?? 1),
31450
32322
  0
31451
32323
  );
31452
32324
  const castCount = votes.length;
@@ -32166,17 +33038,17 @@ ${input.detail}`
32166
33038
  _waitForDagProgress(timeoutMs) {
32167
33039
  const before = this._dagProgressKey();
32168
33040
  if (this.dag.isDone()) return Promise.resolve();
32169
- return new Promise((resolve16) => {
33041
+ return new Promise((resolve17) => {
32170
33042
  let off;
32171
33043
  const timer = setTimeout(() => {
32172
33044
  off?.();
32173
- resolve16();
33045
+ resolve17();
32174
33046
  }, timeoutMs);
32175
33047
  off = this.dag.onEvent(() => {
32176
33048
  if (this._dagProgressKey() === before) return;
32177
33049
  clearTimeout(timer);
32178
33050
  off?.();
32179
- resolve16();
33051
+ resolve17();
32180
33052
  });
32181
33053
  });
32182
33054
  }
@@ -32484,8 +33356,8 @@ var CollaborationBus = class _CollaborationBus {
32484
33356
  if (this.isPaused()) return false;
32485
33357
  this.pausedAtMs = Date.now();
32486
33358
  this.pausedBy = byParticipant;
32487
- this.pausePromise = new Promise((resolve16) => {
32488
- this.pauseResolve = resolve16;
33359
+ this.pausePromise = new Promise((resolve17) => {
33360
+ this.pauseResolve = resolve17;
32489
33361
  });
32490
33362
  return true;
32491
33363
  }
@@ -32521,8 +33393,8 @@ var CollaborationBus = class _CollaborationBus {
32521
33393
  return true;
32522
33394
  }
32523
33395
  let timer;
32524
- const timeoutPromise = new Promise((resolve16) => {
32525
- timer = setTimeout(() => resolve16("timeout"), timeoutMs);
33396
+ const timeoutPromise = new Promise((resolve17) => {
33397
+ timer = setTimeout(() => resolve17("timeout"), timeoutMs);
32526
33398
  });
32527
33399
  const resumedPromise = this.pausePromise.then(() => "resumed").catch(() => "resumed");
32528
33400
  const winner = await Promise.race([resumedPromise, timeoutPromise]);
@@ -32615,6 +33487,843 @@ var CollaborationBus = class _CollaborationBus {
32615
33487
  }
32616
33488
  }
32617
33489
  };
33490
+
33491
+ // src/coordination/agent-status-helpers.ts
33492
+ var TOOL_TEXT_CAP = 360;
33493
+ var TOUCHED_FILE_LIMIT = 200;
33494
+ var TODO_TEXT_CAP = 360;
33495
+ var TODO_LIMIT = 32;
33496
+ function lineCount(value) {
33497
+ return value.length === 0 ? 0 : value.split(/\r?\n/).length;
33498
+ }
33499
+ function patchDelta(value) {
33500
+ let addedLines = 0;
33501
+ let removedLines = 0;
33502
+ const hunkStart = value.indexOf("@@");
33503
+ if (hunkStart === -1) return { addedLines, removedLines };
33504
+ for (const line of value.slice(hunkStart).split(/\r?\n/)) {
33505
+ if (line.startsWith("+++") || line.startsWith("---")) continue;
33506
+ if (line.startsWith("+")) addedLines += 1;
33507
+ else if (line.startsWith("-")) removedLines += 1;
33508
+ }
33509
+ return { addedLines, removedLines };
33510
+ }
33511
+ function compactText(value) {
33512
+ return value.length <= TOOL_TEXT_CAP ? value : `${value.slice(0, TOOL_TEXT_CAP - 1)}\u2026`;
33513
+ }
33514
+ function boundedText(value, cap) {
33515
+ const trimmed = value.trim();
33516
+ return trimmed.length <= cap ? trimmed : `${trimmed.slice(0, cap - 1)}\u2026`;
33517
+ }
33518
+ function compactTodos(value) {
33519
+ if (!Array.isArray(value)) return void 0;
33520
+ const todos = [];
33521
+ for (const candidate of value) {
33522
+ if (typeof candidate !== "object" || candidate === null) continue;
33523
+ const todo = candidate;
33524
+ const id = typeof todo["id"] === "string" ? todo["id"].trim() : "";
33525
+ const content = typeof todo["content"] === "string" ? boundedText(todo["content"], TODO_TEXT_CAP) : "";
33526
+ const status = todo["status"];
33527
+ if (!id || !content || status !== "pending" && status !== "in_progress" && status !== "completed") {
33528
+ continue;
33529
+ }
33530
+ const activeForm = typeof todo["activeForm"] === "string" ? boundedText(todo["activeForm"], TODO_TEXT_CAP) : void 0;
33531
+ todos.push({ id, content, status, ...activeForm ? { activeForm } : {} });
33532
+ if (todos.length >= TODO_LIMIT) break;
33533
+ }
33534
+ return todos;
33535
+ }
33536
+ function compactToolInput(input) {
33537
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
33538
+ return typeof input === "string" ? { input: compactText(input) } : {};
33539
+ }
33540
+ const source = input;
33541
+ const safe = {};
33542
+ const safeKeys = [
33543
+ "file_path",
33544
+ "filePath",
33545
+ "path",
33546
+ "filename",
33547
+ "target_file",
33548
+ "targetFile",
33549
+ "line",
33550
+ "start_line",
33551
+ "startLine",
33552
+ "end_line",
33553
+ "endLine",
33554
+ "offset",
33555
+ "limit",
33556
+ "command",
33557
+ "cmd",
33558
+ "url",
33559
+ "href",
33560
+ "uri",
33561
+ "query",
33562
+ "pattern"
33563
+ ];
33564
+ for (const key of safeKeys) {
33565
+ const value = source[key];
33566
+ if (typeof value === "string") safe[key] = compactText(value);
33567
+ else if (typeof value === "number" || typeof value === "boolean") safe[key] = value;
33568
+ }
33569
+ const content = [source["content"], source["text"], source["data"]].find(
33570
+ (value) => typeof value === "string"
33571
+ );
33572
+ const oldText = [source["old_string"], source["oldString"], source["search"]].find(
33573
+ (value) => typeof value === "string"
33574
+ );
33575
+ const newText = [source["new_string"], source["newString"], source["replacement"]].find(
33576
+ (value) => typeof value === "string"
33577
+ );
33578
+ const patch = [source["patch"], source["diff"]].find(
33579
+ (value) => typeof value === "string"
33580
+ );
33581
+ const delta = patch ? patchDelta(patch) : void 0;
33582
+ return {
33583
+ ...Object.keys(safe).length > 0 ? { input: safe } : {},
33584
+ ...content !== void 0 ? { inputLines: lineCount(content) } : {},
33585
+ ...oldText !== void 0 ? { oldLines: lineCount(oldText) } : {},
33586
+ ...newText !== void 0 ? { newLines: lineCount(newText) } : {},
33587
+ ...delta && delta.addedLines > 0 ? { addedLines: delta.addedLines } : {},
33588
+ ...delta && delta.removedLines > 0 ? { removedLines: delta.removedLines } : {}
33589
+ };
33590
+ }
33591
+ function completedToolReceipt(payload, pending) {
33592
+ const completedAt = Date.now();
33593
+ const durationMs = Math.max(
33594
+ 0,
33595
+ payload.durationMs ?? completedAt - (pending?.startedAt ?? completedAt)
33596
+ );
33597
+ const compact = compactToolInput(payload.input ?? pending?.input);
33598
+ return {
33599
+ id: payload.id ?? `${payload.name}:${completedAt}`,
33600
+ name: payload.name,
33601
+ startedAt: pending?.startedAt ?? Math.max(0, completedAt - durationMs),
33602
+ completedAt,
33603
+ durationMs,
33604
+ ok: payload.ok !== false,
33605
+ ...compact,
33606
+ ...payload.output !== void 0 ? { output: compactText(payload.output) } : {},
33607
+ ...payload.outputLines !== void 0 ? { outputLines: payload.outputLines } : {},
33608
+ ...payload.outputBytes !== void 0 ? { outputBytes: payload.outputBytes } : {},
33609
+ ...payload.outputTokens !== void 0 ? { outputTokens: payload.outputTokens } : {}
33610
+ };
33611
+ }
33612
+ function emptyActivityTotals() {
33613
+ return {
33614
+ filesTouched: [],
33615
+ reads: 0,
33616
+ writes: 0,
33617
+ edits: 0,
33618
+ terminalCalls: 0,
33619
+ webCalls: 0,
33620
+ searches: 0,
33621
+ otherCalls: 0,
33622
+ mailReceived: 0,
33623
+ mailSent: 0,
33624
+ linesRead: 0,
33625
+ linesWritten: 0,
33626
+ linesAdded: 0,
33627
+ linesRemoved: 0
33628
+ };
33629
+ }
33630
+ function addMailTotal(current, direction) {
33631
+ const next = { ...current ?? emptyActivityTotals() };
33632
+ if (direction === "incoming") next.mailReceived += 1;
33633
+ else next.mailSent += 1;
33634
+ return next;
33635
+ }
33636
+ function toolActivityKind(name) {
33637
+ const normalized = name.toLowerCase().replace(/[.:/-]+/g, "_");
33638
+ if (/^(read|view|open_file|read_file)|file_read/.test(normalized)) return "read";
33639
+ if (/^(write|create|save)|file_write/.test(normalized)) return "write";
33640
+ if (/edit|update|patch|replace|apply_patch/.test(normalized)) return "edit";
33641
+ if (/bash|shell|terminal|exec|command|powershell|cmd/.test(normalized)) return "terminal";
33642
+ if (/fetch|browser|browse|http|url|web/.test(normalized)) return "web";
33643
+ if (/search|grep|find|glob|query/.test(normalized)) return "search";
33644
+ return "other";
33645
+ }
33646
+ function receiptFilePath(receipt) {
33647
+ if (!receipt.input || typeof receipt.input !== "object" || Array.isArray(receipt.input)) {
33648
+ return void 0;
33649
+ }
33650
+ const input = receipt.input;
33651
+ for (const key of ["file_path", "filePath", "path", "filename", "target_file", "targetFile"]) {
33652
+ const value = input[key];
33653
+ if (typeof value === "string" && value.trim()) return value.trim();
33654
+ }
33655
+ return void 0;
33656
+ }
33657
+ function addToolActivity(current, receipt) {
33658
+ const next = {
33659
+ ...current ?? emptyActivityTotals(),
33660
+ filesTouched: [...current?.filesTouched ?? []]
33661
+ };
33662
+ const kind = toolActivityKind(receipt.name);
33663
+ if (kind === "read") {
33664
+ next.reads += 1;
33665
+ next.linesRead += receipt.outputLines ?? 0;
33666
+ } else if (kind === "write") {
33667
+ next.writes += 1;
33668
+ next.linesWritten += receipt.inputLines ?? 0;
33669
+ } else if (kind === "edit") {
33670
+ next.edits += 1;
33671
+ next.linesAdded += receipt.addedLines ?? receipt.newLines ?? 0;
33672
+ next.linesRemoved += receipt.removedLines ?? receipt.oldLines ?? 0;
33673
+ } else if (kind === "terminal") next.terminalCalls += 1;
33674
+ else if (kind === "web") next.webCalls += 1;
33675
+ else if (kind === "search") next.searches += 1;
33676
+ else next.otherCalls += 1;
33677
+ const filePath = receiptFilePath(receipt);
33678
+ if (filePath && !next.filesTouched.includes(filePath) && next.filesTouched.length < TOUCHED_FILE_LIMIT) {
33679
+ next.filesTouched.push(filePath);
33680
+ }
33681
+ return next;
33682
+ }
33683
+ function clampPct(pct) {
33684
+ if (!Number.isFinite(pct)) return 0;
33685
+ return Math.max(0, Math.min(100, pct));
33686
+ }
33687
+
33688
+ // src/coordination/agent-status-tracker.ts
33689
+ var AGENT_REAP_MS = 3e4;
33690
+ var AGENT_SWEEP_INTERVAL_MS = 1e4;
33691
+ var PENDING_TOOL_TTL_MS = 3e5;
33692
+ var PARTIAL_TEXT_CAP = 1200;
33693
+ var PARTIAL_FLUSH_THROTTLE_MS = 300;
33694
+ var RECENT_TOOL_LIMIT = 12;
33695
+ var RECENT_MAIL_LIMIT = 12;
33696
+ var TASK_TEXT_CAP = 1200;
33697
+ var PROMPT_TEXT_CAP = 6e3;
33698
+ var AgentStatusTracker = class _AgentStatusTracker {
33699
+ events;
33700
+ registry;
33701
+ sessionId;
33702
+ leaderName;
33703
+ // Live agent map: agentId → AgentEntry
33704
+ agents = /* @__PURE__ */ new Map();
33705
+ // Last full agent list flushed (leader + subagents). Lets external consumers
33706
+ // read the current state synchronously without re-deriving it.
33707
+ lastAgents = [];
33708
+ // Leader tracking
33709
+ leaderStatus = "idle";
33710
+ leaderCurrentTool;
33711
+ leaderCurrentTask;
33712
+ leaderIterations = 0;
33713
+ leaderToolCalls = 0;
33714
+ leaderCostUsd = 0;
33715
+ leaderTokensIn = 0;
33716
+ leaderTokensOut = 0;
33717
+ leaderCtxPct;
33718
+ leaderModel;
33719
+ leaderPartialText = "";
33720
+ leaderStartedAt;
33721
+ leaderRecentTools = [];
33722
+ leaderRecentMail = [];
33723
+ leaderTodos = [];
33724
+ leaderLatestPrompt;
33725
+ leaderLatestPromptAt;
33726
+ leaderActivity = emptyActivityTotals();
33727
+ leaderPendingTools = /* @__PURE__ */ new Map();
33728
+ subagentPendingTools = /* @__PURE__ */ new Map();
33729
+ seenIncomingMail = /* @__PURE__ */ new Set();
33730
+ seenOutgoingMail = /* @__PURE__ */ new Set();
33731
+ static SEEN_MAIL_LIMIT = 1e4;
33732
+ rememberMailId(seen, messageId) {
33733
+ if (seen.has(messageId)) return false;
33734
+ seen.add(messageId);
33735
+ if (seen.size > _AgentStatusTracker.SEEN_MAIL_LIMIT) {
33736
+ const oldest = seen.values().next().value;
33737
+ if (oldest !== void 0) seen.delete(oldest);
33738
+ }
33739
+ return true;
33740
+ }
33741
+ unsubscribers = [];
33742
+ onUpdate;
33743
+ sweepTimer = null;
33744
+ partialTimer = null;
33745
+ /** Serialize registry writes so concurrent flush() calls don't race. */
33746
+ flushPromise = null;
33747
+ constructor(opts) {
33748
+ this.events = opts.events;
33749
+ this.registry = opts.registry;
33750
+ this.sessionId = opts.sessionId;
33751
+ this.leaderName = opts.leaderName ?? "leader";
33752
+ this.onUpdate = opts.onUpdate;
33753
+ }
33754
+ /** Current full agent list (leader + subagents) as of the last flush. */
33755
+ getAgents() {
33756
+ return this.lastAgents.length > 0 ? [...this.lastAgents] : [];
33757
+ }
33758
+ start() {
33759
+ this.stop();
33760
+ const on = (pattern, fn) => this.events.onPattern(pattern, (event, payload) => {
33761
+ if (!this.acceptsSession(payload)) return;
33762
+ fn(event, payload);
33763
+ });
33764
+ this.unsubscribers.push(
33765
+ on("agent.run.started", (_event, payload) => {
33766
+ const p = payload;
33767
+ this.markLeaderStarted(p?.at);
33768
+ this.captureLeaderContext(p?.ctx);
33769
+ if (p?.model) this.leaderModel = p.model;
33770
+ if (p?.inputText?.trim()) {
33771
+ const prompt = boundedText(p.inputText, PROMPT_TEXT_CAP);
33772
+ this.leaderLatestPrompt = prompt;
33773
+ this.leaderLatestPromptAt = p.at ? Date.parse(p.at) : Date.now();
33774
+ if (!Number.isFinite(this.leaderLatestPromptAt)) this.leaderLatestPromptAt = Date.now();
33775
+ this.leaderCurrentTask = boundedText(p.inputText, TASK_TEXT_CAP);
33776
+ }
33777
+ this.leaderStatus = "running";
33778
+ this.leaderIterations++;
33779
+ this.flush();
33780
+ })
33781
+ );
33782
+ this.unsubscribers.push(
33783
+ on("iteration.started", (_e, payload) => {
33784
+ const p = payload;
33785
+ const ctx = p?.ctx;
33786
+ this.markLeaderStarted();
33787
+ this.leaderStatus = "running";
33788
+ if (typeof p?.index === "number") {
33789
+ this.leaderIterations = Math.max(this.leaderIterations, p.index + 1);
33790
+ }
33791
+ if (!ctx) {
33792
+ this.flush();
33793
+ return;
33794
+ }
33795
+ this.captureLeaderContext(ctx);
33796
+ this.flush();
33797
+ })
33798
+ );
33799
+ this.unsubscribers.push(
33800
+ on("agent.run.completed", (_event, payload) => {
33801
+ const p = payload;
33802
+ this.captureLeaderContext(p?.ctx);
33803
+ this.leaderStatus = p?.status === "failed" ? "error" : "idle";
33804
+ this.leaderCurrentTool = void 0;
33805
+ this.leaderCurrentTask = void 0;
33806
+ this.leaderPartialText = "";
33807
+ if (this.leaderStatus === "idle") this.leaderStartedAt = void 0;
33808
+ this.flush();
33809
+ })
33810
+ );
33811
+ this.unsubscribers.push(
33812
+ on("agent.run.error", (_event, payload) => {
33813
+ const p = payload;
33814
+ this.captureLeaderContext(p?.ctx);
33815
+ this.leaderStatus = "error";
33816
+ this.leaderCurrentTool = void 0;
33817
+ this.leaderCurrentTask = void 0;
33818
+ this.leaderPartialText = "";
33819
+ this.flush();
33820
+ })
33821
+ );
33822
+ this.unsubscribers.push(
33823
+ on("iteration.completed", (_event, payload) => {
33824
+ const p = payload;
33825
+ this.captureLeaderContext(p?.ctx);
33826
+ this.flush();
33827
+ })
33828
+ );
33829
+ this.unsubscribers.push(
33830
+ on("tool.started", (_event, payload) => {
33831
+ const p = payload;
33832
+ if (p?.name) {
33833
+ this.markLeaderStarted();
33834
+ this.leaderCurrentTool = p.name;
33835
+ this.leaderToolCalls++;
33836
+ this.leaderPendingTools.set(p.id ?? p.name, {
33837
+ name: p.name,
33838
+ input: p.input,
33839
+ startedAt: Date.now()
33840
+ });
33841
+ }
33842
+ this.leaderStatus = "running";
33843
+ this.flush();
33844
+ })
33845
+ );
33846
+ this.unsubscribers.push(
33847
+ on("tool.executed", (_event, payload) => {
33848
+ const p = payload;
33849
+ if (p?.name) {
33850
+ const key = p.id ?? p.name;
33851
+ const receipt = completedToolReceipt(p, this.leaderPendingTools.get(key));
33852
+ this.leaderRecentTools = [receipt, ...this.leaderRecentTools].slice(0, RECENT_TOOL_LIMIT);
33853
+ this.leaderActivity = addToolActivity(this.leaderActivity, receipt);
33854
+ this.leaderPendingTools.delete(key);
33855
+ }
33856
+ this.leaderCurrentTool = void 0;
33857
+ this.flush();
33858
+ })
33859
+ );
33860
+ this.unsubscribers.push(
33861
+ on("brain.ask_human", () => {
33862
+ this.markLeaderStarted();
33863
+ this.leaderStatus = "waiting_user";
33864
+ this.flush();
33865
+ })
33866
+ );
33867
+ const recordMail = (direction, payload) => {
33868
+ const p = payload;
33869
+ if (!p?.messageId) return;
33870
+ const seen = direction === "incoming" ? this.seenIncomingMail : this.seenOutgoingMail;
33871
+ if (!this.rememberMailId(seen, p.messageId)) return;
33872
+ const receipt = {
33873
+ id: p.messageId,
33874
+ direction,
33875
+ from: p.from ?? "?",
33876
+ to: p.to ?? (direction === "incoming" ? "leader" : "?"),
33877
+ type: p.type ?? "note",
33878
+ subject: p.subject ?? "Message",
33879
+ at: Date.now()
33880
+ };
33881
+ const directAgentId = direction === "outgoing" ? p.from : p.to;
33882
+ const entry = directAgentId ? this.agents.get(directAgentId) : void 0;
33883
+ if (entry) {
33884
+ entry.recentMail = [receipt, ...entry.recentMail ?? []].slice(0, RECENT_MAIL_LIMIT);
33885
+ entry.activity = addMailTotal(entry.activity, direction);
33886
+ } else {
33887
+ this.leaderRecentMail = [receipt, ...this.leaderRecentMail].slice(0, RECENT_MAIL_LIMIT);
33888
+ this.leaderActivity = addMailTotal(this.leaderActivity, direction);
33889
+ }
33890
+ this.flush();
33891
+ };
33892
+ this.unsubscribers.push(
33893
+ on("mailbox.message_sent", (_event, payload) => recordMail("outgoing", payload)),
33894
+ on("mailbox.received", (_event, payload) => recordMail("incoming", payload))
33895
+ );
33896
+ this.unsubscribers.push(
33897
+ on("llm.stream_started", () => {
33898
+ this.markLeaderStarted();
33899
+ this.leaderStatus = "streaming";
33900
+ this.leaderPartialText = "";
33901
+ this.flush();
33902
+ })
33903
+ );
33904
+ this.unsubscribers.push(
33905
+ on("provider.text_delta", (_e, payload) => {
33906
+ const p = payload;
33907
+ const text = p?.text;
33908
+ if (!text) return;
33909
+ this.markLeaderStarted();
33910
+ this.captureLeaderContext(p?.ctx);
33911
+ this.leaderStatus = "streaming";
33912
+ const next = this.leaderPartialText + text;
33913
+ this.leaderPartialText = next.length > PARTIAL_TEXT_CAP ? next.slice(next.length - PARTIAL_TEXT_CAP) : next;
33914
+ this.schedulePartialFlush();
33915
+ })
33916
+ );
33917
+ this.unsubscribers.push(
33918
+ on("provider.response", (_e, payload) => {
33919
+ const p = payload;
33920
+ this.captureLeaderContext(p?.ctx);
33921
+ this.flush();
33922
+ })
33923
+ );
33924
+ this.unsubscribers.push(
33925
+ on("provider.fallback", (_e, payload) => {
33926
+ const p = payload;
33927
+ if (p?.to?.model) {
33928
+ this.leaderModel = p.to.providerId ? `${p.to.providerId}/${p.to.model}` : p.to.model;
33929
+ this.flush();
33930
+ }
33931
+ })
33932
+ );
33933
+ this.unsubscribers.push(
33934
+ on("ctx.pct", (_e, payload) => {
33935
+ const p = payload;
33936
+ if (typeof p?.load === "number" && Number.isFinite(p.load)) {
33937
+ this.leaderCtxPct = clampPct(Math.round(p.load * 100));
33938
+ this.flush();
33939
+ }
33940
+ })
33941
+ );
33942
+ this.unsubscribers.push(
33943
+ on("token.accounted", (_e, payload) => {
33944
+ const p = payload;
33945
+ if (!p) return;
33946
+ this.leaderTokensIn += p.usage?.input ?? 0;
33947
+ this.leaderTokensOut += p.usage?.output ?? 0;
33948
+ this.leaderCostUsd += p.cost?.total ?? 0;
33949
+ this.flush();
33950
+ })
33951
+ );
33952
+ const touch = (id) => {
33953
+ let entry = this.agents.get(id);
33954
+ if (!entry) {
33955
+ const now = (/* @__PURE__ */ new Date()).toISOString();
33956
+ entry = {
33957
+ id,
33958
+ name: id,
33959
+ status: "idle",
33960
+ iterations: 0,
33961
+ toolCalls: 0,
33962
+ startedAt: now,
33963
+ lastActivityAt: now
33964
+ };
33965
+ this.agents.set(id, entry);
33966
+ }
33967
+ entry.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
33968
+ return entry;
33969
+ };
33970
+ this.unsubscribers.push(
33971
+ on("subagent.spawned", (_e, payload) => {
33972
+ const p = payload;
33973
+ if (!p?.subagentId) return;
33974
+ const entry = touch(p.subagentId);
33975
+ entry.name = p.name?.trim() || entry.name;
33976
+ if (p.model) entry.model = p.model;
33977
+ if (p.taskId) entry.taskId = p.taskId;
33978
+ if (p.description?.trim()) entry.currentTask = boundedText(p.description, TASK_TEXT_CAP);
33979
+ if (!entry.startedAt) entry.startedAt = (/* @__PURE__ */ new Date()).toISOString();
33980
+ entry.status = "running";
33981
+ this.flush();
33982
+ })
33983
+ );
33984
+ this.unsubscribers.push(
33985
+ on("subagent.ctx_pct", (_e, payload) => {
33986
+ const p = payload;
33987
+ if (!p?.subagentId) return;
33988
+ const entry = touch(p.subagentId);
33989
+ if (typeof p.load === "number") entry.ctxPct = clampPct(Math.round(p.load * 100));
33990
+ this.flush();
33991
+ })
33992
+ );
33993
+ this.unsubscribers.push(
33994
+ on("subagent.task_started", (_e, payload) => {
33995
+ const p = payload;
33996
+ if (!p?.subagentId) return;
33997
+ const entry = touch(p.subagentId);
33998
+ entry.status = "running";
33999
+ if (p.taskId) entry.taskId = p.taskId;
34000
+ if (p.description?.trim()) entry.currentTask = boundedText(p.description, TASK_TEXT_CAP);
34001
+ if (!entry.startedAt) entry.startedAt = (/* @__PURE__ */ new Date()).toISOString();
34002
+ entry.iterations++;
34003
+ this.flush();
34004
+ })
34005
+ );
34006
+ this.unsubscribers.push(
34007
+ on("subagent.tool_started", (_e, payload) => {
34008
+ const p = payload;
34009
+ if (!p?.subagentId || !p.name) return;
34010
+ const entry = touch(p.subagentId);
34011
+ entry.status = "running";
34012
+ entry.currentTool = p.name;
34013
+ this.subagentPendingTools.set(`${p.subagentId}:${p.id ?? p.name}`, {
34014
+ name: p.name,
34015
+ input: p.input,
34016
+ startedAt: Date.now()
34017
+ });
34018
+ this.flush();
34019
+ })
34020
+ );
34021
+ this.unsubscribers.push(
34022
+ on("subagent.tool_executed", (_e, payload) => {
34023
+ const p = payload;
34024
+ if (!p?.subagentId || !p.name) return;
34025
+ const entry = touch(p.subagentId);
34026
+ entry.status = "running";
34027
+ if (!entry.startedAt) entry.startedAt = (/* @__PURE__ */ new Date()).toISOString();
34028
+ const key = `${p.subagentId}:${p.id ?? p.name}`;
34029
+ const receipt = completedToolReceipt(p, this.subagentPendingTools.get(key));
34030
+ entry.recentTools = [receipt, ...entry.recentTools ?? []].slice(0, RECENT_TOOL_LIMIT);
34031
+ entry.activity = addToolActivity(entry.activity, receipt);
34032
+ this.subagentPendingTools.delete(key);
34033
+ entry.currentTool = void 0;
34034
+ entry.toolCalls++;
34035
+ this.flush();
34036
+ })
34037
+ );
34038
+ this.unsubscribers.push(
34039
+ on("subagent.iteration_summary", (_e, payload) => {
34040
+ const p = payload;
34041
+ if (!p?.subagentId) return;
34042
+ const entry = touch(p.subagentId);
34043
+ entry.status = "running";
34044
+ if (!entry.startedAt) entry.startedAt = (/* @__PURE__ */ new Date()).toISOString();
34045
+ if (typeof p.iteration === "number") entry.iterations = p.iteration;
34046
+ if (typeof p.toolCalls === "number") entry.toolCalls = p.toolCalls;
34047
+ if (typeof p.costUsd === "number") entry.costUsd = p.costUsd;
34048
+ if (p.currentTool) entry.currentTool = p.currentTool;
34049
+ if (typeof p.partialText === "string") {
34050
+ entry.partialText = p.partialText.length > PARTIAL_TEXT_CAP ? p.partialText.slice(p.partialText.length - PARTIAL_TEXT_CAP) : p.partialText;
34051
+ }
34052
+ this.flush();
34053
+ })
34054
+ );
34055
+ this.unsubscribers.push(
34056
+ on("subagent.task_completed", (_e, payload) => {
34057
+ const p = payload;
34058
+ if (!p?.subagentId) return;
34059
+ const entry = this.agents.get(p.subagentId);
34060
+ if (!entry) return;
34061
+ entry.status = p.status === "failed" || p.status === "timeout" ? "error" : "idle";
34062
+ entry.currentTool = void 0;
34063
+ entry.currentTask = void 0;
34064
+ entry.taskId = void 0;
34065
+ entry.partialText = void 0;
34066
+ if (typeof p.iterations === "number") entry.iterations = p.iterations;
34067
+ if (typeof p.toolCalls === "number") entry.toolCalls = p.toolCalls;
34068
+ entry.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
34069
+ this.flush();
34070
+ })
34071
+ );
34072
+ this.unsubscribers.push(
34073
+ on("subagent.stopped", (_e, payload) => {
34074
+ const p = payload;
34075
+ if (!p?.subagentId) return;
34076
+ if (this.agents.delete(p.subagentId)) this.flush();
34077
+ })
34078
+ );
34079
+ this.unsubscribers.push(
34080
+ on("subagent.removed", (_e, payload) => {
34081
+ const p = payload;
34082
+ if (!p?.subagentId) return;
34083
+ if (this.agents.delete(p.subagentId)) this.flush();
34084
+ })
34085
+ );
34086
+ this.sweepTimer = setInterval(() => this.sweep(), AGENT_SWEEP_INTERVAL_MS);
34087
+ if (typeof this.sweepTimer.unref === "function") this.sweepTimer.unref();
34088
+ }
34089
+ stop() {
34090
+ for (const unsub of this.unsubscribers) {
34091
+ try {
34092
+ unsub();
34093
+ } catch {
34094
+ }
34095
+ }
34096
+ this.unsubscribers = [];
34097
+ if (this.sweepTimer) {
34098
+ clearInterval(this.sweepTimer);
34099
+ this.sweepTimer = null;
34100
+ }
34101
+ if (this.partialTimer) {
34102
+ clearTimeout(this.partialTimer);
34103
+ this.partialTimer = null;
34104
+ }
34105
+ this.leaderPendingTools.clear();
34106
+ this.subagentPendingTools.clear();
34107
+ }
34108
+ /**
34109
+ * Coalesce streamed-text flushes: at most one registry write per
34110
+ * {@link PARTIAL_FLUSH_THROTTLE_MS} while text streams in, so per-token
34111
+ * deltas never thrash the cross-process registry file.
34112
+ */
34113
+ schedulePartialFlush() {
34114
+ if (this.partialTimer) return;
34115
+ this.partialTimer = setTimeout(() => {
34116
+ this.partialTimer = null;
34117
+ this.flush();
34118
+ }, PARTIAL_FLUSH_THROTTLE_MS);
34119
+ if (typeof this.partialTimer.unref === "function") this.partialTimer.unref();
34120
+ }
34121
+ /**
34122
+ * Remove subagents that have been finished (idle/error) for longer than
34123
+ * {@link AGENT_REAP_MS}. Running / streaming / waiting_user agents are kept
34124
+ * regardless of age — only *not-working* agents are reaped.
34125
+ *
34126
+ * Also trims orphaned PendingTool entries older than {@link PENDING_TOOL_TTL_MS}
34127
+ * from both leader and subagent pending-tools Maps. Piggybacks on the same
34128
+ * 10-second interval to avoid a second timer.
34129
+ */
34130
+ sweep() {
34131
+ const now = Date.now();
34132
+ let removed = false;
34133
+ for (const [id, a] of this.agents) {
34134
+ const finished = a.status !== "running" && a.status !== "streaming" && a.status !== "waiting_user";
34135
+ const age = now - Date.parse(a.lastActivityAt);
34136
+ if (finished && Number.isFinite(age) && age > AGENT_REAP_MS) {
34137
+ this.agents.delete(id);
34138
+ removed = true;
34139
+ }
34140
+ }
34141
+ this.trimPendingTools(this.leaderPendingTools, now);
34142
+ this.trimPendingTools(this.subagentPendingTools, now);
34143
+ if (removed) this.flush();
34144
+ }
34145
+ /**
34146
+ * Evict pending-tool entries whose `startedAt` is older than
34147
+ * {@link PENDING_TOOL_TTL_MS}. These are tool.started / subagent.tool_started
34148
+ * events whose matching tool.executed / subagent.tool_executed never arrived
34149
+ * (provider crash, abort, process kill). Without periodic cleanup the
34150
+ * Maps retain the full tool-input objects for the process lifetime.
34151
+ */
34152
+ trimPendingTools(map, now) {
34153
+ const cutoff = now - PENDING_TOOL_TTL_MS;
34154
+ for (const [key, entry] of map) {
34155
+ if (entry.startedAt < cutoff) map.delete(key);
34156
+ }
34157
+ }
34158
+ flush() {
34159
+ const leaderEntry = {
34160
+ id: "leader",
34161
+ name: this.leaderName,
34162
+ startedAt: this.leaderStartedAt,
34163
+ status: this.leaderStatus,
34164
+ currentTool: this.leaderCurrentTool,
34165
+ currentTask: this.leaderCurrentTask,
34166
+ iterations: this.leaderIterations,
34167
+ toolCalls: this.leaderToolCalls,
34168
+ costUsd: this.leaderCostUsd,
34169
+ tokensIn: this.leaderTokensIn,
34170
+ tokensOut: this.leaderTokensOut,
34171
+ ctxPct: this.leaderCtxPct,
34172
+ model: this.leaderModel,
34173
+ partialText: this.leaderPartialText || void 0,
34174
+ recentTools: this.leaderRecentTools,
34175
+ recentMail: this.leaderRecentMail,
34176
+ todos: this.leaderTodos,
34177
+ latestPrompt: this.leaderLatestPrompt,
34178
+ latestPromptAt: this.leaderLatestPromptAt,
34179
+ activity: this.leaderActivity,
34180
+ lastActivityAt: (/* @__PURE__ */ new Date()).toISOString()
34181
+ };
34182
+ const allAgents = [leaderEntry, ...this.agents.values()];
34183
+ this.lastAgents = allAgents;
34184
+ try {
34185
+ this.events.emit("session.agents_updated", {
34186
+ sessionId: this.currentSessionId(),
34187
+ agents: allAgents
34188
+ });
34189
+ } catch {
34190
+ }
34191
+ const write = this.registry.updateAgents(allAgents);
34192
+ const chain = write.then(() => {
34193
+ try {
34194
+ this.onUpdate?.();
34195
+ } catch {
34196
+ }
34197
+ });
34198
+ if (this.flushPromise) {
34199
+ this.flushPromise = this.flushPromise.then(
34200
+ () => chain,
34201
+ () => chain
34202
+ ).catch(() => void 0);
34203
+ } else {
34204
+ this.flushPromise = chain.catch(() => void 0);
34205
+ }
34206
+ }
34207
+ currentSessionId() {
34208
+ return typeof this.sessionId === "function" ? this.sessionId() : this.sessionId;
34209
+ }
34210
+ acceptsSession(payload) {
34211
+ const expected = this.currentSessionId();
34212
+ if (!expected) return true;
34213
+ if (typeof payload !== "object" || payload === null) return true;
34214
+ const actual = payload.sessionId;
34215
+ return typeof actual !== "string" || actual.length === 0 || actual === expected;
34216
+ }
34217
+ markLeaderStarted(startedAt) {
34218
+ if (this.leaderStartedAt && (this.leaderStatus === "running" || this.leaderStatus === "streaming" || this.leaderStatus === "waiting_user")) {
34219
+ return;
34220
+ }
34221
+ this.leaderStartedAt = startedAt ?? (/* @__PURE__ */ new Date()).toISOString();
34222
+ }
34223
+ captureLeaderContext(ctx) {
34224
+ if (typeof ctx !== "object" || ctx === null) return;
34225
+ const c = ctx;
34226
+ if (typeof c.model === "string" && c.model.length > 0) this.leaderModel = c.model;
34227
+ const todos = compactTodos(c.todos);
34228
+ if (todos !== void 0) this.leaderTodos = todos;
34229
+ const metaLimit = c.meta?.["effectiveMaxContext"];
34230
+ const providerMax = c.provider?.capabilities?.maxContext;
34231
+ const maxContext = typeof metaLimit === "number" && metaLimit > 0 ? metaLimit : typeof providerMax === "number" && providerMax > 0 ? providerMax : void 0;
34232
+ if (typeof c.lastRequestTokens === "number" && c.lastRequestTokens > 0 && maxContext !== void 0) {
34233
+ this.leaderCtxPct = clampPct(Math.round(c.lastRequestTokens / maxContext * 100));
34234
+ }
34235
+ }
34236
+ };
34237
+
34238
+ // src/coordination/fleet-notifier.ts
34239
+ import * as fs8 from "node:fs/promises";
34240
+ import * as path43 from "node:path";
34241
+ var INSTANCES_FILE = "webui-instances.json";
34242
+ var DISCOVERY_TTL_MS = 2500;
34243
+ var COALESCE_MS = 50;
34244
+ var POST_TIMEOUT_MS = 500;
34245
+ var pidAlive = isPidAlive;
34246
+ function normRoot(root) {
34247
+ const resolved = path43.resolve(root);
34248
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
34249
+ }
34250
+ var FleetNotifier = class {
34251
+ baseDir;
34252
+ projectRoot;
34253
+ selfPid;
34254
+ doPost;
34255
+ cache = null;
34256
+ timer = null;
34257
+ disposed = false;
34258
+ constructor(opts) {
34259
+ this.baseDir = opts.baseDir;
34260
+ this.projectRoot = normRoot(opts.projectRoot);
34261
+ this.selfPid = opts.selfPid ?? process.pid;
34262
+ this.doPost = opts.post ?? defaultPost;
34263
+ }
34264
+ /** Coalesced, best-effort nudge. Safe to call on every status change. */
34265
+ notify() {
34266
+ if (this.disposed || this.timer) return;
34267
+ this.timer = setTimeout(() => {
34268
+ this.timer = null;
34269
+ void this.flush();
34270
+ }, COALESCE_MS);
34271
+ if (typeof this.timer.unref === "function") this.timer.unref();
34272
+ }
34273
+ /** Resolve same-project WebUI ping URLs (cached briefly). Exposed for tests. */
34274
+ async endpoints() {
34275
+ return (await this.targets()).map((t) => t.url);
34276
+ }
34277
+ /** Ping targets with their tokens (cached briefly). */
34278
+ async targets() {
34279
+ const now = Date.now();
34280
+ if (this.cache && now - this.cache.at < DISCOVERY_TTL_MS) return this.cache.targets;
34281
+ const targets = await this.discover();
34282
+ this.cache = { at: now, targets };
34283
+ return targets;
34284
+ }
34285
+ dispose() {
34286
+ this.disposed = true;
34287
+ if (this.timer) {
34288
+ clearTimeout(this.timer);
34289
+ this.timer = null;
34290
+ }
34291
+ }
34292
+ /** Re-scope notifications after an in-process project switch. */
34293
+ setProjectRoot(projectRoot) {
34294
+ this.projectRoot = normRoot(projectRoot);
34295
+ this.cache = null;
34296
+ }
34297
+ async flush() {
34298
+ const targets = await this.targets();
34299
+ await Promise.all(targets.map((t) => this.doPost(t.url, t.token).catch(() => void 0)));
34300
+ }
34301
+ async discover() {
34302
+ try {
34303
+ const raw = await fs8.readFile(path43.join(this.baseDir, INSTANCES_FILE), "utf8");
34304
+ const data = JSON.parse(raw);
34305
+ const list = Array.isArray(data?.instances) ? data.instances : [];
34306
+ return list.filter((i) => i && typeof i.httpPort === "number").filter((i) => i.pid !== this.selfPid).filter((i) => normRoot(i.projectRoot) === this.projectRoot).filter((i) => pidAlive(i.pid)).map((i) => {
34307
+ const host = i.host === "0.0.0.0" || i.host === "::" || !i.host ? "127.0.0.1" : i.host;
34308
+ return {
34309
+ url: `http://${host}:${i.httpPort}/api/fleet/ping`,
34310
+ token: typeof i.authToken === "string" ? i.authToken : void 0
34311
+ };
34312
+ });
34313
+ } catch {
34314
+ return [];
34315
+ }
34316
+ }
34317
+ };
34318
+ async function defaultPost(url, token) {
34319
+ await fetch(url, {
34320
+ method: "POST",
34321
+ // The API requires a token on every bind (H3). Sent as a header rather
34322
+ // than a query param so it never lands in an access log or the URL.
34323
+ ...token ? { headers: { "x-ws-token": token } } : {},
34324
+ signal: AbortSignal.timeout(POST_TIMEOUT_MS)
34325
+ });
34326
+ }
32618
34327
  export {
32619
34328
  ACP_AGENTS,
32620
34329
  AGENTS_BY_PHASE,
@@ -32624,6 +34333,7 @@ export {
32624
34333
  AUDIT_LOG_AGENT,
32625
34334
  AdaptiveConcurrencyController,
32626
34335
  AgentMonitorService,
34336
+ AgentStatusTracker,
32627
34337
  AutonomousBrain,
32628
34338
  AutonomousCoordinator,
32629
34339
  BUG_HUNTER_AGENT,
@@ -32677,6 +34387,7 @@ export {
32677
34387
  FleetBus,
32678
34388
  FleetCostCapError,
32679
34389
  FleetManager,
34390
+ FleetNotifier,
32680
34391
  FleetSpawnBudgetError,
32681
34392
  FleetSupervisor,
32682
34393
  FleetTokenCapError,
@@ -32798,6 +34509,8 @@ export {
32798
34509
  isMailboxProjectServerAvailable,
32799
34510
  isProvenDirective,
32800
34511
  isValidMatrixKey,
34512
+ kanbanBoundaryOps,
34513
+ kanbanDispatch,
32801
34514
  listProjectAgentLearnedEntries,
32802
34515
  listProjectAgentRoles,
32803
34516
  listProjectSkillAugmentations,
@@ -32889,6 +34602,8 @@ export {
32889
34602
  scoreSkillAffinity,
32890
34603
  scrubRetiredLines,
32891
34604
  sessionRecipient,
34605
+ setKanbanBoundaryOps,
34606
+ setKanbanDispatch,
32892
34607
  setSkillPinned,
32893
34608
  slugifyProjectAgentRole,
32894
34609
  startPackageOutdatedWatcher,