@wrongstack/core 0.286.0 → 0.287.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 (251) hide show
  1. package/dist/boot.d.ts.map +1 -1
  2. package/dist/coordination/brain-monitor.d.ts +15 -0
  3. package/dist/coordination/brain-monitor.d.ts.map +1 -1
  4. package/dist/coordination/delegate-tool.d.ts +8 -14
  5. package/dist/coordination/delegate-tool.d.ts.map +1 -1
  6. package/dist/coordination/director/director-btw-notes.d.ts +20 -0
  7. package/dist/coordination/director/director-btw-notes.d.ts.map +1 -0
  8. package/dist/coordination/director/director-collab.d.ts +40 -0
  9. package/dist/coordination/director/director-collab.d.ts.map +1 -0
  10. package/dist/coordination/director.d.ts +27 -8
  11. package/dist/coordination/director.d.ts.map +1 -1
  12. package/dist/coordination/fleet-manager.d.ts +21 -0
  13. package/dist/coordination/fleet-manager.d.ts.map +1 -1
  14. package/dist/coordination/fleet.d.ts +2 -3
  15. package/dist/coordination/fleet.d.ts.map +1 -1
  16. package/dist/coordination/global-mailbox.d.ts +36 -22
  17. package/dist/coordination/global-mailbox.d.ts.map +1 -1
  18. package/dist/coordination/icoordinator.d.ts +1 -3
  19. package/dist/coordination/icoordinator.d.ts.map +1 -1
  20. package/dist/coordination/index.js +708 -442
  21. package/dist/coordination/index.js.map +4 -4
  22. package/dist/coordination/mail-tools.d.ts.map +1 -1
  23. package/dist/coordination/mailbox-message-codec.d.ts +21 -1
  24. package/dist/coordination/mailbox-message-codec.d.ts.map +1 -1
  25. package/dist/coordination/mailbox-types.d.ts +42 -1
  26. package/dist/coordination/mailbox-types.d.ts.map +1 -1
  27. package/dist/core/agent-loop.d.ts.map +1 -1
  28. package/dist/core/agent-response.d.ts +10 -1
  29. package/dist/core/agent-response.d.ts.map +1 -1
  30. package/dist/core/agent-tools.d.ts.map +1 -1
  31. package/dist/core/agent.d.ts +8 -0
  32. package/dist/core/agent.d.ts.map +1 -1
  33. package/dist/core/context.d.ts +25 -0
  34. package/dist/core/context.d.ts.map +1 -1
  35. package/dist/core/conversation-state.d.ts +6 -0
  36. package/dist/core/conversation-state.d.ts.map +1 -1
  37. package/dist/core/fallback-model.d.ts.map +1 -1
  38. package/dist/core/mailbox-loop.d.ts +12 -5
  39. package/dist/core/mailbox-loop.d.ts.map +1 -1
  40. package/dist/core/system-prompt-builder.d.ts +14 -6
  41. package/dist/core/system-prompt-builder.d.ts.map +1 -1
  42. package/dist/defaults/index.d.ts +0 -1
  43. package/dist/defaults/index.d.ts.map +1 -1
  44. package/dist/defaults/index.js +834 -1465
  45. package/dist/defaults/index.js.map +4 -4
  46. package/dist/execution/auto-compaction-middleware.d.ts +8 -1
  47. package/dist/execution/auto-compaction-middleware.d.ts.map +1 -1
  48. package/dist/execution/compaction-core.d.ts.map +1 -1
  49. package/dist/execution/council-brain.d.ts.map +1 -1
  50. package/dist/execution/council-orchestrator.d.ts +32 -0
  51. package/dist/execution/council-orchestrator.d.ts.map +1 -0
  52. package/dist/execution/council-personas.d.ts +19 -0
  53. package/dist/execution/council-personas.d.ts.map +1 -0
  54. package/dist/execution/council-profiles.d.ts +34 -0
  55. package/dist/execution/council-profiles.d.ts.map +1 -0
  56. package/dist/execution/council-prompts.d.ts +21 -0
  57. package/dist/execution/council-prompts.d.ts.map +1 -0
  58. package/dist/execution/council-resolution.d.ts +57 -0
  59. package/dist/execution/council-resolution.d.ts.map +1 -0
  60. package/dist/execution/design-kit-loader.d.ts.map +1 -1
  61. package/dist/execution/enhance-recovery.d.ts +35 -0
  62. package/dist/execution/enhance-recovery.d.ts.map +1 -0
  63. package/dist/execution/error-handler.d.ts.map +1 -1
  64. package/dist/execution/index.d.ts +6 -0
  65. package/dist/execution/index.d.ts.map +1 -1
  66. package/dist/execution/index.js +1738 -161
  67. package/dist/execution/index.js.map +4 -4
  68. package/dist/execution/intelligent-compactor.d.ts +18 -7
  69. package/dist/execution/intelligent-compactor.d.ts.map +1 -1
  70. package/dist/execution/one-shot-llm.d.ts +52 -0
  71. package/dist/execution/one-shot-llm.d.ts.map +1 -0
  72. package/dist/execution/prompt-enhancer.d.ts +23 -4
  73. package/dist/execution/prompt-enhancer.d.ts.map +1 -1
  74. package/dist/execution/prompt-enhancer.js +24 -6
  75. package/dist/execution/prompt-enhancer.js.map +2 -2
  76. package/dist/execution/selective-compactor.d.ts +4 -4
  77. package/dist/execution/strategy-compactor.d.ts +7 -0
  78. package/dist/execution/strategy-compactor.d.ts.map +1 -1
  79. package/dist/execution/tool-executor.d.ts +19 -5
  80. package/dist/execution/tool-executor.d.ts.map +1 -1
  81. package/dist/hooks/registry.d.ts.map +1 -1
  82. package/dist/hooks/runner.d.ts.map +1 -1
  83. package/dist/hooks/shell-executor.d.ts.map +1 -1
  84. package/dist/hq/alerts.d.ts +5 -0
  85. package/dist/hq/alerts.d.ts.map +1 -1
  86. package/dist/hq/auth-store.d.ts +17 -7
  87. package/dist/hq/auth-store.d.ts.map +1 -1
  88. package/dist/hq/commands.d.ts +8 -4
  89. package/dist/hq/commands.d.ts.map +1 -1
  90. package/dist/hq/cost-bridge.d.ts +5 -3
  91. package/dist/hq/cost-bridge.d.ts.map +1 -1
  92. package/dist/hq/fleet-bridge.d.ts.map +1 -1
  93. package/dist/hq/index.d.ts +0 -1
  94. package/dist/hq/index.d.ts.map +1 -1
  95. package/dist/hq/index.js +471 -294
  96. package/dist/hq/index.js.map +4 -4
  97. package/dist/hq/persistence.d.ts +42 -0
  98. package/dist/hq/persistence.d.ts.map +1 -1
  99. package/dist/hq/protocol/brain.d.ts +24 -0
  100. package/dist/hq/protocol/brain.d.ts.map +1 -0
  101. package/dist/hq/protocol/browser.d.ts +12 -0
  102. package/dist/hq/protocol/browser.d.ts.map +1 -0
  103. package/dist/hq/protocol/client.d.ts +69 -0
  104. package/dist/hq/protocol/client.d.ts.map +1 -0
  105. package/dist/hq/protocol/core.d.ts +130 -0
  106. package/dist/hq/protocol/core.d.ts.map +1 -0
  107. package/dist/hq/protocol/fleet.d.ts +73 -0
  108. package/dist/hq/protocol/fleet.d.ts.map +1 -0
  109. package/dist/hq/protocol/mailbox.d.ts +76 -0
  110. package/dist/hq/protocol/mailbox.d.ts.map +1 -0
  111. package/dist/hq/protocol/mcp.d.ts +51 -0
  112. package/dist/hq/protocol/mcp.d.ts.map +1 -0
  113. package/dist/hq/protocol/project.d.ts +26 -0
  114. package/dist/hq/protocol/project.d.ts.map +1 -0
  115. package/dist/hq/protocol/session.d.ts +153 -0
  116. package/dist/hq/protocol/session.d.ts.map +1 -0
  117. package/dist/hq/protocol/tool.d.ts +31 -0
  118. package/dist/hq/protocol/tool.d.ts.map +1 -0
  119. package/dist/hq/protocol.d.ts +10 -611
  120. package/dist/hq/protocol.d.ts.map +1 -1
  121. package/dist/index.d.ts +12 -2
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js +4540 -2283
  124. package/dist/index.js.map +4 -4
  125. package/dist/infrastructure/index.js +13 -11
  126. package/dist/infrastructure/index.js.map +2 -2
  127. package/dist/infrastructure/token-counter.d.ts.map +1 -1
  128. package/dist/kernel/events/agent-events.d.ts +283 -0
  129. package/dist/kernel/events/agent-events.d.ts.map +1 -0
  130. package/dist/kernel/events/brain-events.d.ts +176 -0
  131. package/dist/kernel/events/brain-events.d.ts.map +1 -0
  132. package/dist/kernel/events/fleet-events.d.ts +39 -0
  133. package/dist/kernel/events/fleet-events.d.ts.map +1 -0
  134. package/dist/kernel/events/memory-events.d.ts +165 -0
  135. package/dist/kernel/events/memory-events.d.ts.map +1 -0
  136. package/dist/kernel/events/provider-events.d.ts +98 -0
  137. package/dist/kernel/events/provider-events.d.ts.map +1 -0
  138. package/dist/kernel/events/sdd-events.d.ts +118 -0
  139. package/dist/kernel/events/sdd-events.d.ts.map +1 -0
  140. package/dist/kernel/events/session-events.d.ts +227 -0
  141. package/dist/kernel/events/session-events.d.ts.map +1 -0
  142. package/dist/kernel/events/tool-events.d.ts +185 -0
  143. package/dist/kernel/events/tool-events.d.ts.map +1 -0
  144. package/dist/kernel/events/worktree-events.d.ts +58 -0
  145. package/dist/kernel/events/worktree-events.d.ts.map +1 -0
  146. package/dist/kernel/events.d.ts +10 -1298
  147. package/dist/kernel/events.d.ts.map +1 -1
  148. package/dist/kernel/index.js +22 -2
  149. package/dist/kernel/index.js.map +2 -2
  150. package/dist/kernel/run-controller.d.ts +15 -0
  151. package/dist/kernel/run-controller.d.ts.map +1 -1
  152. package/dist/mailbox-attach.d.ts.map +1 -1
  153. package/dist/models/index.js +19 -6
  154. package/dist/models/index.js.map +2 -2
  155. package/dist/models/llm-selector.d.ts +7 -0
  156. package/dist/models/llm-selector.d.ts.map +1 -1
  157. package/dist/plugins/auto-review-plugin.d.ts +36 -0
  158. package/dist/plugins/auto-review-plugin.d.ts.map +1 -0
  159. package/dist/plugins/chimera-plugin.d.ts +8 -0
  160. package/dist/plugins/chimera-plugin.d.ts.map +1 -1
  161. package/dist/security/index.js +7 -2
  162. package/dist/security/index.js.map +2 -2
  163. package/dist/security/secret-scrubber.d.ts.map +1 -1
  164. package/dist/skills/index.js.map +2 -2
  165. package/dist/storage/config-loader.d.ts.map +1 -1
  166. package/dist/storage/goal-kanban.d.ts +69 -0
  167. package/dist/storage/goal-kanban.d.ts.map +1 -0
  168. package/dist/storage/index.d.ts +1 -1
  169. package/dist/storage/index.d.ts.map +1 -1
  170. package/dist/storage/index.js +425 -717
  171. package/dist/storage/index.js.map +4 -4
  172. package/dist/storage/memory-consolidator.d.ts +9 -1
  173. package/dist/storage/memory-consolidator.d.ts.map +1 -1
  174. package/dist/tools/council-tool.d.ts +25 -0
  175. package/dist/tools/council-tool.d.ts.map +1 -0
  176. package/dist/tools/index.d.ts +2 -0
  177. package/dist/tools/index.d.ts.map +1 -1
  178. package/dist/tools/index.js +1884 -12
  179. package/dist/tools/index.js.map +4 -4
  180. package/dist/tools/one-shot-llm-tool.d.ts +51 -0
  181. package/dist/tools/one-shot-llm-tool.d.ts.map +1 -0
  182. package/dist/types/config.d.ts +27 -3
  183. package/dist/types/config.d.ts.map +1 -1
  184. package/dist/types/context-window.d.ts.map +1 -1
  185. package/dist/types/council.d.ts +134 -0
  186. package/dist/types/council.d.ts.map +1 -0
  187. package/dist/types/default-config.d.ts +3 -3
  188. package/dist/types/hooks.d.ts +14 -0
  189. package/dist/types/hooks.d.ts.map +1 -1
  190. package/dist/types/index.d.ts +2 -0
  191. package/dist/types/index.d.ts.map +1 -1
  192. package/dist/types/index.js +29 -8
  193. package/dist/types/index.js.map +3 -3
  194. package/dist/types/one-shot-llm.d.ts +128 -0
  195. package/dist/types/one-shot-llm.d.ts.map +1 -0
  196. package/dist/types/plugin.d.ts +6 -0
  197. package/dist/types/plugin.d.ts.map +1 -1
  198. package/dist/types/provider.d.ts +17 -0
  199. package/dist/types/provider.d.ts.map +1 -1
  200. package/dist/types/system-prompt.d.ts +2 -1
  201. package/dist/types/system-prompt.d.ts.map +1 -1
  202. package/dist/types/tool-executor.d.ts +10 -0
  203. package/dist/types/tool-executor.d.ts.map +1 -1
  204. package/dist/utils/atomic-write.d.ts.map +1 -1
  205. package/dist/utils/index.js +75 -8
  206. package/dist/utils/index.js.map +3 -3
  207. package/dist/utils/tool-wire-compact.d.ts +1 -0
  208. package/dist/utils/tool-wire-compact.d.ts.map +1 -1
  209. package/dist/utils/wstack-paths.d.ts +20 -4
  210. package/dist/utils/wstack-paths.d.ts.map +1 -1
  211. package/instructions/cli/next-task-predictor.md +5 -3
  212. package/instructions/coordination/subagent-baseline.md +8 -7
  213. package/instructions/leader-after-task.md +18 -9
  214. package/instructions/llm/council-judge.md +19 -0
  215. package/instructions/llm/council-voter.md +22 -0
  216. package/instructions/modes/architect.md +14 -9
  217. package/instructions/modes/audit-lite.md +9 -7
  218. package/instructions/modes/brief-system.md +12 -12
  219. package/instructions/modes/brief.md +7 -5
  220. package/instructions/modes/code-auditor.md +13 -8
  221. package/instructions/modes/code-reviewer.md +13 -8
  222. package/instructions/modes/debug-lite.md +7 -6
  223. package/instructions/modes/debugger.md +13 -9
  224. package/instructions/modes/devops.md +13 -9
  225. package/instructions/modes/plan-lite.md +7 -6
  226. package/instructions/modes/refactor-lite.md +7 -6
  227. package/instructions/modes/refactorer.md +14 -9
  228. package/instructions/modes/research-lite.md +8 -6
  229. package/instructions/modes/research-web.md +18 -55
  230. package/instructions/modes/review-lite.md +7 -6
  231. package/instructions/modes/teach-system.md +15 -15
  232. package/instructions/modes/teach.md +13 -12
  233. package/instructions/modes/test-lite.md +6 -5
  234. package/instructions/modes/tester.md +13 -9
  235. package/instructions/modes/ui-design.md +14 -8
  236. package/instructions/sections/tool/commit-hygiene.md +1 -7
  237. package/instructions/sections/tool/common-patterns.md +5 -5
  238. package/instructions/sections/tool/delegation-full.md +3 -31
  239. package/instructions/sections/tool/mailbox-compact.md +1 -1
  240. package/instructions/sections/tool/mailbox-full.md +6 -22
  241. package/instructions/system.md +278 -39
  242. package/package.json +2 -2
  243. package/skills/mailbox-bridge/SKILL.md +16 -7
  244. package/skills/output-standards/SKILL.md +16 -16
  245. package/skills/wrongstack-mailbox/SKILL.md +6 -0
  246. package/dist/coordination/director-construction.d.ts +0 -90
  247. package/dist/coordination/director-construction.d.ts.map +0 -1
  248. package/dist/hq/agent-bridge.d.ts +0 -27
  249. package/dist/hq/agent-bridge.d.ts.map +0 -1
  250. package/dist/storage/memory-store.d.ts +0 -119
  251. package/dist/storage/memory-store.d.ts.map +0 -1
package/dist/hq/index.js CHANGED
@@ -1,11 +1,13 @@
1
- // src/hq/protocol.ts
2
- var HQ_PROTOCOL_VERSION = 1;
1
+ // src/hq/protocol/tool.ts
3
2
  var DEFAULT_HQ_REDACTION_POLICY = {
4
3
  rawContent: true,
5
4
  toolArgs: "full",
6
5
  paths: "full"
7
6
  };
8
7
  var HQ_TRANSCRIPT_TEXT_CAP = 16e3;
8
+
9
+ // src/hq/protocol/core.ts
10
+ var HQ_PROTOCOL_VERSION = 1;
9
11
  var KNOWN_HQ_CLIENT_FRAME_TYPES = /* @__PURE__ */ new Set([
10
12
  "client.hello",
11
13
  "client.event",
@@ -543,7 +545,12 @@ function hasCredentialAnchors(text) {
543
545
  text.includes("_TOKEN=") || // ACCESS_TOKEN=, AUTH_TOKEN=, ...
544
546
  text.includes("_SECRET=") || // API_SECRET=, CLIENT_SECRET=, ...
545
547
  text.includes("_PASSWORD=") || // DB_PASSWORD=, ROOT_PASSWORD=, ...
546
- text.includes("mongodb://") || text.includes("mongodb+srv://") || text.includes("postgres://") || text.includes("postgresql://") || text.includes("mysql://") || text.includes("redis://");
548
+ text.includes("mongodb://") || text.includes("mongodb+srv://") || text.includes("postgres://") || text.includes("postgresql://") || text.includes("mysql://") || text.includes("redis://") || // JSON-style credential keys: tool outputs often serialise objects as
549
+ // raw JSON strings rather than parsed objects. The value may not start
550
+ // with a recognisable prefix (sk-, ghp_, etc.), so we anchor on common
551
+ // key names. The `"` prefix avoids matching prose that happens to mention
552
+ // these words — JSON serialisation always quotes string keys.
553
+ text.includes('"apiKey"') || text.includes('"api_key"') || text.includes('"token"') || text.includes('"secret"') || text.includes('"password"') || text.includes('"authorization"') || text.includes('"bearer"') || text.includes('"private_key"') || text.includes('"access_token"') || text.includes('"refresh_token"') || text.includes('"client_secret"');
547
554
  }
548
555
  var DefaultSecretScrubber = class {
549
556
  scrub(text) {
@@ -1381,7 +1388,7 @@ var HqPublisher = class {
1381
1388
 
1382
1389
  // src/hq/factory.ts
1383
1390
  import { createHash as createHash2, randomUUID as randomUUID4 } from "node:crypto";
1384
- import * as fs3 from "node:fs";
1391
+ import * as fs4 from "node:fs";
1385
1392
  import { hostname } from "node:os";
1386
1393
  import { basename as basename5 } from "node:path";
1387
1394
 
@@ -1392,7 +1399,8 @@ import * as path4 from "node:path";
1392
1399
 
1393
1400
  // src/utils/atomic-write.ts
1394
1401
  import { randomBytes } from "node:crypto";
1395
- import * as fs from "node:fs/promises";
1402
+ import * as fs2 from "node:fs/promises";
1403
+ import { watch as watchDir } from "node:fs";
1396
1404
  import * as path3 from "node:path";
1397
1405
 
1398
1406
  // src/utils/session-scoped-path.ts
@@ -1419,14 +1427,34 @@ function invalid(sessionId) {
1419
1427
 
1420
1428
  // src/utils/wstack-paths.ts
1421
1429
  import { createHash } from "node:crypto";
1430
+ import * as fs from "node:fs";
1422
1431
  import * as os from "node:os";
1423
1432
  import * as path2 from "node:path";
1433
+ function canonicalProjectRoot(absRoot) {
1434
+ const checkoutRoot = path2.resolve(absRoot);
1435
+ const dotGit = path2.join(checkoutRoot, ".git");
1436
+ try {
1437
+ if (!fs.statSync(dotGit).isFile()) return checkoutRoot;
1438
+ const gitDirLine = fs.readFileSync(dotGit, "utf8").trim();
1439
+ const match = /^gitdir:\s*(.+)$/i.exec(gitDirLine);
1440
+ if (!match?.[1]) return checkoutRoot;
1441
+ const gitDir = path2.resolve(checkoutRoot, match[1].trim());
1442
+ const commonDirFile = path2.join(gitDir, "commondir");
1443
+ if (!fs.statSync(commonDirFile).isFile()) return checkoutRoot;
1444
+ const commonDir = path2.resolve(gitDir, fs.readFileSync(commonDirFile, "utf8").trim());
1445
+ if (path2.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
1446
+ return path2.dirname(commonDir);
1447
+ } catch {
1448
+ return checkoutRoot;
1449
+ }
1450
+ }
1424
1451
  function projectHash(absRoot) {
1425
- return createHash("sha256").update(path2.resolve(absRoot)).digest("hex").slice(0, 12);
1452
+ return createHash("sha256").update(canonicalProjectRoot(absRoot)).digest("hex").slice(0, 12);
1426
1453
  }
1427
1454
  function projectSlug(absRoot) {
1428
- const base = slugify(path2.basename(absRoot));
1429
- const hash = createHash("sha256").update(path2.resolve(absRoot)).digest("hex").slice(0, 6);
1455
+ const identityRoot = canonicalProjectRoot(absRoot);
1456
+ const base = slugify(path2.basename(identityRoot));
1457
+ const hash = createHash("sha256").update(identityRoot).digest("hex").slice(0, 6);
1430
1458
  return `${base}-${hash}`;
1431
1459
  }
1432
1460
  function slugify(name) {
@@ -1597,16 +1625,16 @@ var FsError = class extends WrongStackError {
1597
1625
  // src/utils/atomic-write.ts
1598
1626
  async function atomicWrite(targetPath, content, opts = {}) {
1599
1627
  const dir = path3.dirname(targetPath);
1600
- await fs.mkdir(dir, { recursive: true });
1628
+ await fs2.mkdir(dir, { recursive: true });
1601
1629
  const tmp = path3.join(dir, `.${path3.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
1602
1630
  try {
1603
1631
  if (typeof content === "string") {
1604
- await fs.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
1632
+ await fs2.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
1605
1633
  } else {
1606
- await fs.writeFile(tmp, content, { flag: "wx" });
1634
+ await fs2.writeFile(tmp, content, { flag: "wx" });
1607
1635
  }
1608
1636
  try {
1609
- const fh = await fs.open(tmp, "r+");
1637
+ const fh = await fs2.open(tmp, "r+");
1610
1638
  try {
1611
1639
  await fh.sync();
1612
1640
  } finally {
@@ -1616,24 +1644,24 @@ async function atomicWrite(targetPath, content, opts = {}) {
1616
1644
  }
1617
1645
  let mode;
1618
1646
  try {
1619
- const stat4 = await fs.stat(targetPath);
1647
+ const stat4 = await fs2.stat(targetPath);
1620
1648
  mode = stat4.mode & 511;
1621
1649
  } catch {
1622
1650
  mode = opts.mode;
1623
1651
  }
1624
1652
  if (mode !== void 0) {
1625
- await fs.chmod(tmp, mode);
1653
+ await fs2.chmod(tmp, mode);
1626
1654
  }
1627
1655
  await renameWithRetry(tmp, targetPath);
1628
1656
  if (mode !== void 0 && process.platform === "win32") {
1629
1657
  try {
1630
- await fs.chmod(targetPath, mode);
1658
+ await fs2.chmod(targetPath, mode);
1631
1659
  } catch {
1632
1660
  }
1633
1661
  }
1634
1662
  } catch (err) {
1635
1663
  try {
1636
- await fs.unlink(tmp);
1664
+ await fs2.unlink(tmp);
1637
1665
  } catch {
1638
1666
  }
1639
1667
  throw err;
@@ -1641,7 +1669,7 @@ async function atomicWrite(targetPath, content, opts = {}) {
1641
1669
  }
1642
1670
  async function withFileLock(targetPath, fn, opts = {}) {
1643
1671
  const dir = path3.dirname(targetPath);
1644
- await fs.mkdir(dir, { recursive: true });
1672
+ await fs2.mkdir(dir, { recursive: true });
1645
1673
  const lockPath = path3.join(dir, `.${path3.basename(targetPath)}.lock`);
1646
1674
  const timeoutMs = opts.timeoutMs ?? 5e3;
1647
1675
  const staleMs = opts.staleMs ?? 3e4;
@@ -1649,26 +1677,27 @@ async function withFileLock(targetPath, fn, opts = {}) {
1649
1677
  let handle;
1650
1678
  for (; ; ) {
1651
1679
  try {
1652
- handle = await fs.open(lockPath, "wx");
1680
+ handle = await fs2.open(lockPath, "wx");
1653
1681
  await handle.writeFile(`${process.pid}:${Date.now()}`);
1654
1682
  break;
1655
1683
  } catch (err) {
1656
1684
  const code = err.code;
1657
1685
  if (code === "ENOENT") {
1658
- await fs.mkdir(dir, { recursive: true });
1686
+ await fs2.mkdir(dir, { recursive: true });
1659
1687
  continue;
1660
1688
  }
1661
- if (code !== "EEXIST") throw err;
1689
+ if (code !== "EEXIST" && code !== "EPERM") throw err;
1662
1690
  try {
1663
- const stat4 = await fs.stat(lockPath);
1691
+ const stat4 = await fs2.stat(lockPath);
1664
1692
  if (Date.now() - stat4.mtimeMs > staleMs) {
1665
- await fs.unlink(lockPath);
1693
+ await fs2.unlink(lockPath);
1666
1694
  continue;
1667
1695
  }
1668
1696
  } catch {
1669
1697
  continue;
1670
1698
  }
1671
- if (Date.now() - started >= timeoutMs) {
1699
+ const elapsed = Date.now() - started;
1700
+ if (elapsed >= timeoutMs) {
1672
1701
  throw new FsError({
1673
1702
  message: `Timed out waiting for file lock: ${targetPath}`,
1674
1703
  code: "FS_ATOMIC_WRITE_FAILED",
@@ -1676,7 +1705,7 @@ async function withFileLock(targetPath, fn, opts = {}) {
1676
1705
  context: { timeoutMs }
1677
1706
  });
1678
1707
  }
1679
- await new Promise((resolve4) => setTimeout(resolve4, 25));
1708
+ await waitForLockRelease(lockPath, timeoutMs - elapsed);
1680
1709
  }
1681
1710
  }
1682
1711
  try {
@@ -1687,22 +1716,66 @@ async function withFileLock(targetPath, fn, opts = {}) {
1687
1716
  } catch {
1688
1717
  }
1689
1718
  try {
1690
- await fs.unlink(lockPath);
1719
+ await fs2.unlink(lockPath);
1691
1720
  } catch {
1692
1721
  }
1693
1722
  }
1694
1723
  }
1724
+ async function waitForLockRelease(lockPath, remainingMs) {
1725
+ const parentDir = path3.dirname(lockPath);
1726
+ const lockName = path3.basename(lockPath);
1727
+ const intervalMs = Math.min(remainingMs, 100);
1728
+ return new Promise((resolve4) => {
1729
+ let settled = false;
1730
+ let watcher = null;
1731
+ const timer = setTimeout(() => {
1732
+ settled = true;
1733
+ watcher?.close();
1734
+ resolve4();
1735
+ }, intervalMs);
1736
+ try {
1737
+ watcher = watchDir(parentDir, (eventType, filename) => {
1738
+ if (settled) return;
1739
+ if (filename === lockName && (eventType === "rename" || eventType === "change")) {
1740
+ settled = true;
1741
+ clearTimeout(timer);
1742
+ watcher?.close();
1743
+ resolve4();
1744
+ }
1745
+ });
1746
+ } catch {
1747
+ clearTimeout(timer);
1748
+ if (!settled) {
1749
+ settled = true;
1750
+ setTimeout(resolve4, Math.min(remainingMs, 25));
1751
+ }
1752
+ return;
1753
+ }
1754
+ fs2.access(lockPath).then(
1755
+ () => {
1756
+ },
1757
+ () => {
1758
+ if (!settled) {
1759
+ settled = true;
1760
+ clearTimeout(timer);
1761
+ watcher?.close();
1762
+ resolve4();
1763
+ }
1764
+ }
1765
+ );
1766
+ });
1767
+ }
1695
1768
  var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
1696
1769
  async function renameWithRetry(from, to) {
1697
1770
  if (process.platform !== "win32") {
1698
- await fs.rename(from, to);
1771
+ await fs2.rename(from, to);
1699
1772
  return;
1700
1773
  }
1701
1774
  const delays = [10, 25, 60, 120, 250];
1702
1775
  let lastErr;
1703
1776
  for (let i = 0; i <= delays.length; i++) {
1704
1777
  try {
1705
- await fs.rename(from, to);
1778
+ await fs2.rename(from, to);
1706
1779
  return;
1707
1780
  } catch (err) {
1708
1781
  lastErr = err;
@@ -1854,8 +1927,41 @@ function parseMailboxMessage(value) {
1854
1927
  ...taskContext === void 0 ? {} : { taskContext }
1855
1928
  };
1856
1929
  }
1857
- function parseMailboxMessageLine(line) {
1858
- return parseMailboxMessage(JSON.parse(line));
1930
+ function isAckRecord(value) {
1931
+ return typeof value === "object" && value !== null && !Array.isArray(value) && value["__ack"] === true;
1932
+ }
1933
+ function parseMailboxLine(line) {
1934
+ try {
1935
+ const parsed = JSON.parse(line);
1936
+ if (isAckRecord(parsed)) return parsed;
1937
+ return parseMailboxMessage(parsed);
1938
+ } catch {
1939
+ return null;
1940
+ }
1941
+ }
1942
+ function applyAckToMessage(msg, ack) {
1943
+ if (ack.read && !(ack.readerId in msg.readBy)) {
1944
+ msg.readBy[ack.readerId] = ack.timestamp;
1945
+ }
1946
+ if (ack.completed && !msg.completed) {
1947
+ msg.completed = true;
1948
+ msg.completedBy = ack.completedBy ?? ack.readerId;
1949
+ msg.completedAt = ack.timestamp;
1950
+ }
1951
+ if (ack.outcome !== void 0 && msg.outcome !== ack.outcome) {
1952
+ msg.outcome = ack.outcome;
1953
+ }
1954
+ if (ack.deleted === true) {
1955
+ msg.deletedAt = ack.timestamp;
1956
+ msg.deletedBy = ack.deletedBy ?? ack.readerId;
1957
+ }
1958
+ if (ack.deleted === false) {
1959
+ delete msg.deletedAt;
1960
+ delete msg.deletedBy;
1961
+ }
1962
+ }
1963
+ function serializeAckRecord(ack) {
1964
+ return JSON.stringify(ack) + "\n";
1859
1965
  }
1860
1966
 
1861
1967
  // src/coordination/mailbox-types.ts
@@ -1925,7 +2031,7 @@ var GlobalMailbox = class {
1925
2031
  * appended message). The chain runs each read to completion before
1926
2032
  * the next starts, in issue order — readers are read-only and never
1927
2033
  * conflict with each other on content, only on the cache mutation
1928
- * that follows the read. Pattern mirrors `DefaultMemoryStore.runSerialized`.
2034
+ * that follows the read. Pattern mirrors a serialized mutation chain.
1929
2035
  */
1930
2036
  _readChain = Promise.resolve([]);
1931
2037
  /**
@@ -1936,7 +2042,7 @@ var GlobalMailbox = class {
1936
2042
  *
1937
2043
  * The index is rebuilt whenever the cache array is replaced
1938
2044
  * (`_setMessageCache`), extended when new messages are pushed
1939
- * (`_pushToCache`, `_readNewMessagesOnly`), and cleared on `close()`.
2045
+ * (`_pushToCache`), and cleared on `close()`.
1940
2046
  * It is `null` when the cache itself is null (> MESSAGE_CACHE_MAX_ENTRIES
1941
2047
  * or never populated).
1942
2048
  *
@@ -2075,42 +2181,100 @@ var GlobalMailbox = class {
2075
2181
  const updated = await this.ackMany({ acks: [input] });
2076
2182
  return updated.length > 0 ? updated[0] : null;
2077
2183
  }
2184
+ /** Diagnostic counters exposed for test/tool introspection. */
2185
+ diag = {
2186
+ ackManyCacheDesync: 0,
2187
+ ackManyPreLockMtime: 0,
2188
+ ackManyPostLockMtime: 0,
2189
+ ackManyPreLockSize: 0,
2190
+ ackManyPostLockSize: 0,
2191
+ ackManyMessageCount: 0,
2192
+ ackManyAckCount: 0,
2193
+ cacheHitCount: 0,
2194
+ cacheMissCount: 0,
2195
+ cacheDesyncNoOp: 0,
2196
+ applyAckTargetMissing: 0,
2197
+ setMessageCacheCount: 0,
2198
+ pushToCacheCount: 0
2199
+ };
2078
2200
  async ackMany(input) {
2079
2201
  if (input.acks.length === 0) return [];
2202
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2203
+ const ackRecords = [];
2080
2204
  const updated = [];
2205
+ const targetIds = /* @__PURE__ */ new Set();
2081
2206
  const byId = /* @__PURE__ */ new Map();
2082
2207
  for (const a of input.acks) {
2083
2208
  byId.set(a.messageId, a);
2209
+ targetIds.add(a.messageId);
2084
2210
  }
2085
- await withFileLock(this.messagePath, async () => {
2086
- const all = await this._readMessagesFresh();
2087
- const now = (/* @__PURE__ */ new Date()).toISOString();
2088
- let changed = false;
2089
- for (const msg of all) {
2090
- const a = byId.get(msg.id);
2091
- if (!a) continue;
2092
- updated.push(msg);
2093
- if (a.read !== false && !(a.readerId in msg.readBy)) {
2094
- msg.readBy[a.readerId] = now;
2095
- changed = true;
2096
- }
2097
- if (a.completed && !msg.completed) {
2098
- msg.completed = true;
2099
- msg.completedBy = a.readerId;
2100
- msg.completedAt = now;
2101
- changed = true;
2102
- }
2103
- if (a.outcome !== void 0 && msg.outcome !== a.outcome) {
2104
- msg.outcome = a.outcome;
2105
- changed = true;
2106
- }
2211
+ const preLockMtime = this._messageCacheMtime;
2212
+ const preLockSize = this._messageCacheSize;
2213
+ this.diag.ackManyPreLockMtime = preLockMtime;
2214
+ this.diag.ackManyPreLockSize = preLockSize;
2215
+ const all = await this._readMessagesCached();
2216
+ for (const msg of all) {
2217
+ const a = byId.get(msg.id);
2218
+ if (!a) continue;
2219
+ const needsRead = a.read !== false && !(a.readerId in msg.readBy);
2220
+ const needsComplete = a.completed && !msg.completed;
2221
+ const needsOutcome = a.outcome !== void 0 && msg.outcome !== a.outcome;
2222
+ if (!needsRead && !needsComplete && !needsOutcome) {
2223
+ updated.push({ ...msg, readBy: { ...msg.readBy } });
2224
+ continue;
2107
2225
  }
2108
- if (changed) {
2109
- const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
2110
- await fsp.writeFile(this.messagePath, serialized, "utf8");
2226
+ const copy = { ...msg, readBy: { ...msg.readBy } };
2227
+ if (needsRead) copy.readBy[a.readerId] = now;
2228
+ if (needsComplete) {
2229
+ copy.completed = true;
2230
+ copy.completedBy = a.readerId;
2231
+ copy.completedAt = now;
2111
2232
  }
2233
+ if (needsOutcome) copy.outcome = a.outcome;
2234
+ updated.push(copy);
2235
+ ackRecords.push({
2236
+ __ack: true,
2237
+ messageId: msg.id,
2238
+ readerId: a.readerId,
2239
+ timestamp: now,
2240
+ read: a.read !== false,
2241
+ completed: a.completed,
2242
+ completedBy: a.completed ? a.readerId : void 0,
2243
+ outcome: a.outcome
2244
+ });
2245
+ }
2246
+ if (ackRecords.length === 0) {
2247
+ if (updated.length > 0) {
2248
+ for (const message of updated) {
2249
+ this.publishHqMailboxEvent({
2250
+ mailboxId: this.hqMailboxId,
2251
+ action: message.completed ? "message.completed" : "message.read",
2252
+ message
2253
+ });
2254
+ }
2255
+ }
2256
+ return updated;
2257
+ }
2258
+ const serialized = ackRecords.map((a) => serializeAckRecord(a)).join("");
2259
+ await withFileLock(this.messagePath, async () => {
2260
+ await fsp.appendFile(this.messagePath, serialized, "utf8");
2112
2261
  const { mtimeMs, size } = await this._statMessageFile();
2113
- this._setMessageCache(all, mtimeMs, size);
2262
+ for (const ack of ackRecords) {
2263
+ this._applyAckToCache(ack);
2264
+ }
2265
+ this._messageCacheMtime = mtimeMs;
2266
+ this._messageCacheSize = size;
2267
+ this.diag.ackManyPostLockMtime = mtimeMs;
2268
+ this.diag.ackManyPostLockSize = size;
2269
+ this.diag.ackManyMessageCount = this._messageCache?.length ?? -1;
2270
+ this.diag.ackManyAckCount = ackRecords.length;
2271
+ const expectedMinSize = preLockSize + serialized.length;
2272
+ if (preLockSize > 0 && size > expectedMinSize) {
2273
+ const extraBytes = size - expectedMinSize;
2274
+ console.error(
2275
+ `[MAILBOX-DIAG] ackMany: post-lock size ${size} exceeds expected ${expectedMinSize} by ${extraBytes}B (mtime ${preLockMtime}->${mtimeMs}). Cache may be missing cross-process messages. diag.ackManyCacheDesync=${++this.diag.ackManyCacheDesync}`
2276
+ );
2277
+ }
2114
2278
  });
2115
2279
  for (const message of updated) {
2116
2280
  this.publishHqMailboxEvent({
@@ -2153,87 +2317,87 @@ var GlobalMailbox = class {
2153
2317
  return count;
2154
2318
  }
2155
2319
  async softDelete(mailId, by) {
2156
- let updated = null;
2320
+ const all = await this._readMessagesCached();
2321
+ const target = all.find((m) => m.id === mailId);
2322
+ if (target === void 0) return null;
2323
+ if (target.deletedAt !== void 0) {
2324
+ return { ...target, readBy: { ...target.readBy } };
2325
+ }
2326
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2327
+ const ack = {
2328
+ __ack: true,
2329
+ messageId: mailId,
2330
+ readerId: by,
2331
+ timestamp: now,
2332
+ read: true,
2333
+ deleted: true,
2334
+ deletedBy: by
2335
+ };
2157
2336
  await withFileLock(this.messagePath, async () => {
2158
- const all = await this._readMessagesFresh();
2159
- const now = (/* @__PURE__ */ new Date()).toISOString();
2160
- let changed = false;
2161
- for (const m of all) {
2162
- if (m.id !== mailId) continue;
2163
- if (m.deletedAt !== void 0) {
2164
- updated = m;
2165
- continue;
2166
- }
2167
- m.deletedAt = now;
2168
- m.deletedBy = by;
2169
- updated = m;
2170
- changed = true;
2171
- }
2172
- if (changed) {
2173
- const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
2174
- await fsp.writeFile(this.messagePath, serialized, "utf8");
2175
- }
2337
+ await fsp.appendFile(this.messagePath, serializeAckRecord(ack), "utf8");
2176
2338
  const { mtimeMs, size } = await this._statMessageFile();
2177
- this._setMessageCache(all, mtimeMs, size);
2339
+ this._applyAckToCache(ack);
2340
+ this._messageCacheMtime = mtimeMs;
2341
+ this._messageCacheSize = size;
2178
2342
  });
2179
- if (updated !== null) {
2180
- const msg = updated;
2181
- this.publishHqMailboxEvent({
2182
- mailboxId: this.hqMailboxId,
2183
- action: "message.updated",
2184
- message: msg
2185
- });
2186
- this.publishHqMailboxSnapshot();
2187
- this.eventEmitter?.emit({
2188
- type: "message.deleted",
2189
- messageId: msg.id,
2190
- from: msg.from,
2191
- to: msg.to,
2192
- timestamp: msg.deletedAt ?? (/* @__PURE__ */ new Date()).toISOString()
2193
- });
2194
- }
2195
- return updated;
2343
+ const msg = { ...target, readBy: { ...target.readBy } };
2344
+ msg.deletedAt = now;
2345
+ msg.deletedBy = by;
2346
+ this.publishHqMailboxEvent({
2347
+ mailboxId: this.hqMailboxId,
2348
+ action: "message.updated",
2349
+ message: msg
2350
+ });
2351
+ this.publishHqMailboxSnapshot();
2352
+ this.eventEmitter?.emit({
2353
+ type: "message.deleted",
2354
+ messageId: msg.id,
2355
+ from: msg.from,
2356
+ to: msg.to,
2357
+ timestamp: msg.deletedAt ?? now
2358
+ });
2359
+ return msg;
2196
2360
  }
2197
2361
  async restore(mailId) {
2198
- let updated = null;
2362
+ const all = await this._readMessagesCached();
2363
+ const target = all.find((m) => m.id === mailId);
2364
+ if (target === void 0) return null;
2365
+ if (target.deletedAt === void 0 && target.deletedBy === void 0) {
2366
+ return { ...target, readBy: { ...target.readBy } };
2367
+ }
2368
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2369
+ const ack = {
2370
+ __ack: true,
2371
+ messageId: mailId,
2372
+ readerId: "",
2373
+ timestamp: now,
2374
+ read: true,
2375
+ deleted: false
2376
+ };
2199
2377
  await withFileLock(this.messagePath, async () => {
2200
- const all = await this._readMessagesFresh();
2201
- let changed = false;
2202
- for (const m of all) {
2203
- if (m.id !== mailId) continue;
2204
- if (m.deletedAt === void 0 && m.deletedBy === void 0) {
2205
- updated = m;
2206
- continue;
2207
- }
2208
- delete m.deletedAt;
2209
- delete m.deletedBy;
2210
- updated = m;
2211
- changed = true;
2212
- }
2213
- if (changed) {
2214
- const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
2215
- await fsp.writeFile(this.messagePath, serialized, "utf8");
2216
- }
2378
+ await fsp.appendFile(this.messagePath, serializeAckRecord(ack), "utf8");
2217
2379
  const { mtimeMs, size } = await this._statMessageFile();
2218
- this._setMessageCache(all, mtimeMs, size);
2380
+ this._applyAckToCache(ack);
2381
+ this._messageCacheMtime = mtimeMs;
2382
+ this._messageCacheSize = size;
2219
2383
  });
2220
- if (updated !== null) {
2221
- const msg = updated;
2222
- this.publishHqMailboxEvent({
2223
- mailboxId: this.hqMailboxId,
2224
- action: "message.updated",
2225
- message: msg
2226
- });
2227
- this.publishHqMailboxSnapshot();
2228
- this.eventEmitter?.emit({
2229
- type: "message.restored",
2230
- messageId: msg.id,
2231
- from: msg.from,
2232
- to: msg.to,
2233
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2234
- });
2235
- }
2236
- return updated;
2384
+ const msg = { ...target, readBy: { ...target.readBy } };
2385
+ delete msg.deletedAt;
2386
+ delete msg.deletedBy;
2387
+ this.publishHqMailboxEvent({
2388
+ mailboxId: this.hqMailboxId,
2389
+ action: "message.updated",
2390
+ message: msg
2391
+ });
2392
+ this.publishHqMailboxSnapshot();
2393
+ this.eventEmitter?.emit({
2394
+ type: "message.restored",
2395
+ messageId: msg.id,
2396
+ from: msg.from,
2397
+ to: msg.to,
2398
+ timestamp: now
2399
+ });
2400
+ return msg;
2237
2401
  }
2238
2402
  // ── Agent registry ──────────────────────────────────────────────────────
2239
2403
  async registerAgent(input) {
@@ -2470,11 +2634,18 @@ var GlobalMailbox = class {
2470
2634
  }
2471
2635
  async getClientStatuses() {
2472
2636
  await this._ensureClientRegistry();
2473
- const registry = await this._readClientRegistry();
2637
+ let registry = await this._readClientRegistry();
2474
2638
  const before = registry.size;
2475
2639
  this._pruneStaleClientsInPlace(registry);
2476
2640
  if (registry.size < before) {
2477
- await this._writeClientRegistry(registry);
2641
+ await withFileLock(this.clientRegistryPath, async () => {
2642
+ registry = await this._readClientRegistry({ fresh: true });
2643
+ const postLockBefore = registry.size;
2644
+ this._pruneStaleClientsInPlace(registry);
2645
+ if (registry.size < postLockBefore) {
2646
+ await this._writeClientRegistry(registry);
2647
+ }
2648
+ });
2478
2649
  }
2479
2650
  const now = Date.now();
2480
2651
  return Array.from(registry.values()).map((c) => ({
@@ -2680,75 +2851,22 @@ var GlobalMailbox = class {
2680
2851
  throw err;
2681
2852
  }
2682
2853
  }
2683
- /**
2684
- * Read only newly-appended bytes from the file and append them to the
2685
- * in-memory cache. This avoids re-reading and re-parsing the entire file
2686
- * when another process appended messages since our last read.
2687
- *
2688
- * Only safe when the file grew in size (messages are append-only). When
2689
- * the file was rewritten (ack/purge changed existing content), callers
2690
- * must fall back to {@link _readMessages}.
2691
- *
2692
- * @returns The (now up-to-date) message cache.
2693
- */
2694
- async _readNewMessagesOnly(fd, oldSize, newSize) {
2695
- const tailLen = newSize - oldSize;
2696
- const buf = Buffer.alloc(tailLen);
2697
- await fd.read(buf, 0, tailLen, oldSize);
2698
- const tail = buf.toString("utf8");
2699
- if (!tail.endsWith(LINE_SEPARATOR)) {
2700
- const lastSeparatorIdx = tail.lastIndexOf(LINE_SEPARATOR);
2701
- if (lastSeparatorIdx === -1) {
2702
- return this._messageCache;
2703
- }
2704
- const completeTail = tail.slice(0, lastSeparatorIdx + 1);
2705
- this._appendIncrementalLines(completeTail);
2706
- this._messageCacheSize = oldSize + lastSeparatorIdx + 1;
2707
- return this._messageCache;
2708
- }
2709
- this._appendIncrementalLines(tail);
2710
- this._messageCacheSize = newSize;
2711
- return this._messageCache;
2712
- }
2713
- _appendIncrementalLines(raw) {
2714
- for (const line of raw.split(LINE_SEPARATOR)) {
2715
- if (!line.trim()) continue;
2716
- const message = this._parseLine(line);
2717
- if (message === null) continue;
2718
- this._messageCache.push(message);
2719
- const idx = this._messageCache.length - 1;
2720
- if (this._recipientIndex !== null) {
2721
- let set = this._recipientIndex.get(message.to);
2722
- if (set === void 0) {
2723
- set = /* @__PURE__ */ new Set();
2724
- this._recipientIndex.set(message.to, set);
2725
- }
2726
- set.add(idx);
2727
- }
2728
- if (this._senderIndex !== null) {
2729
- let set = this._senderIndex.get(message.from);
2730
- if (set === void 0) {
2731
- set = /* @__PURE__ */ new Set();
2732
- this._senderIndex.set(message.from, set);
2733
- }
2734
- set.add(idx);
2735
- }
2736
- }
2737
- }
2738
- _parseLine(line) {
2739
- try {
2740
- return parseMailboxMessageLine(line);
2741
- } catch {
2742
- return null;
2743
- }
2744
- }
2745
- /** Parse a JSONL string into MailboxMessage[], including migration. */
2854
+ /** Parse a JSONL string into MailboxMessage[], applying ack records. */
2746
2855
  _parseLines(raw) {
2747
2856
  const lines = raw.split(LINE_SEPARATOR).filter((l) => l.trim().length > 0);
2748
2857
  const messages = [];
2858
+ const acks = [];
2749
2859
  for (const line of lines) {
2750
- const message = this._parseLine(line);
2751
- if (message !== null) messages.push(message);
2860
+ const parsed = parseMailboxLine(line);
2861
+ if (isAckRecord(parsed)) {
2862
+ acks.push(parsed);
2863
+ } else if (parsed !== null) {
2864
+ messages.push(parsed);
2865
+ }
2866
+ }
2867
+ for (const ack of acks) {
2868
+ const target = messages.find((m) => m.id === ack.messageId);
2869
+ if (target) applyAckToMessage(target, ack);
2752
2870
  }
2753
2871
  return messages;
2754
2872
  }
@@ -2831,16 +2949,6 @@ var GlobalMailbox = class {
2831
2949
  if (this._messageCache !== null && this._messageCacheMtime === st.mtimeMs && this._messageCacheSize === st.size) {
2832
2950
  return this._messageCache;
2833
2951
  }
2834
- if (this._messageCache !== null && this._messageCacheSize >= 0 && st.size > this._messageCacheSize) {
2835
- const fd = await fsp.open(this.messagePath, "r");
2836
- try {
2837
- const updated = await this._readNewMessagesOnly(fd, this._messageCacheSize, st.size);
2838
- this._messageCacheMtime = st.mtimeMs;
2839
- return updated;
2840
- } finally {
2841
- await fd.close();
2842
- }
2843
- }
2844
2952
  const all = await this._readMessages();
2845
2953
  this._setMessageCache(all, st.mtimeMs, st.size);
2846
2954
  return all;
@@ -2881,6 +2989,21 @@ var GlobalMailbox = class {
2881
2989
  this._messageCacheSize = size;
2882
2990
  this._rebuildRecipientIndex();
2883
2991
  }
2992
+ /**
2993
+ * Fold a single ack record into the in-memory cache, mutating the target
2994
+ * message in place. Called from `ackMany` after appending the record to
2995
+ * disk so the cache reflects the ack without a full re-read + re-parse.
2996
+ *
2997
+ * An ack only touches `readBy`/`completed*`/`outcome` — none of which
2998
+ * affect the recipient/sender indexes — so no index rebuild is needed.
2999
+ * No-op when the cache is empty or the target message isn't cached.
3000
+ */
3001
+ _applyAckToCache(ack) {
3002
+ const cache = this._messageCache;
3003
+ if (cache === null) return;
3004
+ const target = cache.find((m) => m.id === ack.messageId);
3005
+ if (target !== void 0) applyAckToMessage(target, ack);
3006
+ }
2884
3007
  /**
2885
3008
  * Build the recipient AND sender indexes from the current `_messageCache`.
2886
3009
  * Called only from `_setMessageCache` so both indexes are always in sync
@@ -3053,7 +3176,7 @@ var GlobalMailbox = class {
3053
3176
 
3054
3177
  // src/hq/auth-store.ts
3055
3178
  import { randomBytes as randomBytes2, randomUUID as randomUUID3, scrypt, timingSafeEqual } from "node:crypto";
3056
- import * as fs2 from "node:fs/promises";
3179
+ import * as fs3 from "node:fs/promises";
3057
3180
  import * as syncFs from "node:fs";
3058
3181
  import * as path5 from "node:path";
3059
3182
  var HQ_AUTH_FILE_VERSION = 1;
@@ -3113,29 +3236,31 @@ function readHqRuntimeFileSync(dataDir) {
3113
3236
  return void 0;
3114
3237
  }
3115
3238
  }
3116
- async function readHqAuthFile(dataDir, opts = {}) {
3239
+ async function readHqAuthFile(dataDir, _opts = {}) {
3117
3240
  const file = hqAuthFilePath(dataDir);
3118
3241
  let raw;
3119
3242
  try {
3120
- raw = await fs2.readFile(file, "utf8");
3243
+ raw = await fs3.readFile(file, "utf8");
3121
3244
  } catch (err) {
3122
3245
  if (err.code === "ENOENT") return emptyHqAuthFile();
3123
- opts.warn?.(`HQ auth file read failed at ${file}: ${err.message}`);
3124
- return emptyHqAuthFile();
3246
+ throw new Error(
3247
+ `HQ auth file at ${file} cannot be read: ${err.message}. Fix the file permissions or delete it to start with an empty auth state.`
3248
+ );
3125
3249
  }
3250
+ let parsed;
3126
3251
  try {
3127
- const parsed = JSON.parse(raw);
3128
- if (parsed.version !== HQ_AUTH_FILE_VERSION) {
3129
- opts.warn?.(
3130
- `HQ auth file at ${file} has unsupported version ${String(parsed.version)} (expected ${String(HQ_AUTH_FILE_VERSION)}); ignoring stored policy/tokens.`
3131
- );
3132
- return emptyHqAuthFile();
3133
- }
3134
- return parsed;
3252
+ parsed = JSON.parse(raw);
3135
3253
  } catch (err) {
3136
- opts.warn?.(`HQ auth file at ${file} is not valid JSON; ignoring stored policy/tokens: ${err.message}`);
3137
- return emptyHqAuthFile();
3254
+ throw new Error(
3255
+ `HQ auth file at ${file} is not valid JSON: ${err.message}. Fix the file or delete it to start with an empty auth state.`
3256
+ );
3138
3257
  }
3258
+ if (parsed.version !== HQ_AUTH_FILE_VERSION) {
3259
+ throw new Error(
3260
+ `HQ auth file at ${file} has unsupported version ${String(parsed.version)} (expected ${String(HQ_AUTH_FILE_VERSION)}). Remove or update the file to match the current schema version.`
3261
+ );
3262
+ }
3263
+ return parsed;
3139
3264
  }
3140
3265
  async function writeHqAuthFile(dataDir, file) {
3141
3266
  const target = hqAuthFilePath(dataDir);
@@ -3150,7 +3275,7 @@ async function writeHqAuthFile(dataDir, file) {
3150
3275
  async function ensureHqFirstRunAuthFile(dataDir, opts = {}) {
3151
3276
  const file = hqAuthFilePath(dataDir);
3152
3277
  try {
3153
- await fs2.access(file);
3278
+ await fs3.access(file);
3154
3279
  return { authFile: await readHqAuthFile(dataDir, opts), created: false };
3155
3280
  } catch (err) {
3156
3281
  if (err.code !== "ENOENT") {
@@ -3283,7 +3408,7 @@ function discoverLocalHqEndpoint(options = {}) {
3283
3408
  }
3284
3409
  function readFirstClientTokenFromAuthFile(dataDir) {
3285
3410
  try {
3286
- const raw = fs3.readFileSync(hqAuthFilePath(dataDir), "utf8");
3411
+ const raw = fs4.readFileSync(hqAuthFilePath(dataDir), "utf8");
3287
3412
  const parsed = JSON.parse(raw);
3288
3413
  return parsed.clientTokens?.find((t) => t.token.trim().length > 0)?.token;
3289
3414
  } catch {
@@ -3393,63 +3518,8 @@ function createGlobalMailbox(options) {
3393
3518
  return new GlobalMailbox(options.projectDir, options.events, options.hqPublisher);
3394
3519
  }
3395
3520
 
3396
- // src/hq/agent-bridge.ts
3397
- function startAgentMonitorEventBridge(opts) {
3398
- const { events, clientId, projectId, publish } = opts;
3399
- const seq = { current: 0 };
3400
- function nextSeq() {
3401
- seq.current += 1;
3402
- return seq.current;
3403
- }
3404
- function buildEnvelope(type, payload, sessionId) {
3405
- return createHqEventEnvelope({
3406
- id: `${Date.now().toString(36)}-${nextSeq()}`,
3407
- type,
3408
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3409
- clientId,
3410
- projectId,
3411
- seq: nextSeq(),
3412
- payload,
3413
- // Carry the parent session id so HQ can scope per-agent transcripts by
3414
- // (sessionId, subagentId) — without it, two sessions whose leaders both
3415
- // use the default id 'leader' would collide into one shared ring.
3416
- ...sessionId !== void 0 ? { sessionId } : {}
3417
- });
3418
- }
3419
- const offMessage = events.on("agent.timeline.message", (payload) => {
3420
- if (!publish) return;
3421
- const msgPayload = {
3422
- subagentId: payload.subagentId,
3423
- agentName: payload.agentName,
3424
- content: payload.content,
3425
- kind: payload.kind,
3426
- iteration: payload.iteration,
3427
- ts: payload.ts
3428
- };
3429
- if (payload.toolName !== void 0) msgPayload.toolName = payload.toolName;
3430
- if (payload.costUsd !== void 0) msgPayload.costUsd = payload.costUsd;
3431
- publish(buildEnvelope("agent.message", msgPayload, payload.sessionId));
3432
- });
3433
- const offStatus = events.on("agent.status_changed", (payload) => {
3434
- if (!publish) return;
3435
- const statusPayload = {
3436
- subagentId: payload.subagentId,
3437
- agentName: payload.agentName,
3438
- status: payload.status,
3439
- ts: payload.ts
3440
- };
3441
- if (payload.summary !== void 0) statusPayload.summary = payload.summary;
3442
- if (payload.task !== void 0) statusPayload.task = payload.task;
3443
- publish(buildEnvelope("agent.status", statusPayload));
3444
- });
3445
- return () => {
3446
- offMessage();
3447
- offStatus();
3448
- };
3449
- }
3450
-
3451
3521
  // src/hq/session-bridge.ts
3452
- import * as fs4 from "node:fs";
3522
+ import * as fs5 from "node:fs";
3453
3523
  import * as fsp2 from "node:fs/promises";
3454
3524
 
3455
3525
  // src/hq/transcript-mapper.ts
@@ -3719,7 +3789,7 @@ function startSessionTelemetryBridge(opts) {
3719
3789
  function setupWatcher() {
3720
3790
  if (disposed || watcher) return;
3721
3791
  try {
3722
- const nextWatcher = fs4.watch(sessionFile, () => {
3792
+ const nextWatcher = fs5.watch(sessionFile, () => {
3723
3793
  if (watchPending || disposed) return;
3724
3794
  watchPending = true;
3725
3795
  setTimeout(() => {
@@ -3816,7 +3886,11 @@ function startFleetTelemetryBridge(opts) {
3816
3886
  const subagents = stats.subagentStatuses.map((s) => ({
3817
3887
  subagentId: s.subagentId,
3818
3888
  ...s.taskId ? { task: s.taskId } : {},
3819
- status: normalizeSubagentStatus(s.status)
3889
+ status: normalizeSubagentStatus(s.status),
3890
+ ...s.model !== void 0 ? { model: s.model } : {},
3891
+ ...s.costUsd !== void 0 ? { costUsd: s.costUsd } : {},
3892
+ ...s.runtimeMs !== void 0 ? { runtimeMs: s.runtimeMs } : {},
3893
+ ...s.lastActivityAt !== void 0 ? { lastActivityAt: s.lastActivityAt } : {}
3820
3894
  }));
3821
3895
  return {
3822
3896
  runId,
@@ -3824,6 +3898,7 @@ function startFleetTelemetryBridge(opts) {
3824
3898
  queuedTasks: stats.pending,
3825
3899
  completedTasks: stats.completed,
3826
3900
  failedTasks: stats.stopped,
3901
+ ...stats.totalCostUsd !== void 0 ? { totalCostUsd: stats.totalCostUsd } : {},
3827
3902
  subagents
3828
3903
  };
3829
3904
  }
@@ -4139,6 +4214,10 @@ function startCostTelemetryBridge(opts) {
4139
4214
  totalTokens: p.usage.input + p.usage.output,
4140
4215
  costUsd: p.cost.total
4141
4216
  };
4217
+ if (p.provider !== void 0) payload.provider = p.provider;
4218
+ if (p.model !== void 0) payload.model = p.model;
4219
+ if (p.usage.cacheRead !== void 0) payload.cacheRead = p.usage.cacheRead;
4220
+ if (p.usage.cacheWrite !== void 0) payload.cacheWrite = p.usage.cacheWrite;
4142
4221
  const sessionId = opts.sessionId ?? p.sessionId;
4143
4222
  publisher.publishEvent({
4144
4223
  type: "session.usage",
@@ -4155,7 +4234,7 @@ function startCostTelemetryBridge(opts) {
4155
4234
  }
4156
4235
 
4157
4236
  // src/hq/persistence.ts
4158
- import * as fs5 from "node:fs/promises";
4237
+ import * as fs6 from "node:fs/promises";
4159
4238
  import * as path6 from "node:path";
4160
4239
  var DEFAULT_EVENT_LOG_MAX_LINES = 5e4;
4161
4240
  var DEFAULT_EVENT_LOG_ROTATE_KEEP = 2e4;
@@ -4187,7 +4266,7 @@ var HqEventLog = class {
4187
4266
  this.counted = true;
4188
4267
  }
4189
4268
  const line = JSON.stringify(event) + "\n";
4190
- await fs5.appendFile(this.filePath, line, { encoding: "utf8" });
4269
+ await fs6.appendFile(this.filePath, line, { encoding: "utf8" });
4191
4270
  this.lineCount += 1;
4192
4271
  if (this.lineCount >= this.maxLines) {
4193
4272
  await this.rotate();
@@ -4196,7 +4275,7 @@ var HqEventLog = class {
4196
4275
  async rotate() {
4197
4276
  await withFileLock(this.filePath, async () => {
4198
4277
  try {
4199
- const content = await fs5.readFile(this.filePath, "utf8");
4278
+ const content = await fs6.readFile(this.filePath, "utf8");
4200
4279
  const lines = content.split("\n").filter((l) => l.length > 0);
4201
4280
  if (lines.length <= this.rotateKeep) {
4202
4281
  this.lineCount = lines.length;
@@ -4211,7 +4290,7 @@ var HqEventLog = class {
4211
4290
  }
4212
4291
  async countLines() {
4213
4292
  try {
4214
- const content = await fs5.readFile(this.filePath, "utf8");
4293
+ const content = await fs6.readFile(this.filePath, "utf8");
4215
4294
  return content.split("\n").filter((l) => l.length > 0).length;
4216
4295
  } catch {
4217
4296
  return 0;
@@ -4224,7 +4303,7 @@ var HqEventLog = class {
4224
4303
  async recent(limit, typeFilter) {
4225
4304
  let content;
4226
4305
  try {
4227
- content = await fs5.readFile(this.filePath, "utf8");
4306
+ content = await fs6.readFile(this.filePath, "utf8");
4228
4307
  } catch {
4229
4308
  return [];
4230
4309
  }
@@ -4266,7 +4345,7 @@ var HqSnapshotStore = class {
4266
4345
  /** Read the last persisted snapshot, or `null` if none. */
4267
4346
  async load() {
4268
4347
  try {
4269
- const content = await fs5.readFile(this.filePath, "utf8");
4348
+ const content = await fs6.readFile(this.filePath, "utf8");
4270
4349
  return JSON.parse(content);
4271
4350
  } catch {
4272
4351
  return null;
@@ -4300,6 +4379,21 @@ var HqTimeseriesStore = class {
4300
4379
  if (signal.outputTokens !== void 0) bucket.outputTokens += signal.outputTokens;
4301
4380
  if (signal.toolCalls !== void 0) bucket.toolCalls += signal.toolCalls;
4302
4381
  if (signal.activeAgents !== void 0) bucket.activeAgents = signal.activeAgents;
4382
+ if (signal.model !== void 0 || signal.provider !== void 0) {
4383
+ const entry = {
4384
+ costUsd: signal.costUsd ?? 0,
4385
+ inputTokens: signal.inputTokens ?? 0,
4386
+ outputTokens: signal.outputTokens ?? 0,
4387
+ ...signal.cacheRead !== void 0 ? { cacheRead: signal.cacheRead } : {},
4388
+ ...signal.cacheWrite !== void 0 ? { cacheWrite: signal.cacheWrite } : {}
4389
+ };
4390
+ if (signal.model !== void 0) {
4391
+ bucket.byModel = foldBreakdown(bucket.byModel, signal.model, entry);
4392
+ }
4393
+ if (signal.provider !== void 0) {
4394
+ bucket.byProvider = foldBreakdown(bucket.byProvider, signal.provider, entry);
4395
+ }
4396
+ }
4303
4397
  if (this.buckets.size > this.maxBuckets) {
4304
4398
  const sorted = Array.from(this.buckets.keys()).sort((a, b) => a - b);
4305
4399
  while (this.buckets.size > this.maxBuckets && sorted.length > 0) {
@@ -4324,7 +4418,7 @@ var HqTimeseriesStore = class {
4324
4418
  async flushInternal(toWrite) {
4325
4419
  const lines = toWrite.map((b) => JSON.stringify(b)).join("\n") + "\n";
4326
4420
  await withFileLock(this.filePath, async () => {
4327
- await fs5.appendFile(this.filePath, lines, { encoding: "utf8" });
4421
+ await fs6.appendFile(this.filePath, lines, { encoding: "utf8" });
4328
4422
  });
4329
4423
  const sorted = Array.from(this.buckets.keys()).sort((a, b) => a - b);
4330
4424
  while (this.buckets.size > this.maxBuckets) {
@@ -4343,7 +4437,7 @@ var HqTimeseriesStore = class {
4343
4437
  async load() {
4344
4438
  let content;
4345
4439
  try {
4346
- content = await fs5.readFile(this.filePath, "utf8");
4440
+ content = await fs6.readFile(this.filePath, "utf8");
4347
4441
  } catch {
4348
4442
  return;
4349
4443
  }
@@ -4364,11 +4458,66 @@ var HqTimeseriesStore = class {
4364
4458
  }
4365
4459
  }
4366
4460
  };
4461
+ function foldBreakdown(map, key, entry) {
4462
+ const next = { ...map };
4463
+ const existing = next[key];
4464
+ if (existing === void 0) {
4465
+ next[key] = { ...entry };
4466
+ } else {
4467
+ next[key] = {
4468
+ costUsd: existing.costUsd + entry.costUsd,
4469
+ inputTokens: existing.inputTokens + entry.inputTokens,
4470
+ outputTokens: existing.outputTokens + entry.outputTokens,
4471
+ cacheRead: existing.cacheRead !== void 0 || entry.cacheRead !== void 0 ? (existing.cacheRead ?? 0) + (entry.cacheRead ?? 0) : void 0,
4472
+ cacheWrite: existing.cacheWrite !== void 0 || entry.cacheWrite !== void 0 ? (existing.cacheWrite ?? 0) + (entry.cacheWrite ?? 0) : void 0
4473
+ };
4474
+ }
4475
+ return next;
4476
+ }
4477
+ var HqSimpleLog = class {
4478
+ filePath;
4479
+ writeChain = Promise.resolve();
4480
+ constructor(dataDir, filename) {
4481
+ this.filePath = path6.join(dataDir, filename);
4482
+ }
4483
+ /** Append one record as a JSON line. Best-effort, never rejects. */
4484
+ append(record) {
4485
+ this.writeChain = this.writeChain.then(() => fs6.appendFile(this.filePath, `${JSON.stringify(record)}
4486
+ `, { encoding: "utf8" })).catch(() => {
4487
+ });
4488
+ }
4489
+ /** Resolves once all queued appends have settled. For tests. */
4490
+ async drain() {
4491
+ await this.writeChain.catch(() => {
4492
+ });
4493
+ }
4494
+ /** Read all records oldest-first. Returns `[]` if the file doesn't exist. */
4495
+ async readAll() {
4496
+ let content;
4497
+ try {
4498
+ content = await fs6.readFile(this.filePath, "utf8");
4499
+ } catch {
4500
+ return [];
4501
+ }
4502
+ const out = [];
4503
+ for (const line of content.split("\n")) {
4504
+ const trimmed = line.trim();
4505
+ if (!trimmed) continue;
4506
+ try {
4507
+ out.push(JSON.parse(trimmed));
4508
+ } catch {
4509
+ }
4510
+ }
4511
+ return out;
4512
+ }
4513
+ };
4367
4514
  function createHqPersistence(dataDir) {
4368
4515
  return {
4369
4516
  eventLog: new HqEventLog({ dataDir }),
4370
4517
  snapshotStore: new HqSnapshotStore({ dataDir }),
4371
- timeseries: new HqTimeseriesStore({ dataDir })
4518
+ timeseries: new HqTimeseriesStore({ dataDir }),
4519
+ commandLog: new HqSimpleLog(dataDir, "commands.jsonl"),
4520
+ alertLog: new HqSimpleLog(dataDir, "alerts.jsonl")
4372
4521
  };
4373
4522
  }
4374
4523
 
@@ -4449,18 +4598,24 @@ function validateHqCommand(queued) {
4449
4598
  var HqCommandAuditLog = class {
4450
4599
  entries = [];
4451
4600
  max;
4452
- constructor(max = 1e3) {
4601
+ onPersist;
4602
+ constructor(max = 1e3, onPersist) {
4453
4603
  this.max = max;
4604
+ this.onPersist = onPersist;
4454
4605
  }
4455
4606
  record(entry) {
4456
4607
  this.entries.push(entry);
4457
4608
  if (this.entries.length > this.max) {
4458
4609
  this.entries.splice(0, this.entries.length - this.max);
4459
4610
  }
4611
+ this.onPersist?.(entry);
4460
4612
  }
4461
4613
  update(commandId, patch) {
4462
4614
  const entry = this.entries.find((e) => e.commandId === commandId);
4463
- if (entry) Object.assign(entry, patch);
4615
+ if (entry) {
4616
+ Object.assign(entry, patch);
4617
+ this.onPersist?.(entry);
4618
+ }
4464
4619
  }
4465
4620
  /** Update only when the command belongs to the authenticated client. */
4466
4621
  updateForClient(commandId, clientId, patch) {
@@ -4469,8 +4624,18 @@ var HqCommandAuditLog = class {
4469
4624
  );
4470
4625
  if (!entry) return false;
4471
4626
  Object.assign(entry, patch);
4627
+ this.onPersist?.(entry);
4472
4628
  return true;
4473
4629
  }
4630
+ /** Seed the ring from a durable store on boot (no persist callback fired). */
4631
+ seed(entries) {
4632
+ for (const entry of entries) {
4633
+ this.entries.push(entry);
4634
+ }
4635
+ if (this.entries.length > this.max) {
4636
+ this.entries.splice(0, this.entries.length - this.max);
4637
+ }
4638
+ }
4474
4639
  recent(limit = 200) {
4475
4640
  return this.entries.slice(-limit);
4476
4641
  }
@@ -4545,9 +4710,11 @@ var HqAlertEngine = class {
4545
4710
  maxHistory;
4546
4711
  timer = null;
4547
4712
  onAlert;
4713
+ onPersist;
4548
4714
  constructor(opts) {
4549
4715
  this.onAlert = opts.onAlert;
4550
4716
  this.maxHistory = opts.maxHistory ?? 500;
4717
+ this.onPersist = opts.onPersist;
4551
4718
  }
4552
4719
  /**
4553
4720
  * Evaluate all rules against the snapshot. Emits (via the `onAlert`
@@ -4577,6 +4744,7 @@ var HqAlertEngine = class {
4577
4744
  if (this.history.length > this.maxHistory) this.history.splice(0, this.history.length - this.maxHistory);
4578
4745
  fired.push(alert);
4579
4746
  this.onAlert(alert);
4747
+ this.onPersist?.(alert);
4580
4748
  } else {
4581
4749
  existing.lastFiredAt = now;
4582
4750
  }
@@ -4597,6 +4765,15 @@ var HqAlertEngine = class {
4597
4765
  recentAlerts(limit = 100) {
4598
4766
  return this.history.slice(-limit);
4599
4767
  }
4768
+ /** Seed the history from a durable store on boot (no alert callbacks fired). */
4769
+ seed(alerts) {
4770
+ for (const alert of alerts) {
4771
+ this.history.push(alert);
4772
+ }
4773
+ if (this.history.length > this.maxHistory) {
4774
+ this.history.splice(0, this.history.length - this.maxHistory);
4775
+ }
4776
+ }
4600
4777
  /**
4601
4778
  * Start periodic evaluation against a snapshot getter. The timer is
4602
4779
  * unref'd so it never keeps the process alive. Returns a disposer.
@@ -4638,6 +4815,7 @@ export {
4638
4815
  HqCommandAuditLog,
4639
4816
  HqEventLog,
4640
4817
  HqPublisher,
4818
+ HqSimpleLog,
4641
4819
  HqSnapshotStore,
4642
4820
  HqTimeseriesStore,
4643
4821
  buildTranscriptFromEvents,
@@ -4674,7 +4852,6 @@ export {
4674
4852
  resolveHqDataDir,
4675
4853
  resolveHqRedactionPolicy,
4676
4854
  scrubAndTruncateHqPreview,
4677
- startAgentMonitorEventBridge,
4678
4855
  startBrainTelemetryBridge,
4679
4856
  startCostTelemetryBridge,
4680
4857
  startFleetTelemetryBridge,