@wrongstack/core 0.309.0 → 0.310.0

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 (129) 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/director.d.ts +7 -0
  13. package/dist/coordination/explore-companion.d.ts +9 -6
  14. package/dist/coordination/fleet-status-tool.d.ts +1 -1
  15. package/dist/coordination/index.d.ts +5 -1
  16. package/dist/coordination/index.js +2392 -405
  17. package/dist/coordination/kanban-dispatch-port.d.ts +30 -0
  18. package/dist/coordination/kanban-ops-port.d.ts +21 -0
  19. package/dist/coordination/mailbox-hooks.d.ts +1 -1
  20. package/dist/coordination/mailbox-project-server.js +14 -9
  21. package/dist/coordination/mutation-engine.d.ts +5 -3
  22. package/dist/core/context.d.ts +35 -44
  23. package/dist/core/conversation-state.d.ts +16 -52
  24. package/dist/core/index.js +92 -25
  25. package/dist/core/provider-runner.d.ts +2 -2
  26. package/dist/core/run-env.d.ts +7 -28
  27. package/dist/core/streaming-response-builder.d.ts +2 -2
  28. package/dist/execution/index.js +138 -162
  29. package/dist/goal/index.js +100 -22
  30. package/dist/hq/auth-store.d.ts +9 -0
  31. package/dist/hq/cost-bridge.d.ts +1 -1
  32. package/dist/hq/index.js +69 -7
  33. package/dist/index.d.ts +12 -5
  34. package/dist/index.js +21185 -26807
  35. package/dist/infrastructure/index.js +232 -135
  36. package/dist/infrastructure/provider-cache-ledger.d.ts +1 -1
  37. package/dist/infrastructure/token-counter.d.ts +5 -1
  38. package/dist/kernel/events/file-events.d.ts +6 -0
  39. package/dist/kernel/events/provider-events.d.ts +10 -7
  40. package/dist/kernel/events/session-events.d.ts +4 -4
  41. package/dist/kernel/events/tool-events.d.ts +12 -2
  42. package/dist/observability/index.js +1 -1
  43. package/dist/plugin/index.js +218 -47
  44. package/dist/prompts/index.js +360 -3
  45. package/dist/security/auto-approve-policy.d.ts +2 -2
  46. package/dist/security/index.d.ts +1 -0
  47. package/dist/security/index.js +224 -68
  48. package/dist/security/kanban-boundary.d.ts +1 -1
  49. package/dist/security/kanban-governance-port.d.ts +28 -0
  50. package/dist/security/permission-helpers.d.ts +11 -0
  51. package/dist/security/permission-policy.d.ts +10 -1
  52. package/dist/security/yolo-risk.d.ts +17 -0
  53. package/dist/session-catalog/index.js +34 -5
  54. package/dist/session-catalog/project-server.js +34 -5
  55. package/dist/session-catalog/protocol.d.ts +1 -1
  56. package/dist/session-catalog/registry.d.ts +1 -1
  57. package/dist/session-catalog/store-schema.d.ts +1 -1
  58. package/dist/session-catalog/store.d.ts +1 -1
  59. package/dist/skills/index.js +39 -6
  60. package/dist/storage/annotations-store.d.ts +1 -1
  61. package/dist/storage/board-store-port.d.ts +45 -0
  62. package/dist/storage/completed-work-checkpoint.d.ts +1 -1
  63. package/dist/storage/config-loader/types.d.ts +1 -1
  64. package/dist/storage/event-bus-port.d.ts +27 -0
  65. package/dist/storage/file-session-writer.d.ts +47 -5
  66. package/dist/storage/goal-coordination.d.ts +1 -1
  67. package/dist/storage/goal-store.d.ts +1 -1
  68. package/dist/storage/index.d.ts +3 -4
  69. package/dist/storage/index.js +1107 -1462
  70. package/dist/storage/plan-store.d.ts +1 -1
  71. package/dist/storage/queue-store.d.ts +1 -1
  72. package/dist/storage/replay-log-store.d.ts +1 -1
  73. package/dist/storage/session-recovery.d.ts +10 -0
  74. package/dist/storage/session-resume-validation.d.ts +13 -1
  75. package/dist/storage/session-store/events.d.ts +1 -1
  76. package/dist/storage/session-store/load-session-data.d.ts +1 -1
  77. package/dist/storage/session-store/rename-session.d.ts +1 -1
  78. package/dist/storage/session-store/resume-session.d.ts +3 -1
  79. package/dist/storage/session-store/session-store-index.d.ts +2 -1
  80. package/dist/storage/session-store/types.d.ts +1 -1
  81. package/dist/storage/session-store.d.ts +70 -0
  82. package/dist/storage/session-summary-tracker.d.ts +8 -0
  83. package/dist/storage/session-write-buffer.d.ts +31 -2
  84. package/dist/storage/task-store.d.ts +1 -1
  85. package/dist/storage/todos-checkpoint.d.ts +1 -1
  86. package/dist/storage/tool-audit-log.d.ts +1 -1
  87. package/dist/tasking/index.js +100 -22
  88. package/dist/tasking/task-tracker.d.ts +24 -1
  89. package/dist/types/compactor.d.ts +2 -2
  90. package/dist/types/context.d.ts +212 -0
  91. package/dist/types/conversation-state.d.ts +109 -0
  92. package/dist/types/error-handler.d.ts +2 -2
  93. package/dist/types/file-event-record.d.ts +6 -0
  94. package/dist/types/index.d.ts +3 -3
  95. package/dist/types/index.js +17 -1
  96. package/dist/types/permission.d.ts +3 -3
  97. package/dist/types/plugin.d.ts +3 -3
  98. package/dist/types/provider-runner.d.ts +2 -2
  99. package/dist/types/provider.d.ts +10 -0
  100. package/dist/types/run-env.d.ts +32 -0
  101. package/dist/types/session.d.ts +3 -0
  102. package/dist/types/slash-command.d.ts +2 -2
  103. package/dist/types/token-counter.d.ts +30 -1
  104. package/dist/types/tool-executor.d.ts +2 -2
  105. package/dist/types/tool.d.ts +20 -5
  106. package/dist/utils/context-breakdown.d.ts +2 -2
  107. package/dist/utils/context-evidence.d.ts +12 -12
  108. package/dist/utils/crash-shield.d.ts +9 -0
  109. package/dist/utils/heap-watchdog.js +11 -3
  110. package/dist/utils/index.d.ts +2 -1
  111. package/dist/utils/index.js +126 -160
  112. package/dist/utils/regex-guard.d.ts +7 -30
  113. package/dist/utils/terminal-sanitize.d.ts +41 -0
  114. package/dist/utils/todos-format.d.ts +1 -1
  115. package/dist/utils/tool-subject.d.ts +1 -1
  116. package/dist/utils/tree-kill.d.ts +2 -0
  117. package/dist/utils/tree-kill.js +1 -0
  118. package/instructions/agents/chaos-monkey.md +5 -1
  119. package/instructions/coordination/subagent-baseline.md +10 -0
  120. package/instructions/system-lite.md +2 -0
  121. package/instructions/system-pro.md +40 -0
  122. package/instructions/system.md +15 -0
  123. package/package.json +7 -10
  124. package/dist/defaults/index.d.ts +0 -69
  125. package/dist/defaults/index.js +0 -37755
  126. /package/dist/{fleet-notifier.d.ts → coordination/fleet-notifier.d.ts} +0 -0
  127. /package/dist/{session-registry-atomic-file.d.ts → session-catalog/session-registry-atomic-file.d.ts} +0 -0
  128. /package/dist/{session-registry-types.d.ts → session-catalog/session-registry-types.d.ts} +0 -0
  129. /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;
@@ -8599,6 +8599,14 @@ var PATTERNS = [
8599
8599
  anchor: "sk-ant-"
8600
8600
  },
8601
8601
  { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g, anchor: "sk-" },
8602
+ {
8603
+ // `xai` is a first-class provider in this codebase, but its key shape was
8604
+ // absent here — so the one credential format WrongStack itself hands users
8605
+ // was the one the scrubber could not recognize (audit 2026-08-20).
8606
+ type: "xai_key",
8607
+ regex: /(?<![A-Za-z0-9])xai-[A-Za-z0-9]{20,}(?![A-Za-z0-9])/g,
8608
+ anchor: "xai-"
8609
+ },
8602
8610
  { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g, anchor: "ghp_" },
8603
8611
  { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g, anchor: "github_pat_" },
8604
8612
  { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g, anchor: "AKIA" },
@@ -8689,8 +8697,8 @@ var PATTERNS = [
8689
8697
  // replacement so the separator between adjacent secrets is preserved
8690
8698
  // rather than collapsed. Capture groups are therefore: 1=leading
8691
8699
  // delimiter, 2=key name, 3=value.
8692
- regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
8693
- anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
8700
+ regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD|PASSPHRASE))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
8701
+ anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD", "PASSPHRASE"]
8694
8702
  },
8695
8703
  {
8696
8704
  type: "json_credential_key",
@@ -8807,6 +8815,27 @@ var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key
8807
8815
  var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
8808
8816
  var SCRUB_CHUNK_BYTES = 64 * 1024;
8809
8817
  var SCRUB_OVERLAP_BYTES = 1024;
8818
+ var PEM_PRIVATE_KEY_BEGIN_RE = /-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----/;
8819
+ var PEM_END_MARKER = "-----END";
8820
+ var MAX_PEM_BLOCK_BYTES = 64 * 1024;
8821
+ var PEM_END_LINE_TOLERANCE = 64;
8822
+ function extendChunkBoundaryPastPem(text, chunkStart, proposedEnd) {
8823
+ const head = text.slice(chunkStart, proposedEnd);
8824
+ const lastBegin = head.lastIndexOf("-----BEGIN ");
8825
+ if (lastBegin === -1) return proposedEnd;
8826
+ const fromBegin = text.slice(chunkStart + lastBegin);
8827
+ const marker = PEM_PRIVATE_KEY_BEGIN_RE.exec(fromBegin);
8828
+ if (!marker || marker.index !== 0) return proposedEnd;
8829
+ const bodyStart = marker[0].length;
8830
+ const cap = Math.min(text.length, chunkStart + lastBegin + MAX_PEM_BLOCK_BYTES);
8831
+ const closeIdx = fromBegin.indexOf(PEM_END_MARKER, bodyStart);
8832
+ if (closeIdx === -1 || chunkStart + lastBegin + closeIdx >= cap + PEM_END_LINE_TOLERANCE) {
8833
+ return proposedEnd;
8834
+ }
8835
+ const lineEnd = fromBegin.indexOf("\n", closeIdx);
8836
+ const end = lineEnd === -1 ? text.length : chunkStart + lastBegin + lineEnd + 1;
8837
+ return Math.max(proposedEnd, end);
8838
+ }
8810
8839
  var PATTERN_ANCHORS = [
8811
8840
  ...new Set(
8812
8841
  PATTERNS.flatMap(
@@ -8843,6 +8872,7 @@ var DefaultSecretScrubber = class {
8843
8872
  }
8844
8873
  }
8845
8874
  end = safe === -1 ? end : safe + 1;
8875
+ end = extendChunkBoundaryPastPem(text, i, end);
8846
8876
  }
8847
8877
  out.push(this.scrubOne(text.slice(i, end)));
8848
8878
  i = end;
@@ -9259,49 +9289,57 @@ async function expandGlob(pattern) {
9259
9289
  async function walk(dir, pat) {
9260
9290
  let entries;
9261
9291
  try {
9262
- entries = await fsp.readdir(dir);
9292
+ entries = await fsp.readdir(dir, { withFileTypes: true });
9263
9293
  } catch {
9264
9294
  return;
9265
9295
  }
9266
- const firstGlob = pat.search(/[*?[[]/);
9267
- if (firstGlob < 0) {
9268
- const re = globToRegex(pat);
9296
+ if (pat.startsWith("**/")) {
9297
+ const rest = pat.slice(3);
9298
+ await walk(dir, rest);
9269
9299
  for (const e of entries) {
9270
- if (re.test(e)) {
9271
- const full = `${dir}${SEP}${e}`;
9272
- results.add(abs ? resolve4(full) : full);
9300
+ if (e.isDirectory()) {
9301
+ const subDir = `${dir}${SEP}${e.name}`;
9302
+ await walk(subDir, pat);
9273
9303
  }
9274
9304
  }
9275
9305
  return;
9276
9306
  }
9277
- const before = pat.slice(0, firstGlob);
9278
- const rest = pat.slice(firstGlob);
9279
- if (before.endsWith("**")) {
9280
- await walk(dir, rest);
9307
+ if (pat === "**") {
9281
9308
  for (const e of entries) {
9282
- const full = `${dir}${SEP}${e}`;
9283
- try {
9284
- const stat13 = await fsp.stat(full);
9285
- if (stat13.isDirectory()) await walk(full, rest);
9286
- } catch {
9309
+ const full = `${dir}${SEP}${e.name}`;
9310
+ results.add(abs ? resolve4(full) : full);
9311
+ if (e.isDirectory()) {
9312
+ await walk(full, "**");
9287
9313
  }
9288
9314
  }
9289
- } else if (before === "") {
9290
- const re = globToRegex(rest);
9315
+ return;
9316
+ }
9317
+ const firstSlash = pat.indexOf("/");
9318
+ if (firstSlash < 0) {
9319
+ const re = globToRegex(pat);
9291
9320
  for (const e of entries) {
9292
- if (re.test(e)) {
9293
- const full = `${dir}${SEP}${e}`;
9321
+ if (re.test(e.name)) {
9322
+ const full = `${dir}${SEP}${e.name}`;
9294
9323
  results.add(abs ? resolve4(full) : full);
9295
9324
  }
9296
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
+ }
9297
9338
  } else {
9298
- const seg = before.replace(/[*?[\]]/g, "").replace(/\/$/, "");
9299
- if (entries.includes(seg)) {
9300
- const full = `${dir}${SEP}${seg}`;
9301
- try {
9302
- const stat13 = await fsp.stat(full);
9303
- if (stat13.isDirectory()) await walk(full, rest);
9304
- } 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);
9305
9343
  }
9306
9344
  }
9307
9345
  }
@@ -10436,7 +10474,7 @@ async function gitOtherWorktrees(cwd, signal) {
10436
10474
  return branches.slice(1);
10437
10475
  }
10438
10476
  function runGit(args, cwd, signal) {
10439
- return new Promise((resolve16, reject) => {
10477
+ return new Promise((resolve17, reject) => {
10440
10478
  let stdout = "";
10441
10479
  let stderr = "";
10442
10480
  const child = spawn("git", args, {
@@ -10455,7 +10493,7 @@ function runGit(args, cwd, signal) {
10455
10493
  });
10456
10494
  child.on("error", (err) => reject(err));
10457
10495
  child.on("close", (code) => {
10458
- if (code === 0) resolve16(stdout);
10496
+ if (code === 0) resolve17(stdout);
10459
10497
  else reject(new Error(stderr || `git ${args[0]} exited ${code}`));
10460
10498
  });
10461
10499
  });
@@ -10547,11 +10585,13 @@ var CHAOS_MONKEY_AGENT = {
10547
10585
  tools: [...TOOLS.build],
10548
10586
  skillNames: ["testing", "typescript-strict"],
10549
10587
  spawnBudgetExempt: true,
10550
- // Follow fleet worktree policy (NOT 'required'): mutation targets are
10551
- // often freshly written and uncommitted — a worktree spawned from HEAD
10552
- // would not contain them and every mutant would drift. Callers pass
10553
- // `worktree: 'off'` in the mutation_test input for uncommitted targets.
10554
- worktree: "auto",
10588
+ // Run in the live checkout: mutation targets are usually freshly
10589
+ // written and uncommitted — a worktree spawned from HEAD would not
10590
+ // contain them and every mutant would drift. The mutation_test tool
10591
+ // honors this value as its default; callers can still override per
10592
+ // call via its `chaosWorktree` input when targets are committed and
10593
+ // isolation is wanted.
10594
+ worktree: "off",
10555
10595
  // Report travels via submit_result + final text, not the leader's stream.
10556
10596
  textStream: "silent",
10557
10597
  toolStream: "silent"
@@ -11131,7 +11171,7 @@ function createDelegateTool(opts) {
11131
11171
  };
11132
11172
  }
11133
11173
  async function awaitDelegateAttempt(director, subagentId, taskId, timeoutMs, abortSignal) {
11134
- return new Promise((resolve16) => {
11174
+ return new Promise((resolve17) => {
11135
11175
  let settled = false;
11136
11176
  let timer;
11137
11177
  let offAbort = () => {
@@ -11144,7 +11184,7 @@ async function awaitDelegateAttempt(director, subagentId, taskId, timeoutMs, abo
11144
11184
  offIter();
11145
11185
  offProgress();
11146
11186
  offAbort();
11147
- resolve16(value);
11187
+ resolve17(value);
11148
11188
  };
11149
11189
  const arm = () => {
11150
11190
  if (timer) clearTimeout(timer);
@@ -11726,16 +11766,14 @@ var ExploreCompanion = class {
11726
11766
  this.running = true;
11727
11767
  this.unsubscribers.push(
11728
11768
  this.opts.events.on("tool.executed", (e) => {
11729
- const lsid = this.resolveLeaderSessionId();
11730
- if (lsid && e.sessionId && e.sessionId !== lsid) return;
11769
+ if (e.sessionId !== this.resolveLeaderSessionId()) return;
11731
11770
  this.trackToolExecuted(e);
11732
11771
  })
11733
11772
  );
11734
11773
  if (this.cfg.signals.todoInProgress && this.resolveLeaderAgentId()) {
11735
11774
  this.unsubscribers.push(
11736
11775
  this.opts.events.on("session.agents_updated", (e) => {
11737
- const lsid = this.resolveLeaderSessionId();
11738
- if (lsid && e.sessionId && e.sessionId !== lsid) return;
11776
+ if (e.sessionId !== this.resolveLeaderSessionId()) return;
11739
11777
  this.trackAgentTodos(e.agents);
11740
11778
  })
11741
11779
  );
@@ -11743,8 +11781,7 @@ var ExploreCompanion = class {
11743
11781
  if (this.cfg.signals.errorSymbol) {
11744
11782
  this.unsubscribers.push(
11745
11783
  this.opts.events.on("error", (e) => {
11746
- const lsid = this.resolveLeaderSessionId();
11747
- if (lsid && e.sessionId && e.sessionId !== lsid) return;
11784
+ if (e.sessionId !== this.resolveLeaderSessionId()) return;
11748
11785
  this.trackError(e.err);
11749
11786
  })
11750
11787
  );
@@ -11768,31 +11805,31 @@ var ExploreCompanion = class {
11768
11805
  // ── signal handlers ──────────────────────────────────────────────────────
11769
11806
  trackToolExecuted(e) {
11770
11807
  const tool = e.name.toLowerCase();
11771
- const path42 = extractedPath(e.input);
11772
- if (e.ok && this.cfg.signals.editUnreadFile && this.cfg.fileEditTools.has(tool) && path42) {
11773
- 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)) {
11774
11811
  this.engage({
11775
11812
  id: randomUUID6(),
11776
- probe: `Map file ${path42}: role, exports, dependencies, and callers \u2014 the leader is about to edit it.`,
11777
- hint: { file: path42 },
11778
- 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.`,
11779
11816
  source: "edit_unread_file",
11780
- subject: `file:${path42}`,
11817
+ subject: `file:${path44}`,
11781
11818
  createdAt: this.now()
11782
11819
  });
11783
11820
  }
11784
11821
  return;
11785
11822
  }
11786
- if (e.ok && this.cfg.signals.unfamiliarRead && tool === "read" && path42) {
11787
- if (!this.readSet.has(path42)) {
11788
- 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);
11789
11826
  this.engage({
11790
11827
  id: randomUUID6(),
11791
- probe: `Skeleton + callers + dependents of ${path42}: what it exports, who imports it, and how it fits the feature flow.`,
11792
- hint: { file: path42 },
11793
- 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}.`,
11794
11831
  source: "unfamiliar_read",
11795
- subject: `file:${path42}`,
11832
+ subject: `file:${path44}`,
11796
11833
  createdAt: this.now()
11797
11834
  });
11798
11835
  }
@@ -11857,9 +11894,18 @@ var ExploreCompanion = class {
11857
11894
  limit: 20
11858
11895
  });
11859
11896
  const lsid = this.resolveLeaderSessionId();
11897
+ const selfRecipients = new Set(
11898
+ [
11899
+ this.cfg.companionAgentId,
11900
+ mailboxIdentityBase(this.cfg.companionAgentId),
11901
+ ...lsid != null ? [sessionRecipient(lsid)] : []
11902
+ ].map((r) => r.toLowerCase())
11903
+ );
11860
11904
  for (const msg of messages) {
11861
11905
  if (msg.type !== "ask" && msg.type !== "assign") continue;
11862
- const fromLeader = isMailboxLeader(msg.from) || lsid != null && msg.senderSessionId === lsid;
11906
+ const to = msg.to.trim().toLowerCase();
11907
+ if (to !== "*" && !selfRecipients.has(to)) continue;
11908
+ const fromLeader = msg.senderSessionId === void 0 && isMailboxLeader(msg.from) || lsid != null && msg.senderSessionId === lsid;
11863
11909
  if (!fromLeader) continue;
11864
11910
  this.engage({
11865
11911
  id: randomUUID6(),
@@ -11972,13 +12018,13 @@ function makeDependencyWatcherConfig(opts) {
11972
12018
  const globPatterns = patterns.filter((p) => p.includes("*"));
11973
12019
  const plainPatterns = patterns.filter((p) => !p.includes("*"));
11974
12020
  function matchesPattern(filePath) {
11975
- const basename6 = filePath.split("/").pop()?.split("\\").pop() ?? "";
11976
- if (plainPatterns.includes(basename6)) return true;
12021
+ const basename7 = filePath.split("/").pop()?.split("\\").pop() ?? "";
12022
+ if (plainPatterns.includes(basename7)) return true;
11977
12023
  for (const gp of globPatterns) {
11978
12024
  const regex = new RegExp(
11979
12025
  "^" + gp.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$"
11980
12026
  );
11981
- if (regex.test(basename6)) return true;
12027
+ if (regex.test(basename7)) return true;
11982
12028
  }
11983
12029
  return false;
11984
12030
  }
@@ -12950,10 +12996,10 @@ var DirectorTaskRegistry = class _DirectorTaskRegistry {
12950
12996
  pending: taskIds.filter((id) => !done.has(id))
12951
12997
  });
12952
12998
  }
12953
- return new Promise((resolve16) => {
12999
+ return new Promise((resolve17) => {
12954
13000
  const entry = {
12955
13001
  ids: new Set(taskIds),
12956
- resolve: (result) => resolve16({
13002
+ resolve: (result) => resolve17({
12957
13003
  completed: [result],
12958
13004
  pending: taskIds.filter((id) => id !== result.taskId)
12959
13005
  })
@@ -12961,7 +13007,7 @@ var DirectorTaskRegistry = class _DirectorTaskRegistry {
12961
13007
  if (opts?.timeoutMs !== void 0) {
12962
13008
  entry.timer = setTimeout(() => {
12963
13009
  this.anyWaiters.delete(entry);
12964
- resolve16({ completed: [], pending: [...taskIds], timedOut: true });
13010
+ resolve17({ completed: [], pending: [...taskIds], timedOut: true });
12965
13011
  }, opts.timeoutMs);
12966
13012
  }
12967
13013
  this.anyWaiters.add(entry);
@@ -13079,11 +13125,11 @@ ${JSON.stringify(result.result, null, 2)}
13079
13125
  this.makeStoppedResult(taskId, "director", `Unknown task id "${taskId}" \u2014 never assigned`)
13080
13126
  );
13081
13127
  }
13082
- let resolve16;
13128
+ let resolve17;
13083
13129
  const promise = new Promise((done) => {
13084
- resolve16 = done;
13130
+ resolve17 = done;
13085
13131
  });
13086
- this.taskWaiters.set(taskId, { promise, resolve: resolve16 });
13132
+ this.taskWaiters.set(taskId, { promise, resolve: resolve17 });
13087
13133
  return promise;
13088
13134
  }
13089
13135
  recordAssignment(task) {
@@ -13144,16 +13190,21 @@ ${JSON.stringify(result.result, null, 2)}
13144
13190
 
13145
13191
  // src/coordination/director-tools.ts
13146
13192
  import { randomUUID as randomUUID12 } from "node:crypto";
13147
- import {
13148
- completeKanbanDispatch,
13149
- failKanbanDispatch,
13150
- getBoard,
13151
- heartbeatTaskAssignment,
13152
- listReadyTasks,
13153
- reserveKanbanDispatch,
13154
- startKanbanDispatch,
13155
- updateTaskAssignment
13156
- } 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
+ }
13157
13208
 
13158
13209
  // src/coordination/director-input-helpers.ts
13159
13210
  import { randomUUID as randomUUID8 } from "node:crypto";
@@ -13174,8 +13225,21 @@ function instantiateRosterConfig2(role, base) {
13174
13225
  };
13175
13226
  }
13176
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
+
13177
13242
  // src/coordination/director-kanban-queue-helpers.ts
13178
- import { describeKanbanBoundary } from "@wrongstack/kanban";
13179
13243
  function normalizeKanbanQueueInput(input) {
13180
13244
  const raw = input ?? {};
13181
13245
  return {
@@ -13259,8 +13323,8 @@ function buildKanbanFleetTaskPrompt(board, task, lease) {
13259
13323
  task.origin.taskId ? `sourceTaskId: ${task.origin.taskId}` : ""
13260
13324
  ].filter(Boolean).join("\n") : "";
13261
13325
  const boundaries = [
13262
- board.boundary?.enabled ? `board: ${describeKanbanBoundary(board.boundary)}` : "",
13263
- 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)}` : "",
13264
13328
  ...(board.boundary?.allow ?? []).map(
13265
13329
  (selector) => `board allow ${selector.access} ${selector.kind}:${selector.path}`
13266
13330
  ),
@@ -14384,24 +14448,29 @@ var TOKEN_PATTERNS = [
14384
14448
  kind: "return-null",
14385
14449
  // `return <expr>;` where expr is not already null/undefined/void.
14386
14450
  regex: /(?<indent>\breturn\b)(?<expr>\s+[^;{}\n]+?)\s*;/g,
14387
- replace: () => "return null;"
14451
+ replace: () => "return null;",
14452
+ endpointsInCode: true
14388
14453
  }
14389
14454
  ];
14390
14455
  function planMutations(file, source, opts = {}) {
14391
14456
  const maxPerFile = opts.maxPerFile ?? 25;
14392
14457
  const out = [];
14393
14458
  const lines = source.split("\n");
14459
+ const masks = computeLineMasks(source);
14394
14460
  for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
14395
14461
  const line = lines[lineIdx];
14396
14462
  const t = line.trim();
14397
- if (t.startsWith("//") || t.startsWith("*") || t.startsWith("/*")) continue;
14463
+ if (t.startsWith("//")) continue;
14464
+ const codeRanges = masks[lineIdx];
14465
+ const inCode = (start) => codeRanges.some(([s, e]) => start >= s && start < e);
14398
14466
  for (const pattern of TOKEN_PATTERNS) {
14399
14467
  pattern.regex.lastIndex = 0;
14400
14468
  let m;
14401
14469
  while ((m = pattern.regex.exec(line)) !== null) {
14402
14470
  const token = m.groups?.["op"] ?? m[0];
14403
14471
  const tokenStart = m.index + m[0].indexOf(token);
14404
- if (isMasked(line, tokenStart, token.length)) continue;
14472
+ if (!inCode(tokenStart)) continue;
14473
+ if (pattern.endpointsInCode && !inCode(tokenStart + token.length - 1)) continue;
14405
14474
  const original = line.slice(tokenStart, tokenStart + token.length);
14406
14475
  const replacement = pattern.replace(token);
14407
14476
  if (replacement === original) continue;
@@ -14420,19 +14489,179 @@ function planMutations(file, source, opts = {}) {
14420
14489
  }
14421
14490
  return out.slice(0, maxPerFile);
14422
14491
  }
14423
- function isMasked(line, start, len) {
14424
- let inSingle = false;
14425
- let inDouble = false;
14426
- for (let i = 0; i < start; i++) {
14427
- const c = line[i];
14428
- const prev = i > 0 ? line[i - 1] : void 0;
14429
- if (c === "'" && prev !== "\\") inSingle = !inSingle;
14430
- else if (c === '"' && prev !== "\\") inDouble = !inDouble;
14431
- if (!inSingle && !inDouble && c === "/" && prev === "/") return true;
14492
+ function computeLineMasks(source) {
14493
+ const lines = source.split("\n");
14494
+ const masks = lines.map(() => []);
14495
+ const stack = [{ kind: "code", depth: 0, parens: [] }];
14496
+ let inBlockComment = false;
14497
+ let lastToken = null;
14498
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
14499
+ const line = lines[lineIdx];
14500
+ const ranges = masks[lineIdx];
14501
+ let runStart = null;
14502
+ const closeRun = (end) => {
14503
+ if (runStart !== null && end > runStart) ranges.push([runStart, end]);
14504
+ runStart = null;
14505
+ };
14506
+ let i = 0;
14507
+ if (inBlockComment) {
14508
+ const close = line.indexOf("*/");
14509
+ if (close === -1) continue;
14510
+ inBlockComment = false;
14511
+ i = close + 2;
14512
+ }
14513
+ while (i < line.length) {
14514
+ const top = stack[stack.length - 1];
14515
+ const c = line[i];
14516
+ if (top.kind === "template") {
14517
+ if (c === "\\") {
14518
+ i += 2;
14519
+ continue;
14520
+ }
14521
+ if (c === "`") {
14522
+ stack.pop();
14523
+ lastToken = "`";
14524
+ i++;
14525
+ continue;
14526
+ }
14527
+ if (c === "$" && line[i + 1] === "{") {
14528
+ stack.push({ kind: "code", depth: 0, parens: [] });
14529
+ lastToken = "${";
14530
+ i += 2;
14531
+ continue;
14532
+ }
14533
+ i++;
14534
+ continue;
14535
+ }
14536
+ if (/[\w$]/.test(c)) {
14537
+ let j = i + 1;
14538
+ while (j < line.length && /[\w$]/.test(line[j])) j++;
14539
+ lastToken = line.slice(i, j);
14540
+ if (runStart === null) runStart = i;
14541
+ i = j;
14542
+ continue;
14543
+ }
14544
+ if (c === "'" || c === '"') {
14545
+ closeRun(i);
14546
+ i++;
14547
+ while (i < line.length && line[i] !== c) {
14548
+ if (line[i] === "\\") i++;
14549
+ i++;
14550
+ }
14551
+ i++;
14552
+ lastToken = c;
14553
+ continue;
14554
+ }
14555
+ if (c === "`") {
14556
+ closeRun(i);
14557
+ stack.push({ kind: "template", depth: 0, parens: [] });
14558
+ i++;
14559
+ continue;
14560
+ }
14561
+ if (c === "/" && line[i + 1] === "/") {
14562
+ closeRun(i);
14563
+ break;
14564
+ }
14565
+ if (c === "/" && line[i + 1] === "*") {
14566
+ closeRun(i);
14567
+ const close = line.indexOf("*/", i + 2);
14568
+ if (close === -1) {
14569
+ inBlockComment = true;
14570
+ break;
14571
+ }
14572
+ i = close + 2;
14573
+ continue;
14574
+ }
14575
+ if (c === "/") {
14576
+ if (!tokenCanEndOperand(lastToken)) {
14577
+ closeRun(i);
14578
+ const next = skipRegexLiteral(line, i);
14579
+ lastToken = next > i + 1 ? "regex" : "/";
14580
+ i = next;
14581
+ continue;
14582
+ }
14583
+ }
14584
+ if (c === "(") {
14585
+ top.parens.push(CONTROL_KEYWORDS.has(lastToken ?? "") ? "control" : "expr");
14586
+ lastToken = c;
14587
+ } else if (c === ")") {
14588
+ const kind = top.parens.pop() ?? "expr";
14589
+ lastToken = kind === "control" ? "control-paren-close" : ")";
14590
+ } else if (c === "{") {
14591
+ top.depth++;
14592
+ lastToken = c;
14593
+ } else if (c === "}") {
14594
+ if (top.depth > 0) {
14595
+ top.depth--;
14596
+ lastToken = c;
14597
+ } else if (stack.length > 1) {
14598
+ closeRun(i);
14599
+ stack.pop();
14600
+ i++;
14601
+ continue;
14602
+ } else {
14603
+ lastToken = c;
14604
+ }
14605
+ } else if (c !== " " && c !== " " && c !== "\r") {
14606
+ lastToken = c;
14607
+ }
14608
+ if (runStart === null) runStart = i;
14609
+ i++;
14610
+ }
14611
+ closeRun(line.length);
14612
+ }
14613
+ return masks;
14614
+ }
14615
+ var KEYWORDS_BEFORE_REGEX = /* @__PURE__ */ new Set([
14616
+ "return",
14617
+ "typeof",
14618
+ "instanceof",
14619
+ "in",
14620
+ "of",
14621
+ "new",
14622
+ "delete",
14623
+ "void",
14624
+ "throw",
14625
+ "case",
14626
+ "do",
14627
+ "else",
14628
+ "yield",
14629
+ "await"
14630
+ ]);
14631
+ var CONTROL_KEYWORDS = /* @__PURE__ */ new Set(["if", "for", "while", "switch", "catch", "with", "await"]);
14632
+ function tokenCanEndOperand(token) {
14633
+ if (token === null) return false;
14634
+ if (/^[\w$]+$/.test(token)) return !KEYWORDS_BEFORE_REGEX.has(token);
14635
+ return token === ")" || token === "]" || token === "." || token === '"' || token === "'" || token === "`";
14636
+ }
14637
+ function skipRegexLiteral(line, start) {
14638
+ let i = start + 1;
14639
+ let inClass = false;
14640
+ while (i < line.length) {
14641
+ const ch = line[i];
14642
+ if (ch === "\\") {
14643
+ i += 2;
14644
+ continue;
14645
+ }
14646
+ if (inClass) {
14647
+ if (ch === "]") inClass = false;
14648
+ i++;
14649
+ continue;
14650
+ }
14651
+ if (ch === "[") {
14652
+ inClass = true;
14653
+ i++;
14654
+ continue;
14655
+ }
14656
+ if (ch === "/") {
14657
+ i++;
14658
+ break;
14659
+ }
14660
+ if (ch === "\n" || ch === "\r") return line.length;
14661
+ i++;
14432
14662
  }
14433
- if (inSingle || inDouble) return true;
14434
- const window = line.slice(start, start + len);
14435
- return /['"]/.test(window);
14663
+ while (i < line.length && /[a-z]/.test(line[i])) i++;
14664
+ return i;
14436
14665
  }
14437
14666
  function parseMutationReport(text) {
14438
14667
  const candidates = [];
@@ -14483,7 +14712,7 @@ function normalizeMutantEntry(value) {
14483
14712
  const rec = value;
14484
14713
  const id = typeof rec["id"] === "string" ? rec["id"] : void 0;
14485
14714
  const status = rec["status"];
14486
- if (!id || status !== "killed" && status !== "survived" && status !== "skipped") {
14715
+ if (!id || status !== "killed" && status !== "survived" && status !== "skipped" && status !== "killed-by-hang") {
14487
14716
  return void 0;
14488
14717
  }
14489
14718
  return {
@@ -14539,7 +14768,7 @@ function makeMutationTestTool(director, roster, opts = {}) {
14539
14768
  },
14540
14769
  chaosWorktree: {
14541
14770
  anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
14542
- description: "Worktree override for the chaos agent. Use 'off' when targets are uncommitted \u2014 a worktree from HEAD would not contain them."
14771
+ description: "Worktree override for the chaos agent. Defaults to the roster policy for chaos-monkey ('off'), because mutation targets are usually freshly written and uncommitted \u2014 a worktree from HEAD would not contain them and every mutant would drift to skipped. Only pass 'auto' or 'required' when the targets are committed."
14543
14772
  },
14544
14773
  timeoutMs: { type: "number", minimum: 1, description: "Per-task timeout for chaos/strengthen/rerun tasks." },
14545
14774
  reportOnly: {
@@ -14561,8 +14790,16 @@ function makeMutationTestTool(director, roster, opts = {}) {
14561
14790
  error: "No mutable sites found in the given targets (after comment/string filtering)."
14562
14791
  };
14563
14792
  }
14793
+ const chaosBase = roster?.[CHAOS_ROLE];
14794
+ if (!chaosBase) {
14795
+ return {
14796
+ verdict: "inconclusive",
14797
+ passed: false,
14798
+ error: "chaos-monkey role missing from the roster \u2014 refusing to spawn a saboteur without its prompt/tools contract. Build the toolset with a roster that includes 'chaos-monkey' (FLEET_ROSTER does)."
14799
+ };
14800
+ }
14564
14801
  const chaosSubagentId = await director.spawn(
14565
- makeChaosConfig(roster, i.chaosWorktree ?? "off")
14802
+ makeChaosConfig(chaosBase, i.chaosWorktree ?? chaosBase.worktree ?? "off")
14566
14803
  );
14567
14804
  const chaosTaskId = await director.assign({
14568
14805
  id: randomUUID11(),
@@ -14580,6 +14817,7 @@ function makeMutationTestTool(director, roster, opts = {}) {
14580
14817
  );
14581
14818
  const attempts = [];
14582
14819
  let current = survivors;
14820
+ let rerunUnknowns = [];
14583
14821
  while (current.length > 0 && attempts.length < maxAttempts && i.repairSubagentId) {
14584
14822
  const attemptNo = attempts.length + 1;
14585
14823
  const strengthenTaskId = await director.assign({
@@ -14601,7 +14839,7 @@ function makeMutationTestTool(director, roster, opts = {}) {
14601
14839
  }
14602
14840
  const survivorPlan = plan.filter((p) => current.some((s) => s.id === p.id));
14603
14841
  const rerunSubagentId = await director.spawn(
14604
- makeChaosConfig(roster, i.chaosWorktree ?? "off")
14842
+ makeChaosConfig(chaosBase, i.chaosWorktree ?? chaosBase.worktree ?? "off")
14605
14843
  );
14606
14844
  const rerunTaskId = await director.assign({
14607
14845
  id: randomUUID11(),
@@ -14611,29 +14849,36 @@ function makeMutationTestTool(director, roster, opts = {}) {
14611
14849
  });
14612
14850
  const [rerunResult] = await director.awaitTasks([rerunTaskId]);
14613
14851
  const passN = collectOutcomes(rerunResult, survivorPlan);
14614
- const stillSurviving = passN.filter((m) => m.status === "survived" || m.status === "skipped");
14852
+ const stillSurviving = passN.filter((m) => !isKill(m.status));
14853
+ rerunUnknowns = passN.filter((m) => m.status === "skipped");
14615
14854
  attempts.push({
14616
14855
  attempt: attemptNo,
14617
14856
  survivorsBefore: current,
14618
14857
  strengthenResult: { taskId: strengthenResult.taskId, status: strengthenResult.status },
14619
14858
  rerunResult: { taskId: rerunTaskId, status: rerunResult?.status ?? "unknown" },
14620
14859
  survivorsAfter: stillSurviving,
14621
- suspectedEquivalent: stillSurviving.filter((m) => current.some((c) => c.id === m.id)).map((m) => m.id)
14860
+ suspectedEquivalent: stillSurviving.filter((m) => m.status === "survived" && current.some((c) => c.id === m.id)).map((m) => m.id)
14622
14861
  });
14623
- current = stillSurviving.filter((m) => m.status === "survived");
14624
- if (passN.every((m) => m.status === "skipped")) break;
14862
+ current = stillSurviving;
14625
14863
  }
14626
- const finalSurvivors = current;
14864
+ const finalSurvivors = current.filter((m) => m.status === "survived");
14627
14865
  const verifiedCount = pass1.filter((m) => m.status !== "skipped").length;
14628
14866
  const skippedCount = pass1.filter((m) => m.status === "skipped").length;
14629
- const score = plan.length === 0 ? 0 : pass1.filter((m) => m.status === "killed").length / plan.length;
14630
- const verdict = verifiedCount === 0 ? "inconclusive" : finalSurvivors.length === 0 ? skippedCount > 0 ? "partial" : "pass" : score >= 0.8 ? "partial" : "fail";
14867
+ const rerunUnknownCount = rerunUnknowns.length;
14868
+ const score = plan.length === 0 ? 0 : pass1.filter((m) => isKill(m.status)).length / plan.length;
14869
+ const verdict = verifiedCount === 0 ? "inconclusive" : finalSurvivors.length === 0 ? skippedCount > 0 || rerunUnknownCount > 0 ? "partial" : "pass" : score >= 0.8 ? "partial" : "fail";
14631
14870
  return {
14632
14871
  verdict,
14633
14872
  passed: verdict === "pass",
14634
14873
  mutationScore: Number.parseFloat(score.toFixed(3)),
14635
14874
  planned: plan.length,
14636
- killed: pass1.filter((m) => m.status === "killed").length,
14875
+ killed: pass1.filter((m) => isKill(m.status)).length,
14876
+ // Breakout of `killed`: how many kills were detected by the test
14877
+ // command hanging rather than by a failing assertion. A subset of
14878
+ // `killed`, surfaced so a director can distinguish a hang-heavy
14879
+ // suite (mutants breaking termination, not assertions) from an
14880
+ // assertion-strong one. hangHeavy = killedByHang === killed.
14881
+ killedByHang: pass1.filter((m) => m.status === "killed-by-hang").length,
14637
14882
  survived: pass1.filter((m) => m.status === "survived").length,
14638
14883
  skipped: pass1.filter((m) => m.status === "skipped").length,
14639
14884
  finalSurvivors: finalSurvivors.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
@@ -14641,7 +14886,11 @@ function makeMutationTestTool(director, roster, opts = {}) {
14641
14886
  strengthenAttempts: attempts.length,
14642
14887
  attempts,
14643
14888
  chaosTaskId,
14644
- nextAction: finalSurvivors.length === 0 ? "accept" : attempts.length >= maxAttempts && i.repairSubagentId ? "manual_review_survivors" : "strengthen_tests"
14889
+ // Unverified leftovers from the strengthen loop: surfaced so the
14890
+ // caller can see WHICH mutants lack kill evidence, and counted by
14891
+ // the verdict gate above.
14892
+ unverifiedFromRerun: rerunUnknowns.map((m) => ({ id: m.id, file: m.file, kind: m.kind })),
14893
+ nextAction: finalSurvivors.length === 0 && rerunUnknownCount === 0 && skippedCount === 0 ? "accept" : attempts.length >= maxAttempts && i.repairSubagentId ? "manual_review_survivors" : "strengthen_tests"
14645
14894
  };
14646
14895
  }
14647
14896
  };
@@ -14657,7 +14906,7 @@ function normalizeMutationTestInput(input) {
14657
14906
  maxPerFile: typeof raw["maxPerFile"] === "number" ? raw["maxPerFile"] : void 0,
14658
14907
  maxStrengthenAttempts: typeof raw["maxStrengthenAttempts"] === "number" ? raw["maxStrengthenAttempts"] : void 0,
14659
14908
  repairSubagentId: typeof raw["repairSubagentId"] === "string" && raw["repairSubagentId"].trim() ? raw["repairSubagentId"].trim() : void 0,
14660
- chaosWorktree: raw["chaosWorktree"] ?? void 0,
14909
+ chaosWorktree: normalizeWorktreeOverride(raw["chaosWorktree"]),
14661
14910
  timeoutMs: typeof raw["timeoutMs"] === "number" ? raw["timeoutMs"] : void 0,
14662
14911
  reportOnly: raw["reportOnly"] === true
14663
14912
  };
@@ -14679,8 +14928,7 @@ function buildPlan(i, projectRoot) {
14679
14928
  }
14680
14929
  return plan;
14681
14930
  }
14682
- function makeChaosConfig(roster, worktree) {
14683
- const base = roster?.[CHAOS_ROLE] ?? getAgentDefinition(CHAOS_ROLE)?.config ?? { name: "Chaos Monkey", role: CHAOS_ROLE };
14931
+ function makeChaosConfig(base, worktree) {
14684
14932
  return { ...instantiateRosterConfig2(CHAOS_ROLE, base), worktree };
14685
14933
  }
14686
14934
  function buildChaosTask(plan, i, pass, priorSurvivors) {
@@ -14696,7 +14944,7 @@ ${priorSurvivors.map((s) => `- ${s.id} (${s.kind} @ ${s.file}:${s.line})`).join(
14696
14944
  "For each mutant, in order:",
14697
14945
  "1. Apply ONLY that mutation at its exact (file, line, column).",
14698
14946
  `2. Run the test command: ${i.testCommand}${i.cwd ? ` (cwd: ${i.cwd})` : ""}`,
14699
- "3. Record killed (tests failed \u2014 quote first failing assertion) or survived (suite green).",
14947
+ "3. Record killed (tests failed \u2014 quote first failing assertion), survived (suite green), or killed-by-hang (the test command timed out or was aborted \u2014 the mutation broke the suite by non-termination; record the timeout as evidence, do NOT report it as survived).",
14700
14948
  "4. Restore the file byte-for-byte before the next mutant.",
14701
14949
  "",
14702
14950
  "Mutants:",
@@ -14708,23 +14956,49 @@ ${priorSurvivors.map((s) => `- ${s.id} (${s.kind} @ ${s.file}:${s.line})`).join(
14708
14956
  ].join("\n");
14709
14957
  }
14710
14958
  function buildStrengthenTask(survivors, i, attempt) {
14959
+ const confirmed = survivors.filter((s) => s.status === "survived");
14960
+ const unverified = survivors.filter((s) => s.status === "skipped");
14961
+ const row = (s) => `- ${s.id} | ${s.file}:${s.line} | ${s.kind}${s.evidence ? ` | ${s.evidence}` : ""}`;
14711
14962
  return [
14712
- `Strengthen the tests so these SURVIVING mutants die (attempt ${attempt}).`,
14963
+ `Strengthen the tests so the mutants below die (attempt ${attempt}).`,
14713
14964
  "",
14714
- "Each survivor below was a deliberate sabotage of production code that the current suite did NOT catch:",
14715
- ...survivors.map((s) => `- ${s.id} | ${s.file}:${s.line} | ${s.kind}${s.evidence ? ` | ${s.evidence}` : ""}`),
14716
- "",
14717
- `Test command that must fail under each mutant: ${i.testCommand}`,
14965
+ ...confirmed.length > 0 ? [
14966
+ "CONFIRMED SURVIVORS \u2014 each was a deliberate sabotage of production code that the current suite did NOT catch:",
14967
+ ...confirmed.map(row),
14968
+ ""
14969
+ ] : [],
14970
+ ...unverified.length > 0 ? [
14971
+ "UNVERIFIED \u2014 these mutations were never actually re-tested (the re-verify pass skipped or did not report them). Do NOT assume the suite misses them: first apply each mutation, run the tests, and confirm it really survives; if the tests already fail, report that instead of writing new assertions.",
14972
+ ...unverified.map(row),
14973
+ ""
14974
+ ] : [],
14975
+ `Test command that must fail under each CONFIRMED mutant: ${i.testCommand}`,
14718
14976
  "",
14719
- "For each survivor add or tighten exactly one assertion that pins the sabotaged boundary/behavior. Do not change production code. Do not weaken other tests. Run the suite green on clean code before finishing."
14977
+ "For each CONFIRMED survivor add or tighten exactly one assertion that pins the sabotaged boundary/behavior. Do not change production code. Do not weaken other tests. Run the suite green on clean code before finishing."
14720
14978
  ].join("\n");
14721
14979
  }
14722
14980
  function collectOutcomes(result, plan) {
14723
14981
  const fromText = parseTextOutcomes(result);
14724
14982
  if (fromText.length > 0) {
14725
- const planned = new Set(plan.map((p) => p.id));
14726
- const matched = fromText.filter((m) => planned.has(m.id));
14727
- if (matched.length > 0) return matched;
14983
+ const remaining = [...plan];
14984
+ const matched = [];
14985
+ for (const m of fromText) {
14986
+ const idx = remaining.findIndex((p) => p.id === m.id);
14987
+ if (idx === -1) continue;
14988
+ remaining.splice(idx, 1);
14989
+ matched.push(m);
14990
+ }
14991
+ if (matched.length > 0) {
14992
+ const missing = remaining.map((p) => ({
14993
+ id: p.id,
14994
+ file: p.file,
14995
+ line: p.line,
14996
+ kind: p.kind,
14997
+ status: "skipped",
14998
+ evidence: "not reported by chaos task"
14999
+ }));
15000
+ return [...matched, ...missing];
15001
+ }
14728
15002
  }
14729
15003
  return plan.map((p) => ({
14730
15004
  id: p.id,
@@ -14735,6 +15009,9 @@ function collectOutcomes(result, plan) {
14735
15009
  evidence: result ? `chaos task ended ${result.status}` : "chaos task produced no result"
14736
15010
  }));
14737
15011
  }
15012
+ function isKill(status) {
15013
+ return status === "killed" || status === "killed-by-hang";
15014
+ }
14738
15015
  function parseTextOutcomes(result) {
14739
15016
  const text = typeof result?.result === "string" ? result.result : void 0;
14740
15017
  if (!text) return [];
@@ -14980,7 +15257,7 @@ function makeKanbanQueueTool(director, roster) {
14980
15257
  i.heartbeatIntervalMs ?? Math.floor(leaseTtlMs / 2)
14981
15258
  );
14982
15259
  const effectiveLeaseTtlMs = Math.max(leaseTtlMs, heartbeatIntervalMs * 2);
14983
- 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, {
14984
15261
  ...i.boardId !== void 0 ? { boardId: i.boardId } : {}
14985
15262
  })).filter((candidate) => matchesKanbanQueueQuery(candidate.task, i.query ?? "")).slice(0, maxTasks).map((candidate) => candidate.task.id) : void 0;
14986
15263
  const dispatches = [];
@@ -14991,7 +15268,7 @@ function makeKanbanQueueTool(director, roster) {
14991
15268
  const candidateTaskId = candidateTaskIds?.[index];
14992
15269
  if (candidateTaskIds && !candidateTaskId) break;
14993
15270
  if (candidateTaskId && budgetRejectedTaskIds.has(candidateTaskId)) continue;
14994
- const reserved = await reserveKanbanDispatch(projectRoot, {
15271
+ const reserved = await kanbanDispatch().reserveKanbanDispatch(projectRoot, {
14995
15272
  ...i.boardId !== void 0 ? { boardId: i.boardId } : {},
14996
15273
  ...candidateTaskId !== void 0 ? { taskId: candidateTaskId } : {},
14997
15274
  routing: {
@@ -15021,7 +15298,7 @@ function makeKanbanQueueTool(director, roster) {
15021
15298
  if (remaining !== void 0 && remaining < costCeiling) {
15022
15299
  budgetRejectedTaskIds.add(claim.task.id);
15023
15300
  const budgetError = `Cost ceiling ${costCeiling} exceeds remaining budget ${remaining.toFixed(4)}`;
15024
- await updateTaskAssignment(
15301
+ await kanbanDispatch().updateTaskAssignment(
15025
15302
  projectRoot,
15026
15303
  claim.board.id,
15027
15304
  claim.task.id,
@@ -15072,7 +15349,7 @@ function makeKanbanQueueTool(director, roster) {
15072
15349
  }
15073
15350
  }
15074
15351
  };
15075
- const started = await startKanbanDispatch(projectRoot, {
15352
+ const started = await kanbanDispatch().startKanbanDispatch(projectRoot, {
15076
15353
  boardId: claim.board.id,
15077
15354
  taskId: claim.task.id,
15078
15355
  leaseId: ourLeaseId,
@@ -15117,7 +15394,7 @@ function makeKanbanQueueTool(director, roster) {
15117
15394
  message = `${message}; cleanup failed for spawned subagent ${subagentId}: ${toErrorMessage(cleanupErr)}`;
15118
15395
  }
15119
15396
  }
15120
- await failKanbanDispatch(projectRoot, {
15397
+ await kanbanDispatch().failKanbanDispatch(projectRoot, {
15121
15398
  boardId: claim.board.id,
15122
15399
  taskId: claim.task.id,
15123
15400
  leaseId: ourLeaseId,
@@ -15180,7 +15457,7 @@ function makeKanbanQueueTool(director, roster) {
15180
15457
  for (const dispatch of dispatches) {
15181
15458
  if (!runTaskIds.has(dispatch.runTaskId)) continue;
15182
15459
  try {
15183
- const board = await getBoard(projectRoot, dispatch.boardId);
15460
+ const board = await kanbanDispatch().getBoard(projectRoot, dispatch.boardId);
15184
15461
  const liveTask = board?.tasks.find((t) => t.id === dispatch.taskId);
15185
15462
  const liveLeaseId = liveTask?.assignment?.leaseId;
15186
15463
  if (liveLeaseId !== void 0 && liveLeaseId !== dispatch.leaseId) {
@@ -15192,10 +15469,15 @@ function makeKanbanQueueTool(director, roster) {
15192
15469
  } catch {
15193
15470
  }
15194
15471
  try {
15195
- await heartbeatTaskAssignment(projectRoot, dispatch.boardId, dispatch.taskId, {
15196
- leaseExpiresAt: refreshedExpiry,
15197
- expectedLeaseId: dispatch.leaseId
15198
- });
15472
+ await kanbanDispatch().heartbeatTaskAssignment(
15473
+ projectRoot,
15474
+ dispatch.boardId,
15475
+ dispatch.taskId,
15476
+ {
15477
+ leaseExpiresAt: refreshedExpiry,
15478
+ expectedLeaseId: dispatch.leaseId
15479
+ }
15480
+ );
15199
15481
  } catch {
15200
15482
  }
15201
15483
  }
@@ -15218,7 +15500,7 @@ function makeKanbanQueueTool(director, roster) {
15218
15500
  if (!dispatch) continue;
15219
15501
  runTaskIds.delete(dispatch.runTaskId);
15220
15502
  if (result.status === "success") {
15221
- const completed = await completeKanbanDispatch(projectRoot, {
15503
+ const completed = await kanbanDispatch().completeKanbanDispatch(projectRoot, {
15222
15504
  boardId: dispatch.boardId,
15223
15505
  taskId: dispatch.taskId,
15224
15506
  leaseId: dispatch.leaseId,
@@ -15234,7 +15516,7 @@ function makeKanbanQueueTool(director, roster) {
15234
15516
  });
15235
15517
  }
15236
15518
  } else {
15237
- const failed = await failKanbanDispatch(projectRoot, {
15519
+ const failed = await kanbanDispatch().failKanbanDispatch(projectRoot, {
15238
15520
  boardId: dispatch.boardId,
15239
15521
  taskId: dispatch.taskId,
15240
15522
  leaseId: dispatch.leaseId,
@@ -15286,7 +15568,7 @@ async function renewAndRevokeLease(opts) {
15286
15568
  } = opts;
15287
15569
  let revoked = false;
15288
15570
  try {
15289
- const board = await getBoard(projectRoot, boardId);
15571
+ const board = await kanbanDispatch().getBoard(projectRoot, boardId);
15290
15572
  const liveTask = board?.tasks.find((t) => t.id === taskId);
15291
15573
  const liveLeaseId = liveTask?.assignment?.leaseId;
15292
15574
  if (liveLeaseId !== void 0 && liveLeaseId !== ourLeaseId) {
@@ -15299,7 +15581,7 @@ async function renewAndRevokeLease(opts) {
15299
15581
  } catch {
15300
15582
  }
15301
15583
  if (!revoked) {
15302
- await heartbeatTaskAssignment(projectRoot, boardId, taskId, {
15584
+ await kanbanDispatch().heartbeatTaskAssignment(projectRoot, boardId, taskId, {
15303
15585
  leaseExpiresAt: refreshedExpiry,
15304
15586
  expectedLeaseId: ourLeaseId
15305
15587
  }).catch(() => {
@@ -15402,12 +15684,12 @@ function rosterSummaryFromConfigs(roster) {
15402
15684
 
15403
15685
  // src/coordination/director-session.ts
15404
15686
  import * as fsp24 from "node:fs/promises";
15405
- import * as path32 from "node:path";
15687
+ import * as path33 from "node:path";
15406
15688
 
15407
15689
  // src/storage/session-store.ts
15408
15690
  import { randomUUID as randomUUID14 } from "node:crypto";
15409
15691
  import * as fsp23 from "node:fs/promises";
15410
- import * as path31 from "node:path";
15692
+ import * as path32 from "node:path";
15411
15693
 
15412
15694
  // src/session-catalog/client.ts
15413
15695
  import { spawn as spawn2 } from "node:child_process";
@@ -15485,7 +15767,7 @@ function normalize2(value) {
15485
15767
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
15486
15768
  }
15487
15769
  function delay(ms) {
15488
- return new Promise((resolve16) => setTimeout(resolve16, ms));
15770
+ return new Promise((resolve17) => setTimeout(resolve17, ms));
15489
15771
  }
15490
15772
  var SessionCatalogProjectClient = class {
15491
15773
  constructor(options) {
@@ -15575,8 +15857,8 @@ var SessionCatalogProjectClient = class {
15575
15857
  this.socket = null;
15576
15858
  this.info = null;
15577
15859
  if (socket && !socket.destroyed)
15578
- await new Promise((resolve16) => {
15579
- socket.once("close", resolve16);
15860
+ await new Promise((resolve17) => {
15861
+ socket.once("close", resolve17);
15580
15862
  socket.end();
15581
15863
  });
15582
15864
  }
@@ -15614,7 +15896,7 @@ var SessionCatalogProjectClient = class {
15614
15896
  this.info = null;
15615
15897
  this.authToken = void 0;
15616
15898
  this.buffer = "";
15617
- return new Promise((resolve16, reject) => {
15899
+ return new Promise((resolve17, reject) => {
15618
15900
  const socket = net.createConnection(this.endpoint);
15619
15901
  this.socket = socket;
15620
15902
  socket.setEncoding("utf8");
@@ -15627,7 +15909,7 @@ var SessionCatalogProjectClient = class {
15627
15909
  clearTimeout(timer);
15628
15910
  this.connectResolve = null;
15629
15911
  this.connectReject = null;
15630
- resolve16();
15912
+ resolve17();
15631
15913
  };
15632
15914
  this.connectReject = (error) => {
15633
15915
  clearTimeout(timer);
@@ -15672,7 +15954,7 @@ var SessionCatalogProjectClient = class {
15672
15954
  });
15673
15955
  if (encoded.length > SESSION_CATALOG_MAX_FRAME_CHARS)
15674
15956
  return Promise.reject(new Error("Session Catalog request exceeded frame limit"));
15675
- return new Promise((resolve16, reject) => {
15957
+ return new Promise((resolve17, reject) => {
15676
15958
  const timer = setTimeout(() => {
15677
15959
  const pending = this.pending.get(id);
15678
15960
  if (!pending) return;
@@ -15684,7 +15966,7 @@ var SessionCatalogProjectClient = class {
15684
15966
  );
15685
15967
  }, timeoutMs);
15686
15968
  timer.unref?.();
15687
- this.pending.set(id, { resolve: resolve16, reject, timer });
15969
+ this.pending.set(id, { resolve: resolve17, reject, timer });
15688
15970
  socket.write(encoded);
15689
15971
  });
15690
15972
  }
@@ -16098,6 +16380,29 @@ var SessionSummaryTracker = class {
16098
16380
  get currentSummary() {
16099
16381
  return this.summary;
16100
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
+ }
16101
16406
  get pendingToolUses() {
16102
16407
  return Array.from(this.openToolUses);
16103
16408
  }
@@ -16290,6 +16595,12 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
16290
16595
  lastAppendWarnAt = 0;
16291
16596
  writeChain = Promise.resolve();
16292
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;
16293
16604
  get isClosed() {
16294
16605
  return false;
16295
16606
  }
@@ -16330,14 +16641,20 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
16330
16641
  return true;
16331
16642
  }
16332
16643
  enqueueWrite(data) {
16644
+ return this.enqueueFlight({ data, stolen: false, started: false });
16645
+ }
16646
+ enqueueFlight(flight) {
16333
16647
  const write = this.writeChain.then(async () => {
16648
+ if (flight.stolen) return;
16649
+ flight.started = true;
16650
+ if (flight.stolen) return;
16334
16651
  try {
16335
- return await this.opts.getHandle().appendFile(data, "utf8");
16652
+ return await this.opts.getHandle().appendFile(flight.data, "utf8");
16336
16653
  } catch (err) {
16337
16654
  if (isClosedHandleError(err)) {
16338
16655
  const reloaded = await fsp7.open(this.opts.filePath, "a", 384);
16339
16656
  this.opts.setHandle(reloaded);
16340
- return await reloaded.appendFile(data, "utf8");
16657
+ return await reloaded.appendFile(flight.data, "utf8");
16341
16658
  }
16342
16659
  throw err;
16343
16660
  }
@@ -16362,17 +16679,29 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
16362
16679
  this.flushTimer = null;
16363
16680
  }
16364
16681
  }
16365
- async flushBuffer(isClosed = false) {
16366
- 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
+ }
16367
16696
  const flush = (async () => {
16368
- while (this.writeBuffer.length > 0) await this.flushBufferOnce(isClosed);
16697
+ while (this.writeBuffer.length > 0) await this.flushBufferOnce(isClosed, opts);
16369
16698
  })().finally(() => {
16370
16699
  if (this.flushPromise === flush) this.flushPromise = null;
16371
16700
  });
16372
16701
  this.flushPromise = flush;
16373
16702
  return flush;
16374
16703
  }
16375
- async flushBufferOnce(isClosed) {
16704
+ async flushBufferOnce(isClosed, opts) {
16376
16705
  if (this.writeBuffer.length === 0) return;
16377
16706
  const events = this.writeBuffer;
16378
16707
  const eventCount = events.length;
@@ -16380,12 +16709,21 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
16380
16709
  const batch = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
16381
16710
  this.writeBuffer = [];
16382
16711
  this.writeBufferBytes = 0;
16712
+ const flight = { data: batch, stolen: false, started: false };
16713
+ this.inFlight = flight;
16383
16714
  const t0 = Date.now();
16384
16715
  let outcome = "success";
16385
16716
  let errorMsg;
16386
16717
  try {
16387
- 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
+ }
16388
16725
  } catch (err) {
16726
+ if (flight.stolen) return;
16389
16727
  outcome = "failure";
16390
16728
  errorMsg = toErrorMessage(err);
16391
16729
  const newer = this.writeBuffer;
@@ -16423,6 +16761,7 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
16423
16761
  ...eventCount !== void 0 ? { eventCount } : {},
16424
16762
  ...this.opts.getTraceId?.() ? { traceId: this.opts.getTraceId() } : {}
16425
16763
  });
16764
+ if (this.inFlight === flight) this.inFlight = null;
16426
16765
  }
16427
16766
  }
16428
16767
  async drainWriteChain() {
@@ -16431,18 +16770,56 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
16431
16770
  async drainFlushPromise() {
16432
16771
  await this.flushPromise?.catch(() => void 0);
16433
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
+ */
16434
16784
  flushSync() {
16435
- if (this.writeBuffer.length === 0 || !this.opts.filePath) return;
16785
+ if (!this.opts.filePath) return;
16436
16786
  this.cancelTimer();
16437
- const batch = this.writeBuffer.map((e) => JSON.stringify(e)).join("\n") + "\n";
16438
- this.writeBuffer = [];
16439
- 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;
16440
16799
  let fd;
16441
16800
  try {
16442
16801
  fd = openSync(this.opts.filePath, "a");
16443
- writeSync(fd, batch, null, "utf8");
16802
+ writeSync(fd, chunks.join(""), null, "utf8");
16444
16803
  fsyncSync(fd);
16445
- } 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
+ );
16446
16823
  } finally {
16447
16824
  if (fd !== void 0) {
16448
16825
  try {
@@ -16610,6 +16987,17 @@ function isClosedHandleError2(err) {
16610
16987
  const code = err?.code;
16611
16988
  return code === "EBADF" || code === "ERR_CLOSED_RESOURCE" || code === "ERR_INVALID_HANDLE";
16612
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;
16613
17001
  var FileSessionWriter = class _FileSessionWriter {
16614
17002
  constructor(id, handle, startedAt, meta, events, opts = {}, traceId) {
16615
17003
  this.id = id;
@@ -16625,6 +17013,8 @@ var FileSessionWriter = class _FileSessionWriter {
16625
17013
  this._onAppend = opts.onAppend;
16626
17014
  this._onAppendBatch = opts.onAppendBatch;
16627
17015
  this.onCloseCb = opts.onClose;
17016
+ this.onMetadataCheckpointCb = opts.onMetadataCheckpoint;
17017
+ this.metadataCheckpointMs = opts.metadataCheckpointMs ?? METADATA_CHECKPOINT_INTERVAL_MS;
16628
17018
  this.summaryTracker = new SessionSummaryTracker({
16629
17019
  id,
16630
17020
  startedAt,
@@ -16692,6 +17082,15 @@ var FileSessionWriter = class _FileSessionWriter {
16692
17082
  this._onAppendBatch = cb;
16693
17083
  }
16694
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;
16695
17094
  /** Implements SessionWriter.traceId — propagated from ContextInit.traceId. */
16696
17095
  traceId;
16697
17096
  /**
@@ -16723,18 +17122,31 @@ var FileSessionWriter = class _FileSessionWriter {
16723
17122
  void this.ensureInit();
16724
17123
  const appendEvent = scrubSessionWriterEvent(event, this.secretScrubber);
16725
17124
  this.summaryTracker.observe(appendEvent);
17125
+ this.metadataDirty = true;
17126
+ this.scheduleMetadataCheckpoint();
16726
17127
  try {
16727
17128
  this._onAppend?.(appendEvent);
16728
17129
  } catch {
16729
17130
  }
17131
+ const critical = isCriticalEvent(appendEvent);
16730
17132
  if (!this.buffer.push(appendEvent)) {
16731
17133
  this.buffer.cancelTimer();
16732
- void this.buffer.flushBuffer(this.closed).catch(() => void 0).then(() => {
16733
- 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);
16734
17146
  });
16735
- } else if (this.buffer.shouldFlushNow()) {
17147
+ } else if (critical || this.buffer.shouldFlushNow()) {
16736
17148
  this.buffer.cancelTimer();
16737
- void this.buffer.flushBuffer(this.closed).catch(() => {
17149
+ void this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => {
16738
17150
  });
16739
17151
  } else {
16740
17152
  this.buffer.scheduleFlush(this.closed);
@@ -16805,23 +17217,112 @@ var FileSessionWriter = class _FileSessionWriter {
16805
17217
  provider: this.meta.provider ?? "unknown"
16806
17218
  });
16807
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
+ }
16808
17299
  async append(event) {
16809
17300
  if (this.closed) return;
16810
17301
  await this.ensureInit();
16811
17302
  const scrubbed = scrubSessionWriterEvent(event, this.secretScrubber);
16812
17303
  this.summaryTracker.observe(scrubbed);
17304
+ this.metadataDirty = true;
17305
+ this.scheduleMetadataCheckpoint();
16813
17306
  try {
16814
17307
  this._onAppend?.(scrubbed);
16815
17308
  } catch {
16816
17309
  }
16817
- if (!this.buffer.push(scrubbed)) {
17310
+ let pushed = this.buffer.push(scrubbed);
17311
+ if (!pushed) {
16818
17312
  this.buffer.cancelTimer();
16819
- await this.buffer.flushBuffer(this.closed).catch(() => void 0);
16820
- 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
+ }
16821
17322
  }
16822
- if (this.buffer.shouldFlushNow()) {
17323
+ if (isCriticalEvent(scrubbed) || this.buffer.shouldFlushNow()) {
16823
17324
  this.buffer.cancelTimer();
16824
- await this.buffer.flushBuffer(this.closed).catch(() => {
17325
+ await this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => {
16825
17326
  });
16826
17327
  } else {
16827
17328
  this.buffer.scheduleFlush(this.closed);
@@ -16838,20 +17339,33 @@ var FileSessionWriter = class _FileSessionWriter {
16838
17339
  this._onAppend?.(scrubbed);
16839
17340
  } catch {
16840
17341
  }
16841
- if (!this.buffer.push(scrubbed)) {
17342
+ let pushed = this.buffer.push(scrubbed);
17343
+ if (!pushed) {
16842
17344
  this.buffer.cancelTimer();
16843
- await this.buffer.flushBuffer(this.closed).catch(() => void 0);
16844
- 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
+ }
16845
17354
  }
16846
17355
  scrubbedBatch.push(scrubbed);
16847
17356
  }
17357
+ if (scrubbedBatch.length > 0) {
17358
+ this.metadataDirty = true;
17359
+ this.scheduleMetadataCheckpoint();
17360
+ }
16848
17361
  try {
16849
17362
  this._onAppendBatch?.(scrubbedBatch);
16850
17363
  } catch {
16851
17364
  }
16852
- if (this.buffer.shouldFlushNow()) {
17365
+ const hasCritical = scrubbedBatch.some(isCriticalEvent);
17366
+ if (hasCritical || this.buffer.shouldFlushNow()) {
16853
17367
  this.buffer.cancelTimer();
16854
- await this.buffer.flushBuffer(this.closed).catch(() => {
17368
+ await this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => {
16855
17369
  });
16856
17370
  } else {
16857
17371
  this.buffer.scheduleFlush(this.closed);
@@ -16859,8 +17373,10 @@ var FileSessionWriter = class _FileSessionWriter {
16859
17373
  }
16860
17374
  /**
16861
17375
  * Flush buffered events to disk immediately. Critical events
16862
- * (user_input, llm_response) call this so they survive SIGKILL/crash
16863
- * 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.
16864
17380
  *
16865
17381
  * Idempotent — cancels any pending timer, writes whatever has accumulated,
16866
17382
  * then asks the OS to synchronize the file data before resolving. Even an
@@ -16869,7 +17385,7 @@ var FileSessionWriter = class _FileSessionWriter {
16869
17385
  async flush() {
16870
17386
  if (this.closed) return;
16871
17387
  this.buffer.cancelTimer();
16872
- await this.buffer.flushBuffer(this.closed);
17388
+ await this.buffer.flushBuffer(this.closed, { datasync: true });
16873
17389
  await this.buffer.drainWriteChain();
16874
17390
  try {
16875
17391
  await this.handle.datasync();
@@ -16885,23 +17401,35 @@ var FileSessionWriter = class _FileSessionWriter {
16885
17401
  * Last-gasp synchronous drain for hard-exit paths (process.exit after
16886
17402
  * rapid Ctrl+C). The async write chain cannot be awaited when the process
16887
17403
  * is about to die, but whatever still sits in the in-memory buffer CAN be
16888
- * saved with a blocking append. Best-effort: an in-flight async write may
16889
- * 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.
16890
17407
  */
16891
17408
  flushSync() {
16892
17409
  this.buffer.flushSync();
16893
17410
  }
16894
17411
  async close() {
16895
17412
  if (this.closePromise) return this.closePromise;
16896
- 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);
16897
17419
  this.closed = false;
16898
17420
  this.closePromise = null;
16899
17421
  if (this.buffer.length > 0) this.buffer.scheduleFlush(this.closed);
17422
+ if (this.metadataDirty) this.scheduleMetadataCheckpoint();
16900
17423
  throw err;
16901
17424
  });
16902
17425
  return this.closePromise;
16903
17426
  }
16904
17427
  async doClose() {
17428
+ if (this.metadataTimer) {
17429
+ clearTimeout(this.metadataTimer);
17430
+ this.metadataTimer = null;
17431
+ }
17432
+ await this.metadataCheckpointInFlight?.catch(() => void 0);
16905
17433
  await this.ensureInit();
16906
17434
  if (this.pendingFileSnapshots.length > 0) {
16907
17435
  await this.writeFileSnapshot(this.activePromptIndex ?? 0, [...this.pendingFileSnapshots]);
@@ -16909,8 +17437,13 @@ var FileSessionWriter = class _FileSessionWriter {
16909
17437
  this.pendingFileSnapshotBytes = 0;
16910
17438
  }
16911
17439
  this.closed = true;
17440
+ if (this.metadataTimer) {
17441
+ clearTimeout(this.metadataTimer);
17442
+ this.metadataTimer = null;
17443
+ }
17444
+ await this.metadataCheckpointInFlight?.catch(() => void 0);
16912
17445
  this.buffer.cancelTimer();
16913
- await this.buffer.flushBuffer(this.closed);
17446
+ await this.buffer.flushBuffer(this.closed, { datasync: true });
16914
17447
  await this.buffer.drainWriteChain();
16915
17448
  try {
16916
17449
  await this.handle.datasync();
@@ -17025,10 +17558,23 @@ var FileSessionWriter = class _FileSessionWriter {
17025
17558
  async truncateToCheckpoint(targetPromptIndex, revertedFiles = []) {
17026
17559
  if (!this.filePath) return 0;
17027
17560
  this.buffer.cancelTimer();
17028
- await this.buffer.flushBuffer(this.closed);
17561
+ await this.buffer.flushBuffer(this.closed, { datasync: true });
17029
17562
  await this.buffer.drainWriteChain();
17030
- const plan = await findSessionCheckpointTruncatePlan(this.filePath, targetPromptIndex);
17031
- 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
+ }
17032
17578
  await this.buffer.drainWriteChain();
17033
17579
  try {
17034
17580
  await this.handle.close();
@@ -17039,6 +17585,7 @@ var FileSessionWriter = class _FileSessionWriter {
17039
17585
  this.handle = await fsp9.open(this.filePath, "a", 384);
17040
17586
  } catch (err) {
17041
17587
  this.handle = await fsp9.open(this.filePath, "a", 384).catch(() => this.handle);
17588
+ this.scheduleMetadataCheckpoint();
17042
17589
  throw err;
17043
17590
  }
17044
17591
  await this.summaryTracker.recomputeFromDisk(this.filePath);
@@ -17064,6 +17611,11 @@ var FileSessionWriter = class _FileSessionWriter {
17064
17611
  await this.buffer.drainFlushPromise();
17065
17612
  this.buffer.clear();
17066
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);
17067
17619
  const resetAt = (/* @__PURE__ */ new Date()).toISOString();
17068
17620
  const record = `${JSON.stringify({
17069
17621
  type: "session_start",
@@ -17074,8 +17626,10 @@ var FileSessionWriter = class _FileSessionWriter {
17074
17626
  })}
17075
17627
  `;
17076
17628
  await this.handle.close();
17077
- await fsp9.writeFile(this.filePath, record, "utf8");
17629
+ await atomicWrite(this.filePath, record, { mode: 384 });
17078
17630
  this.summaryTracker.reset(resetAt);
17631
+ this.metadataDirty = true;
17632
+ this.scheduleMetadataCheckpoint();
17079
17633
  this.activePromptIndex = null;
17080
17634
  this.pendingFileSnapshots = [];
17081
17635
  this.pendingFileSnapshotBytes = 0;
@@ -17466,7 +18020,7 @@ var SessionCheckpointCas = class {
17466
18020
  }
17467
18021
  };
17468
18022
  function defaultRunGit(args, cwd) {
17469
- return new Promise((resolve16) => {
18023
+ return new Promise((resolve17) => {
17470
18024
  const stdoutChunks = [];
17471
18025
  const stderrChunks = [];
17472
18026
  let stdoutBytes = 0;
@@ -17509,8 +18063,8 @@ function defaultRunGit(args, cwd) {
17509
18063
  stdoutTruncated,
17510
18064
  stderrTruncated
17511
18065
  });
17512
- child.on("error", (err) => resolve16(result(1, err.message)));
17513
- 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)));
17514
18068
  });
17515
18069
  }
17516
18070
 
@@ -18324,7 +18878,7 @@ async function executeRenameSession(params) {
18324
18878
 
18325
18879
  // src/storage/session-store/resume-session.ts
18326
18880
  import * as fsp16 from "node:fs/promises";
18327
- import * as path30 from "node:path";
18881
+ import * as path31 from "node:path";
18328
18882
 
18329
18883
  // src/storage/session-resume-validation.ts
18330
18884
  import { createHash as createHash5 } from "node:crypto";
@@ -18420,8 +18974,22 @@ async function validateResumeFileObservations(events, projectRoot) {
18420
18974
  }
18421
18975
  var RESUME_NOTICE_HEADERS = [
18422
18976
  "[SESSION RESUME FILE VALIDATION]",
18423
- "[SESSION RESUME INTERRUPTED WORK]"
18977
+ "[SESSION RESUME INTERRUPTED WORK]",
18978
+ "[SESSION RESUME CRASH RECOVERY]"
18424
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
+ }
18425
18993
  function isResumeNoticeMessage(message) {
18426
18994
  if (message.role !== "system" || typeof message.content !== "string") return false;
18427
18995
  return RESUME_NOTICE_HEADERS.some((header) => message.content === header || message.content.startsWith(`${header}
@@ -18454,9 +19022,345 @@ function formatInterruptedToolNotice(pendingToolUseCount) {
18454
19022
  ].join("\n");
18455
19023
  }
18456
19024
 
18457
- // src/storage/session-store/summary-builder.ts
19025
+ // src/storage/session-recovery.ts
18458
19026
  import { createReadStream as createReadStream4 } from "node:fs";
19027
+ import * as fs4 from "node:fs/promises";
19028
+ import * as path30 from "node:path";
18459
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";
18460
19364
  async function summarizeSessionFile(opts) {
18461
19365
  return summarizeSessionEventSequence({
18462
19366
  id: opts.id,
@@ -18590,8 +19494,8 @@ async function summarizeSessionEventSequence(opts) {
18590
19494
  }
18591
19495
  }
18592
19496
  async function* iterateSessionEvents(file, secretScrubber) {
18593
- const stream = createReadStream4(file, { encoding: "utf8" });
18594
- const lines = createInterface4({ input: stream, crlfDelay: Infinity });
19497
+ const stream = createReadStream5(file, { encoding: "utf8" });
19498
+ const lines = createInterface5({ input: stream, crlfDelay: Infinity });
18595
19499
  try {
18596
19500
  for await (const line of lines) {
18597
19501
  if (!line.trim()) continue;
@@ -18624,7 +19528,8 @@ async function executeResumeSession(params) {
18624
19528
  readSummaryManifest,
18625
19529
  searchEvents,
18626
19530
  persistCatalogSummary,
18627
- logWarn
19531
+ logWarn,
19532
+ sessionsDir
18628
19533
  } = params;
18629
19534
  const t0 = Date.now();
18630
19535
  const data = await load(canonicalId);
@@ -18645,6 +19550,24 @@ async function executeResumeSession(params) {
18645
19550
  ...derivedSummary,
18646
19551
  ...persistedSummary?.name !== void 0 ? { name: persistedSummary.name } : {}
18647
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);
18648
19571
  const noticeMessages = [];
18649
19572
  let resumeValidation;
18650
19573
  if (projectRoot) {
@@ -18673,7 +19596,7 @@ async function executeResumeSession(params) {
18673
19596
  );
18674
19597
  }
18675
19598
  }
18676
- const interruptedNotice = formatInterruptedToolNotice(data.pendingToolUseCount ?? 0);
19599
+ const interruptedNotice = recoveryPlan.stale ? null : formatInterruptedToolNotice(data.pendingToolUseCount ?? 0);
18677
19600
  if (interruptedNotice) {
18678
19601
  noticeMessages.push({
18679
19602
  role: "system",
@@ -18681,6 +19604,14 @@ async function executeResumeSession(params) {
18681
19604
  ts: (/* @__PURE__ */ new Date()).toISOString()
18682
19605
  });
18683
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
+ }
18684
19615
  const carriedMessages = data.messages.filter((message) => !isResumeNoticeMessage(message));
18685
19616
  const resumedData = {
18686
19617
  ...data,
@@ -18711,7 +19642,7 @@ async function executeResumeSession(params) {
18711
19642
  {
18712
19643
  resumed: true,
18713
19644
  initialSummary,
18714
- dir: path30.dirname(file),
19645
+ dir: path31.dirname(file),
18715
19646
  filePath: file,
18716
19647
  secretScrubber,
18717
19648
  checkpointCas,
@@ -18722,9 +19653,21 @@ async function executeResumeSession(params) {
18722
19653
  if (!current) return null;
18723
19654
  return current.name === void 0 ? {} : { name: sessionContentText(secretScrubber.scrub(current.name)) };
18724
19655
  },
18725
- 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)
18726
19661
  }
18727
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
+ }
18728
19671
  emitSessionStoreWrite(events, canonicalId, file, "resume", "success", Date.now() - t0);
18729
19672
  return { writer, data: resumedData };
18730
19673
  } catch (err) {
@@ -18943,6 +19886,11 @@ function applySessionIndexLines(raw, byId, deleted) {
18943
19886
  byId.delete(entry.id);
18944
19887
  continue;
18945
19888
  }
19889
+ if (entry.action === "create" && entry.id) {
19890
+ deleted.delete(entry.id);
19891
+ byId.delete(entry.id);
19892
+ continue;
19893
+ }
18946
19894
  if (entry.id && !deleted.has(entry.id)) {
18947
19895
  byId.set(entry.id, entry);
18948
19896
  }
@@ -18987,6 +19935,7 @@ async function readFileRange(file, start, end) {
18987
19935
 
18988
19936
  // src/storage/session-store/session-store-index.ts
18989
19937
  var COMPACT_EVERY = 30;
19938
+ var NO_DELETED_IDS = /* @__PURE__ */ new Set();
18990
19939
  async function appendToIndexStrict(dir, indexFile, summary, invalidateShard, onAppended, compactInner) {
18991
19940
  await ensureDir(dir);
18992
19941
  let shouldCompact = false;
@@ -19019,10 +19968,14 @@ async function readIndexFile(indexFile, currentCache) {
19019
19968
  const s = await fsp20.stat(indexFile);
19020
19969
  stat13 = { mtimeMs: s.mtimeMs, size: s.size, ino: s.ino, birthtimeMs: s.birthtimeMs };
19021
19970
  } catch {
19022
- return { summaries: [], cache: null };
19971
+ return { summaries: [], deletedIds: NO_DELETED_IDS, cache: null };
19023
19972
  }
19024
19973
  if (currentCache !== null && currentCache.mtimeMs === stat13.mtimeMs && currentCache.size === stat13.size && currentCache.ino === stat13.ino && currentCache.birthtimeMs === stat13.birthtimeMs) {
19025
- return { summaries: currentCache.summaries, cache: currentCache };
19974
+ return {
19975
+ summaries: currentCache.summaries,
19976
+ deletedIds: currentCache.deleted,
19977
+ cache: currentCache
19978
+ };
19026
19979
  }
19027
19980
  const cached = currentCache;
19028
19981
  const sameFile = cached !== null && cached.ino === stat13.ino && cached.birthtimeMs === stat13.birthtimeMs;
@@ -19038,14 +19991,14 @@ async function readIndexFile(indexFile, currentCache) {
19038
19991
  byId: cached.byId,
19039
19992
  deleted: cached.deleted
19040
19993
  };
19041
- return { summaries: summaries2, cache: nextCache2 };
19994
+ return { summaries: summaries2, deletedIds: cached.deleted, cache: nextCache2 };
19042
19995
  }
19043
19996
  }
19044
19997
  let raw;
19045
19998
  try {
19046
19999
  raw = await fsp20.readFile(indexFile, "utf8");
19047
20000
  } catch {
19048
- return { summaries: [], cache: null };
20001
+ return { summaries: [], deletedIds: NO_DELETED_IDS, cache: null };
19049
20002
  }
19050
20003
  const deleted = /* @__PURE__ */ new Set();
19051
20004
  const byId = /* @__PURE__ */ new Map();
@@ -19053,7 +20006,18 @@ async function readIndexFile(indexFile, currentCache) {
19053
20006
  const summaries = Array.from(byId.values());
19054
20007
  summaries.sort(compareSessionSummaries);
19055
20008
  const nextCache = { ...stat13, summaries, byId, deleted };
19056
- 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 });
19057
20021
  }
19058
20022
 
19059
20023
  // src/storage/session-store/shard-manifest.ts
@@ -19091,12 +20055,12 @@ async function readOrBuildShardManifestEntry(opts) {
19091
20055
  }
19092
20056
 
19093
20057
  // src/storage/session-store/strict-empty-check.ts
19094
- import { createReadStream as createReadStream5 } from "node:fs";
19095
- import { createInterface as createInterface5 } from "node:readline";
20058
+ import { createReadStream as createReadStream6 } from "node:fs";
20059
+ import { createInterface as createInterface6 } from "node:readline";
19096
20060
  var EMPTY_SESSION_EVENT_TYPES = /* @__PURE__ */ new Set(["session_start", "session_resumed", "session_end"]);
19097
20061
  async function isStrictlyEmptySessionFile(file) {
19098
- const input = createReadStream5(file, { encoding: "utf8" });
19099
- const lines = createInterface5({ input, crlfDelay: Infinity });
20062
+ const input = createReadStream6(file, { encoding: "utf8" });
20063
+ const lines = createInterface6({ input, crlfDelay: Infinity });
19100
20064
  let sawSessionStart = false;
19101
20065
  try {
19102
20066
  for await (const line of lines) {
@@ -19172,6 +20136,7 @@ function damagedSummary(id, startedAt) {
19172
20136
  }
19173
20137
 
19174
20138
  // src/storage/session-store.ts
20139
+ var SESSION_FILTER_POOL_LIMIT = 1e4;
19175
20140
  var DefaultSessionStore = class _DefaultSessionStore {
19176
20141
  dir;
19177
20142
  events;
@@ -19187,14 +20152,36 @@ var DefaultSessionStore = class _DefaultSessionStore {
19187
20152
  _loadCache = /* @__PURE__ */ new Map();
19188
20153
  loadCache = new SessionLoadCache(this._loadCache);
19189
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();
19190
20177
  shardManifestCache = /* @__PURE__ */ new Map();
19191
20178
  static LIST_SCAN_CONCURRENCY = 32;
19192
20179
  indexAppendCount = 0;
19193
20180
  constructor(opts) {
19194
20181
  this.dir = opts.dir;
19195
- this.projectRoot = opts.projectRoot ? path31.resolve(opts.projectRoot) : void 0;
20182
+ this.projectRoot = opts.projectRoot ? path32.resolve(opts.projectRoot) : void 0;
19196
20183
  this.checkpointCas = this.projectRoot ? new SessionCheckpointCas({
19197
- rootDir: path31.join(this.dir, "_cas"),
20184
+ rootDir: path32.join(this.dir, "_cas"),
19198
20185
  projectRoot: this.projectRoot
19199
20186
  }) : void 0;
19200
20187
  this.events = opts.events;
@@ -19205,7 +20192,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
19205
20192
  this.onAppendBatch = opts.onAppendBatch;
19206
20193
  const builtRuntime = import.meta.url.includes("/dist/");
19207
20194
  this.catalogClient = this.projectRoot && (builtRuntime || process.env["WRONGSTACK_SESSION_CATALOG_FORCE"] === "1") && resolveSessionCatalogProjectServerUrl() ? new SessionCatalogProjectClient({
19208
- projectDir: path31.dirname(this.dir),
20195
+ projectDir: path32.dirname(this.dir),
19209
20196
  projectRoot: this.projectRoot
19210
20197
  }) : void 0;
19211
20198
  }
@@ -19227,7 +20214,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
19227
20214
  this.clearLoadCache();
19228
20215
  }
19229
20216
  get indexFile() {
19230
- return path31.join(this.dir, "_index.jsonl");
20217
+ return path32.join(this.dir, "_index.jsonl");
19231
20218
  }
19232
20219
  sessionPath(id, ext) {
19233
20220
  return sessionPath(this.dir, id, ext);
@@ -19253,19 +20240,94 @@ var DefaultSessionStore = class _DefaultSessionStore {
19253
20240
  async ensureShardDir(id) {
19254
20241
  return ensureShardDir(this.dir, id);
19255
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
+ */
19256
20254
  async create(meta) {
19257
20255
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
19258
20256
  const id = meta.id && meta.id.length > 0 ? meta.id : generateSessionId(startedAt);
19259
20257
  const shardDir = await this.ensureShardDir(id);
19260
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
+ }
19261
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
+ }
19262
20323
  let handle;
19263
20324
  try {
19264
- handle = await fsp23.open(file, "a", 384);
20325
+ handle = await fsp23.open(file, "w", 384);
19265
20326
  } catch (err) {
19266
20327
  emitSessionStoreError(this.events, id, file, "create", toErrorMessage(err), false);
19267
20328
  throw new Error(`Failed to open session file: ${toErrorMessage(err)}`, { cause: err });
19268
20329
  }
20330
+ await this.invalidateShardManifestBySessionId(id).catch(() => void 0);
19269
20331
  try {
19270
20332
  const writer = new FileSessionWriter(id, handle, startedAt, meta, this.events, {
19271
20333
  dir: shardDir,
@@ -19279,23 +20341,11 @@ var DefaultSessionStore = class _DefaultSessionStore {
19279
20341
  if (!current) return null;
19280
20342
  return current.name === void 0 ? {} : { name: sessionContentText(this.secretScrubber.scrub(current.name)) };
19281
20343
  },
19282
- 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)
19283
20348
  });
19284
- if (this.catalogClient) {
19285
- await this.catalogClient.call("upsert_summary", {
19286
- summary: {
19287
- id,
19288
- title: meta.title ?? "",
19289
- startedAt,
19290
- model: meta.model ?? "",
19291
- provider: meta.provider ?? "",
19292
- tokenTotal: 0,
19293
- lastActivityAt: startedAt
19294
- },
19295
- transcriptRelativePath: `${id}.jsonl`,
19296
- summaryRelativePath: `${id}.summary.json`
19297
- });
19298
- }
19299
20349
  emitSessionStoreWrite(this.events, id, file, "create", "success", Date.now() - t0);
19300
20350
  return writer;
19301
20351
  } catch (err) {
@@ -19357,7 +20407,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
19357
20407
  readSummaryManifest: (summaryId) => this.readSummaryManifest(summaryId),
19358
20408
  searchEvents: (searchId, pred) => this.searchEvents(searchId, pred),
19359
20409
  persistCatalogSummary: (sum) => this.persistCatalogSummary(sum),
19360
- logWarn: (msg, ctx) => this.logWarn(msg, ctx)
20410
+ logWarn: (msg, ctx) => this.logWarn(msg, ctx),
20411
+ sessionsDir: this.dir
19361
20412
  });
19362
20413
  }
19363
20414
  async load(id) {
@@ -19431,11 +20482,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
19431
20482
  return this.scrubSummaries(records);
19432
20483
  }
19433
20484
  try {
19434
- const indexed = await this.readIndex();
19435
- if (indexed.length > 0) {
19436
- return this.scrubSummaries(indexed.slice(0, limit));
19437
- }
19438
- 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));
19439
20493
  } catch {
19440
20494
  return [];
19441
20495
  }
@@ -19450,15 +20504,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
19450
20504
  return this.scrubSummaries(records);
19451
20505
  }
19452
20506
  try {
19453
- const indexed = await this.readIndex();
19454
- if (indexed.length === 0) {
19455
- const raw = await this.list(Math.max(limit, 100));
19456
- return raw.filter((s) => matchesSessionFilter(s, criteria)).slice(0, limit);
19457
- }
19458
- const filtered = this.scrubSummaries(indexed).filter(
19459
- (s) => matchesSessionFilter(s, criteria)
19460
- );
19461
- 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);
19462
20515
  } catch {
19463
20516
  return [];
19464
20517
  }
@@ -19500,6 +20553,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
19500
20553
  id,
19501
20554
  (sid) => this.invalidateShardManifestBySessionId(sid),
19502
20555
  () => {
20556
+ this._manualTombstones.add(id);
20557
+ this._indexDeletedIds.add(id);
19503
20558
  this._indexCache = null;
19504
20559
  this.indexAppendCount++;
19505
20560
  }
@@ -19527,34 +20582,92 @@ var DefaultSessionStore = class _DefaultSessionStore {
19527
20582
  );
19528
20583
  }
19529
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
+ */
19530
20603
  async compactIndexInner() {
19531
20604
  const entries = await this.readIndex();
19532
- if (entries.length === 0) return;
19533
- const lines = entries.map((s) => JSON.stringify(s)).join("\n") + "\n";
19534
- await atomicWrite(this.indexFile, lines, { mode: 384 });
20605
+ await compactIndexInner(this.indexFile, entries, this._indexFileDeletedIds);
19535
20606
  this._indexCache = null;
19536
20607
  }
19537
20608
  async readIndex() {
19538
- const { summaries, cache } = await readIndexFile(this.indexFile, this._indexCache);
20609
+ const { summaries, deletedIds, cache } = await readIndexFile(this.indexFile, this._indexCache);
19539
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;
19540
20618
  return summaries;
19541
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
+ */
19542
20644
  async rebuildIndex() {
19543
20645
  if (this.catalogClient) {
19544
20646
  const result = await this.catalogClient.call("rebuild_catalog", {}, { timeoutMs: 12e4 });
19545
20647
  return result.indexed;
19546
20648
  }
19547
- const ids = await this.collectSessionIds(this.dir);
19548
- const summaries = await Promise.all(
19549
- ids.map((id) => this.summaryFor(id).catch(() => null))
19550
- );
19551
- const valid = summaries.filter((s) => s !== null);
19552
- const lines = valid.map((s) => JSON.stringify(s)).join("\n") + "\n";
19553
- 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";
19554
20667
  await atomicWrite(this.indexFile, lines, { mode: 384 });
19555
20668
  this._indexCache = null;
20669
+ return valid.length - tombstoned;
19556
20670
  });
19557
- return valid.length;
19558
20671
  }
19559
20672
  async listFromDirectoryScan(limit) {
19560
20673
  const shardKeys = await this.collectShardKeys();
@@ -19636,7 +20749,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
19636
20749
  return void 0;
19637
20750
  }
19638
20751
  async collectSessionFilesInShard(shardKey) {
19639
- const dir = shardKey ? path31.join(this.dir, shardKey) : this.dir;
20752
+ const dir = shardKey ? path32.join(this.dir, shardKey) : this.dir;
19640
20753
  const entries = await this.collectSessionFiles(dir, shardKey);
19641
20754
  return shardKey ? entries.filter((entry) => entry.id.startsWith(`${shardKey}/`)) : entries.filter((entry) => !entry.id.includes("/"));
19642
20755
  }
@@ -19812,9 +20925,9 @@ function makeDirectorSessionFactory(opts) {
19812
20925
  let dir;
19813
20926
  if (opts.store) {
19814
20927
  store = opts.store;
19815
- dir = opts.sessionsRoot ? path32.join(opts.sessionsRoot, runId) : "(caller-managed)";
20928
+ dir = opts.sessionsRoot ? path33.join(opts.sessionsRoot, runId) : "(caller-managed)";
19816
20929
  } else if (opts.sessionsRoot) {
19817
- dir = path32.join(opts.sessionsRoot, runId);
20930
+ dir = path33.join(opts.sessionsRoot, runId);
19818
20931
  store = new DefaultSessionStore({ dir });
19819
20932
  } else {
19820
20933
  throw new Error("makeDirectorSessionFactory requires either `store` or `sessionsRoot`");
@@ -19838,7 +20951,7 @@ function makeDirectorSessionFactory(opts) {
19838
20951
  }
19839
20952
  async function readDirectorSubagentSession(args) {
19840
20953
  if (!args.sessionsRoot) return null;
19841
- const filePath = path32.join(args.sessionsRoot, args.directorRunId, `${args.subagentId}.jsonl`);
20954
+ const filePath = path33.join(args.sessionsRoot, args.directorRunId, `${args.subagentId}.jsonl`);
19842
20955
  let raw;
19843
20956
  try {
19844
20957
  raw = await fsp24.readFile(filePath, "utf8");
@@ -21538,12 +22651,12 @@ async function executeSubagentWithTimeout({
21538
22651
  }
21539
22652
  return new Promise((resolveDecision) => {
21540
22653
  let settled = false;
21541
- const resolve16 = (d) => {
22654
+ const resolve17 = (d) => {
21542
22655
  if (settled) return;
21543
22656
  settled = true;
21544
22657
  resolveDecision(d);
21545
22658
  };
21546
- const fallback = setTimeout(() => resolve16("stop"), DECISION_TIMEOUT_MS);
22659
+ const fallback = setTimeout(() => resolve17("stop"), DECISION_TIMEOUT_MS);
21547
22660
  const sessionId = currentSessionId();
21548
22661
  budget._events?.emit("budget.threshold_reached", {
21549
22662
  ...sessionId ? { sessionId } : {},
@@ -21553,11 +22666,11 @@ async function executeSubagentWithTimeout({
21553
22666
  timeoutMs: DECISION_TIMEOUT_MS,
21554
22667
  extend: (extra) => {
21555
22668
  clearTimeout(fallback);
21556
- queueMicrotask(() => resolve16({ extend: extra }));
22669
+ queueMicrotask(() => resolve17({ extend: extra }));
21557
22670
  },
21558
22671
  deny: () => {
21559
22672
  clearTimeout(fallback);
21560
- resolve16("stop");
22673
+ resolve17("stop");
21561
22674
  }
21562
22675
  });
21563
22676
  });
@@ -21964,7 +23077,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
21964
23077
  taskIds.map((id) => {
21965
23078
  const cached = this.completedResults.find((r) => r.taskId === id);
21966
23079
  if (cached) return cached;
21967
- return new Promise((resolve16, reject) => {
23080
+ return new Promise((resolve17, reject) => {
21968
23081
  const timeout = setTimeout(() => {
21969
23082
  this.off("task.completed", handler);
21970
23083
  reject(new Error(`awaitTasks timed out waiting for task "${id}"`));
@@ -21973,7 +23086,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
21973
23086
  if (result.taskId === id) {
21974
23087
  clearTimeout(timeout);
21975
23088
  this.off("task.completed", handler);
21976
- resolve16(result);
23089
+ resolve17(result);
21977
23090
  }
21978
23091
  };
21979
23092
  this.on("task.completed", handler);
@@ -21998,13 +23111,13 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
21998
23111
  const done = new Set(completed.map((r) => r.taskId));
21999
23112
  return { completed, pending: taskIds.filter((id) => !done.has(id)) };
22000
23113
  }
22001
- return new Promise((resolve16) => {
23114
+ return new Promise((resolve17) => {
22002
23115
  let timer;
22003
23116
  const handler = ({ result }) => {
22004
23117
  if (!ids.has(result.taskId)) return;
22005
23118
  if (timer) clearTimeout(timer);
22006
23119
  this.off("task.completed", handler);
22007
- resolve16({
23120
+ resolve17({
22008
23121
  completed: [result],
22009
23122
  pending: taskIds.filter((id) => id !== result.taskId)
22010
23123
  });
@@ -22012,7 +23125,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
22012
23125
  if (opts?.timeoutMs !== void 0) {
22013
23126
  timer = setTimeout(() => {
22014
23127
  this.off("task.completed", handler);
22015
- resolve16({ completed: [], pending: [...taskIds], timedOut: true });
23128
+ resolve17({ completed: [], pending: [...taskIds], timedOut: true });
22016
23129
  }, opts.timeoutMs);
22017
23130
  }
22018
23131
  this.on("task.completed", handler);
@@ -22612,6 +23725,7 @@ function worktreeOwnerLabel(task, config) {
22612
23725
  }
22613
23726
 
22614
23727
  // src/coordination/director.ts
23728
+ var BUSY_REARM_FLOOR_MS = 1e3;
22615
23729
  var Director = class _Director {
22616
23730
  /* eslint-disable-next-line @typescript-eslint/no-unused-vars — just a cast helper */
22617
23731
  static _asManifestEntry(v) {
@@ -22677,6 +23791,13 @@ var Director = class _Director {
22677
23791
  subagentIdleTimeoutMs;
22678
23792
  retireSubagentOnTaskComplete;
22679
23793
  subagentIdleTimers = /* @__PURE__ */ new Map();
23794
+ /**
23795
+ * Effective idle window per subagent (spawn-time `idleTimeoutMs` override
23796
+ * or the Director-wide default; undefined = no window). Internal-task
23797
+ * completion re-arms with THIS value, not the Director-wide default, so
23798
+ * a subagent-configured window survives its first internal probe.
23799
+ */
23800
+ subagentIdleDelayMs = /* @__PURE__ */ new Map();
22680
23801
  sharedScratchpadPath;
22681
23802
  maxSpawns;
22682
23803
  maxSpawnDepth;
@@ -22833,7 +23954,13 @@ var Director = class _Director {
22833
23954
  handleTaskCompleted(payload) {
22834
23955
  const r = payload.result;
22835
23956
  const settled = this.tasks.settle(r);
22836
- if (settled.internal) return;
23957
+ if (settled.internal) {
23958
+ this.armSubagentIdleRetirement(
23959
+ r.subagentId,
23960
+ this.subagentIdleDelayMs.get(r.subagentId) ?? this.subagentIdleTimeoutMs
23961
+ );
23962
+ return;
23963
+ }
22837
23964
  const title = this.tasks.descriptionFor(r.taskId, payload.task.description ?? r.taskId);
22838
23965
  if (!settled.consumedInBand && this.taskResultNotifier) {
22839
23966
  const resultText = typeof r.result === "string" ? r.result : r.result !== void 0 ? safeStringify(r.result) : void 0;
@@ -22898,7 +24025,7 @@ var Director = class _Director {
22898
24025
  }
22899
24026
  this.armSubagentIdleRetirement(
22900
24027
  r.subagentId,
22901
- this.retireSubagentOnTaskComplete ? 0 : this.subagentIdleTimeoutMs
24028
+ this.retireSubagentOnTaskComplete ? 0 : this.subagentIdleDelayMs.get(r.subagentId) ?? this.subagentIdleTimeoutMs
22902
24029
  );
22903
24030
  }
22904
24031
  extensionsFor(subagentId) {
@@ -22984,6 +24111,7 @@ var Director = class _Director {
22984
24111
  this.resolveSpawnModel(config);
22985
24112
  const subagentId = await spawn4(this, config, priceLookup);
22986
24113
  const perSubagentIdleMs = typeof config.idleTimeoutMs === "number" && Number.isFinite(config.idleTimeoutMs) && config.idleTimeoutMs >= 0 ? config.idleTimeoutMs : this.subagentIdleTimeoutMs;
24114
+ this.subagentIdleDelayMs.set(subagentId, perSubagentIdleMs);
22987
24115
  this.armSubagentIdleRetirement(subagentId, perSubagentIdleMs);
22988
24116
  return subagentId;
22989
24117
  }
@@ -23037,6 +24165,7 @@ var Director = class _Director {
23037
24165
  this.budgetPolicy.dispose();
23038
24166
  for (const timer of this.subagentIdleTimers.values()) clearTimeout(timer);
23039
24167
  this.subagentIdleTimers.clear();
24168
+ this.subagentIdleDelayMs.clear();
23040
24169
  await this.coordinator.stopAll();
23041
24170
  this.tasks.resolveWaitersOnShutdown();
23042
24171
  for (const b of this.subagentBridges.values()) {
@@ -23095,6 +24224,7 @@ var Director = class _Director {
23095
24224
  }
23096
24225
  async remove(subagentId) {
23097
24226
  this.clearSubagentIdleRetirement(subagentId);
24227
+ this.subagentIdleDelayMs.delete(subagentId);
23098
24228
  void this.appendSessionEvent({
23099
24229
  type: "agent_stopped",
23100
24230
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -23147,9 +24277,13 @@ var Director = class _Director {
23147
24277
  const timer = setTimeout(() => {
23148
24278
  this.subagentIdleTimers.delete(subagentId);
23149
24279
  const entry = this.coordinator.getStatus().subagents.find((a) => a.id === subagentId);
23150
- if (entry?.status !== "idle") return;
24280
+ if (entry === void 0) return;
24281
+ if (entry.status !== "idle") {
24282
+ this.armSubagentIdleRetirement(subagentId, Math.max(delayMs, BUSY_REARM_FLOOR_MS));
24283
+ return;
24284
+ }
23151
24285
  if (this.coordinator.listPendingTasks().some((task) => task.subagentId === subagentId)) {
23152
- this.armSubagentIdleRetirement(subagentId, this.subagentIdleTimeoutMs);
24286
+ this.armSubagentIdleRetirement(subagentId, Math.max(delayMs, BUSY_REARM_FLOOR_MS));
23153
24287
  return;
23154
24288
  }
23155
24289
  void this.remove(subagentId).catch(
@@ -23259,7 +24393,7 @@ var Director = class _Director {
23259
24393
  // src/coordination/fleet-manager.ts
23260
24394
  import { randomUUID as randomUUID17 } from "node:crypto";
23261
24395
  import * as fsp26 from "node:fs/promises";
23262
- import * as path33 from "node:path";
24396
+ import * as path34 from "node:path";
23263
24397
  var FleetManager = class {
23264
24398
  /** The fleet-wide event bus. */
23265
24399
  fleet;
@@ -23593,7 +24727,7 @@ var FleetManager = class {
23593
24727
  })),
23594
24728
  usage: this.usage.snapshot()
23595
24729
  };
23596
- await fsp26.mkdir(path33.dirname(this.manifestPath), { recursive: true });
24730
+ await fsp26.mkdir(path34.dirname(this.manifestPath), { recursive: true });
23597
24731
  await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
23598
24732
  return this.manifestPath;
23599
24733
  }
@@ -23756,7 +24890,7 @@ var FleetManager = class {
23756
24890
  };
23757
24891
 
23758
24892
  // src/coordination/remote-mailbox.ts
23759
- import * as path37 from "node:path";
24893
+ import * as path38 from "node:path";
23760
24894
 
23761
24895
  // src/coordination/mailbox-constants.ts
23762
24896
  var HQ_MAILBOX_SNAPSHOT_MIN_INTERVAL_MS = 1e4;
@@ -23766,15 +24900,15 @@ var MAILBOX_MAX_ACK_BATCH = 500;
23766
24900
 
23767
24901
  // src/coordination/mailbox-project-server-client.ts
23768
24902
  import { spawn as spawn5 } from "node:child_process";
23769
- import * as fs4 from "node:fs";
24903
+ import * as fs5 from "node:fs";
23770
24904
  import * as net2 from "node:net";
23771
- import * as path35 from "node:path";
24905
+ import * as path36 from "node:path";
23772
24906
  import { fileURLToPath as fileURLToPath4 } from "node:url";
23773
24907
 
23774
24908
  // src/coordination/mailbox-project-server-endpoint.ts
23775
24909
  import { createHash as createHash6 } from "node:crypto";
23776
24910
  import * as os3 from "node:os";
23777
- import * as path34 from "node:path";
24911
+ import * as path35 from "node:path";
23778
24912
 
23779
24913
  // src/coordination/mailbox-project-server-protocol.ts
23780
24914
  var MAILBOX_PROJECT_SERVER_PROTOCOL_VERSION = 4;
@@ -23807,7 +24941,7 @@ function encodeMailboxProjectServerMessage(message) {
23807
24941
  // src/coordination/mailbox-project-server-endpoint.ts
23808
24942
  var MAILBOX_PROJECT_SERVER_METADATA_FILE = ".mailbox-server.json";
23809
24943
  function normalizeLocalPath(value) {
23810
- const resolved = path34.resolve(value);
24944
+ const resolved = path35.resolve(value);
23811
24945
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
23812
24946
  }
23813
24947
  function mailboxProjectServerKey(projectDir) {
@@ -23818,14 +24952,14 @@ function mailboxProjectServerEndpoint(projectDir) {
23818
24952
  if (process.platform === "win32") {
23819
24953
  return `\\\\.\\pipe\\wrongstack-mailbox-v${MAILBOX_PROJECT_SERVER_PROTOCOL_VERSION}-${key}`;
23820
24954
  }
23821
- return path34.join(
24955
+ return path35.join(
23822
24956
  os3.tmpdir(),
23823
24957
  `wsmb-v${MAILBOX_PROJECT_SERVER_PROTOCOL_VERSION}`,
23824
24958
  `${key}.sock`
23825
24959
  );
23826
24960
  }
23827
24961
  function mailboxProjectServerMetadataPath(projectDir) {
23828
- return path34.join(path34.resolve(projectDir), MAILBOX_PROJECT_SERVER_METADATA_FILE);
24962
+ return path35.join(path35.resolve(projectDir), MAILBOX_PROJECT_SERVER_METADATA_FILE);
23829
24963
  }
23830
24964
 
23831
24965
  // src/coordination/mailbox-project-server-client.ts
@@ -23836,7 +24970,7 @@ var DEFAULT_HEARTBEAT_INTERVAL_MS = 1e4;
23836
24970
  var AUTH_RETRY_DELAY_MS = 150;
23837
24971
  var AUTH_RETRY_MAX_ATTEMPTS = 13;
23838
24972
  function normalizePath(value) {
23839
- const resolved = path35.resolve(value);
24973
+ const resolved = path36.resolve(value);
23840
24974
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
23841
24975
  }
23842
24976
  function resolveProjectServerUrl() {
@@ -23846,7 +24980,7 @@ function resolveProjectServerUrl() {
23846
24980
  ]) {
23847
24981
  try {
23848
24982
  const url = new URL(relative4, import.meta.url);
23849
- if (url.protocol === "file:" && fs4.existsSync(fileURLToPath4(url))) return url;
24983
+ if (url.protocol === "file:" && fs5.existsSync(fileURLToPath4(url))) return url;
23850
24984
  } catch {
23851
24985
  }
23852
24986
  }
@@ -23856,7 +24990,7 @@ function isMailboxProjectServerAvailable() {
23856
24990
  return resolveProjectServerUrl() !== null;
23857
24991
  }
23858
24992
  function delay2(ms) {
23859
- return new Promise((resolve16) => setTimeout(resolve16, ms));
24993
+ return new Promise((resolve17) => setTimeout(resolve17, ms));
23860
24994
  }
23861
24995
  function isUnauthorizedMailboxError(error) {
23862
24996
  return error instanceof Error && error.name === "UnauthorizedMailboxRequest";
@@ -23864,7 +24998,7 @@ function isUnauthorizedMailboxError(error) {
23864
24998
  var MailboxProjectServerConnection = class {
23865
24999
  constructor(projectDir) {
23866
25000
  this.projectDir = projectDir;
23867
- this.projectDir = path35.resolve(projectDir);
25001
+ this.projectDir = path36.resolve(projectDir);
23868
25002
  this.endpoint = mailboxProjectServerEndpoint(this.projectDir);
23869
25003
  this.state = {
23870
25004
  status: isMailboxProjectServerAvailable() ? "offline" : "unavailable",
@@ -24029,7 +25163,7 @@ var MailboxProjectServerConnection = class {
24029
25163
  this.info = null;
24030
25164
  this.buffer = "";
24031
25165
  this.authToken = void 0;
24032
- return new Promise((resolve16, reject) => {
25166
+ return new Promise((resolve17, reject) => {
24033
25167
  const socket = net2.createConnection(this.endpoint);
24034
25168
  this.socket = socket;
24035
25169
  socket.setEncoding("utf8");
@@ -24044,7 +25178,7 @@ var MailboxProjectServerConnection = class {
24044
25178
  this.connectReject = null;
24045
25179
  this.startHeartbeat();
24046
25180
  this.transition("connected");
24047
- resolve16();
25181
+ resolve17();
24048
25182
  void this.request({ type: "request", op: "ping", args: {} }, 3e3).catch(
24049
25183
  () => void 0
24050
25184
  );
@@ -24073,7 +25207,7 @@ var MailboxProjectServerConnection = class {
24073
25207
  currentAuthToken() {
24074
25208
  if (this.authToken === void 0) {
24075
25209
  try {
24076
- const raw = fs4.readFileSync(mailboxProjectServerMetadataPath(this.projectDir), "utf8");
25210
+ const raw = fs5.readFileSync(mailboxProjectServerMetadataPath(this.projectDir), "utf8");
24077
25211
  const parsed = JSON.parse(raw);
24078
25212
  if (typeof parsed.authToken === "string" && parsed.authToken.length > 0) {
24079
25213
  this.authToken = parsed.authToken;
@@ -24097,7 +25231,7 @@ var MailboxProjectServerConnection = class {
24097
25231
  if (encoded.length > MAILBOX_PROJECT_SERVER_MAX_FRAME_CHARS) {
24098
25232
  return Promise.reject(new Error("Mailbox project server request exceeded frame limit"));
24099
25233
  }
24100
- return new Promise((resolve16, reject) => {
25234
+ return new Promise((resolve17, reject) => {
24101
25235
  const timer = setTimeout(() => {
24102
25236
  const pending = this.pending.get(id);
24103
25237
  if (!pending) return;
@@ -24105,7 +25239,7 @@ var MailboxProjectServerConnection = class {
24105
25239
  pending.reject(new Error(`Mailbox ${message.type} exceeded its ${timeoutMs}ms timeout`));
24106
25240
  }, timeoutMs);
24107
25241
  timer.unref?.();
24108
- this.pending.set(id, { resolve: resolve16, reject, timer });
25242
+ this.pending.set(id, { resolve: resolve17, reject, timer });
24109
25243
  socket.write(encoded);
24110
25244
  });
24111
25245
  }
@@ -24220,7 +25354,7 @@ var MailboxProjectServerConnection = class {
24220
25354
  spawnDetachedServer() {
24221
25355
  const url = resolveProjectServerUrl();
24222
25356
  if (!url) throw new Error("Mailbox project server entrypoint is unavailable");
24223
- fs4.mkdirSync(this.projectDir, { recursive: true });
25357
+ fs5.mkdirSync(this.projectDir, { recursive: true });
24224
25358
  const child = spawn5(process.execPath, [fileURLToPath4(url), "--project-dir", this.projectDir], {
24225
25359
  cwd: this.projectDir,
24226
25360
  detached: process.platform !== "win32",
@@ -24321,9 +25455,9 @@ var CredentialVerifyThrottle = class {
24321
25455
  var credentialVerifyThrottle = new CredentialVerifyThrottle();
24322
25456
 
24323
25457
  // src/coordination/global-mailbox-paths.ts
24324
- import * as path36 from "node:path";
25458
+ import * as path37 from "node:path";
24325
25459
  function resolveProjectDir(projectRoot, globalRoot) {
24326
- return path36.join(globalRoot, "projects", projectSlug(projectRoot));
25460
+ return path37.join(globalRoot, "projects", projectSlug(projectRoot));
24327
25461
  }
24328
25462
 
24329
25463
  // src/coordination/sqlite-mailbox.ts
@@ -24373,8 +25507,8 @@ var RemoteMailbox = class {
24373
25507
  hqPublisher,
24374
25508
  eventEmitter
24375
25509
  } : optionsOrProjectDir;
24376
- this.projectDir = path37.resolve(options.projectDir);
24377
- 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);
24378
25512
  this.registryPath = this.messagePath;
24379
25513
  this.clientRegistryPath = this.messagePath;
24380
25514
  this.events = options.events;
@@ -24620,7 +25754,7 @@ var RemoteMailbox = class {
24620
25754
  this.hqEventPending.clear();
24621
25755
  return;
24622
25756
  }
24623
- const mailboxId = `${path37.basename(this.projectDir)}:mailbox`;
25757
+ const mailboxId = `${path38.basename(this.projectDir)}:mailbox`;
24624
25758
  const inFlight = this.query({ includeDeleted: true, limit: 100 }).then((messages) => {
24625
25759
  const message = messages.find((candidate) => candidate.id === event.messageId);
24626
25760
  const action = event.type === "message.sent" ? "message.sent" : "message.updated";
@@ -24643,7 +25777,7 @@ var RemoteMailbox = class {
24643
25777
  if (!publisher || this.closed || !event.startsWith("mailbox.agent_") && !event.startsWith("mailbox.client_")) {
24644
25778
  return;
24645
25779
  }
24646
- const mailboxId = `${path37.basename(this.projectDir)}:mailbox`;
25780
+ const mailboxId = `${path38.basename(this.projectDir)}:mailbox`;
24647
25781
  const record = typeof payload === "object" && payload !== null ? payload : {};
24648
25782
  const agentId = typeof record["agentId"] === "string" ? record["agentId"] : void 0;
24649
25783
  const action = event === "mailbox.agent_registered" ? "agent.registered" : event === "mailbox.agent_heartbeat" ? "agent.heartbeat" : event === "mailbox.agent_deregistered" ? "agent.deregistered" : void 0;
@@ -24676,7 +25810,7 @@ function createProjectMailbox(options) {
24676
25810
  return new RemoteMailbox(options);
24677
25811
  }
24678
25812
  function getSharedProjectMailbox(projectDir, events, hqPublisher) {
24679
- const key = path37.resolve(projectDir);
25813
+ const key = path38.resolve(projectDir);
24680
25814
  let projectCache = sharedRemoteMailboxes.get(key);
24681
25815
  if (!projectCache) {
24682
25816
  projectCache = {
@@ -27026,25 +28160,25 @@ function requiredSendCapability(type) {
27026
28160
  }
27027
28161
  return "mail.send.informational";
27028
28162
  }
27029
- function requiredCredentialCapability(method, path42) {
27030
- if (method === "POST" && path42 === "/mailbox/send") return "mail.send.informational";
27031
- 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")) {
27032
28166
  return "mail.read.self";
27033
28167
  }
27034
- if (method === "POST" && (path42 === "/mailbox/ack" || path42 === "/mailbox/ack-many")) {
28168
+ if (method === "POST" && (path44 === "/mailbox/ack" || path44 === "/mailbox/ack-many")) {
27035
28169
  return "mail.ack.self";
27036
28170
  }
27037
- if (method === "POST" && path42 === "/mailbox/unread-count") return "mail.read.self";
27038
- 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") {
27039
28173
  return "mail.presence.register.self";
27040
28174
  }
27041
- if (method === "POST" && path42 === "/mailbox/agents/heartbeat") {
28175
+ if (method === "POST" && path44 === "/mailbox/agents/heartbeat") {
27042
28176
  return "mail.presence.heartbeat.self";
27043
28177
  }
27044
- if (method === "GET" && (path42 === "/mailbox/agents" || path42 === "/mailbox/agents/online")) {
28178
+ if (method === "GET" && (path44 === "/mailbox/agents" || path44 === "/mailbox/agents/online")) {
27045
28179
  return "mail.presence.read";
27046
28180
  }
27047
- if (method === "GET" && path42 === "/mailbox/events") return "mail.events.self";
28181
+ if (method === "GET" && path44 === "/mailbox/events") return "mail.events.self";
27048
28182
  return void 0;
27049
28183
  }
27050
28184
  async function checkMailbox(mailbox, input, minTimestampIso, includeReceiptState = false, eligibleRecipients, readerRole) {
@@ -27264,7 +28398,7 @@ function createMailboxHttpRouter(options) {
27264
28398
  return;
27265
28399
  }
27266
28400
  const customAccess = options.authorize ? await options.authorize(request) : void 0;
27267
- let access5 = customAccess ?? { allowed: true };
28401
+ let access6 = customAccess ?? { allowed: true };
27268
28402
  const presentedCredential = parseCredentialAuthorization(request);
27269
28403
  if (options.credentialStore !== void 0 && presentedCredential !== void 0) {
27270
28404
  const persistedAccess = await authorizePersistedMailboxCredential(
@@ -27272,30 +28406,30 @@ function createMailboxHttpRouter(options) {
27272
28406
  options.credentialStore
27273
28407
  );
27274
28408
  if (!persistedAccess.allowed) {
27275
- access5 = persistedAccess;
28409
+ access6 = persistedAccess;
27276
28410
  } else if (persistedAccess.actor === void 0) {
27277
- access5 = { allowed: false };
27278
- } else if (access5.allowed) {
27279
- if (access5.actor !== void 0 && (access5.actor.actorId !== persistedAccess.actor.actorId || access5.actor.projectId !== persistedAccess.actor.projectId)) {
27280
- 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 };
27281
28415
  } else {
27282
- access5 = {
27283
- ...access5,
28416
+ access6 = {
28417
+ ...access6,
27284
28418
  actor: persistedAccess.actor,
27285
- ...access5.rateLimitKey ?? persistedAccess.rateLimitKey ? { rateLimitKey: access5.rateLimitKey ?? persistedAccess.rateLimitKey } : {}
28419
+ ...access6.rateLimitKey ?? persistedAccess.rateLimitKey ? { rateLimitKey: access6.rateLimitKey ?? persistedAccess.rateLimitKey } : {}
27286
28420
  };
27287
28421
  }
27288
28422
  }
27289
28423
  } else if (customAccess === void 0 && options.credentialStore !== void 0) {
27290
- access5 = { allowed: false };
28424
+ access6 = { allowed: false };
27291
28425
  }
27292
- 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) {
27293
28427
  writeJson(response, 403, {
27294
28428
  error: { code: "FORBIDDEN", message: "credential is scoped to a different project" }
27295
28429
  });
27296
28430
  return;
27297
28431
  }
27298
- if (!access5.allowed) {
28432
+ if (!access6.allowed) {
27299
28433
  const forwardedFor = request.headers["x-forwarded-for"];
27300
28434
  const clientIp = (Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor)?.split(",")[0]?.trim() ?? request.socket?.remoteAddress ?? "unknown";
27301
28435
  console.warn(
@@ -27311,14 +28445,14 @@ function createMailboxHttpRouter(options) {
27311
28445
  );
27312
28446
  writeJson(
27313
28447
  response,
27314
- access5.status ?? 401,
27315
- access5.body ?? {
28448
+ access6.status ?? 401,
28449
+ access6.body ?? {
27316
28450
  error: { code: "UNAUTHORIZED", message: "invalid or missing authorization credential" }
27317
28451
  }
27318
28452
  );
27319
28453
  return;
27320
28454
  }
27321
- 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)) {
27322
28456
  writeJson(response, 429, {
27323
28457
  error: {
27324
28458
  code: "RATE_LIMITED",
@@ -27338,7 +28472,7 @@ function createMailboxHttpRouter(options) {
27338
28472
  defaultMaxAgeMs,
27339
28473
  closeSseStreams,
27340
28474
  routePath,
27341
- access5.actor,
28475
+ access6.actor,
27342
28476
  options.credentialStore
27343
28477
  );
27344
28478
  } catch (error) {
@@ -27366,16 +28500,16 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
27366
28500
  throw validationError(`routePath must not start with '?' (got ${JSON.stringify(routePath)})`);
27367
28501
  }
27368
28502
  const queryIndex = url.indexOf("?");
27369
- const path42 = queryIndex === -1 ? url : url.slice(0, queryIndex);
28503
+ const path44 = queryIndex === -1 ? url : url.slice(0, queryIndex);
27370
28504
  if (actor !== void 0) {
27371
- const requiredCapability = requiredCredentialCapability(method, path42);
28505
+ const requiredCapability = requiredCredentialCapability(method, path44);
27372
28506
  if (requiredCapability === void 0) {
27373
28507
  writeJson(response, 403, {
27374
- 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}` }
27375
28509
  });
27376
28510
  return;
27377
28511
  }
27378
- if (path42 !== "/mailbox/send" && !hasMailboxCapability(actor, requiredCapability)) {
28512
+ if (path44 !== "/mailbox/send" && !hasMailboxCapability(actor, requiredCapability)) {
27379
28513
  writeJson(response, 403, {
27380
28514
  error: {
27381
28515
  code: "FORBIDDEN",
@@ -27385,7 +28519,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
27385
28519
  return;
27386
28520
  }
27387
28521
  }
27388
- if (method === "POST" && path42 === "/mailbox/send") {
28522
+ if (method === "POST" && path44 === "/mailbox/send") {
27389
28523
  const input = validateSend(
27390
28524
  await readJsonBody(request, maxBodyBytes),
27391
28525
  actor?.actorId,
@@ -27407,7 +28541,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
27407
28541
  writeJson(response, 201, await mailbox.send(input));
27408
28542
  return;
27409
28543
  }
27410
- if (method === "POST" && path42 === "/mailbox/query") {
28544
+ if (method === "POST" && path44 === "/mailbox/query") {
27411
28545
  const queryContext = parseSinceMs(url, defaultMaxAgeMs);
27412
28546
  if ("error" in queryContext) {
27413
28547
  writeJson(response, 400, { error: queryContext.error });
@@ -27427,7 +28561,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
27427
28561
  writeJson(response, 200, { data: projected, count: projected.length });
27428
28562
  return;
27429
28563
  }
27430
- if (method === "POST" && path42 === "/mailbox/check") {
28564
+ if (method === "POST" && path44 === "/mailbox/check") {
27431
28565
  const queryContext = parseSinceMs(url, defaultMaxAgeMs);
27432
28566
  if ("error" in queryContext) {
27433
28567
  writeJson(response, 400, { error: queryContext.error });
@@ -27461,7 +28595,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
27461
28595
  writeJson(response, 200, { data: projected, count: projected.length });
27462
28596
  return;
27463
28597
  }
27464
- if (method === "POST" && path42 === "/mailbox/ack") {
28598
+ if (method === "POST" && path44 === "/mailbox/ack") {
27465
28599
  const input = validateAck(await readJsonBody(request, maxBodyBytes), actor?.actorId);
27466
28600
  if (actor !== void 0) {
27467
28601
  input.readerId = actor.actorId;
@@ -27475,7 +28609,7 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
27475
28609
  writeJson(response, 200, { updated: projectedAck });
27476
28610
  return;
27477
28611
  }
27478
- if (method === "POST" && path42 === "/mailbox/ack-many") {
28612
+ if (method === "POST" && path44 === "/mailbox/ack-many") {
27479
28613
  const input = validateAckMany(await readJsonBody(request, maxBodyBytes), actor?.actorId);
27480
28614
  if (actor !== void 0) {
27481
28615
  const requestedIds = new Set(input.acks.map((ack) => ack.messageId));
@@ -27491,54 +28625,54 @@ async function dispatchMailboxRoute(mailbox, eventEmitter, request, response, me
27491
28625
  writeJson(response, 200, { updated: projectedMany, count: projectedMany.length });
27492
28626
  return;
27493
28627
  }
27494
- if (method === "POST" && path42 === "/mailbox/unread-count") {
28628
+ if (method === "POST" && path44 === "/mailbox/unread-count") {
27495
28629
  const body = await readJsonBody(request, maxBodyBytes);
27496
28630
  const count = actor === void 0 ? await mailbox.unreadCount(requireString2(body, "forAgentId")) : await unreadCountForActor(mailbox, actor);
27497
28631
  writeJson(response, 200, { count });
27498
28632
  return;
27499
28633
  }
27500
- if (method === "POST" && path42 === "/mailbox/agents/register") {
28634
+ if (method === "POST" && path44 === "/mailbox/agents/register") {
27501
28635
  const input = validateAgentRegistration(await readJsonBody(request, maxBodyBytes), actor);
27502
28636
  await mailbox.registerAgent(input);
27503
28637
  writeJson(response, 200, { ok: true });
27504
28638
  return;
27505
28639
  }
27506
- if (method === "POST" && path42 === "/mailbox/agents/heartbeat") {
28640
+ if (method === "POST" && path44 === "/mailbox/agents/heartbeat") {
27507
28641
  const input = validateAgentHeartbeat(await readJsonBody(request, maxBodyBytes), actor?.actorId);
27508
28642
  if (actor !== void 0) input.agentId = actor.actorId;
27509
28643
  await mailbox.heartbeat(input);
27510
28644
  writeJson(response, 200, { ok: true });
27511
28645
  return;
27512
28646
  }
27513
- if (method === "POST" && path42 === "/mailbox/register-client") {
28647
+ if (method === "POST" && path44 === "/mailbox/register-client") {
27514
28648
  await mailbox.registerClient(
27515
28649
  validateClientRegistration(await readJsonBody(request, maxBodyBytes))
27516
28650
  );
27517
28651
  writeJson(response, 200, { ok: true });
27518
28652
  return;
27519
28653
  }
27520
- if (method === "POST" && path42 === "/mailbox/heartbeat") {
28654
+ if (method === "POST" && path44 === "/mailbox/heartbeat") {
27521
28655
  await mailbox.clientHeartbeat(
27522
28656
  validateClientHeartbeat(await readJsonBody(request, maxBodyBytes))
27523
28657
  );
27524
28658
  writeJson(response, 200, { ok: true });
27525
28659
  return;
27526
28660
  }
27527
- if (method === "POST" && path42 === "/mailbox/purge-clients") {
28661
+ if (method === "POST" && path44 === "/mailbox/purge-clients") {
27528
28662
  writeJson(response, 200, { ok: true, purged: await mailbox.purgeClients() });
27529
28663
  return;
27530
28664
  }
27531
- if (method === "GET" && path42 === "/mailbox/agents") {
28665
+ if (method === "GET" && path44 === "/mailbox/agents") {
27532
28666
  const agents = await mailbox.getAgentStatuses();
27533
28667
  writeJson(response, 200, { data: agents, count: agents.length });
27534
28668
  return;
27535
28669
  }
27536
- if (method === "GET" && path42 === "/mailbox/agents/online") {
28670
+ if (method === "GET" && path44 === "/mailbox/agents/online") {
27537
28671
  const agents = await mailbox.getOnlineAgents();
27538
28672
  writeJson(response, 200, { data: agents, count: agents.length });
27539
28673
  return;
27540
28674
  }
27541
- if (method === "GET" && path42 === "/mailbox/events" && eventEmitter) {
28675
+ if (method === "GET" && path44 === "/mailbox/events" && eventEmitter) {
27542
28676
  const queryContext = parseSinceMs(url, defaultMaxAgeMs);
27543
28677
  if ("error" in queryContext) {
27544
28678
  writeJson(response, 400, { error: queryContext.error });
@@ -27598,16 +28732,16 @@ function writeJson(response, status, body) {
27598
28732
  var NULL_FLEET_BUS = new FleetBus();
27599
28733
 
27600
28734
  // src/coordination/package-author-tracker.ts
27601
- import * as fs5 from "node:fs/promises";
27602
- import * as path38 from "node:path";
28735
+ import * as fs6 from "node:fs/promises";
28736
+ import * as path39 from "node:path";
27603
28737
  var DEFAULT_MAX_ENTRIES2 = 1e4;
27604
28738
  var LOG_FILENAME2 = "package-authors.json";
27605
28739
  function logPath2(storageDir) {
27606
- return path38.join(storageDir, LOG_FILENAME2);
28740
+ return path39.join(storageDir, LOG_FILENAME2);
27607
28741
  }
27608
28742
  async function loadLog2(storageDir, projectRoot) {
27609
28743
  try {
27610
- const raw = await fs5.readFile(logPath2(storageDir), "utf-8");
28744
+ const raw = await fs6.readFile(logPath2(storageDir), "utf-8");
27611
28745
  const parsed = JSON.parse(raw);
27612
28746
  if (!parsed.entries || !Array.isArray(parsed.entries)) {
27613
28747
  return { projectRoot, entries: [] };
@@ -27625,7 +28759,7 @@ async function saveLog2(storageDir, log) {
27625
28759
  `);
27626
28760
  }
27627
28761
  function detectEcosystem(manifestPath) {
27628
- const name = path38.win32.basename(manifestPath).toLowerCase();
28762
+ const name = path39.win32.basename(manifestPath).toLowerCase();
27629
28763
  if (name === "package.json") return "npm";
27630
28764
  if (name === "go.mod") return "go";
27631
28765
  if (name === "cargo.toml") return "cargo";
@@ -28515,12 +29649,12 @@ import { randomBytes as randomBytes2 } from "node:crypto";
28515
29649
  import * as fsp27 from "node:fs/promises";
28516
29650
  import { execFile } from "node:child_process";
28517
29651
  import * as os4 from "node:os";
28518
- import * as path39 from "node:path";
29652
+ import * as path40 from "node:path";
28519
29653
  var MAILBOX_BRIDGE_LOCK_FILENAME = ".mailbox-bridge.lock";
28520
29654
  var MAILBOX_BRIDGE_TOKEN_FILENAME = ".mailbox.token";
28521
29655
  async function acquireOrJoin(opts) {
28522
- const lockPath = path39.join(opts.projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
28523
- 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);
28524
29658
  const inspected = await readLockForInspection(lockPath);
28525
29659
  if (inspected.kind === "live") {
28526
29660
  const existing = inspected.lock;
@@ -28548,8 +29682,8 @@ async function acquireOrJoin(opts) {
28548
29682
  return { kind: "acquired", lock: tentative, tokenPath };
28549
29683
  }
28550
29684
  async function finalize(projectDir, tentative, boundPort) {
28551
- const lockPath = path39.join(projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
28552
- 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);
28553
29687
  const finalized = {
28554
29688
  ...tentative,
28555
29689
  port: boundPort,
@@ -28560,8 +29694,8 @@ async function finalize(projectDir, tentative, boundPort) {
28560
29694
  return finalized;
28561
29695
  }
28562
29696
  async function release(projectDir, generation) {
28563
- const lockPath = path39.join(projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
28564
- 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);
28565
29699
  try {
28566
29700
  const raw = await fsp27.readFile(lockPath, "utf-8");
28567
29701
  const parsed = JSON.parse(raw);
@@ -28595,7 +29729,7 @@ async function readLockForInspection(lockPath) {
28595
29729
  return { kind: "live", lock: parsed };
28596
29730
  }
28597
29731
  async function readLiveLock(projectDir) {
28598
- const lockPath = path39.join(projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
29732
+ const lockPath = path40.join(projectDir, MAILBOX_BRIDGE_LOCK_FILENAME);
28599
29733
  const result = await readLockForInspection(lockPath);
28600
29734
  if (result.kind === "live") {
28601
29735
  return { kind: "live", lock: result.lock };
@@ -28606,7 +29740,7 @@ async function readLiveLock(projectDir) {
28606
29740
  return { kind: "absent" };
28607
29741
  }
28608
29742
  async function atomicWriteJson(targetPath, value) {
28609
- const dir = path39.dirname(targetPath);
29743
+ const dir = path40.dirname(targetPath);
28610
29744
  await fsp27.mkdir(dir, { recursive: true });
28611
29745
  const tmp = `${targetPath}.tmp.${process.pid}.${randomBytes2(4).toString("hex")}`;
28612
29746
  const body = JSON.stringify(value, null, 2) + "\n";
@@ -28622,7 +29756,7 @@ async function isProcessAlive(pid) {
28622
29756
  if (!Number.isInteger(pid) || pid <= 0) return false;
28623
29757
  if (pid === process.pid) return true;
28624
29758
  if (os4.platform() === "win32") {
28625
- return new Promise((resolve16) => {
29759
+ return new Promise((resolve17) => {
28626
29760
  execFile(
28627
29761
  "tasklist",
28628
29762
  ["/FI", `PID eq ${pid}`, "/NH", "/FO", "CSV"],
@@ -28634,11 +29768,11 @@ async function isProcessAlive(pid) {
28634
29768
  },
28635
29769
  (error, out) => {
28636
29770
  if (error) {
28637
- resolve16(false);
29771
+ resolve17(false);
28638
29772
  return;
28639
29773
  }
28640
29774
  const match = /(?:^|\n)"[^"]*","(\d+)"/.exec(out);
28641
- resolve16(match !== null && match[1] === String(pid));
29775
+ resolve17(match !== null && match[1] === String(pid));
28642
29776
  }
28643
29777
  );
28644
29778
  });
@@ -28789,8 +29923,8 @@ function acceptManifestCandidate(candidate) {
28789
29923
  if (normalized.split("/").includes("..")) return false;
28790
29924
  return isManifestFile(normalized);
28791
29925
  }
28792
- function isManifestFile(path42) {
28793
- const name = pathBasename(path42).toLowerCase();
29926
+ function isManifestFile(path44) {
29927
+ const name = pathBasename(path44).toLowerCase();
28794
29928
  const manifests = [
28795
29929
  "package.json",
28796
29930
  "package-lock.json",
@@ -28870,7 +30004,7 @@ function buildTechStackTask(msg, manifestPath) {
28870
30004
  ].join("\n");
28871
30005
  }
28872
30006
 
28873
- // src/middleware/collab-pause.ts
30007
+ // src/coordination/collab-pause.ts
28874
30008
  function collabPauseMiddleware(bus, opts = {}) {
28875
30009
  const timeoutMs = opts.defaultTimeoutMs ?? 6e4;
28876
30010
  const logger = opts.logger;
@@ -29101,10 +30235,10 @@ var AdaptiveConcurrencyController = class {
29101
30235
  };
29102
30236
 
29103
30237
  // src/coordination/agent-monitor.ts
29104
- import { createReadStream as createReadStream6 } from "node:fs";
29105
- import * as fs6 from "node:fs/promises";
29106
- import * as path40 from "node:path";
29107
- 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";
29108
30242
  var AgentMonitorService = class _AgentMonitorService {
29109
30243
  _fleetBus;
29110
30244
  _events;
@@ -29183,7 +30317,7 @@ var AgentMonitorService = class _AgentMonitorService {
29183
30317
  async loadSessionsFromDisk() {
29184
30318
  let subagentIds;
29185
30319
  try {
29186
- const dirents = await fs6.readdir(this._transcriptsDir, { withFileTypes: true });
30320
+ const dirents = await fs7.readdir(this._transcriptsDir, { withFileTypes: true });
29187
30321
  subagentIds = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
29188
30322
  } catch {
29189
30323
  return this.getAllSessions();
@@ -29207,12 +30341,12 @@ var AgentMonitorService = class _AgentMonitorService {
29207
30341
  return this.getAllSessions();
29208
30342
  }
29209
30343
  async _readTranscriptFile(subagentId) {
29210
- const file = path40.join(this._transcriptsDir, subagentId, "transcript.jsonl");
29211
- 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);
29212
30346
  if (!accessible) return [];
29213
30347
  const out = [];
29214
- const input = createReadStream6(file, { encoding: "utf8" });
29215
- const lines = createInterface6({ input, crlfDelay: Number.POSITIVE_INFINITY });
30348
+ const input = createReadStream7(file, { encoding: "utf8" });
30349
+ const lines = createInterface7({ input, crlfDelay: Number.POSITIVE_INFINITY });
29216
30350
  try {
29217
30351
  for await (const line of lines) {
29218
30352
  const trimmed = line.trim();
@@ -29453,9 +30587,9 @@ var AgentMonitorService = class _AgentMonitorService {
29453
30587
  * firehose (or, worse, only the first word).
29454
30588
  */
29455
30589
  _appendStreamDelta(subagentId, session, kind, text, iteration) {
29456
- const open9 = this._openStreams.get(subagentId);
29457
- if (open9 && open9.entry.kind === kind && open9.entry.iteration === iteration && open9.entry.content.length + text.length <= _AgentMonitorService._MAX_SEGMENT_CHARS) {
29458
- 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;
29459
30593
  return;
29460
30594
  }
29461
30595
  this._closeStream(subagentId);
@@ -29479,11 +30613,11 @@ var AgentMonitorService = class _AgentMonitorService {
29479
30613
  * announce the completed entry on the local bus + HQ callback.
29480
30614
  */
29481
30615
  _closeStream(subagentId) {
29482
- const open9 = this._openStreams.get(subagentId);
29483
- if (!open9) return;
30616
+ const open10 = this._openStreams.get(subagentId);
30617
+ if (!open10) return;
29484
30618
  this._openStreams.delete(subagentId);
29485
- this._enqueueWrite(subagentId, open9.entry);
29486
- this._emitEntry(open9.entry);
30619
+ this._enqueueWrite(subagentId, open10.entry);
30620
+ this._emitEntry(open10.entry);
29487
30621
  }
29488
30622
  _addEntry(subagentId, entry) {
29489
30623
  const session = this._sessions.get(subagentId);
@@ -29548,14 +30682,14 @@ var AgentMonitorService = class _AgentMonitorService {
29548
30682
  this._writeDrain = drain;
29549
30683
  }
29550
30684
  async _appendToFile(subagentId, line) {
29551
- const dir = path40.join(this._transcriptsDir, subagentId);
30685
+ const dir = path41.join(this._transcriptsDir, subagentId);
29552
30686
  if (!this._ensuredDirs.has(dir)) {
29553
- await fs6.mkdir(dir, { recursive: true });
30687
+ await fs7.mkdir(dir, { recursive: true });
29554
30688
  this._ensuredDirs.add(dir);
29555
30689
  }
29556
- const filePath = path40.join(dir, "transcript.jsonl");
30690
+ const filePath = path41.join(dir, "transcript.jsonl");
29557
30691
  try {
29558
- await fs6.appendFile(filePath, line, { encoding: "utf8", mode: SECRET_FILE_MODE });
30692
+ await fs7.appendFile(filePath, line, { encoding: "utf8", mode: SECRET_FILE_MODE });
29559
30693
  } catch (error) {
29560
30694
  this._ensuredDirs.delete(dir);
29561
30695
  throw error;
@@ -29946,7 +31080,7 @@ import { randomUUID as randomUUID22 } from "node:crypto";
29946
31080
  // src/coordination/knowledge-graph.ts
29947
31081
  import { randomUUID as randomUUID20 } from "node:crypto";
29948
31082
  import * as fsp28 from "node:fs/promises";
29949
- import * as path41 from "node:path";
31083
+ import * as path42 from "node:path";
29950
31084
  var DEFAULT_MAX_NODES = 2e3;
29951
31085
  var MAX_SUBSCRIPTIONS = 1e3;
29952
31086
  var MAX_PENDING_DELIVERIES_PER_SUBSCRIPTION = 1e3;
@@ -29981,8 +31115,8 @@ var KnowledgeGraph = class _KnowledgeGraph {
29981
31115
  return this.index;
29982
31116
  }
29983
31117
  constructor(sessionDir, maxNodes = DEFAULT_MAX_NODES, compactEveryWrites = Math.max(1e3, maxNodes * 4)) {
29984
- this.filePath = path41.join(sessionDir, "_knowledge_graph");
29985
- 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");
29986
31120
  this.maxNodes = maxNodes;
29987
31121
  this.compactEveryWrites = Math.max(1, Math.floor(compactEveryWrites));
29988
31122
  }
@@ -31086,6 +32220,11 @@ var ConsensusProtocol = class {
31086
32220
  if (change?.type !== "change") {
31087
32221
  throw new Error(`ConsensusProtocol: no change found with id "${changeId}"`);
31088
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
+ }
31089
32228
  await this.graph.update(changeId, { status: "proposed", votes: [] });
31090
32229
  const eligible = this._eligibleVoters(change);
31091
32230
  this._notifyVoters(change, eligible, "vote_initiated");
@@ -31099,6 +32238,11 @@ var ConsensusProtocol = class {
31099
32238
  if (change?.type !== "change") {
31100
32239
  throw new Error(`ConsensusProtocol: no change found for "${changeId}"`);
31101
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
+ }
31102
32246
  const voter = this.voters.get(voterId);
31103
32247
  if (!voter) {
31104
32248
  throw new Error(`ConsensusProtocol: unknown voter "${voterId}"`);
@@ -31173,8 +32317,8 @@ var ConsensusProtocol = class {
31173
32317
  (sum, v) => sum + (this.voters.get(v.agentId)?.weight ?? 1),
31174
32318
  0
31175
32319
  );
31176
- const totalWeight = Array.from(this.voters.values()).reduce(
31177
- (sum, v) => sum + v.weight,
32320
+ const totalWeight = eligible.reduce(
32321
+ (sum, id) => sum + (this.voters.get(id)?.weight ?? 1),
31178
32322
  0
31179
32323
  );
31180
32324
  const castCount = votes.length;
@@ -31894,17 +33038,17 @@ ${input.detail}`
31894
33038
  _waitForDagProgress(timeoutMs) {
31895
33039
  const before = this._dagProgressKey();
31896
33040
  if (this.dag.isDone()) return Promise.resolve();
31897
- return new Promise((resolve16) => {
33041
+ return new Promise((resolve17) => {
31898
33042
  let off;
31899
33043
  const timer = setTimeout(() => {
31900
33044
  off?.();
31901
- resolve16();
33045
+ resolve17();
31902
33046
  }, timeoutMs);
31903
33047
  off = this.dag.onEvent(() => {
31904
33048
  if (this._dagProgressKey() === before) return;
31905
33049
  clearTimeout(timer);
31906
33050
  off?.();
31907
- resolve16();
33051
+ resolve17();
31908
33052
  });
31909
33053
  });
31910
33054
  }
@@ -32212,8 +33356,8 @@ var CollaborationBus = class _CollaborationBus {
32212
33356
  if (this.isPaused()) return false;
32213
33357
  this.pausedAtMs = Date.now();
32214
33358
  this.pausedBy = byParticipant;
32215
- this.pausePromise = new Promise((resolve16) => {
32216
- this.pauseResolve = resolve16;
33359
+ this.pausePromise = new Promise((resolve17) => {
33360
+ this.pauseResolve = resolve17;
32217
33361
  });
32218
33362
  return true;
32219
33363
  }
@@ -32249,8 +33393,8 @@ var CollaborationBus = class _CollaborationBus {
32249
33393
  return true;
32250
33394
  }
32251
33395
  let timer;
32252
- const timeoutPromise = new Promise((resolve16) => {
32253
- timer = setTimeout(() => resolve16("timeout"), timeoutMs);
33396
+ const timeoutPromise = new Promise((resolve17) => {
33397
+ timer = setTimeout(() => resolve17("timeout"), timeoutMs);
32254
33398
  });
32255
33399
  const resumedPromise = this.pausePromise.then(() => "resumed").catch(() => "resumed");
32256
33400
  const winner = await Promise.race([resumedPromise, timeoutPromise]);
@@ -32343,6 +33487,843 @@ var CollaborationBus = class _CollaborationBus {
32343
33487
  }
32344
33488
  }
32345
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
+ }
32346
34327
  export {
32347
34328
  ACP_AGENTS,
32348
34329
  AGENTS_BY_PHASE,
@@ -32352,6 +34333,7 @@ export {
32352
34333
  AUDIT_LOG_AGENT,
32353
34334
  AdaptiveConcurrencyController,
32354
34335
  AgentMonitorService,
34336
+ AgentStatusTracker,
32355
34337
  AutonomousBrain,
32356
34338
  AutonomousCoordinator,
32357
34339
  BUG_HUNTER_AGENT,
@@ -32405,6 +34387,7 @@ export {
32405
34387
  FleetBus,
32406
34388
  FleetCostCapError,
32407
34389
  FleetManager,
34390
+ FleetNotifier,
32408
34391
  FleetSpawnBudgetError,
32409
34392
  FleetSupervisor,
32410
34393
  FleetTokenCapError,
@@ -32526,6 +34509,8 @@ export {
32526
34509
  isMailboxProjectServerAvailable,
32527
34510
  isProvenDirective,
32528
34511
  isValidMatrixKey,
34512
+ kanbanBoundaryOps,
34513
+ kanbanDispatch,
32529
34514
  listProjectAgentLearnedEntries,
32530
34515
  listProjectAgentRoles,
32531
34516
  listProjectSkillAugmentations,
@@ -32617,6 +34602,8 @@ export {
32617
34602
  scoreSkillAffinity,
32618
34603
  scrubRetiredLines,
32619
34604
  sessionRecipient,
34605
+ setKanbanBoundaryOps,
34606
+ setKanbanDispatch,
32620
34607
  setSkillPinned,
32621
34608
  slugifyProjectAgentRole,
32622
34609
  startPackageOutdatedWatcher,