@wrongstack/core 0.285.0 → 0.286.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 (145) hide show
  1. package/dist/autophase/phase-orchestrator.d.ts.map +1 -1
  2. package/dist/boot.d.ts.map +1 -1
  3. package/dist/coordination/agent-monitor.d.ts +29 -0
  4. package/dist/coordination/agent-monitor.d.ts.map +1 -1
  5. package/dist/coordination/autonomous-brain.d.ts +0 -2
  6. package/dist/coordination/autonomous-brain.d.ts.map +1 -1
  7. package/dist/coordination/autonomous-coordinator.d.ts +4 -0
  8. package/dist/coordination/autonomous-coordinator.d.ts.map +1 -1
  9. package/dist/coordination/brain-ledger.d.ts +142 -0
  10. package/dist/coordination/brain-ledger.d.ts.map +1 -0
  11. package/dist/coordination/brain-monitor.d.ts +31 -2
  12. package/dist/coordination/brain-monitor.d.ts.map +1 -1
  13. package/dist/coordination/brain.d.ts +43 -0
  14. package/dist/coordination/brain.d.ts.map +1 -1
  15. package/dist/coordination/delegate-tool.d.ts.map +1 -1
  16. package/dist/coordination/director-construction.d.ts.map +1 -1
  17. package/dist/coordination/director.d.ts +1 -1
  18. package/dist/coordination/director.d.ts.map +1 -1
  19. package/dist/coordination/fleet.d.ts +4 -4
  20. package/dist/coordination/fleet.d.ts.map +1 -1
  21. package/dist/coordination/global-mailbox.d.ts +63 -2
  22. package/dist/coordination/global-mailbox.d.ts.map +1 -1
  23. package/dist/coordination/index.d.ts +2 -1
  24. package/dist/coordination/index.d.ts.map +1 -1
  25. package/dist/coordination/index.js +1566 -462
  26. package/dist/coordination/index.js.map +4 -4
  27. package/dist/coordination/knowledge-graph.d.ts +13 -1
  28. package/dist/coordination/knowledge-graph.d.ts.map +1 -1
  29. package/dist/coordination/mailbox-actions.d.ts +12 -13
  30. package/dist/coordination/mailbox-actions.d.ts.map +1 -1
  31. package/dist/coordination/mailbox-constants.d.ts +64 -0
  32. package/dist/coordination/mailbox-constants.d.ts.map +1 -0
  33. package/dist/coordination/mailbox-events.d.ts +32 -0
  34. package/dist/coordination/mailbox-events.d.ts.map +1 -0
  35. package/dist/coordination/mailbox-message-codec.d.ts +8 -0
  36. package/dist/coordination/mailbox-message-codec.d.ts.map +1 -0
  37. package/dist/coordination/mailbox-types.d.ts +88 -4
  38. package/dist/coordination/mailbox-types.d.ts.map +1 -1
  39. package/dist/coordination/mailbox.d.ts +21 -2
  40. package/dist/coordination/mailbox.d.ts.map +1 -1
  41. package/dist/coordination/package-author-tracker.d.ts.map +1 -1
  42. package/dist/coordination/subagent-result-tool.d.ts.map +1 -1
  43. package/dist/defaults/index.js +846 -523
  44. package/dist/defaults/index.js.map +4 -4
  45. package/dist/execution/autonomous-runner.d.ts +7 -1
  46. package/dist/execution/autonomous-runner.d.ts.map +1 -1
  47. package/dist/execution/autonomy-brain.d.ts +72 -11
  48. package/dist/execution/autonomy-brain.d.ts.map +1 -1
  49. package/dist/execution/brain-chain.d.ts +53 -0
  50. package/dist/execution/brain-chain.d.ts.map +1 -0
  51. package/dist/execution/brain-evaluation.d.ts +66 -0
  52. package/dist/execution/brain-evaluation.d.ts.map +1 -0
  53. package/dist/execution/brain-runtime.d.ts +171 -0
  54. package/dist/execution/brain-runtime.d.ts.map +1 -0
  55. package/dist/execution/council-brain.d.ts +79 -0
  56. package/dist/execution/council-brain.d.ts.map +1 -0
  57. package/dist/execution/eternal-autonomy.d.ts +12 -0
  58. package/dist/execution/eternal-autonomy.d.ts.map +1 -1
  59. package/dist/execution/index.d.ts +5 -1
  60. package/dist/execution/index.d.ts.map +1 -1
  61. package/dist/execution/index.js +1350 -377
  62. package/dist/execution/index.js.map +4 -4
  63. package/dist/execution/parallel-eternal-engine.d.ts +12 -0
  64. package/dist/execution/parallel-eternal-engine.d.ts.map +1 -1
  65. package/dist/execution/tool-executor.d.ts.map +1 -1
  66. package/dist/hq/auth-store.d.ts +21 -3
  67. package/dist/hq/auth-store.d.ts.map +1 -1
  68. package/dist/hq/factory.d.ts +3 -0
  69. package/dist/hq/factory.d.ts.map +1 -1
  70. package/dist/hq/index.js +628 -72
  71. package/dist/hq/index.js.map +4 -4
  72. package/dist/hq/protocol.d.ts +54 -2
  73. package/dist/hq/protocol.d.ts.map +1 -1
  74. package/dist/hq/publisher.d.ts +4 -0
  75. package/dist/hq/publisher.d.ts.map +1 -1
  76. package/dist/index.d.ts +12 -6
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +3482 -1495
  79. package/dist/index.js.map +4 -4
  80. package/dist/kernel/events.d.ts +22 -4
  81. package/dist/kernel/events.d.ts.map +1 -1
  82. package/dist/kernel/index.js.map +2 -2
  83. package/dist/mailbox-attach.d.ts.map +1 -1
  84. package/dist/observability/index.js +16 -15
  85. package/dist/observability/index.js.map +2 -2
  86. package/dist/security/index.d.ts +1 -0
  87. package/dist/security/index.d.ts.map +1 -1
  88. package/dist/security/index.js +253 -21
  89. package/dist/security/index.js.map +4 -4
  90. package/dist/security/permission-policy-schema.d.ts +77 -0
  91. package/dist/security/permission-policy-schema.d.ts.map +1 -0
  92. package/dist/security/permission-policy.d.ts +5 -0
  93. package/dist/security/permission-policy.d.ts.map +1 -1
  94. package/dist/security/secret-vault.d.ts +9 -0
  95. package/dist/security/secret-vault.d.ts.map +1 -1
  96. package/dist/session-registry.d.ts.map +1 -1
  97. package/dist/storage/config-loader.d.ts +10 -0
  98. package/dist/storage/config-loader.d.ts.map +1 -1
  99. package/dist/storage/goal-store.d.ts +1 -1
  100. package/dist/storage/goal-store.d.ts.map +1 -1
  101. package/dist/storage/index.js +147 -162
  102. package/dist/storage/index.js.map +2 -2
  103. package/dist/storage/plan-store.d.ts +1 -1
  104. package/dist/storage/plan-store.d.ts.map +1 -1
  105. package/dist/storage/queue-store.d.ts +4 -0
  106. package/dist/storage/queue-store.d.ts.map +1 -1
  107. package/dist/storage/session-store.d.ts +9 -0
  108. package/dist/storage/session-store.d.ts.map +1 -1
  109. package/dist/storage/task-store.d.ts +1 -1
  110. package/dist/storage/task-store.d.ts.map +1 -1
  111. package/dist/storage/todos-checkpoint.d.ts +2 -2
  112. package/dist/storage/todos-checkpoint.d.ts.map +1 -1
  113. package/dist/types/config.d.ts +209 -1
  114. package/dist/types/config.d.ts.map +1 -1
  115. package/dist/types/index.js +11 -0
  116. package/dist/types/index.js.map +2 -2
  117. package/dist/types/multi-agent.d.ts +8 -0
  118. package/dist/types/multi-agent.d.ts.map +1 -1
  119. package/dist/types/observability.d.ts +5 -0
  120. package/dist/types/observability.d.ts.map +1 -1
  121. package/dist/types/tool.d.ts +7 -0
  122. package/dist/types/tool.d.ts.map +1 -1
  123. package/dist/utils/glob-expand.d.ts.map +1 -1
  124. package/dist/utils/incoming-images.d.ts +39 -0
  125. package/dist/utils/incoming-images.d.ts.map +1 -0
  126. package/dist/utils/index.d.ts +1 -0
  127. package/dist/utils/index.d.ts.map +1 -1
  128. package/dist/utils/index.js +81 -3
  129. package/dist/utils/index.js.map +3 -3
  130. package/dist/worktree/worktree-manager.d.ts +13 -4
  131. package/dist/worktree/worktree-manager.d.ts.map +1 -1
  132. package/instructions/agents/browser.md +25 -16
  133. package/instructions/coordination/subagent-baseline.md +16 -2
  134. package/instructions/leader-after-task.md +3 -1
  135. package/instructions/sections/tool/common-patterns.md +7 -1
  136. package/instructions/system.md +47 -0
  137. package/package.json +2 -2
  138. package/skills/docker-deploy/SKILL.md +1 -1
  139. package/skills/wrongstack-mailbox/SKILL.md +51 -12
  140. package/dist/plugins/git-plugin.d.ts +0 -15
  141. package/dist/plugins/git-plugin.d.ts.map +0 -1
  142. package/dist/plugins/observability-plugin.d.ts +0 -19
  143. package/dist/plugins/observability-plugin.d.ts.map +0 -1
  144. package/dist/plugins/plan-plugin.d.ts +0 -16
  145. package/dist/plugins/plan-plugin.d.ts.map +0 -1
package/dist/hq/index.js CHANGED
@@ -140,8 +140,57 @@ var KNOWN_HQ_EVENT_PAYLOAD_TYPES = /* @__PURE__ */ new Set([
140
140
  "tool.started",
141
141
  "tool.completed",
142
142
  "session.usage",
143
- "client.heartbeat"
143
+ "client.heartbeat",
144
+ "mcp.health.snapshot",
145
+ "mcp.operation"
144
146
  ]);
147
+ function isHqMcpLatencySummary(x) {
148
+ if (typeof x !== "object" || x === null) return false;
149
+ const v = x;
150
+ return typeof v.count === "number" && (v.lastMs === void 0 || typeof v.lastMs === "number") && (v.minMs === void 0 || typeof v.minMs === "number") && (v.maxMs === void 0 || typeof v.maxMs === "number") && (v.p50Ms === void 0 || typeof v.p50Ms === "number") && (v.p95Ms === void 0 || typeof v.p95Ms === "number");
151
+ }
152
+ function isHqMcpFailureCounts(x) {
153
+ if (typeof x !== "object" || x === null) return false;
154
+ const v = x;
155
+ return typeof v.transport === "number" && typeof v.protocol === "number" && typeof v.tool === "number";
156
+ }
157
+ function isHqMcpHealthCheck(x) {
158
+ if (typeof x !== "object" || x === null) return false;
159
+ const v = x;
160
+ return typeof v.name === "string" && typeof v.passed === "boolean" && (v.value === void 0 || typeof v.value === "number") && (v.threshold === void 0 || typeof v.threshold === "number");
161
+ }
162
+ var HQ_MCP_HEALTH_STATES = /* @__PURE__ */ new Set([
163
+ "disabled",
164
+ "dormant",
165
+ "connecting",
166
+ "healthy",
167
+ "degraded",
168
+ "failed"
169
+ ]);
170
+ var HQ_MCP_CONNECTION_STATES = /* @__PURE__ */ new Set([
171
+ "idle",
172
+ "dormant",
173
+ "connecting",
174
+ "connected",
175
+ "reconnecting",
176
+ "disconnected",
177
+ "failed"
178
+ ]);
179
+ function isHqMcpServerHealth(x) {
180
+ if (typeof x !== "object" || x === null) return false;
181
+ const v = x;
182
+ return typeof v.name === "string" && typeof v.projectId === "string" && typeof v.clientId === "string" && typeof v.connectionState === "string" && HQ_MCP_CONNECTION_STATES.has(v.connectionState) && typeof v.healthState === "string" && HQ_MCP_HEALTH_STATES.has(v.healthState) && (v.lastSuccessAt === void 0 || typeof v.lastSuccessAt === "string") && (v.lastFailureAt === void 0 || typeof v.lastFailureAt === "string") && (v.lastFailureKind === void 0 || v.lastFailureKind === "transport" || v.lastFailureKind === "protocol" || v.lastFailureKind === "tool") && typeof v.consecutiveFailures === "number" && isHqMcpFailureCounts(v.failures) && typeof v.reconnectCount === "number" && typeof v.wakeCount === "number" && typeof v.sleepCount === "number" && typeof v.restartCount === "number" && isHqMcpLatencySummary(v.connectionLatency) && isHqMcpLatencySummary(v.discoveryLatency) && isHqMcpLatencySummary(v.callLatency) && typeof v.inFlightCalls === "number" && typeof v.peakInFlightCalls === "number" && Array.isArray(v.healthChecks) && v.healthChecks.every(isHqMcpHealthCheck);
183
+ }
184
+ function isHqMcpHealthSnapshotPayload(x) {
185
+ if (typeof x !== "object" || x === null) return false;
186
+ const v = x;
187
+ return Array.isArray(v.servers) && v.servers.every(isHqMcpServerHealth);
188
+ }
189
+ function isHqMcpOperationPayload(x) {
190
+ if (typeof x !== "object" || x === null) return false;
191
+ const v = x;
192
+ return typeof v.operation === "object" && v.operation !== null && Array.isArray(v.servers) && v.servers.every(isHqMcpServerHealth);
193
+ }
145
194
  function isHqMailboxMessageSummary(x) {
146
195
  if (typeof x !== "object" || x === null) return false;
147
196
  const v = x;
@@ -351,6 +400,10 @@ function parseHqEventPayload(eventType, payload) {
351
400
  return isHqUsagePayload(payload) ? { ok: true, payload } : { ok: false, reason: "malformed-payload" };
352
401
  case "client.heartbeat":
353
402
  return isHqClientHeartbeatPayload(payload) ? { ok: true, payload } : { ok: false, reason: "malformed-payload" };
403
+ case "mcp.health.snapshot":
404
+ return isHqMcpHealthSnapshotPayload(payload) ? { ok: true, payload } : { ok: false, reason: "malformed-payload" };
405
+ case "mcp.operation":
406
+ return isHqMcpOperationPayload(payload) ? { ok: true, payload } : { ok: false, reason: "malformed-payload" };
354
407
  default: {
355
408
  const _exhaustive = eventType;
356
409
  return _exhaustive;
@@ -941,6 +994,7 @@ var HqPublisher = class {
941
994
  this.reconnectMaxMs = options.reconnectMaxMs ?? DEFAULT_RECONNECT_MAX_MS;
942
995
  this.maxQueuedMessages = options.maxQueuedMessages ?? DEFAULT_MAX_QUEUED_MESSAGES;
943
996
  this.resolvedRedactionPolicy = resolveHqRedactionPolicy(options.redactionPolicy);
997
+ this.logger = options.logger;
944
998
  }
945
999
  options;
946
1000
  socketFactory;
@@ -952,6 +1006,7 @@ var HqPublisher = class {
952
1006
  reconnectMaxMs;
953
1007
  maxQueuedMessages;
954
1008
  resolvedRedactionPolicy;
1009
+ logger;
955
1010
  socket = null;
956
1011
  seq = 0;
957
1012
  queue = [];
@@ -1290,6 +1345,10 @@ var HqPublisher = class {
1290
1345
  emitConnectWarning() {
1291
1346
  const attempt = this.lastAttempt;
1292
1347
  const message = `WrongStack HQ publisher: ${this.reconnectAttempt} consecutive connection failures${attempt !== null ? ` to ${attempt.url} (client token ${attempt.hadToken ? "present" : "absent"})` : ""}. Either the HQ server is unreachable or it rejected the token (401). If HQ runs in client-token mode, verify WRONGSTACK_HQ_TOKEN / auth.json. Retries continue with backoff.`;
1348
+ if (this.logger) {
1349
+ this.logger.warn(message, { event: "hq.publisher.connect_failed" });
1350
+ return;
1351
+ }
1293
1352
  const warn = this.options.warn ?? ((msg) => console.warn(
1294
1353
  JSON.stringify({
1295
1354
  level: "warn",
@@ -1657,6 +1716,148 @@ async function renameWithRetry(from, to) {
1657
1716
  throw lastErr;
1658
1717
  }
1659
1718
 
1719
+ // src/coordination/mailbox-constants.ts
1720
+ var AGENT_STALE_MS = 6e4;
1721
+ var CLIENT_STALE_MS = 6e4;
1722
+ var HEARTBEAT_THROTTLE_MS = 5e3;
1723
+ var REGISTRY_CACHE_TTL_MS = 2e3;
1724
+ var LINE_SEPARATOR = "\n";
1725
+ var MESSAGE_CACHE_MAX_ENTRIES = 1e4;
1726
+ var AUTO_COMPACT_INTERVAL_MS = 3e5;
1727
+ var AUTO_COMPACT_READ_MAX_AGE_MS = 6e5;
1728
+ var AUTO_COMPACT_DEFAULT_TTL_MS = 864e5;
1729
+
1730
+ // src/coordination/mailbox-message-codec.ts
1731
+ var MESSAGE_TYPES = /* @__PURE__ */ new Set([
1732
+ "note",
1733
+ "ask",
1734
+ "assign",
1735
+ "steer",
1736
+ "btw",
1737
+ "broadcast",
1738
+ "status",
1739
+ "result",
1740
+ "review",
1741
+ "control"
1742
+ ]);
1743
+ var PRIORITIES = /* @__PURE__ */ new Set(["low", "normal", "high"]);
1744
+ var TASK_STATUSES = /* @__PURE__ */ new Set([
1745
+ "pending",
1746
+ "in_progress",
1747
+ "completed",
1748
+ "failed",
1749
+ "idle",
1750
+ "running",
1751
+ "streaming",
1752
+ "waiting_user",
1753
+ "error",
1754
+ "offline",
1755
+ "busy"
1756
+ ]);
1757
+ function isRecord(value) {
1758
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1759
+ }
1760
+ function requiredString(record, key) {
1761
+ const value = record[key];
1762
+ if (typeof value !== "string") {
1763
+ throw new TypeError(`mailbox message field "${key}" must be a string`);
1764
+ }
1765
+ return value;
1766
+ }
1767
+ function optionalString(record, key) {
1768
+ const value = record[key];
1769
+ if (value === void 0) return {};
1770
+ if (typeof value !== "string") {
1771
+ throw new TypeError(`mailbox message field "${key}" must be a string when present`);
1772
+ }
1773
+ return { [key]: value };
1774
+ }
1775
+ function parseMessageType(value) {
1776
+ if (value === "info") return "note";
1777
+ if (value === "task") return "assign";
1778
+ if (typeof value === "string" && MESSAGE_TYPES.has(value)) {
1779
+ return value;
1780
+ }
1781
+ throw new TypeError('mailbox message field "type" is invalid');
1782
+ }
1783
+ function normalizeMailboxMessageType(value) {
1784
+ return parseMessageType(value);
1785
+ }
1786
+ function parsePriority(value) {
1787
+ if (typeof value === "string" && PRIORITIES.has(value)) {
1788
+ return value;
1789
+ }
1790
+ if (typeof value === "string") return "normal";
1791
+ throw new TypeError('mailbox message field "priority" must be a string');
1792
+ }
1793
+ function parseReadReceipts(record, to) {
1794
+ const value = record["readBy"];
1795
+ if (value === void 0) {
1796
+ const legacyReadAt = record["readAt"];
1797
+ return record["read"] === true && typeof legacyReadAt === "string" ? { [to || "unknown"]: legacyReadAt } : {};
1798
+ }
1799
+ if (!isRecord(value)) {
1800
+ throw new TypeError('mailbox message field "readBy" must be an object');
1801
+ }
1802
+ const receipts = {};
1803
+ for (const [agentId, timestamp] of Object.entries(value)) {
1804
+ if (typeof timestamp !== "string") {
1805
+ throw new TypeError("mailbox message read receipt timestamps must be strings");
1806
+ }
1807
+ receipts[agentId] = timestamp;
1808
+ }
1809
+ return receipts;
1810
+ }
1811
+ function parseTaskContext(value) {
1812
+ if (value === void 0) return void 0;
1813
+ if (!isRecord(value)) {
1814
+ throw new TypeError('mailbox message field "taskContext" must be an object');
1815
+ }
1816
+ const status = value["status"];
1817
+ if (status !== void 0 && (typeof status !== "string" || !TASK_STATUSES.has(status))) {
1818
+ throw new TypeError("mailbox message taskContext status is invalid");
1819
+ }
1820
+ return {
1821
+ ...optionalString(value, "agentRole"),
1822
+ ...optionalString(value, "agentName"),
1823
+ ...optionalString(value, "taskId"),
1824
+ ...status === void 0 ? {} : { status }
1825
+ };
1826
+ }
1827
+ function parseMailboxMessage(value) {
1828
+ if (!isRecord(value)) throw new TypeError("mailbox message must be an object");
1829
+ const to = value["to"] === void 0 ? "" : requiredString(value, "to");
1830
+ const completed = value["completed"];
1831
+ if (typeof completed !== "boolean") {
1832
+ throw new TypeError('mailbox message field "completed" must be a boolean');
1833
+ }
1834
+ const taskContext = parseTaskContext(value["taskContext"]);
1835
+ return {
1836
+ id: requiredString(value, "id"),
1837
+ from: requiredString(value, "from"),
1838
+ to,
1839
+ type: parseMessageType(value["type"]),
1840
+ subject: requiredString(value, "subject"),
1841
+ body: requiredString(value, "body"),
1842
+ priority: parsePriority(value["priority"]),
1843
+ readBy: parseReadReceipts(value, to),
1844
+ completed,
1845
+ timestamp: requiredString(value, "timestamp"),
1846
+ ...optionalString(value, "completedBy"),
1847
+ ...optionalString(value, "outcome"),
1848
+ ...optionalString(value, "completedAt"),
1849
+ ...optionalString(value, "deletedAt"),
1850
+ ...optionalString(value, "deletedBy"),
1851
+ ...optionalString(value, "replyTo"),
1852
+ ...optionalString(value, "senderSessionId"),
1853
+ ...optionalString(value, "expiresAt"),
1854
+ ...taskContext === void 0 ? {} : { taskContext }
1855
+ };
1856
+ }
1857
+ function parseMailboxMessageLine(line) {
1858
+ return parseMailboxMessage(JSON.parse(line));
1859
+ }
1860
+
1660
1861
  // src/coordination/mailbox-types.ts
1661
1862
  function normalizeRecipient(to) {
1662
1863
  return to.trim().toLowerCase() === "all" ? "*" : to.trim();
@@ -1665,13 +1866,6 @@ function normalizeRecipient(to) {
1665
1866
  // src/coordination/global-mailbox.ts
1666
1867
  var MAILBOX_FILE = "_mailbox.jsonl";
1667
1868
  var CLIENT_REGISTRY_FILE = "_mailbox.clients.json";
1668
- var AGENT_STALE_MS = 6e4;
1669
- var AGENT_PURGE_MS = 864e5;
1670
- var CLIENT_STALE_MS = 6e4;
1671
- var HEARTBEAT_THROTTLE_MS = 5e3;
1672
- var REGISTRY_CACHE_TTL_MS = 2e3;
1673
- var LINE_SEPARATOR = "\n";
1674
- var MESSAGE_CACHE_MAX_ENTRIES = 1e4;
1675
1869
  var GlobalMailbox = class {
1676
1870
  /** Path to the JSONL message file. */
1677
1871
  messagePath;
@@ -1683,6 +1877,12 @@ var GlobalMailbox = class {
1683
1877
  _events;
1684
1878
  /** Optional HQ publisher for cross-project command-center telemetry. */
1685
1879
  _hqPublisher;
1880
+ /**
1881
+ * Optional SSE event emitter for real-time push to HTTP bridge clients.
1882
+ * When present, `send`/`ack`/`softDelete`/`restore` emit events that
1883
+ * connected SSE subscribers receive instantly.
1884
+ */
1885
+ eventEmitter;
1686
1886
  /**
1687
1887
  * Local cache of the agent registry to avoid re-reading on every call.
1688
1888
  * Time-bounded: the registry file is shared ACROSS PROCESSES (that's the
@@ -1728,17 +1928,44 @@ var GlobalMailbox = class {
1728
1928
  * that follows the read. Pattern mirrors `DefaultMemoryStore.runSerialized`.
1729
1929
  */
1730
1930
  _readChain = Promise.resolve([]);
1931
+ /**
1932
+ * Recipient → Set of indices into `_messageCache`. Maintained alongside
1933
+ * the cache so `query({ to })` and `unreadCount(agentId)` can skip the
1934
+ * O(N) full scan and iterate only the messages addressed to the target
1935
+ * recipient (plus broadcasts `*`).
1936
+ *
1937
+ * The index is rebuilt whenever the cache array is replaced
1938
+ * (`_setMessageCache`), extended when new messages are pushed
1939
+ * (`_pushToCache`, `_readNewMessagesOnly`), and cleared on `close()`.
1940
+ * It is `null` when the cache itself is null (> MESSAGE_CACHE_MAX_ENTRIES
1941
+ * or never populated).
1942
+ *
1943
+ * Safety: array indices are valid only as long as the cache array
1944
+ * reference hasn't changed. Since we rebuild the index in the same
1945
+ * synchronous step that replaces the cache, a reader that calls
1946
+ * `_readMessagesCached()` either gets the old array + old index, or
1947
+ * the new array + new index — never a mismatch.
1948
+ */
1949
+ _recipientIndex = null;
1950
+ /**
1951
+ * Sender → Set of indices into `_messageCache`. Same lifecycle and
1952
+ * safety properties as `_recipientIndex`, but keyed on `msg.from`
1953
+ * so `query({ from })` can skip the full scan.
1954
+ */
1955
+ _senderIndex = null;
1731
1956
  /**
1732
1957
  * @param projectDir — `~/.wrongstack/projects/<slug>/`
1733
1958
  * @param events — optional EventBus for real-time TUI/WebUI notifications
1734
1959
  * @param hqPublisher — optional HQ publisher, or getter, for cross-project telemetry
1960
+ * @param eventEmitter — optional SSE event emitter for HTTP bridge push
1735
1961
  */
1736
- constructor(projectDir, events, hqPublisher) {
1962
+ constructor(projectDir, events, hqPublisher, eventEmitter) {
1737
1963
  this.messagePath = path4.join(projectDir, MAILBOX_FILE);
1738
1964
  this.registryPath = path4.join(projectDir, "_mailbox.registry.json");
1739
1965
  this.clientRegistryPath = path4.join(projectDir, CLIENT_REGISTRY_FILE);
1740
1966
  this._events = events;
1741
1967
  this._hqPublisher = hqPublisher;
1968
+ this.eventEmitter = eventEmitter;
1742
1969
  }
1743
1970
  get hqMailboxId() {
1744
1971
  return `${path4.basename(path4.dirname(this.messagePath))}:mailbox`;
@@ -1767,7 +1994,7 @@ var GlobalMailbox = class {
1767
1994
  // "all" is an accepted spelling of the broadcast address — canonical
1768
1995
  // form on disk is '*' so every query/checker matches it.
1769
1996
  to: normalizeRecipient(input.to),
1770
- type: input.type,
1997
+ type: normalizeMailboxMessageType(input.type),
1771
1998
  subject: input.subject,
1772
1999
  body: input.body,
1773
2000
  priority: input.priority ?? "normal",
@@ -1775,7 +2002,8 @@ var GlobalMailbox = class {
1775
2002
  completed: false,
1776
2003
  timestamp: now,
1777
2004
  replyTo: input.replyTo,
1778
- taskContext: input.taskContext
2005
+ taskContext: input.taskContext,
2006
+ expiresAt: input.ttlMs !== void 0 ? new Date(Date.now() + input.ttlMs).toISOString() : void 0
1779
2007
  };
1780
2008
  const line = JSON.stringify(msg) + LINE_SEPARATOR;
1781
2009
  await fsp.mkdir(path4.dirname(this.messagePath), { recursive: true });
@@ -1790,21 +2018,49 @@ var GlobalMailbox = class {
1790
2018
  message: msg
1791
2019
  });
1792
2020
  this.publishHqMailboxSnapshot();
2021
+ this._events?.emitCustom("mailbox.message_sent", {
2022
+ messageId: msg.id,
2023
+ from: msg.from,
2024
+ to: msg.to,
2025
+ type: msg.type,
2026
+ subject: msg.subject
2027
+ });
2028
+ this.eventEmitter?.emit({
2029
+ type: "message.sent",
2030
+ messageId: msg.id,
2031
+ from: msg.from,
2032
+ to: msg.to,
2033
+ timestamp: now
2034
+ });
1793
2035
  return msg;
1794
2036
  }
1795
2037
  async query(q) {
2038
+ const queryType = q.type === void 0 ? void 0 : normalizeMailboxMessageType(q.type);
1796
2039
  const all = await this._readMessagesCached();
1797
2040
  const limit = q.limit ?? 50;
1798
2041
  const order = q.minPriority !== void 0 ? { low: 0, normal: 1, high: 2 } : null;
1799
2042
  const minPriorityRank = order && q.minPriority !== void 0 ? order[q.minPriority] : 0;
1800
2043
  const out = [];
1801
- for (let i = 0; i < all.length; i++) {
1802
- const m = all[i];
2044
+ let candidates;
2045
+ if (q.to !== void 0 && this._recipientIndex !== null && q.from === void 0 && q.type === void 0 && q.minPriority === void 0) {
2046
+ const indices = /* @__PURE__ */ new Set();
2047
+ const direct = this._recipientIndex.get(q.to);
2048
+ if (direct !== void 0) for (const i of direct) indices.add(i);
2049
+ const broadcasts = this._recipientIndex.get("*");
2050
+ if (broadcasts !== void 0) for (const i of broadcasts) indices.add(i);
2051
+ candidates = [...indices].sort((a, b) => a - b).map((i) => all[i]);
2052
+ } else if (q.from !== void 0 && q.to === void 0 && this._senderIndex !== null && q.type === void 0 && q.minPriority === void 0) {
2053
+ const indices = this._senderIndex.get(q.from);
2054
+ candidates = indices !== void 0 ? [...indices].sort((a, b) => a - b).map((i) => all[i]) : [];
2055
+ } else {
2056
+ candidates = all;
2057
+ }
2058
+ for (const m of candidates) {
1803
2059
  if (q.to !== void 0 && m.to !== q.to && m.to !== "*") continue;
1804
2060
  if (q.from !== void 0 && m.from !== q.from) continue;
1805
2061
  if (q.unreadBy !== void 0 && q.unreadBy in m.readBy) continue;
1806
2062
  if (q.incompleteOnly && m.completed) continue;
1807
- if (q.type !== void 0 && m.type !== q.type) continue;
2063
+ if (queryType !== void 0 && m.type !== queryType) continue;
1808
2064
  if (order !== null && (order[m.priority] ?? 1) < minPriorityRank) {
1809
2065
  continue;
1810
2066
  }
@@ -1862,6 +2118,13 @@ var GlobalMailbox = class {
1862
2118
  action: message.completed ? "message.completed" : "message.read",
1863
2119
  message
1864
2120
  });
2121
+ this.eventEmitter?.emit({
2122
+ type: "message.acked",
2123
+ messageId: message.id,
2124
+ from: message.from,
2125
+ to: message.to,
2126
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2127
+ });
1865
2128
  }
1866
2129
  if (updated.length > 0) this.publishHqMailboxSnapshot();
1867
2130
  return updated;
@@ -1869,10 +2132,22 @@ var GlobalMailbox = class {
1869
2132
  async unreadCount(forAgentId) {
1870
2133
  const all = await this._readMessagesCached();
1871
2134
  let count = 0;
1872
- for (let i = 0; i < all.length; i++) {
1873
- const m = all[i];
1874
- if ((m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed) {
1875
- count++;
2135
+ if (this._recipientIndex !== null) {
2136
+ const indices = /* @__PURE__ */ new Set();
2137
+ const direct = this._recipientIndex.get(forAgentId);
2138
+ if (direct !== void 0) for (const i of direct) indices.add(i);
2139
+ const broadcasts = this._recipientIndex.get("*");
2140
+ if (broadcasts !== void 0) for (const i of broadcasts) indices.add(i);
2141
+ for (const i of indices) {
2142
+ const m = all[i];
2143
+ if (!(forAgentId in m.readBy) && !m.completed) count++;
2144
+ }
2145
+ } else {
2146
+ for (let i = 0; i < all.length; i++) {
2147
+ const m = all[i];
2148
+ if ((m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed) {
2149
+ count++;
2150
+ }
1876
2151
  }
1877
2152
  }
1878
2153
  return count;
@@ -1882,6 +2157,7 @@ var GlobalMailbox = class {
1882
2157
  await withFileLock(this.messagePath, async () => {
1883
2158
  const all = await this._readMessagesFresh();
1884
2159
  const now = (/* @__PURE__ */ new Date()).toISOString();
2160
+ let changed = false;
1885
2161
  for (const m of all) {
1886
2162
  if (m.id !== mailId) continue;
1887
2163
  if (m.deletedAt !== void 0) {
@@ -1891,19 +2167,30 @@ var GlobalMailbox = class {
1891
2167
  m.deletedAt = now;
1892
2168
  m.deletedBy = by;
1893
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");
1894
2175
  }
1895
- const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
1896
- await fsp.writeFile(this.messagePath, serialized, "utf8");
1897
2176
  const { mtimeMs, size } = await this._statMessageFile();
1898
2177
  this._setMessageCache(all, mtimeMs, size);
1899
2178
  });
1900
2179
  if (updated !== null) {
2180
+ const msg = updated;
1901
2181
  this.publishHqMailboxEvent({
1902
2182
  mailboxId: this.hqMailboxId,
1903
2183
  action: "message.updated",
1904
- message: updated
2184
+ message: msg
1905
2185
  });
1906
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
+ });
1907
2194
  }
1908
2195
  return updated;
1909
2196
  }
@@ -1911,24 +2198,40 @@ var GlobalMailbox = class {
1911
2198
  let updated = null;
1912
2199
  await withFileLock(this.messagePath, async () => {
1913
2200
  const all = await this._readMessagesFresh();
2201
+ let changed = false;
1914
2202
  for (const m of all) {
1915
2203
  if (m.id !== mailId) continue;
2204
+ if (m.deletedAt === void 0 && m.deletedBy === void 0) {
2205
+ updated = m;
2206
+ continue;
2207
+ }
1916
2208
  delete m.deletedAt;
1917
2209
  delete m.deletedBy;
1918
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");
1919
2216
  }
1920
- const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
1921
- await fsp.writeFile(this.messagePath, serialized, "utf8");
1922
2217
  const { mtimeMs, size } = await this._statMessageFile();
1923
2218
  this._setMessageCache(all, mtimeMs, size);
1924
2219
  });
1925
2220
  if (updated !== null) {
2221
+ const msg = updated;
1926
2222
  this.publishHqMailboxEvent({
1927
2223
  mailboxId: this.hqMailboxId,
1928
2224
  action: "message.updated",
1929
- message: updated
2225
+ message: msg
1930
2226
  });
1931
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
+ });
1932
2235
  }
1933
2236
  return updated;
1934
2237
  }
@@ -2061,8 +2364,19 @@ var GlobalMailbox = class {
2061
2364
  }
2062
2365
  async getAgentStatuses() {
2063
2366
  await this._ensureRegistry();
2064
- const registry = await this._readRegistry();
2367
+ let registry = await this._readRegistry();
2368
+ const before = registry.size;
2065
2369
  this._pruneStaleInPlace(registry);
2370
+ if (registry.size < before) {
2371
+ await withFileLock(this.registryPath, async () => {
2372
+ const fresh = await this._readRegistry({ fresh: true });
2373
+ this._pruneStaleInPlace(fresh);
2374
+ this._registryCache = fresh;
2375
+ this._registryCacheAt = Date.now();
2376
+ await this._writeRegistry(fresh);
2377
+ registry = fresh;
2378
+ });
2379
+ }
2066
2380
  const now = Date.now();
2067
2381
  return Array.from(registry.values()).map((a) => ({
2068
2382
  agentId: a.agentId,
@@ -2114,6 +2428,20 @@ var GlobalMailbox = class {
2114
2428
  });
2115
2429
  this.publishHqMailboxSnapshot();
2116
2430
  }
2431
+ async deregisterClient(clientId) {
2432
+ await this._ensureClientRegistry();
2433
+ await withFileLock(this.clientRegistryPath, async () => {
2434
+ const registry = await this._readClientRegistry({ fresh: true });
2435
+ this._pruneStaleClientsInPlace(registry);
2436
+ registry.delete(clientId);
2437
+ this._clientRegistryCache = registry;
2438
+ this._clientRegistryCacheAt = Date.now();
2439
+ await this._writeClientRegistry(registry);
2440
+ });
2441
+ this._lastClientHeartbeat.delete(clientId);
2442
+ this._events?.emitCustom("mailbox.client_deregistered", { clientId });
2443
+ this.publishHqMailboxSnapshot();
2444
+ }
2117
2445
  async clientHeartbeat(input) {
2118
2446
  const last = this._lastClientHeartbeat.get(input.clientId) ?? 0;
2119
2447
  const now = Date.now();
@@ -2143,7 +2471,11 @@ var GlobalMailbox = class {
2143
2471
  async getClientStatuses() {
2144
2472
  await this._ensureClientRegistry();
2145
2473
  const registry = await this._readClientRegistry();
2474
+ const before = registry.size;
2146
2475
  this._pruneStaleClientsInPlace(registry);
2476
+ if (registry.size < before) {
2477
+ await this._writeClientRegistry(registry);
2478
+ }
2147
2479
  const now = Date.now();
2148
2480
  return Array.from(registry.values()).map((c) => ({
2149
2481
  clientId: c.clientId,
@@ -2155,6 +2487,32 @@ var GlobalMailbox = class {
2155
2487
  pid: c.pid
2156
2488
  })).sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt));
2157
2489
  }
2490
+ /**
2491
+ * Explicitly purge stale clients from the registry and write back to disk.
2492
+ * Removes client entries whose lastSeenAt is older than CLIENT_STALE_MS.
2493
+ * Returns the number of entries purged.
2494
+ *
2495
+ * Idempotent — safe to call regularly. The same pruning runs implicitly
2496
+ * inside registerClient, clientHeartbeat, and getClientStatuses, but if
2497
+ * no client registers or heartbeats for a long time, this public method
2498
+ * ensures the file gets cleaned up on demand.
2499
+ */
2500
+ async purgeClients() {
2501
+ await this._ensureClientRegistry();
2502
+ let purged = 0;
2503
+ await withFileLock(this.clientRegistryPath, async () => {
2504
+ const registry = await this._readClientRegistry({ fresh: true });
2505
+ const before = registry.size;
2506
+ this._pruneStaleClientsInPlace(registry);
2507
+ purged = before - registry.size;
2508
+ if (purged > 0) {
2509
+ this._clientRegistryCache = registry;
2510
+ this._clientRegistryCacheAt = Date.now();
2511
+ await this._writeClientRegistry(registry);
2512
+ }
2513
+ });
2514
+ return purged;
2515
+ }
2158
2516
  // ── Lifecycle ───────────────────────────────────────────────────────────
2159
2517
  async close() {
2160
2518
  this._registryCache = null;
@@ -2162,6 +2520,8 @@ var GlobalMailbox = class {
2162
2520
  this._messageCache = null;
2163
2521
  this._messageCacheMtime = -1;
2164
2522
  this._messageCacheSize = -1;
2523
+ this._recipientIndex = null;
2524
+ this._senderIndex = null;
2165
2525
  }
2166
2526
  async clearAll() {
2167
2527
  await withFileLock(this.messagePath, async () => {
@@ -2210,6 +2570,100 @@ var GlobalMailbox = class {
2210
2570
  remaining
2211
2571
  };
2212
2572
  }
2573
+ // ── Auto-compaction ─────────────────────────────────────────────────────
2574
+ _autoCompactTimer = null;
2575
+ async autoCompact(opts) {
2576
+ const readMaxAgeMs = opts?.readMaxAgeMs ?? AUTO_COMPACT_READ_MAX_AGE_MS;
2577
+ const defaultTtlMs = opts?.defaultTtlMs ?? AUTO_COMPACT_DEFAULT_TTL_MS;
2578
+ const completedMaxAgeMs = opts?.completedMaxAgeMs ?? 864e5;
2579
+ const incompleteMaxAgeMs = opts?.incompleteMaxAgeMs ?? 6048e5;
2580
+ let expiredPurged = 0;
2581
+ let readByAllPurged = 0;
2582
+ let completedPurged = 0;
2583
+ let incompletePurged = 0;
2584
+ let remaining = 0;
2585
+ let onlineAgentIds = null;
2586
+ try {
2587
+ const statuses = await this.getAgentStatuses();
2588
+ const online = statuses.filter((s) => s.online);
2589
+ onlineAgentIds = online.length > 0 ? new Set(online.map((s) => s.agentId)) : null;
2590
+ } catch {
2591
+ onlineAgentIds = null;
2592
+ }
2593
+ await withFileLock(this.messagePath, async () => {
2594
+ const all = await this._readMessagesFresh();
2595
+ const now = Date.now();
2596
+ const cutoffReadAge = now - readMaxAgeMs;
2597
+ const cutoffCompleted = now - completedMaxAgeMs;
2598
+ const cutoffIncomplete = now - incompleteMaxAgeMs;
2599
+ const cutoffDefaultTtl = now - defaultTtlMs;
2600
+ const kept = [];
2601
+ for (const msg of all) {
2602
+ const msgTime = new Date(msg.timestamp).getTime();
2603
+ if (msg.expiresAt !== void 0) {
2604
+ if (new Date(msg.expiresAt).getTime() < now) {
2605
+ expiredPurged++;
2606
+ continue;
2607
+ }
2608
+ } else if (msgTime < cutoffDefaultTtl) {
2609
+ expiredPurged++;
2610
+ continue;
2611
+ }
2612
+ if (onlineAgentIds !== null && !msg.completed) {
2613
+ const readByAll = onlineAgentIds.size > 0 && [...onlineAgentIds].every((id) => id in msg.readBy);
2614
+ if (readByAll) {
2615
+ const readTimes = Object.values(msg.readBy).map((t) => new Date(t).getTime());
2616
+ const latestRead = Math.max(...readTimes);
2617
+ if (latestRead < cutoffReadAge) {
2618
+ readByAllPurged++;
2619
+ continue;
2620
+ }
2621
+ }
2622
+ }
2623
+ const completedTime = msg.completedAt ? new Date(msg.completedAt).getTime() : 0;
2624
+ if (msg.completed && completedTime < cutoffCompleted) {
2625
+ completedPurged++;
2626
+ continue;
2627
+ }
2628
+ if (!msg.completed && msgTime < cutoffIncomplete) {
2629
+ incompletePurged++;
2630
+ continue;
2631
+ }
2632
+ kept.push(msg);
2633
+ }
2634
+ remaining = kept.length;
2635
+ if (kept.length < all.length) {
2636
+ const content = kept.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
2637
+ await fsp.writeFile(this.messagePath, content, "utf8");
2638
+ }
2639
+ const { mtimeMs, size } = await this._statMessageFile();
2640
+ this._setMessageCache(kept, mtimeMs, size);
2641
+ });
2642
+ const totalRemoved = expiredPurged + readByAllPurged + completedPurged + incompletePurged;
2643
+ return {
2644
+ expiredRemoved: expiredPurged,
2645
+ readByAllRemoved: readByAllPurged,
2646
+ stalePurged: completedPurged + incompletePurged,
2647
+ totalRemoved,
2648
+ remaining
2649
+ };
2650
+ }
2651
+ startAutoCompactTimer(opts) {
2652
+ if (this._autoCompactTimer !== null) {
2653
+ clearInterval(this._autoCompactTimer);
2654
+ }
2655
+ const intervalMs = opts?.intervalMs ?? AUTO_COMPACT_INTERVAL_MS;
2656
+ const timer = setInterval(() => {
2657
+ this.autoCompact(opts).catch(() => {
2658
+ });
2659
+ }, intervalMs);
2660
+ timer.unref?.();
2661
+ this._autoCompactTimer = timer;
2662
+ return () => {
2663
+ clearInterval(timer);
2664
+ this._autoCompactTimer = null;
2665
+ };
2666
+ }
2213
2667
  // ── Internal ────────────────────────────────────────────────────────────
2214
2668
  /**
2215
2669
  * Read all messages from the JSONL file. Always reads + parses the file.
@@ -2242,44 +2696,59 @@ var GlobalMailbox = class {
2242
2696
  const buf = Buffer.alloc(tailLen);
2243
2697
  await fd.read(buf, 0, tailLen, oldSize);
2244
2698
  const tail = buf.toString("utf8");
2245
- for (const line of tail.split(LINE_SEPARATOR)) {
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)) {
2246
2715
  if (!line.trim()) continue;
2247
- try {
2248
- const parsed = JSON.parse(line);
2249
- if (!parsed["readBy"]) {
2250
- const readBy = {};
2251
- if (parsed["read"] && parsed["readAt"]) {
2252
- readBy[parsed["to"]] = parsed["readAt"];
2253
- }
2254
- parsed["readBy"] = readBy;
2255
- delete parsed["read"];
2256
- delete parsed["readAt"];
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);
2257
2725
  }
2258
- this._messageCache.push(parsed);
2259
- } catch {
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);
2260
2735
  }
2261
2736
  }
2262
- return this._messageCache;
2737
+ }
2738
+ _parseLine(line) {
2739
+ try {
2740
+ return parseMailboxMessageLine(line);
2741
+ } catch {
2742
+ return null;
2743
+ }
2263
2744
  }
2264
2745
  /** Parse a JSONL string into MailboxMessage[], including migration. */
2265
2746
  _parseLines(raw) {
2266
2747
  const lines = raw.split(LINE_SEPARATOR).filter((l) => l.trim().length > 0);
2267
2748
  const messages = [];
2268
2749
  for (const line of lines) {
2269
- try {
2270
- const parsed = JSON.parse(line);
2271
- if (!parsed["readBy"]) {
2272
- const readBy = {};
2273
- if (parsed["read"] && parsed["readAt"]) {
2274
- readBy[parsed["to"]] = parsed["readAt"];
2275
- }
2276
- parsed["readBy"] = readBy;
2277
- delete parsed["read"];
2278
- delete parsed["readAt"];
2279
- }
2280
- messages.push(parsed);
2281
- } catch {
2282
- }
2750
+ const message = this._parseLine(line);
2751
+ if (message !== null) messages.push(message);
2283
2752
  }
2284
2753
  return messages;
2285
2754
  }
@@ -2367,7 +2836,6 @@ var GlobalMailbox = class {
2367
2836
  try {
2368
2837
  const updated = await this._readNewMessagesOnly(fd, this._messageCacheSize, st.size);
2369
2838
  this._messageCacheMtime = st.mtimeMs;
2370
- this._messageCacheSize = st.size;
2371
2839
  return updated;
2372
2840
  } finally {
2373
2841
  await fd.close();
@@ -2404,11 +2872,46 @@ var GlobalMailbox = class {
2404
2872
  this._messageCache = null;
2405
2873
  this._messageCacheMtime = -1;
2406
2874
  this._messageCacheSize = -1;
2875
+ this._recipientIndex = null;
2876
+ this._senderIndex = null;
2407
2877
  return;
2408
2878
  }
2409
2879
  this._messageCache = messages;
2410
2880
  this._messageCacheMtime = mtime;
2411
2881
  this._messageCacheSize = size;
2882
+ this._rebuildRecipientIndex();
2883
+ }
2884
+ /**
2885
+ * Build the recipient AND sender indexes from the current `_messageCache`.
2886
+ * Called only from `_setMessageCache` so both indexes are always in sync
2887
+ * with a freshly-replaced cache array.
2888
+ */
2889
+ _rebuildRecipientIndex() {
2890
+ const cache = this._messageCache;
2891
+ if (cache === null) {
2892
+ this._recipientIndex = null;
2893
+ this._senderIndex = null;
2894
+ return;
2895
+ }
2896
+ const recIdx = /* @__PURE__ */ new Map();
2897
+ const sndIdx = /* @__PURE__ */ new Map();
2898
+ for (let i = 0; i < cache.length; i++) {
2899
+ const msg = cache[i];
2900
+ let rSet = recIdx.get(msg.to);
2901
+ if (rSet === void 0) {
2902
+ rSet = /* @__PURE__ */ new Set();
2903
+ recIdx.set(msg.to, rSet);
2904
+ }
2905
+ rSet.add(i);
2906
+ let sSet = sndIdx.get(msg.from);
2907
+ if (sSet === void 0) {
2908
+ sSet = /* @__PURE__ */ new Set();
2909
+ sndIdx.set(msg.from, sSet);
2910
+ }
2911
+ sSet.add(i);
2912
+ }
2913
+ this._recipientIndex = recIdx;
2914
+ this._senderIndex = sndIdx;
2412
2915
  }
2413
2916
  /**
2414
2917
  * Append a single just-sent message to the in-memory cache without
@@ -2435,6 +2938,23 @@ var GlobalMailbox = class {
2435
2938
  return;
2436
2939
  }
2437
2940
  this._messageCache.push(msg);
2941
+ const newIdx = this._messageCache.length - 1;
2942
+ if (this._recipientIndex !== null) {
2943
+ let set = this._recipientIndex.get(msg.to);
2944
+ if (set === void 0) {
2945
+ set = /* @__PURE__ */ new Set();
2946
+ this._recipientIndex.set(msg.to, set);
2947
+ }
2948
+ set.add(newIdx);
2949
+ }
2950
+ if (this._senderIndex !== null) {
2951
+ let set = this._senderIndex.get(msg.from);
2952
+ if (set === void 0) {
2953
+ set = /* @__PURE__ */ new Set();
2954
+ this._senderIndex.set(msg.from, set);
2955
+ }
2956
+ set.add(newIdx);
2957
+ }
2438
2958
  this._messageCacheMtime = mtime;
2439
2959
  this._messageCacheSize = size;
2440
2960
  }
@@ -2466,22 +2986,13 @@ var GlobalMailbox = class {
2466
2986
  }
2467
2987
  }
2468
2988
  _pruneStaleInPlace(registry) {
2469
- const staleCutoff = Date.now() - AGENT_STALE_MS;
2470
- const purgeCutoff = Date.now() - AGENT_PURGE_MS;
2471
- const toDelete = [];
2989
+ const cutoff = Date.now() - AGENT_STALE_MS;
2472
2990
  for (const [id, agent] of registry) {
2473
- const lastSeen = new Date(agent.lastSeenAt).getTime();
2474
- if (lastSeen < purgeCutoff) {
2475
- toDelete.push(id);
2476
- continue;
2477
- }
2478
- if (lastSeen < staleCutoff) {
2479
- agent.status = "idle";
2991
+ const lastSeen = Date.parse(agent.lastSeenAt);
2992
+ if (!Number.isFinite(lastSeen) || lastSeen < cutoff) {
2993
+ registry.delete(id);
2480
2994
  }
2481
2995
  }
2482
- for (const id of toDelete) {
2483
- registry.delete(id);
2484
- }
2485
2996
  }
2486
2997
  async _writeRegistry(registry) {
2487
2998
  const obj = {};
@@ -2522,9 +3033,10 @@ var GlobalMailbox = class {
2522
3033
  }
2523
3034
  _pruneStaleClientsInPlace(registry) {
2524
3035
  const cutoff = Date.now() - CLIENT_STALE_MS;
2525
- for (const client of registry.values()) {
2526
- if (new Date(client.lastSeenAt).getTime() < cutoff) {
2527
- client.lastSeenAt = new Date(cutoff).toISOString();
3036
+ for (const [id, client] of registry) {
3037
+ const lastSeen = Date.parse(client.lastSeenAt);
3038
+ if (!Number.isFinite(lastSeen) || lastSeen < cutoff) {
3039
+ registry.delete(id);
2528
3040
  }
2529
3041
  }
2530
3042
  }
@@ -2540,7 +3052,7 @@ var GlobalMailbox = class {
2540
3052
  };
2541
3053
 
2542
3054
  // src/hq/auth-store.ts
2543
- import { randomUUID as randomUUID3 } from "node:crypto";
3055
+ import { randomBytes as randomBytes2, randomUUID as randomUUID3, scrypt, timingSafeEqual } from "node:crypto";
2544
3056
  import * as fs2 from "node:fs/promises";
2545
3057
  import * as syncFs from "node:fs";
2546
3058
  import * as path5 from "node:path";
@@ -2654,6 +3166,10 @@ async function ensureHqFirstRunAuthFile(dataDir, opts = {}) {
2654
3166
  browserTokens: [browserToken],
2655
3167
  clientTokens: [clientToken]
2656
3168
  };
3169
+ if (opts.password) {
3170
+ authFile.passwordHash = await hashHqPassword(opts.password);
3171
+ authFile.cookieSecret = mintHqCookieSecret();
3172
+ }
2657
3173
  await writeHqAuthFile(dataDir, authFile);
2658
3174
  return { authFile: await readHqAuthFile(dataDir, opts), created: true, browserToken, clientToken };
2659
3175
  }
@@ -2663,6 +3179,42 @@ async function mutateHqAuthFile(dataDir, mutator, opts = {}) {
2663
3179
  await writeHqAuthFile(dataDir, next);
2664
3180
  return next;
2665
3181
  }
3182
+ var HQ_PASSWORD_SALT_BYTES = 16;
3183
+ var HQ_PASSWORD_HASH_BYTES = 32;
3184
+ var HQ_PASSWORD_SCRYPT_PARAMS = { N: 16384, r: 8, p: 1 };
3185
+ function hashHqPassword(password) {
3186
+ return new Promise((resolve4, reject) => {
3187
+ const salt = randomBytes2(HQ_PASSWORD_SALT_BYTES);
3188
+ scrypt(password, salt, HQ_PASSWORD_HASH_BYTES, HQ_PASSWORD_SCRYPT_PARAMS, (err, derivedKey) => {
3189
+ if (err) return reject(err);
3190
+ const payload = `scrypt$${salt.toString("base64url")}$${derivedKey.toString("base64url")}`;
3191
+ resolve4(payload);
3192
+ });
3193
+ });
3194
+ }
3195
+ async function verifyHqPassword(password, hash) {
3196
+ const parts = hash.split("$");
3197
+ if (parts.length !== 3 || parts[0] !== "scrypt" || !parts[1] || !parts[2]) return false;
3198
+ let salt;
3199
+ let expected;
3200
+ try {
3201
+ salt = Buffer.from(parts[1], "base64url");
3202
+ expected = Buffer.from(parts[2], "base64url");
3203
+ } catch {
3204
+ return false;
3205
+ }
3206
+ if (salt.length === 0 || expected.length === 0) return false;
3207
+ const derived = await new Promise((resolve4, reject) => {
3208
+ scrypt(password, salt, expected.length, HQ_PASSWORD_SCRYPT_PARAMS, (err, key) => {
3209
+ if (err) reject(err);
3210
+ else resolve4(key);
3211
+ });
3212
+ });
3213
+ return timingSafeEqual(derived, expected);
3214
+ }
3215
+ function mintHqCookieSecret() {
3216
+ return randomBytes2(32).toString("base64url");
3217
+ }
2666
3218
  function mintHqToken(label) {
2667
3219
  const now = (/* @__PURE__ */ new Date()).toISOString();
2668
3220
  return {
@@ -2833,7 +3385,8 @@ function createHqPublisherFromEnv(options) {
2833
3385
  // Auto-discovery: re-read the local HQ runtime marker + client token on
2834
3386
  // every connect attempt so late-started/restarted HQs are picked up.
2835
3387
  ...config.discover ? { resolveEndpoint: () => discoverLocalHqEndpoint({ dataDir: discoveryDataDir }) } : {},
2836
- ...options.discoveryPollMs !== void 0 ? { discoveryPollMs: options.discoveryPollMs } : {}
3388
+ ...options.discoveryPollMs !== void 0 ? { discoveryPollMs: options.discoveryPollMs } : {},
3389
+ ...options.logger !== void 0 ? { logger: options.logger } : {}
2837
3390
  });
2838
3391
  }
2839
3392
  function createGlobalMailbox(options) {
@@ -4099,6 +4652,7 @@ export {
4099
4652
  discoverLocalHqEndpoint,
4100
4653
  emptyHqAuthFile,
4101
4654
  ensureHqFirstRunAuthFile,
4655
+ hashHqPassword,
4102
4656
  hqAuthFilePath,
4103
4657
  hqRuntimeFilePath,
4104
4658
  mapMailboxAgentToHqSummary,
@@ -4106,6 +4660,7 @@ export {
4106
4660
  mapSessionEventToEntries,
4107
4661
  mergeToolResults,
4108
4662
  mintHqBrowserToken,
4663
+ mintHqCookieSecret,
4109
4664
  mintHqToken,
4110
4665
  mutateHqAuthFile,
4111
4666
  parseHqEventPayload,
@@ -4131,6 +4686,7 @@ export {
4131
4686
  toAlertMessage,
4132
4687
  tokenHasCapability,
4133
4688
  validateHqCommand,
4689
+ verifyHqPassword,
4134
4690
  watchHqAuthFile,
4135
4691
  writeHqAuthFile,
4136
4692
  writeHqRuntimeFile