@wrongstack/core 0.265.1 → 0.268.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 (75) hide show
  1. package/dist/{agent-bridge-DrkBxszZ.d.ts → agent-bridge-UhojbpWx.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-DM2pP-B6.d.ts → agent-subagent-runner-Bvtf1o9K.d.ts} +25 -7
  3. package/dist/{brain-BXd_61kQ.d.ts → brain-69wzMKp1.d.ts} +73 -1
  4. package/dist/{compactor-B8pOf45Y.d.ts → compactor-CBQAJoDc.d.ts} +19 -1
  5. package/dist/{config-BMCj_XDs.d.ts → config-VKfOZ-6X.d.ts} +122 -3
  6. package/dist/{context-MRk5PhNv.d.ts → context-C0U8B9NF.d.ts} +88 -1
  7. package/dist/coordination/index.d.ts +57 -161
  8. package/dist/coordination/index.js +471 -177
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/defaults/index.d.ts +26 -25
  11. package/dist/defaults/index.js +1818 -844
  12. package/dist/defaults/index.js.map +1 -1
  13. package/dist/execution/index.d.ts +72 -16
  14. package/dist/execution/index.js +1270 -265
  15. package/dist/execution/index.js.map +1 -1
  16. package/dist/execution/prompt-enhancer.d.ts +1 -1
  17. package/dist/extension/index.d.ts +7 -6
  18. package/dist/global-mailbox-KByEFFBa.d.ts +663 -0
  19. package/dist/{goal-preamble-DvHDSKSe.d.ts → goal-preamble-CrYjmdw4.d.ts} +28 -11
  20. package/dist/{goal-store-DtLMySNb.d.ts → goal-store-Y_zdLZ3q.d.ts} +1 -1
  21. package/dist/hq/index.d.ts +195 -0
  22. package/dist/hq/index.js +1884 -0
  23. package/dist/hq/index.js.map +1 -0
  24. package/dist/index-BfaS-f_m.d.ts +82 -0
  25. package/dist/{index-B-ch8K9C.d.ts → index-CtQnmkaS.d.ts} +8 -8
  26. package/dist/{index-CEDeNodM.d.ts → index-gCv830d7.d.ts} +5 -5
  27. package/dist/index.d.ts +124 -47
  28. package/dist/index.js +5600 -2662
  29. package/dist/index.js.map +1 -1
  30. package/dist/infrastructure/index.d.ts +6 -6
  31. package/dist/infrastructure/index.js +117 -19
  32. package/dist/infrastructure/index.js.map +1 -1
  33. package/dist/kernel/index.d.ts +10 -9
  34. package/dist/kernel/index.js.map +1 -1
  35. package/dist/{pipeline-DPDxH_7m.d.ts → mailbox-types-Ct2hJq0P.d.ts} +1 -244
  36. package/dist/{mcp-servers-2x4w6Jn9.d.ts → mcp-servers-HT3Fi7Bl.d.ts} +10 -4
  37. package/dist/models/index.d.ts +5 -5
  38. package/dist/models/index.js +33 -3
  39. package/dist/models/index.js.map +1 -1
  40. package/dist/{models-registry-DmJlKuNp.d.ts → models-registry-Bvcl3Vaa.d.ts} +1 -1
  41. package/dist/{multi-agent-coordinator-DyCkCZnU.d.ts → multi-agent-coordinator-BACjsmkC.d.ts} +1 -1
  42. package/dist/{null-fleet-bus-CG9QY2aP.d.ts → null-fleet-bus-DA7fvhUg.d.ts} +14 -9
  43. package/dist/observability/index.d.ts +2 -2
  44. package/dist/{parallel-eternal-engine-Jw9uhEoT.d.ts → parallel-eternal-engine-Ci71gYu_.d.ts} +11 -15
  45. package/dist/{path-resolver-Dy2ej-gE.d.ts → path-resolver-O1IJnmKE.d.ts} +4 -3
  46. package/dist/{permission-B9SB45lp.d.ts → permission-Bd-57Lbl.d.ts} +1 -1
  47. package/dist/{permission-policy-CkjSXabK.d.ts → permission-policy-uNXC6Kge.d.ts} +2 -3
  48. package/dist/pipeline-BDNvENyV.d.ts +245 -0
  49. package/dist/{plan-templates-CzD9GnAU.d.ts → plan-templates-EMsalEtN.d.ts} +5 -5
  50. package/dist/{llm-selector-C0tfTCUe.d.ts → provider-model-resolve-CEb9x886.d.ts} +40 -3
  51. package/dist/{provider-runner-DMa70ODu.d.ts → provider-runner-DWJbpo70.d.ts} +3 -3
  52. package/dist/{retry-policy-CN0khdlj.d.ts → retry-policy-C3s_lvdK.d.ts} +1 -1
  53. package/dist/sdd/index.d.ts +9 -8
  54. package/dist/sdd/index.js +44 -14
  55. package/dist/sdd/index.js.map +1 -1
  56. package/dist/{secret-vault-B2yw84VT.d.ts → secret-vault-Cgduf5xL.d.ts} +2 -2
  57. package/dist/security/index.d.ts +5 -67
  58. package/dist/security/index.js +129 -99
  59. package/dist/security/index.js.map +1 -1
  60. package/dist/{selector-CzHh_igB.d.ts → selector-47LBnBVk.d.ts} +1 -1
  61. package/dist/{session-event-bridge-BUI6Jf-4.d.ts → session-event-bridge-Cw7oqmW2.d.ts} +1 -1
  62. package/dist/{session-reader-CMgdMSRP.d.ts → session-reader-DD4v2Obw.d.ts} +1 -1
  63. package/dist/storage/index.d.ts +14 -12
  64. package/dist/storage/index.js +144 -120
  65. package/dist/storage/index.js.map +1 -1
  66. package/dist/tools/index.d.ts +4 -2
  67. package/dist/tools/index.js +166 -31
  68. package/dist/tools/index.js.map +1 -1
  69. package/dist/types/index.d.ts +20 -19
  70. package/dist/types/index.js +1358 -476
  71. package/dist/types/index.js.map +1 -1
  72. package/dist/utils/index.d.ts +472 -405
  73. package/dist/utils/index.js +2321 -1193
  74. package/dist/utils/index.js.map +1 -1
  75. package/package.json +5 -1
@@ -0,0 +1,1884 @@
1
+ import { randomUUID, randomBytes, createHash } from 'crypto';
2
+ import * as syncFs from 'fs';
3
+ import * as os from 'os';
4
+ import { hostname } from 'os';
5
+ import * as path3 from 'path';
6
+ import { basename } from 'path';
7
+ import * as fsp from 'fs/promises';
8
+
9
+ // src/hq/protocol.ts
10
+ var HQ_PROTOCOL_VERSION = 1;
11
+ var DEFAULT_HQ_REDACTION_POLICY = {
12
+ rawContent: false,
13
+ toolArgs: "summary",
14
+ paths: "project-relative"
15
+ };
16
+ var KNOWN_HQ_CLIENT_FRAME_TYPES = /* @__PURE__ */ new Set([
17
+ "client.hello",
18
+ "client.event",
19
+ "client.command_poll",
20
+ "client.command_ack"
21
+ ]);
22
+ function hasStringType(x) {
23
+ return typeof x === "object" && x !== null && typeof x.type === "string";
24
+ }
25
+ function isHqClientIdentity(x) {
26
+ if (typeof x !== "object" || x === null) return false;
27
+ const v = x;
28
+ return typeof v.clientId === "string" && typeof v.kind === "string" && typeof v.machineId === "string" && typeof v.startedAt === "string";
29
+ }
30
+ function isHqProjectIdentity(x) {
31
+ if (typeof x !== "object" || x === null) return false;
32
+ const v = x;
33
+ return typeof v.projectId === "string" && typeof v.projectRoot === "string" && typeof v.projectName === "string" && typeof v.machineId === "string" && typeof v.workspaceKind === "string";
34
+ }
35
+ function isHqClientHelloPayload(x) {
36
+ if (typeof x !== "object" || x === null) return false;
37
+ const v = x;
38
+ return typeof v.protocolVersion === "number" && isHqClientIdentity(v.client) && isHqProjectIdentity(v.project) && Array.isArray(v.capabilities);
39
+ }
40
+ function isHqEventEnvelope(x) {
41
+ if (typeof x !== "object" || x === null) return false;
42
+ const v = x;
43
+ return typeof v.id === "string" && typeof v.type === "string" && typeof v.schemaVersion === "number" && typeof v.timestamp === "string" && typeof v.clientId === "string" && typeof v.projectId === "string" && typeof v.seq === "number";
44
+ }
45
+ function isHqClientCommandPollMessage(x) {
46
+ if (typeof x !== "object" || x === null) return false;
47
+ const v = x;
48
+ return typeof v.clientId === "string" && typeof v.projectId === "string";
49
+ }
50
+ function isHqClientCommandAckMessage(x) {
51
+ if (typeof x !== "object" || x === null) return false;
52
+ const v = x;
53
+ return typeof v.clientId === "string" && typeof v.projectId === "string" && typeof v.commandId === "string" && typeof v.status === "string";
54
+ }
55
+ function parseHqFrame(raw) {
56
+ let parsed;
57
+ try {
58
+ parsed = JSON.parse(typeof raw === "string" ? raw : raw.toString("utf8"));
59
+ } catch {
60
+ return { ok: false, reason: "invalid-json" };
61
+ }
62
+ if (!hasStringType(parsed)) {
63
+ return { ok: false, reason: "malformed" };
64
+ }
65
+ const obj = parsed;
66
+ if (!KNOWN_HQ_CLIENT_FRAME_TYPES.has(obj.type)) {
67
+ return { ok: false, reason: "unknown-type" };
68
+ }
69
+ switch (obj.type) {
70
+ case "client.hello":
71
+ if (!isHqClientHelloPayload(obj.payload)) {
72
+ return { ok: false, reason: "malformed" };
73
+ }
74
+ return { ok: true, frame: { type: "client.hello", payload: obj.payload } };
75
+ case "client.event":
76
+ if (!isHqEventEnvelope(obj.event)) {
77
+ return { ok: false, reason: "malformed" };
78
+ }
79
+ return { ok: true, frame: { type: "client.event", event: obj.event } };
80
+ case "client.command_poll":
81
+ if (!isHqClientCommandPollMessage(obj)) {
82
+ return { ok: false, reason: "malformed" };
83
+ }
84
+ return {
85
+ ok: true,
86
+ frame: { type: "client.command_poll", clientId: obj.clientId, projectId: obj.projectId }
87
+ };
88
+ case "client.command_ack":
89
+ if (!isHqClientCommandAckMessage(obj)) {
90
+ return { ok: false, reason: "malformed" };
91
+ }
92
+ return {
93
+ ok: true,
94
+ frame: {
95
+ type: "client.command_ack",
96
+ clientId: obj.clientId,
97
+ projectId: obj.projectId,
98
+ commandId: obj.commandId,
99
+ status: obj.status
100
+ }
101
+ };
102
+ default: {
103
+ const _exhaustive = obj.type;
104
+ return _exhaustive;
105
+ }
106
+ }
107
+ }
108
+ var KNOWN_HQ_EVENT_PAYLOAD_TYPES = /* @__PURE__ */ new Set(["mailbox.snapshot", "mailbox.event"]);
109
+ function isHqMailboxMessageSummary(x) {
110
+ if (typeof x !== "object" || x === null) return false;
111
+ const v = x;
112
+ return typeof v.messageId === "string" && typeof v.from === "string" && typeof v.to === "string" && typeof v.subject === "string" && typeof v.priority === "string" && typeof v.timestamp === "string" && typeof v.completed === "boolean" && typeof v.hasBody === "boolean";
113
+ }
114
+ function isHqMailboxAgentSummary(x) {
115
+ if (typeof x !== "object" || x === null) return false;
116
+ const v = x;
117
+ return typeof v.agentId === "string" && typeof v.name === "string" && typeof v.sessionId === "string" && typeof v.status === "string" && typeof v.iterations === "number" && typeof v.toolCalls === "number" && typeof v.lastActivityAt === "string" && typeof v.lastSeenAt === "string" && typeof v.online === "boolean";
118
+ }
119
+ function isHqMailboxSnapshotPayload(x) {
120
+ if (typeof x !== "object" || x === null) return false;
121
+ const v = x;
122
+ if (typeof v.mailboxId !== "string" || v.scope !== "project" && v.scope !== "global" || !Array.isArray(v.messages) || !Array.isArray(v.agents) || typeof v.totals !== "object" || v.totals === null) {
123
+ return false;
124
+ }
125
+ const totals = v.totals;
126
+ if (typeof totals.messages !== "number" || typeof totals.unread !== "number" || typeof totals.incomplete !== "number" || typeof totals.highPriority !== "number" || typeof totals.onlineAgents !== "number") {
127
+ return false;
128
+ }
129
+ for (const message of v.messages) {
130
+ if (!isHqMailboxMessageSummary(message)) return false;
131
+ }
132
+ for (const agent of v.agents) {
133
+ if (!isHqMailboxAgentSummary(agent)) return false;
134
+ }
135
+ return true;
136
+ }
137
+ var HQ_MAILBOX_EVENT_ACTIONS = /* @__PURE__ */ new Set([
138
+ "message.sent",
139
+ "message.read",
140
+ "message.completed",
141
+ "message.updated",
142
+ "agent.registered",
143
+ "agent.heartbeat",
144
+ "agent.offline"
145
+ ]);
146
+ function isHqMailboxEventPayload(x) {
147
+ if (typeof x !== "object" || x === null) return false;
148
+ const v = x;
149
+ if (typeof v.mailboxId !== "string") return false;
150
+ if (typeof v.action !== "string" || !HQ_MAILBOX_EVENT_ACTIONS.has(v.action)) return false;
151
+ if (v.message !== void 0 && !isHqMailboxMessageSummary(v.message)) return false;
152
+ if (v.agent !== void 0 && !isHqMailboxAgentSummary(v.agent)) return false;
153
+ return true;
154
+ }
155
+ function parseHqEventPayload(eventType, payload) {
156
+ if (!KNOWN_HQ_EVENT_PAYLOAD_TYPES.has(eventType)) {
157
+ return { ok: true, payload };
158
+ }
159
+ switch (eventType) {
160
+ case "mailbox.snapshot":
161
+ return isHqMailboxSnapshotPayload(payload) ? { ok: true, payload } : { ok: false, reason: "malformed-payload" };
162
+ case "mailbox.event":
163
+ return isHqMailboxEventPayload(payload) ? { ok: true, payload } : { ok: false, reason: "malformed-payload" };
164
+ default: {
165
+ const _exhaustive = eventType;
166
+ return _exhaustive;
167
+ }
168
+ }
169
+ }
170
+ function createHqEventEnvelope(input) {
171
+ return {
172
+ id: input.id,
173
+ type: input.type,
174
+ schemaVersion: HQ_PROTOCOL_VERSION,
175
+ timestamp: input.timestamp,
176
+ clientId: input.clientId,
177
+ projectId: input.projectId,
178
+ seq: input.seq,
179
+ payload: input.payload,
180
+ ...input.sessionId !== void 0 ? { sessionId: input.sessionId } : {},
181
+ ...input.runId !== void 0 ? { runId: input.runId } : {}
182
+ };
183
+ }
184
+
185
+ // src/security/secret-scrubber.ts
186
+ var PATTERNS = [
187
+ // Anchored at the start where possible so partial matches inside larger
188
+ // strings don't trigger false positives.
189
+ {
190
+ type: "anthropic_key",
191
+ regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g
192
+ },
193
+ { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
194
+ { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g },
195
+ { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g },
196
+ { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g },
197
+ { type: "gcp_key", regex: /(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9])/g },
198
+ { type: "slack_token", regex: /(?<![A-Za-z0-9-])xox[abpos]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g },
199
+ {
200
+ type: "stripe_key",
201
+ regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g
202
+ },
203
+ {
204
+ type: "twilio_sid",
205
+ regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g
206
+ },
207
+ {
208
+ type: "telegram_bot_token",
209
+ // Telegram tokens are of the form bot<digits>:<alphanum> in URL paths
210
+ regex: /\/bot\d+:[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
211
+ },
212
+ {
213
+ type: "jwt",
214
+ // Anchored: look for literal "eyJ" which is unambiguous for JWT header
215
+ regex: /(?<![A-Za-z0-9/+=])eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?![A-Za-z0-9/+=])/g
216
+ },
217
+ {
218
+ type: "private_key",
219
+ // Anchored: start must be BEGIN, end must be END with no extra dashes after END
220
+ regex: /(?:^|\n)-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----[\s\S]*?-----END[^-]*-----(?:\n|$)/g
221
+ },
222
+ { type: "mongodb_uri", regex: /mongodb(?:\+srv)?:\/\/[^\s"'`]+/g },
223
+ { type: "postgres_uri", regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g },
224
+ { type: "mysql_uri", regex: /mysql:\/\/[^\s"'`]+/g },
225
+ { type: "redis_uri", regex: /redis:\/\/[^\s"'`]+/g },
226
+ // AI/ML provider keys — modern LLM services with well-known prefixes
227
+ {
228
+ type: "huggingface_token",
229
+ // HuggingFace tokens: hf_ followed by 34 alphanumeric chars
230
+ regex: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{34}(?![A-Za-z0-9])/g
231
+ },
232
+ {
233
+ type: "replicate_token",
234
+ // Replicate tokens: r8_ followed by 40+ alphanumeric chars
235
+ regex: /(?<![A-Za-z0-9])r8_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
236
+ },
237
+ {
238
+ type: "perplexity_key",
239
+ // Perplexity API keys: pplx- followed by 40+ alphanumeric chars
240
+ regex: /(?<![A-Za-z0-9])pplx-[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
241
+ },
242
+ {
243
+ type: "groq_key",
244
+ // Groq API keys: gsk_ followed by 40+ alphanumeric chars
245
+ regex: /(?<![A-Za-z0-9])gsk_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
246
+ },
247
+ {
248
+ type: "bearer_token",
249
+ // Anchored with alternation instead of negative lookahead — avoids V8
250
+ // backtracking risk on adversarial input. Bounded at 512 chars.
251
+ // Min 12 chars: some OAuth providers issue shorter-lived tokens (< 20
252
+ // chars). A 12-char base64 string has ~71 bits of entropy — above the
253
+ // threshold where random strings are unlikely to produce false matches.
254
+ regex: /(?:^|[^A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{12,512}=*(?:$|[^A-Za-z0-9_.~+/-])/g
255
+ },
256
+ {
257
+ type: "high_entropy_env",
258
+ // Anchored with alternation instead of lookbehind to avoid backtracking.
259
+ // Value bounded at 512 chars.
260
+ regex: /(?:^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?:\s|$)/g
261
+ }
262
+ ];
263
+ var SIMPLE_PATTERNS = PATTERNS.filter((p) => p.type !== "high_entropy_env");
264
+ var COMBINED_REGEX = new RegExp(SIMPLE_PATTERNS.map((p) => `(${p.regex.source})`).join("|"), "g");
265
+ var HIGH_ENTROPY_REGEX = PATTERNS.find((p) => p.type === "high_entropy_env").regex;
266
+ var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
267
+ var SCRUB_CHUNK_BYTES = 64 * 1024;
268
+ function hasCredentialAnchors(text) {
269
+ return text.includes("-----BEGIN") || // Private keys (most unique → cheap reject)
270
+ text.includes("sk-") || // Anthropic + OpenAI keys
271
+ text.includes("sk_") || // Stripe live/test keys
272
+ text.includes("ghp_") || // GitHub PAT v1
273
+ text.includes("github_pat_") || // GitHub PAT v2
274
+ text.includes("eyJ") || // JWT
275
+ text.includes("AKIA") || // AWS access key
276
+ text.includes("AIza") || // GCP service key
277
+ text.includes("xox") || // Slack token (xoxa/xoxb/xoxp/xoxo/xoxs)
278
+ text.includes("Bearer ") || // Bearer token (space suffix reduces false positives)
279
+ text.includes("/bot") || // Telegram bot token (URL path pattern)
280
+ text.includes("hf_") || // HuggingFace token
281
+ text.includes("r8_") || // Replicate token
282
+ text.includes("pplx-") || // Perplexity API key
283
+ text.includes("gsk_") || // Groq API key
284
+ text.includes("_KEY=") || // High-entropy env vars: API_KEY=, SECRET_KEY=, ...
285
+ text.includes("_TOKEN=") || // ACCESS_TOKEN=, AUTH_TOKEN=, ...
286
+ text.includes("_SECRET=") || // API_SECRET=, CLIENT_SECRET=, ...
287
+ text.includes("_PASSWORD=") || // DB_PASSWORD=, ROOT_PASSWORD=, ...
288
+ text.includes("mongodb://") || text.includes("mongodb+srv://") || text.includes("postgres://") || text.includes("postgresql://") || text.includes("mysql://") || text.includes("redis://");
289
+ }
290
+ var DefaultSecretScrubber = class {
291
+ scrub(text) {
292
+ if (!text) return text;
293
+ if (!hasCredentialAnchors(text)) return text;
294
+ if (text.length <= SCRUB_CHUNK_BYTES) {
295
+ return this.scrubOne(text);
296
+ }
297
+ const out = [];
298
+ let i = 0;
299
+ while (i < text.length) {
300
+ let end = Math.min(i + SCRUB_CHUNK_BYTES, text.length);
301
+ if (end < text.length) {
302
+ const nl = text.lastIndexOf("\n", end);
303
+ if (nl > i + SCRUB_CHUNK_BYTES / 2) end = nl + 1;
304
+ }
305
+ out.push(this.scrubOne(text.slice(i, end)));
306
+ i = end;
307
+ }
308
+ return out.join("");
309
+ }
310
+ scrubOne(text) {
311
+ if (!hasCredentialAnchors(text)) return text;
312
+ let out = text.replace(
313
+ COMBINED_REGEX,
314
+ (match, ...groups) => {
315
+ const idx = groups.findIndex((g) => g !== void 0);
316
+ if (idx < 0) return match;
317
+ const replacement = COMBINED_REPLACEMENTS[idx];
318
+ return replacement !== void 0 ? replacement : match;
319
+ }
320
+ );
321
+ out = out.replace(HIGH_ENTROPY_REGEX, (_match, group1, _group2) => {
322
+ return `${group1}=[REDACTED:high_entropy_env]`;
323
+ });
324
+ return out;
325
+ }
326
+ /**
327
+ * Recursively scrub every string value in an object/array graph. Secrets can
328
+ * appear under any key — a URL query param, an `authorization` header, an
329
+ * arbitrarily-named nested field — so we don't gate recursion on key names.
330
+ * The per-string `scrub()` fast-path (anchor pre-scan) keeps this cheap: any
331
+ * value without a credential anchor returns immediately without regex work.
332
+ */
333
+ scrubObject(obj) {
334
+ const seen = /* @__PURE__ */ new WeakSet();
335
+ const visit = (v) => {
336
+ if (typeof v === "string") return this.scrub(v);
337
+ if (v === null || typeof v !== "object") return v;
338
+ if (seen.has(v)) return v;
339
+ seen.add(v);
340
+ if (Array.isArray(v)) return v.map(visit);
341
+ const out = {};
342
+ for (const [k, val] of Object.entries(v)) {
343
+ out[k] = visit(val);
344
+ }
345
+ return out;
346
+ };
347
+ return visit(obj);
348
+ }
349
+ };
350
+
351
+ // src/hq/redaction.ts
352
+ var RAW_CONTENT_REPLACEMENT = "[REDACTED:hq_raw_content]";
353
+ var SENSITIVE_FIELD_REPLACEMENT = "[REDACTED:hq_sensitive_field]";
354
+ var REDACTED_PATH_REPLACEMENT = "[REDACTED:hq_path]";
355
+ var RAW_CONTENT_KEYS = /* @__PURE__ */ new Set([
356
+ "content",
357
+ "contents",
358
+ "raw",
359
+ "rawContent",
360
+ "body",
361
+ "message",
362
+ "messages",
363
+ "prompt",
364
+ "prompts",
365
+ "completion",
366
+ "response",
367
+ "assistantMessage",
368
+ "userMessage",
369
+ "stdout",
370
+ "stderr",
371
+ "output",
372
+ "toolInput",
373
+ "toolOutput",
374
+ "body",
375
+ "mailboxBody",
376
+ "messageBody",
377
+ "outcome",
378
+ "fileContent",
379
+ "diff",
380
+ "patch",
381
+ "transcript",
382
+ "logs"
383
+ ]);
384
+ var SENSITIVE_KEYS = /* @__PURE__ */ new Set([
385
+ "authorization",
386
+ "cookie",
387
+ "password",
388
+ "passwd",
389
+ "pwd",
390
+ "secret",
391
+ "token",
392
+ "accessToken",
393
+ "refreshToken",
394
+ "apiKey",
395
+ "apikey",
396
+ "clientSecret",
397
+ "privateKey"
398
+ ]);
399
+ var PATH_KEYS = /* @__PURE__ */ new Set([
400
+ "path",
401
+ "paths",
402
+ "file",
403
+ "files",
404
+ "filepath",
405
+ "filePath",
406
+ "filename",
407
+ "projectRoot",
408
+ "projectRootDisplay",
409
+ "cwd",
410
+ "workingDir",
411
+ "workingDirectory"
412
+ ]);
413
+ var defaultScrubber = new DefaultSecretScrubber();
414
+ function resolvePolicy(policy) {
415
+ return {
416
+ ...DEFAULT_HQ_REDACTION_POLICY,
417
+ ...policy
418
+ };
419
+ }
420
+ function isPlainObject(value) {
421
+ return Object.prototype.toString.call(value) === "[object Object]";
422
+ }
423
+ function isSensitiveKey(key) {
424
+ const lower = key.toLowerCase();
425
+ return SENSITIVE_KEYS.has(key) || lower.includes("authorization") || lower.includes("password") || lower.includes("secret") || lower.endsWith("token") || lower.endsWith("apikey") || lower.endsWith("api_key");
426
+ }
427
+ function isRawContentKey(key) {
428
+ return RAW_CONTENT_KEYS.has(key) || RAW_CONTENT_KEYS.has(key.toLowerCase());
429
+ }
430
+ function isPathKey(key) {
431
+ return PATH_KEYS.has(key) || PATH_KEYS.has(key.toLowerCase());
432
+ }
433
+ function normalizePathForCompare(value) {
434
+ return value.replace(/\\/g, "/").replace(/\/+$/g, "");
435
+ }
436
+ function redactPath(value, projectRoot, policy) {
437
+ if (policy.paths === "full") return defaultScrubber.scrub(value);
438
+ if (policy.paths === "none" || policy.paths === "redacted") return REDACTED_PATH_REPLACEMENT;
439
+ const normalizedValue = normalizePathForCompare(value);
440
+ if (!projectRoot) return normalizedValue.split("/").at(-1) ?? REDACTED_PATH_REPLACEMENT;
441
+ const normalizedRoot = normalizePathForCompare(projectRoot);
442
+ if (normalizedValue === normalizedRoot) return ".";
443
+ if (normalizedValue.startsWith(`${normalizedRoot}/`)) {
444
+ return normalizedValue.slice(normalizedRoot.length + 1) || ".";
445
+ }
446
+ return normalizedValue.split("/").at(-1) ?? REDACTED_PATH_REPLACEMENT;
447
+ }
448
+ function summarizeString(value, maxSummaryLength) {
449
+ const scrubbed = defaultScrubber.scrub(value);
450
+ if (scrubbed.length <= maxSummaryLength) return scrubbed;
451
+ return `${scrubbed.slice(0, maxSummaryLength)}\u2026[truncated:${scrubbed.length - maxSummaryLength}]`;
452
+ }
453
+ function scrubAndTruncateHqPreview(value, maxLength = 280) {
454
+ if (typeof value !== "string" || value.length === 0) return void 0;
455
+ return summarizeString(value, maxLength);
456
+ }
457
+ function visitValue(value, options, key) {
458
+ if (key !== void 0 && isSensitiveKey(key)) {
459
+ return { value: SENSITIVE_FIELD_REPLACEMENT, redacted: true };
460
+ }
461
+ if (typeof value === "string") {
462
+ if (key !== void 0 && isPathKey(key)) {
463
+ const redactedPath = redactPath(value, options.projectRoot, options.policy);
464
+ return { value: redactedPath, redacted: redactedPath !== value };
465
+ }
466
+ if (!options.policy.rawContent && key !== void 0 && isRawContentKey(key)) {
467
+ return { value: RAW_CONTENT_REPLACEMENT, redacted: true };
468
+ }
469
+ const scrubbed = summarizeString(value, options.maxSummaryLength);
470
+ return { value: scrubbed, redacted: scrubbed !== value };
471
+ }
472
+ if (value === null || typeof value !== "object") return { value, redacted: false };
473
+ if (options.seen.has(value)) return { value: "[REDACTED:hq_circular]", redacted: true };
474
+ options.seen.add(value);
475
+ if (Array.isArray(value)) {
476
+ let redacted2 = false;
477
+ const out2 = value.map((item) => {
478
+ const next = visitValue(item, options, key);
479
+ redacted2 ||= next.redacted;
480
+ return next.value;
481
+ });
482
+ return { value: out2, redacted: redacted2 };
483
+ }
484
+ if (!isPlainObject(value)) return { value: String(value), redacted: true };
485
+ let redacted = false;
486
+ const out = {};
487
+ for (const [childKey, childValue] of Object.entries(value)) {
488
+ const next = visitValue(childValue, options, childKey);
489
+ redacted ||= next.redacted;
490
+ out[childKey] = next.value;
491
+ }
492
+ return { value: out, redacted };
493
+ }
494
+ function redactHqValue(value, options = {}) {
495
+ const result = visitValue(value, {
496
+ policy: resolvePolicy(options.policy),
497
+ ...options.projectRoot !== void 0 ? { projectRoot: options.projectRoot } : {},
498
+ maxSummaryLength: options.maxSummaryLength ?? 500,
499
+ seen: /* @__PURE__ */ new WeakSet()
500
+ });
501
+ return { value: result.value, redacted: result.redacted };
502
+ }
503
+ function redactHqEvent(event, options = {}) {
504
+ const payload = redactHqValue(event.payload, options);
505
+ const nextEvent = {
506
+ ...event,
507
+ payload: payload.value
508
+ };
509
+ return { value: nextEvent, redacted: payload.redacted };
510
+ }
511
+ function summarizeHqToolArgs(value, options = {}) {
512
+ const policy = resolvePolicy(options.policy);
513
+ if (policy.toolArgs === "none") return "[REDACTED:hq_tool_args]";
514
+ if (policy.toolArgs === "redacted") return redactHqValue(value, options).value;
515
+ if (value === null || typeof value !== "object") return redactHqValue(value, options).value;
516
+ if (Array.isArray(value)) return `[array:${value.length}]`;
517
+ const entries = Object.entries(value);
518
+ const summary = {};
519
+ for (const [key, item] of entries.slice(0, 20)) {
520
+ if (isSensitiveKey(key)) {
521
+ summary[key] = SENSITIVE_FIELD_REPLACEMENT;
522
+ } else if (typeof item === "string") {
523
+ summary[key] = isPathKey(key) ? redactPath(item, options.projectRoot, policy) : summarizeString(item, 120);
524
+ } else if (Array.isArray(item)) {
525
+ summary[key] = `[array:${item.length}]`;
526
+ } else if (item !== null && typeof item === "object") {
527
+ summary[key] = "[object]";
528
+ } else {
529
+ summary[key] = item;
530
+ }
531
+ }
532
+ if (entries.length > 20) summary.__truncatedKeys = entries.length - 20;
533
+ return summary;
534
+ }
535
+
536
+ // src/hq/mailbox-mapper.ts
537
+ function previewText(value, maxLength, policy) {
538
+ if (value === void 0 || value.length === 0) return void 0;
539
+ const redacted = redactHqValue(value, { maxSummaryLength: maxLength, policy: { rawContent: true, ...policy } }).value;
540
+ if (redacted.length <= maxLength) return redacted;
541
+ return `${redacted.slice(0, maxLength)}\u2026`;
542
+ }
543
+ function readCount(message) {
544
+ return Object.keys(message.readBy).length;
545
+ }
546
+ function taskSummary(message) {
547
+ if (message.taskContext === void 0) return void 0;
548
+ const task = {
549
+ ...message.taskContext.taskId !== void 0 ? { taskId: message.taskContext.taskId } : {},
550
+ ...message.taskContext.agentRole !== void 0 ? { agentRole: message.taskContext.agentRole } : {},
551
+ ...message.taskContext.agentName !== void 0 ? { agentName: message.taskContext.agentName } : {},
552
+ ...message.taskContext.status !== void 0 ? { status: message.taskContext.status } : {}
553
+ };
554
+ return Object.keys(task).length > 0 ? task : void 0;
555
+ }
556
+ function mapMailboxMessageToHqSummary(message, options = {}) {
557
+ const previewLength = options.previewLength ?? 160;
558
+ const bodyPreview = previewText(message.body, previewLength, options.redactionPolicy);
559
+ const outcomePreview = previewText(message.outcome, previewLength, options.redactionPolicy);
560
+ const task = taskSummary(message);
561
+ return {
562
+ messageId: message.id,
563
+ from: message.from,
564
+ to: message.to,
565
+ type: message.type,
566
+ subject: previewText(message.subject, previewLength, options.redactionPolicy) ?? "",
567
+ priority: message.priority,
568
+ timestamp: message.timestamp,
569
+ ...message.replyTo !== void 0 ? { replyTo: message.replyTo } : {},
570
+ ...message.senderSessionId !== void 0 ? { senderSessionId: message.senderSessionId } : {},
571
+ completed: message.completed,
572
+ ...message.completedBy !== void 0 ? { completedBy: message.completedBy } : {},
573
+ ...message.completedAt !== void 0 ? { completedAt: message.completedAt } : {},
574
+ readCount: readCount(message),
575
+ hasBody: message.body.length > 0,
576
+ ...bodyPreview !== void 0 ? { bodyPreview } : {},
577
+ ...outcomePreview !== void 0 ? { outcomePreview } : {},
578
+ ...task !== void 0 ? { task } : {}
579
+ };
580
+ }
581
+ function mapMailboxAgentToHqSummary(agent) {
582
+ return {
583
+ agentId: agent.agentId,
584
+ name: agent.name,
585
+ ...agent.role !== void 0 ? { role: agent.role } : {},
586
+ sessionId: agent.sessionId,
587
+ status: agent.status,
588
+ ...agent.currentTool !== void 0 ? { currentTool: agent.currentTool } : {},
589
+ ...agent.currentTask !== void 0 ? { currentTask: agent.currentTask } : {},
590
+ iterations: agent.iterations,
591
+ toolCalls: agent.toolCalls,
592
+ lastActivityAt: agent.lastActivityAt,
593
+ lastSeenAt: agent.lastSeenAt,
594
+ online: agent.online,
595
+ ...agent.source !== void 0 ? { source: agent.source } : {}
596
+ };
597
+ }
598
+ function createMailboxSnapshotPayload(messages, agents, options) {
599
+ const includeCompleted = options.includeCompleted ?? true;
600
+ const filteredMessages = includeCompleted ? messages : messages.filter((message) => !message.completed);
601
+ const limit = options.limit ?? 50;
602
+ const sortedMessages = [...filteredMessages].sort((a, b) => b.timestamp.localeCompare(a.timestamp)).slice(0, limit);
603
+ const summaries = sortedMessages.map((message) => mapMailboxMessageToHqSummary(message, options));
604
+ const agentSummaries = agents.map(mapMailboxAgentToHqSummary);
605
+ return {
606
+ mailboxId: options.mailboxId,
607
+ scope: options.scope ?? "project",
608
+ messages: summaries,
609
+ agents: agentSummaries,
610
+ totals: {
611
+ messages: messages.length,
612
+ unread: messages.filter((message) => !message.completed && readCount(message) === 0).length,
613
+ incomplete: messages.filter((message) => !message.completed).length,
614
+ highPriority: messages.filter((message) => message.priority === "high").length,
615
+ onlineAgents: agents.filter((agent) => agent.online).length
616
+ }
617
+ };
618
+ }
619
+ async function createMailboxSnapshotPayloadFromMailbox(mailbox, options) {
620
+ const limit = options.limit ?? 50;
621
+ const [messages, agents] = await Promise.all([
622
+ mailbox.query({ limit }),
623
+ mailbox.getAgentStatuses()
624
+ ]);
625
+ return createMailboxSnapshotPayload(messages, agents, options);
626
+ }
627
+ function createMailboxEventPayload(input) {
628
+ const summaryOptions = {
629
+ ...input.previewLength !== void 0 ? { previewLength: input.previewLength } : {},
630
+ ...input.redactionPolicy !== void 0 ? { redactionPolicy: input.redactionPolicy } : {}
631
+ };
632
+ return {
633
+ mailboxId: input.mailboxId,
634
+ action: input.action,
635
+ ...input.message !== void 0 ? { message: mapMailboxMessageToHqSummary(input.message, summaryOptions) } : {},
636
+ ...input.agent !== void 0 ? { agent: mapMailboxAgentToHqSummary(input.agent) } : {},
637
+ ...input.summary !== void 0 ? { summary: previewText(input.summary, input.previewLength ?? 160, input.redactionPolicy) ?? "" } : {}
638
+ };
639
+ }
640
+ var OPEN_STATE = 1;
641
+ var DEFAULT_RECONNECT_BASE_MS = 1e3;
642
+ var DEFAULT_RECONNECT_MAX_MS = 3e4;
643
+ var DEFAULT_MAX_QUEUED_MESSAGES = 250;
644
+ var DEFAULT_COMMAND_POLL_INTERVAL_MS = 1e4;
645
+ var DEFAULT_COMMAND_POLL_LIMIT = 25;
646
+ function defaultSocketFactory(url) {
647
+ const WebSocketCtor = globalThis.WebSocket;
648
+ if (WebSocketCtor === void 0) {
649
+ throw new Error("No global WebSocket implementation is available; provide HqPublisherOptions.socketFactory.");
650
+ }
651
+ return new WebSocketCtor(url);
652
+ }
653
+ function toClientUrl(baseUrl, token) {
654
+ const url = new URL(baseUrl);
655
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
656
+ if (url.pathname === "/" || url.pathname === "") url.pathname = "/ws/client";
657
+ if (token !== void 0 && token.length > 0) url.searchParams.set("token", token);
658
+ return url.toString();
659
+ }
660
+ function addSocketListener(socket, type, listener) {
661
+ if (socket.addEventListener !== void 0) {
662
+ socket.addEventListener(type, listener);
663
+ return;
664
+ }
665
+ socket.on?.(type, listener);
666
+ }
667
+ function removeSocketListener(socket, type, listener) {
668
+ if (socket.removeEventListener !== void 0) {
669
+ socket.removeEventListener(type, listener);
670
+ return;
671
+ }
672
+ socket.off?.(type, listener);
673
+ }
674
+ var HqPublisher = class {
675
+ constructor(options) {
676
+ this.options = options;
677
+ this.socketFactory = options.socketFactory ?? defaultSocketFactory;
678
+ this.now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
679
+ this.idFactory = options.idFactory ?? randomUUID;
680
+ this.capabilities = options.capabilities ?? ["telemetry.publish", "mailbox.summary"];
681
+ this.reconnect = options.reconnect ?? true;
682
+ this.reconnectBaseMs = options.reconnectBaseMs ?? DEFAULT_RECONNECT_BASE_MS;
683
+ this.reconnectMaxMs = options.reconnectMaxMs ?? DEFAULT_RECONNECT_MAX_MS;
684
+ this.maxQueuedMessages = options.maxQueuedMessages ?? DEFAULT_MAX_QUEUED_MESSAGES;
685
+ }
686
+ options;
687
+ socketFactory;
688
+ now;
689
+ idFactory;
690
+ capabilities;
691
+ reconnect;
692
+ reconnectBaseMs;
693
+ reconnectMaxMs;
694
+ maxQueuedMessages;
695
+ socket = null;
696
+ seq = 0;
697
+ queue = [];
698
+ stopped = false;
699
+ reconnectAttempt = 0;
700
+ reconnectTimer = null;
701
+ commandPollTimer = null;
702
+ lastCommandId;
703
+ connect() {
704
+ if (this.socket !== null || this.stopped) return;
705
+ const socket = this.socketFactory(toClientUrl(this.options.url, this.options.token), {
706
+ ...this.options.token !== void 0 ? { token: this.options.token } : {}
707
+ });
708
+ this.socket = socket;
709
+ const onOpen = () => {
710
+ this.reconnectAttempt = 0;
711
+ this.sendHello();
712
+ this.flushQueue();
713
+ if (this.options.onCommand !== void 0) {
714
+ this.startCommandPolling();
715
+ this.pollCommands();
716
+ }
717
+ };
718
+ const onMessage = (event) => {
719
+ void this.handleServerMessage(event);
720
+ };
721
+ const onCloseOrError = () => {
722
+ removeSocketListener(socket, "open", onOpen);
723
+ removeSocketListener(socket, "message", onMessage);
724
+ removeSocketListener(socket, "close", onCloseOrError);
725
+ removeSocketListener(socket, "error", onCloseOrError);
726
+ this.stopCommandPolling();
727
+ if (this.socket === socket) this.socket = null;
728
+ this.scheduleReconnect();
729
+ };
730
+ addSocketListener(socket, "open", onOpen);
731
+ addSocketListener(socket, "message", onMessage);
732
+ addSocketListener(socket, "close", onCloseOrError);
733
+ addSocketListener(socket, "error", onCloseOrError);
734
+ if (socket.readyState === OPEN_STATE) onOpen();
735
+ }
736
+ close() {
737
+ this.stopped = true;
738
+ if (this.reconnectTimer !== null) {
739
+ clearTimeout(this.reconnectTimer);
740
+ this.reconnectTimer = null;
741
+ }
742
+ this.stopCommandPolling();
743
+ this.queue = [];
744
+ const socket = this.socket;
745
+ this.socket = null;
746
+ socket?.close(1e3, "hq publisher closed");
747
+ }
748
+ publishEvent(options) {
749
+ const event = createHqEventEnvelope({
750
+ id: this.idFactory(),
751
+ type: options.type,
752
+ timestamp: options.timestamp ?? this.now(),
753
+ clientId: this.options.client.clientId,
754
+ projectId: this.options.project.projectId,
755
+ seq: ++this.seq,
756
+ payload: options.payload,
757
+ ...options.sessionId !== void 0 ? { sessionId: options.sessionId } : {},
758
+ ...options.runId !== void 0 ? { runId: options.runId } : {}
759
+ });
760
+ this.sendFrame({ type: "client.event", event });
761
+ return event;
762
+ }
763
+ async publishMailboxSnapshot(mailbox, options) {
764
+ const payload = await createMailboxSnapshotPayloadFromMailbox(mailbox, {
765
+ ...options,
766
+ ...this.options.redactionPolicy !== void 0 ? { redactionPolicy: this.options.redactionPolicy } : {}
767
+ });
768
+ return this.publishEvent({
769
+ type: "mailbox.snapshot",
770
+ payload,
771
+ ...options.sessionId !== void 0 ? { sessionId: options.sessionId } : {},
772
+ ...options.timestamp !== void 0 ? { timestamp: options.timestamp } : {}
773
+ });
774
+ }
775
+ publishMailboxEvent(input) {
776
+ const payload = createMailboxEventPayload({
777
+ mailboxId: input.mailboxId,
778
+ action: input.action,
779
+ ...input.message !== void 0 ? { message: input.message } : {},
780
+ ...input.agent !== void 0 ? { agent: input.agent } : {},
781
+ ...input.summary !== void 0 ? { summary: input.summary } : {},
782
+ ...input.previewLength !== void 0 ? { previewLength: input.previewLength } : {},
783
+ ...this.options.redactionPolicy !== void 0 ? { redactionPolicy: this.options.redactionPolicy } : {}
784
+ });
785
+ return this.publishEvent({
786
+ type: "mailbox.event",
787
+ payload,
788
+ ...input.sessionId !== void 0 ? { sessionId: input.sessionId } : {},
789
+ ...input.timestamp !== void 0 ? { timestamp: input.timestamp } : {}
790
+ });
791
+ }
792
+ pollCommands() {
793
+ this.sendFrame({
794
+ type: "client.command_poll",
795
+ clientId: this.options.client.clientId,
796
+ projectId: this.options.project.projectId,
797
+ ...this.lastCommandId !== void 0 ? { afterCommandId: this.lastCommandId } : {},
798
+ limit: this.options.commandPollLimit ?? DEFAULT_COMMAND_POLL_LIMIT
799
+ });
800
+ }
801
+ ackCommand(result) {
802
+ this.sendFrame({
803
+ type: "client.command_ack",
804
+ clientId: this.options.client.clientId,
805
+ projectId: this.options.project.projectId,
806
+ commandId: result.commandId,
807
+ status: result.status,
808
+ ...result.message !== void 0 ? { message: result.message } : {}
809
+ });
810
+ }
811
+ sendHello() {
812
+ this.sendFrame({
813
+ type: "client.hello",
814
+ payload: {
815
+ protocolVersion: HQ_PROTOCOL_VERSION,
816
+ client: this.options.client,
817
+ project: this.options.project,
818
+ capabilities: this.capabilities
819
+ }
820
+ });
821
+ }
822
+ sendFrame(frame) {
823
+ const serialized = JSON.stringify(frame);
824
+ const socket = this.socket;
825
+ if (socket?.readyState === OPEN_STATE) {
826
+ socket.send(serialized);
827
+ return;
828
+ }
829
+ this.enqueue(serialized);
830
+ this.connect();
831
+ }
832
+ enqueue(serialized) {
833
+ if (this.queue.length >= this.maxQueuedMessages) this.queue.shift();
834
+ this.queue.push(serialized);
835
+ }
836
+ flushQueue() {
837
+ const socket = this.socket;
838
+ if (socket?.readyState !== OPEN_STATE) return;
839
+ const pending = this.queue;
840
+ this.queue = [];
841
+ for (const frame of pending) socket.send(frame);
842
+ }
843
+ startCommandPolling() {
844
+ if (this.options.onCommand === void 0 || this.commandPollTimer !== null) return;
845
+ this.commandPollTimer = setInterval(() => this.pollCommands(), this.options.commandPollIntervalMs ?? DEFAULT_COMMAND_POLL_INTERVAL_MS);
846
+ }
847
+ stopCommandPolling() {
848
+ if (this.commandPollTimer === null) return;
849
+ clearInterval(this.commandPollTimer);
850
+ this.commandPollTimer = null;
851
+ }
852
+ async handleServerMessage(event) {
853
+ const message = this.parseServerMessage(event);
854
+ if (message?.type !== "hq.command_batch") return;
855
+ await this.handleCommandBatch(message);
856
+ }
857
+ parseServerMessage(event) {
858
+ const data = this.extractMessageData(event);
859
+ if (data === null) return null;
860
+ try {
861
+ const parsed = JSON.parse(data);
862
+ if (parsed.type !== "hq.command_batch" || !Array.isArray(parsed.commands)) return null;
863
+ return parsed;
864
+ } catch {
865
+ return null;
866
+ }
867
+ }
868
+ extractMessageData(event) {
869
+ const value = typeof event === "object" && event !== null && "data" in event ? event.data : event;
870
+ if (typeof value === "string") return value;
871
+ if (value instanceof ArrayBuffer) return new TextDecoder().decode(value);
872
+ if (ArrayBuffer.isView(value)) {
873
+ const bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
874
+ return new TextDecoder().decode(bytes);
875
+ }
876
+ return null;
877
+ }
878
+ async handleCommandBatch(message) {
879
+ const handler = this.options.onCommand;
880
+ if (handler === void 0) return;
881
+ for (const command of message.commands) {
882
+ this.lastCommandId = command.commandId;
883
+ try {
884
+ const result = await handler(command);
885
+ if (result !== void 0) this.ackCommand(result);
886
+ else if (command.requiresAck) this.ackCommand({ commandId: command.commandId, status: "accepted" });
887
+ } catch (err) {
888
+ this.ackCommand({
889
+ commandId: command.commandId,
890
+ status: "failed",
891
+ message: err instanceof Error ? err.message : String(err)
892
+ });
893
+ }
894
+ }
895
+ }
896
+ scheduleReconnect() {
897
+ if (this.stopped || !this.reconnect || this.reconnectTimer !== null) return;
898
+ const delay = Math.min(this.reconnectMaxMs, this.reconnectBaseMs * 2 ** this.reconnectAttempt);
899
+ this.reconnectAttempt += 1;
900
+ this.reconnectTimer = setTimeout(() => {
901
+ this.reconnectTimer = null;
902
+ this.connect();
903
+ }, delay);
904
+ }
905
+ };
906
+ async function atomicWrite(targetPath, content, opts = {}) {
907
+ const dir = path3.dirname(targetPath);
908
+ await fsp.mkdir(dir, { recursive: true });
909
+ const tmp = path3.join(dir, `.${path3.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
910
+ try {
911
+ if (typeof content === "string") {
912
+ await fsp.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
913
+ } else {
914
+ await fsp.writeFile(tmp, content, { flag: "wx" });
915
+ }
916
+ try {
917
+ const fh = await fsp.open(tmp, "r+");
918
+ try {
919
+ await fh.sync();
920
+ } finally {
921
+ await fh.close();
922
+ }
923
+ } catch {
924
+ }
925
+ let mode;
926
+ try {
927
+ const stat3 = await fsp.stat(targetPath);
928
+ mode = stat3.mode & 511;
929
+ } catch {
930
+ mode = opts.mode;
931
+ }
932
+ if (mode !== void 0) {
933
+ await fsp.chmod(tmp, mode);
934
+ }
935
+ await renameWithRetry(tmp, targetPath);
936
+ } catch (err) {
937
+ try {
938
+ await fsp.unlink(tmp);
939
+ } catch {
940
+ }
941
+ throw err;
942
+ }
943
+ }
944
+ async function withFileLock(targetPath, fn, opts = {}) {
945
+ const dir = path3.dirname(targetPath);
946
+ await fsp.mkdir(dir, { recursive: true });
947
+ const lockPath = path3.join(dir, `.${path3.basename(targetPath)}.lock`);
948
+ const timeoutMs = opts.timeoutMs ?? 5e3;
949
+ const staleMs = opts.staleMs ?? 3e4;
950
+ const started = Date.now();
951
+ let handle;
952
+ for (; ; ) {
953
+ try {
954
+ handle = await fsp.open(lockPath, "wx");
955
+ await handle.writeFile(`${process.pid}:${Date.now()}`);
956
+ break;
957
+ } catch (err) {
958
+ const code = err.code;
959
+ if (code === "ENOENT") {
960
+ await fsp.mkdir(dir, { recursive: true });
961
+ continue;
962
+ }
963
+ if (code !== "EEXIST") throw err;
964
+ try {
965
+ const stat3 = await fsp.stat(lockPath);
966
+ if (Date.now() - stat3.mtimeMs > staleMs) {
967
+ await fsp.unlink(lockPath);
968
+ continue;
969
+ }
970
+ } catch {
971
+ continue;
972
+ }
973
+ if (Date.now() - started >= timeoutMs) {
974
+ throw new Error(`Timed out waiting for file lock: ${targetPath}`);
975
+ }
976
+ await new Promise((resolve3) => setTimeout(resolve3, 25));
977
+ }
978
+ }
979
+ try {
980
+ return await fn();
981
+ } finally {
982
+ try {
983
+ await handle?.close();
984
+ } catch {
985
+ }
986
+ try {
987
+ await fsp.unlink(lockPath);
988
+ } catch {
989
+ }
990
+ }
991
+ }
992
+ var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
993
+ async function renameWithRetry(from, to) {
994
+ if (process.platform !== "win32") {
995
+ await fsp.rename(from, to);
996
+ return;
997
+ }
998
+ const delays = [10, 25, 60, 120, 250];
999
+ let lastErr;
1000
+ for (let i = 0; i <= delays.length; i++) {
1001
+ try {
1002
+ await fsp.rename(from, to);
1003
+ return;
1004
+ } catch (err) {
1005
+ lastErr = err;
1006
+ const code = err?.code;
1007
+ if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
1008
+ throw err;
1009
+ }
1010
+ await new Promise((resolve3) => setTimeout(resolve3, delays[i]));
1011
+ }
1012
+ }
1013
+ throw lastErr;
1014
+ }
1015
+ function wstackGlobalRoot() {
1016
+ const fromEnv = process.env["WRONGSTACK_HOME"];
1017
+ if (fromEnv && fromEnv.trim().length > 0) return path3.resolve(fromEnv);
1018
+ return path3.join(os.homedir(), ".wrongstack");
1019
+ }
1020
+
1021
+ // src/coordination/mailbox-types.ts
1022
+ function normalizeRecipient(to) {
1023
+ return to.trim().toLowerCase() === "all" ? "*" : to.trim();
1024
+ }
1025
+
1026
+ // src/coordination/global-mailbox.ts
1027
+ var MAILBOX_FILE = "_mailbox.jsonl";
1028
+ var CLIENT_REGISTRY_FILE = "_mailbox.clients.json";
1029
+ var AGENT_STALE_MS = 6e4;
1030
+ var CLIENT_STALE_MS = 6e4;
1031
+ var HEARTBEAT_THROTTLE_MS = 5e3;
1032
+ var REGISTRY_CACHE_TTL_MS = 2e3;
1033
+ var LINE_SEPARATOR = "\n";
1034
+ var MESSAGE_CACHE_MAX_ENTRIES = 1e4;
1035
+ var GlobalMailbox = class {
1036
+ /** Path to the JSONL message file. */
1037
+ messagePath;
1038
+ /** Path to the JSON agent registry file. */
1039
+ registryPath;
1040
+ /** Path to the JSON client registry file. */
1041
+ clientRegistryPath;
1042
+ /** Optional event bus for emitting agent registration/heartbeat events. */
1043
+ _events;
1044
+ /** Optional HQ publisher for cross-project command-center telemetry. */
1045
+ _hqPublisher;
1046
+ /**
1047
+ * Local cache of the agent registry to avoid re-reading on every call.
1048
+ * Time-bounded: the registry file is shared ACROSS PROCESSES (that's the
1049
+ * whole point of GlobalMailbox), so a cache served forever would never see
1050
+ * agents registered by other sessions. Writers always bypass it.
1051
+ */
1052
+ _registryCache = null;
1053
+ /** When the registry cache was last refreshed from disk (epoch ms). */
1054
+ _registryCacheAt = 0;
1055
+ /**
1056
+ * Local cache of the client registry to avoid re-reading on every call.
1057
+ * Same reasoning as agent registry cache.
1058
+ */
1059
+ _clientRegistryCache = null;
1060
+ /** When the client registry cache was last refreshed from disk (epoch ms). */
1061
+ _clientRegistryCacheAt = 0;
1062
+ /** Last time each local agent sent a heartbeat (throttle). */
1063
+ _lastHeartbeat = /* @__PURE__ */ new Map();
1064
+ /** Last time each local client sent a heartbeat (throttle). */
1065
+ _lastClientHeartbeat = /* @__PURE__ */ new Map();
1066
+ /**
1067
+ * In-memory mirror of the JSONL message file. The mailbox is shared
1068
+ * ACROSS PROCESSES, so reads cannot trust the cache blindly — we pair it
1069
+ * with an mtime check. The file lock serializes every write, so a
1070
+ * changed mtimeMs is a definitive signal that another process (or this
1071
+ * one) wrote; an unchanged mtimeMs guarantees no write happened and the
1072
+ * cache is current. This collapses the per-iteration `query()` cost from
1073
+ * O(file_size) disk + parse to O(messages) in memory.
1074
+ */
1075
+ _messageCache = null;
1076
+ /** mtimeMs of the file when `_messageCache` was populated. */
1077
+ _messageCacheMtime = -1;
1078
+ /** Size of the file when `_messageCache` was populated (extra guard). */
1079
+ _messageCacheSize = -1;
1080
+ /**
1081
+ * @param projectDir — `~/.wrongstack/projects/<slug>/`
1082
+ * @param events — optional EventBus for real-time TUI/WebUI notifications
1083
+ * @param hqPublisher — optional HQ publisher for cross-project telemetry
1084
+ */
1085
+ constructor(projectDir, events, hqPublisher) {
1086
+ this.messagePath = path3.join(projectDir, MAILBOX_FILE);
1087
+ this.registryPath = path3.join(projectDir, "_mailbox.registry.json");
1088
+ this.clientRegistryPath = path3.join(projectDir, CLIENT_REGISTRY_FILE);
1089
+ this._events = events;
1090
+ this._hqPublisher = hqPublisher;
1091
+ }
1092
+ get hqMailboxId() {
1093
+ return `${path3.basename(path3.dirname(this.messagePath))}:mailbox`;
1094
+ }
1095
+ publishHqMailboxEvent(input) {
1096
+ try {
1097
+ this._hqPublisher?.publishMailboxEvent(input);
1098
+ } catch {
1099
+ }
1100
+ }
1101
+ publishHqMailboxSnapshot() {
1102
+ if (this._hqPublisher === void 0) return;
1103
+ void this._hqPublisher.publishMailboxSnapshot(this, { mailboxId: this.hqMailboxId }).catch(() => {
1104
+ });
1105
+ }
1106
+ // ── Messages ────────────────────────────────────────────────────────────
1107
+ async send(input) {
1108
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1109
+ const msg = {
1110
+ id: randomUUID(),
1111
+ from: input.from,
1112
+ // "all" is an accepted spelling of the broadcast address — canonical
1113
+ // form on disk is '*' so every query/checker matches it.
1114
+ to: normalizeRecipient(input.to),
1115
+ type: input.type,
1116
+ subject: input.subject,
1117
+ body: input.body,
1118
+ priority: input.priority ?? "normal",
1119
+ readBy: {},
1120
+ completed: false,
1121
+ timestamp: now,
1122
+ replyTo: input.replyTo,
1123
+ taskContext: input.taskContext
1124
+ };
1125
+ const line = JSON.stringify(msg) + LINE_SEPARATOR;
1126
+ await fsp.mkdir(path3.dirname(this.messagePath), { recursive: true });
1127
+ await withFileLock(this.messagePath, async () => {
1128
+ await fsp.appendFile(this.messagePath, line, "utf8");
1129
+ this._pushToCache(msg);
1130
+ });
1131
+ this.publishHqMailboxEvent({ mailboxId: this.hqMailboxId, action: "message.sent", message: msg });
1132
+ this.publishHqMailboxSnapshot();
1133
+ return msg;
1134
+ }
1135
+ async query(q) {
1136
+ const all = await this._readMessagesCached();
1137
+ const limit = q.limit ?? 50;
1138
+ const order = q.minPriority !== void 0 ? { low: 0, normal: 1, high: 2 } : null;
1139
+ const minPriorityRank = order && q.minPriority !== void 0 ? order[q.minPriority] : 0;
1140
+ const out = [];
1141
+ for (let i = 0; i < all.length; i++) {
1142
+ const m = all[i];
1143
+ if (q.to !== void 0 && m.to !== q.to && m.to !== "*") continue;
1144
+ if (q.from !== void 0 && m.from !== q.from) continue;
1145
+ if (q.unreadBy !== void 0 && q.unreadBy in m.readBy) continue;
1146
+ if (q.incompleteOnly && m.completed) continue;
1147
+ if (q.type !== void 0 && m.type !== q.type) continue;
1148
+ if (order !== null && (order[m.priority] ?? 1) < minPriorityRank) {
1149
+ continue;
1150
+ }
1151
+ if (q.since !== void 0 && m.timestamp <= q.since) continue;
1152
+ out.push(m);
1153
+ }
1154
+ out.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
1155
+ return out.slice(0, limit).map((m) => ({ ...m, readBy: { ...m.readBy } }));
1156
+ }
1157
+ async ack(input) {
1158
+ const updated = await this.ackMany({ acks: [input] });
1159
+ return updated.length > 0 ? updated[0] : null;
1160
+ }
1161
+ async ackMany(input) {
1162
+ if (input.acks.length === 0) return [];
1163
+ const updated = [];
1164
+ const byId = /* @__PURE__ */ new Map();
1165
+ for (const a of input.acks) {
1166
+ byId.set(a.messageId, a);
1167
+ }
1168
+ let cacheSnapshot = null;
1169
+ await withFileLock(this.messagePath, async () => {
1170
+ const all = await this._readMessagesFresh();
1171
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1172
+ let changed = false;
1173
+ for (const msg of all) {
1174
+ const a = byId.get(msg.id);
1175
+ if (!a) continue;
1176
+ updated.push(msg);
1177
+ if (a.read !== false && !(a.readerId in msg.readBy)) {
1178
+ msg.readBy[a.readerId] = now;
1179
+ changed = true;
1180
+ }
1181
+ if (a.completed && !msg.completed) {
1182
+ msg.completed = true;
1183
+ msg.completedBy = a.readerId;
1184
+ msg.completedAt = now;
1185
+ changed = true;
1186
+ }
1187
+ if (a.outcome !== void 0 && msg.outcome !== a.outcome) {
1188
+ msg.outcome = a.outcome;
1189
+ changed = true;
1190
+ }
1191
+ }
1192
+ if (changed) {
1193
+ const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
1194
+ await fsp.writeFile(this.messagePath, serialized, "utf8");
1195
+ }
1196
+ cacheSnapshot = all;
1197
+ });
1198
+ if (cacheSnapshot) this._setMessageCache(cacheSnapshot);
1199
+ for (const message of updated) {
1200
+ this.publishHqMailboxEvent({
1201
+ mailboxId: this.hqMailboxId,
1202
+ action: message.completed ? "message.completed" : "message.read",
1203
+ message
1204
+ });
1205
+ }
1206
+ if (updated.length > 0) this.publishHqMailboxSnapshot();
1207
+ return updated;
1208
+ }
1209
+ async unreadCount(forAgentId) {
1210
+ const all = await this._readMessagesCached();
1211
+ let count = 0;
1212
+ for (let i = 0; i < all.length; i++) {
1213
+ const m = all[i];
1214
+ if ((m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed) {
1215
+ count++;
1216
+ }
1217
+ }
1218
+ return count;
1219
+ }
1220
+ // ── Agent registry ──────────────────────────────────────────────────────
1221
+ async registerAgent(input) {
1222
+ await this._ensureRegistry();
1223
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1224
+ const agent = {
1225
+ agentId: input.agentId,
1226
+ sessionId: input.sessionId,
1227
+ name: input.name,
1228
+ role: input.role,
1229
+ status: "idle",
1230
+ currentTool: void 0,
1231
+ currentTask: void 0,
1232
+ iterations: 0,
1233
+ toolCalls: 0,
1234
+ registeredAt: now,
1235
+ lastSeenAt: now,
1236
+ pid: input.pid,
1237
+ source: input.source
1238
+ };
1239
+ await withFileLock(this.registryPath, async () => {
1240
+ const registry = await this._readRegistry({ fresh: true });
1241
+ this._pruneStaleInPlace(registry);
1242
+ registry.set(input.agentId, agent);
1243
+ this._registryCache = registry;
1244
+ this._registryCacheAt = Date.now();
1245
+ await this._writeRegistry(registry);
1246
+ });
1247
+ this._events?.emitCustom("mailbox.agent_registered", {
1248
+ agentId: input.agentId,
1249
+ sessionId: input.sessionId,
1250
+ name: input.name,
1251
+ role: input.role,
1252
+ source: input.source
1253
+ });
1254
+ this.publishHqMailboxEvent({
1255
+ mailboxId: this.hqMailboxId,
1256
+ action: "agent.registered",
1257
+ agent: {
1258
+ agentId: input.agentId,
1259
+ name: input.name,
1260
+ ...input.role !== void 0 ? { role: input.role } : {},
1261
+ sessionId: input.sessionId,
1262
+ status: "idle",
1263
+ iterations: 0,
1264
+ toolCalls: 0,
1265
+ lastActivityAt: now,
1266
+ lastSeenAt: now,
1267
+ online: true,
1268
+ pid: input.pid,
1269
+ ...input.source !== void 0 ? { source: input.source } : {}
1270
+ }
1271
+ });
1272
+ this.publishHqMailboxSnapshot();
1273
+ }
1274
+ async heartbeat(input) {
1275
+ const last = this._lastHeartbeat.get(input.agentId) ?? 0;
1276
+ const now = Date.now();
1277
+ if (now - last < HEARTBEAT_THROTTLE_MS) return;
1278
+ this._lastHeartbeat.set(input.agentId, now);
1279
+ await this._ensureRegistry();
1280
+ await withFileLock(this.registryPath, async () => {
1281
+ const registry = await this._readRegistry({ fresh: true });
1282
+ this._pruneStaleInPlace(registry);
1283
+ const agent = registry.get(input.agentId);
1284
+ if (agent) {
1285
+ const iso = (/* @__PURE__ */ new Date()).toISOString();
1286
+ agent.lastSeenAt = iso;
1287
+ if (input.status !== void 0) agent.status = input.status;
1288
+ if (input.currentTool !== void 0) agent.currentTool = input.currentTool;
1289
+ if (input.currentTask !== void 0) agent.currentTask = input.currentTask;
1290
+ if (input.iterations !== void 0) agent.iterations = input.iterations;
1291
+ if (input.toolCalls !== void 0) agent.toolCalls = input.toolCalls;
1292
+ }
1293
+ this._registryCache = registry;
1294
+ this._registryCacheAt = Date.now();
1295
+ await this._writeRegistry(registry);
1296
+ });
1297
+ this._events?.emitCustom("mailbox.agent_heartbeat", {
1298
+ agentId: input.agentId,
1299
+ status: input.status,
1300
+ currentTool: input.currentTool,
1301
+ currentTask: input.currentTask
1302
+ });
1303
+ this.publishHqMailboxEvent({
1304
+ mailboxId: this.hqMailboxId,
1305
+ action: "agent.heartbeat",
1306
+ summary: input.agentId
1307
+ });
1308
+ this.publishHqMailboxSnapshot();
1309
+ }
1310
+ async getAgentStatuses() {
1311
+ await this._ensureRegistry();
1312
+ const registry = await this._readRegistry();
1313
+ this._pruneStaleInPlace(registry);
1314
+ const now = Date.now();
1315
+ return Array.from(registry.values()).map((a) => ({
1316
+ agentId: a.agentId,
1317
+ name: a.name,
1318
+ role: a.role,
1319
+ sessionId: a.sessionId,
1320
+ status: a.status,
1321
+ currentTool: a.currentTool,
1322
+ currentTask: a.currentTask,
1323
+ iterations: a.iterations,
1324
+ toolCalls: a.toolCalls,
1325
+ lastActivityAt: a.lastSeenAt,
1326
+ lastSeenAt: a.lastSeenAt,
1327
+ online: now - new Date(a.lastSeenAt).getTime() < AGENT_STALE_MS,
1328
+ pid: a.pid,
1329
+ source: a.source
1330
+ })).sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt));
1331
+ }
1332
+ async getOnlineAgents() {
1333
+ const all = await this.getAgentStatuses();
1334
+ return all.filter((a) => a.online);
1335
+ }
1336
+ // ── Client registry ─────────────────────────────────────────────────────
1337
+ async registerClient(input) {
1338
+ await this._ensureClientRegistry();
1339
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1340
+ const client = {
1341
+ clientId: input.clientId,
1342
+ sessionId: input.sessionId,
1343
+ name: input.name,
1344
+ source: input.source,
1345
+ registeredAt: now,
1346
+ lastSeenAt: now,
1347
+ pid: input.pid
1348
+ };
1349
+ await withFileLock(this.clientRegistryPath, async () => {
1350
+ const registry = await this._readClientRegistry({ fresh: true });
1351
+ this._pruneStaleClientsInPlace(registry);
1352
+ registry.set(input.clientId, client);
1353
+ this._clientRegistryCache = registry;
1354
+ this._clientRegistryCacheAt = Date.now();
1355
+ await this._writeClientRegistry(registry);
1356
+ });
1357
+ this._events?.emitCustom("mailbox.client_registered", {
1358
+ clientId: input.clientId,
1359
+ sessionId: input.sessionId,
1360
+ name: input.name,
1361
+ source: input.source
1362
+ });
1363
+ }
1364
+ async clientHeartbeat(input) {
1365
+ const last = this._lastClientHeartbeat.get(input.clientId) ?? 0;
1366
+ const now = Date.now();
1367
+ if (now - last < HEARTBEAT_THROTTLE_MS) return;
1368
+ this._lastClientHeartbeat.set(input.clientId, now);
1369
+ await this._ensureClientRegistry();
1370
+ await withFileLock(this.clientRegistryPath, async () => {
1371
+ const registry = await this._readClientRegistry({ fresh: true });
1372
+ this._pruneStaleClientsInPlace(registry);
1373
+ const client = registry.get(input.clientId);
1374
+ if (client) {
1375
+ client.lastSeenAt = (/* @__PURE__ */ new Date()).toISOString();
1376
+ }
1377
+ this._clientRegistryCache = registry;
1378
+ this._clientRegistryCacheAt = Date.now();
1379
+ await this._writeClientRegistry(registry);
1380
+ });
1381
+ this._events?.emitCustom("mailbox.client_heartbeat", {
1382
+ clientId: input.clientId
1383
+ });
1384
+ }
1385
+ async getClientStatuses() {
1386
+ await this._ensureClientRegistry();
1387
+ const registry = await this._readClientRegistry();
1388
+ this._pruneStaleClientsInPlace(registry);
1389
+ const now = Date.now();
1390
+ return Array.from(registry.values()).map((c) => ({
1391
+ clientId: c.clientId,
1392
+ name: c.name,
1393
+ source: c.source,
1394
+ sessionId: c.sessionId,
1395
+ lastSeenAt: c.lastSeenAt,
1396
+ online: now - new Date(c.lastSeenAt).getTime() < CLIENT_STALE_MS,
1397
+ pid: c.pid
1398
+ })).sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt));
1399
+ }
1400
+ // ── Lifecycle ───────────────────────────────────────────────────────────
1401
+ async close() {
1402
+ this._registryCache = null;
1403
+ this._clientRegistryCache = null;
1404
+ this._messageCache = null;
1405
+ this._messageCacheMtime = -1;
1406
+ this._messageCacheSize = -1;
1407
+ }
1408
+ async clearAll() {
1409
+ await withFileLock(this.messagePath, async () => {
1410
+ await fsp.writeFile(this.messagePath, "", "utf8");
1411
+ });
1412
+ this._setMessageCache([]);
1413
+ }
1414
+ async purgeStale(opts) {
1415
+ const COMPLETED_MAX_AGE_MS = opts?.completedMaxAgeMs ?? 864e5;
1416
+ const INCOMPLETE_MAX_AGE_MS = opts?.incompleteMaxAgeMs ?? 6048e5;
1417
+ let completedPurged = 0;
1418
+ let incompletePurged = 0;
1419
+ let remaining = 0;
1420
+ await withFileLock(this.messagePath, async () => {
1421
+ const all = await this._readMessagesFresh();
1422
+ const now = Date.now();
1423
+ const cutoffCompleted = now - COMPLETED_MAX_AGE_MS;
1424
+ const cutoffIncomplete = now - INCOMPLETE_MAX_AGE_MS;
1425
+ const kept = [];
1426
+ for (const msg of all) {
1427
+ const msgTime = new Date(msg.timestamp).getTime();
1428
+ const completedTime = msg.completedAt ? new Date(msg.completedAt).getTime() : 0;
1429
+ if (msg.completed && completedTime < cutoffCompleted) {
1430
+ completedPurged++;
1431
+ continue;
1432
+ }
1433
+ if (!msg.completed && msgTime < cutoffIncomplete) {
1434
+ incompletePurged++;
1435
+ continue;
1436
+ }
1437
+ kept.push(msg);
1438
+ }
1439
+ remaining = kept.length;
1440
+ if (kept.length < all.length) {
1441
+ const content = kept.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
1442
+ await fsp.writeFile(this.messagePath, content, "utf8");
1443
+ }
1444
+ this._setMessageCache(kept);
1445
+ });
1446
+ return {
1447
+ completedPurged,
1448
+ incompletePurged,
1449
+ totalPurged: completedPurged + incompletePurged,
1450
+ remaining
1451
+ };
1452
+ }
1453
+ // ── Internal ────────────────────────────────────────────────────────────
1454
+ /**
1455
+ * Read all messages from the JSONL file. Always reads + parses the file.
1456
+ * Callers that can tolerate a stale-by-mtime view should use
1457
+ * {@link _readMessagesCached}; writers that need the post-lock truth
1458
+ * should call this directly (it's what {@link _readMessagesFresh} aliases).
1459
+ */
1460
+ async _readMessages() {
1461
+ try {
1462
+ const raw = await fsp.readFile(this.messagePath, "utf8");
1463
+ const lines = raw.split(LINE_SEPARATOR).filter((l) => l.trim().length > 0);
1464
+ const messages = [];
1465
+ for (const line of lines) {
1466
+ try {
1467
+ const parsed = JSON.parse(line);
1468
+ if (!parsed["readBy"]) {
1469
+ const readBy = {};
1470
+ if (parsed["read"] && parsed["readAt"]) {
1471
+ readBy[parsed["to"]] = parsed["readAt"];
1472
+ }
1473
+ parsed["readBy"] = readBy;
1474
+ delete parsed["read"];
1475
+ delete parsed["readAt"];
1476
+ }
1477
+ messages.push(parsed);
1478
+ } catch {
1479
+ }
1480
+ }
1481
+ return messages;
1482
+ } catch (err) {
1483
+ if (err.code === "ENOENT") return [];
1484
+ throw err;
1485
+ }
1486
+ }
1487
+ /**
1488
+ * Read messages, then adopt the result as the in-memory cache. Use this
1489
+ * from writers that just took the file lock — the read reflects the
1490
+ * authoritative post-lock state and should be served to subsequent
1491
+ * queries without re-reading.
1492
+ */
1493
+ async _readMessagesFresh() {
1494
+ const all = await this._readMessages();
1495
+ this._setMessageCache(all);
1496
+ return all;
1497
+ }
1498
+ /**
1499
+ * Read messages, consulting the mtime-bounded in-memory cache first.
1500
+ * The mailbox file is shared across processes; every `send`/`ack`/
1501
+ * `clearAll`/`purgeStale` takes the file lock, so writes are serialized
1502
+ * and a changed mtimeMs is a definitive freshness signal. When the
1503
+ * stat matches the cached mtime+size we return the cached array — no
1504
+ * file read and no JSON.parse — collapsing the per-iteration query
1505
+ * cost on the mailbox-loop hot path.
1506
+ */
1507
+ async _readMessagesCached() {
1508
+ try {
1509
+ const st = await fsp.stat(this.messagePath);
1510
+ if (this._messageCache !== null && this._messageCacheMtime === st.mtimeMs && this._messageCacheSize === st.size) {
1511
+ return this._messageCache;
1512
+ }
1513
+ const all = await this._readMessages();
1514
+ this._setMessageCache(all, st.mtimeMs, st.size);
1515
+ return all;
1516
+ } catch (err) {
1517
+ if (err.code === "ENOENT") {
1518
+ this._setMessageCache([], -1, -1);
1519
+ return [];
1520
+ }
1521
+ throw err;
1522
+ }
1523
+ }
1524
+ /**
1525
+ * Replace the in-memory cache. Caller is responsible for guaranteeing
1526
+ * that `messages` reflects the current on-disk state (e.g. they just
1527
+ * read or wrote it under the file lock).
1528
+ */
1529
+ _setMessageCache(messages, mtime, size) {
1530
+ if (messages.length > MESSAGE_CACHE_MAX_ENTRIES) {
1531
+ this._messageCache = null;
1532
+ this._messageCacheMtime = -1;
1533
+ this._messageCacheSize = -1;
1534
+ return;
1535
+ }
1536
+ this._messageCache = messages;
1537
+ if (mtime !== void 0 && size !== void 0) {
1538
+ this._messageCacheMtime = mtime;
1539
+ this._messageCacheSize = size;
1540
+ } else {
1541
+ void fsp.stat(this.messagePath).then((st) => {
1542
+ this._messageCacheMtime = st.mtimeMs;
1543
+ this._messageCacheSize = st.size;
1544
+ }).catch(() => {
1545
+ });
1546
+ }
1547
+ }
1548
+ /**
1549
+ * Append a single just-sent message to the in-memory cache without
1550
+ * re-reading the file. The caller must hold the file lock (or have
1551
+ * just released it after a successful append) so the cache stays
1552
+ * consistent with on-disk state.
1553
+ */
1554
+ _pushToCache(msg) {
1555
+ if (this._messageCache === null) return;
1556
+ if (this._messageCache.length >= MESSAGE_CACHE_MAX_ENTRIES) {
1557
+ this._messageCache = null;
1558
+ this._messageCacheMtime = -1;
1559
+ this._messageCacheSize = -1;
1560
+ return;
1561
+ }
1562
+ this._messageCache.push(msg);
1563
+ }
1564
+ async _ensureRegistry() {
1565
+ await fsp.mkdir(path3.dirname(this.registryPath), { recursive: true });
1566
+ }
1567
+ async _readRegistry(opts) {
1568
+ if (!opts?.fresh && this._registryCache && Date.now() - this._registryCacheAt < REGISTRY_CACHE_TTL_MS) {
1569
+ return new Map(this._registryCache);
1570
+ }
1571
+ try {
1572
+ const raw = await fsp.readFile(this.registryPath, "utf8");
1573
+ const data = JSON.parse(raw);
1574
+ const map = /* @__PURE__ */ new Map();
1575
+ for (const [id, agent] of Object.entries(data)) {
1576
+ map.set(id, agent);
1577
+ }
1578
+ this._registryCache = map;
1579
+ this._registryCacheAt = Date.now();
1580
+ return new Map(map);
1581
+ } catch (err) {
1582
+ if (err.code === "ENOENT") {
1583
+ const empty = /* @__PURE__ */ new Map();
1584
+ this._registryCache = empty;
1585
+ this._registryCacheAt = Date.now();
1586
+ return empty;
1587
+ }
1588
+ throw err;
1589
+ }
1590
+ }
1591
+ _pruneStaleInPlace(registry) {
1592
+ const cutoff = Date.now() - AGENT_STALE_MS;
1593
+ for (const agent of registry.values()) {
1594
+ if (new Date(agent.lastSeenAt).getTime() < cutoff) {
1595
+ agent.status = "idle";
1596
+ }
1597
+ }
1598
+ }
1599
+ async _writeRegistry(registry) {
1600
+ const obj = {};
1601
+ for (const [id, agent] of registry) {
1602
+ obj[id] = agent;
1603
+ }
1604
+ const tmp = `${this.registryPath}.${randomUUID().slice(0, 8)}.tmp`;
1605
+ await fsp.writeFile(tmp, JSON.stringify(obj, null, 2), "utf8");
1606
+ await fsp.rename(tmp, this.registryPath);
1607
+ }
1608
+ // ── Client registry internals ───────────────────────────────────────────
1609
+ async _ensureClientRegistry() {
1610
+ await fsp.mkdir(path3.dirname(this.clientRegistryPath), { recursive: true });
1611
+ }
1612
+ async _readClientRegistry(opts) {
1613
+ if (!opts?.fresh && this._clientRegistryCache && Date.now() - this._clientRegistryCacheAt < REGISTRY_CACHE_TTL_MS) {
1614
+ return new Map(this._clientRegistryCache);
1615
+ }
1616
+ try {
1617
+ const raw = await fsp.readFile(this.clientRegistryPath, "utf8");
1618
+ const data = JSON.parse(raw);
1619
+ const map = /* @__PURE__ */ new Map();
1620
+ for (const [id, client] of Object.entries(data)) {
1621
+ map.set(id, client);
1622
+ }
1623
+ this._clientRegistryCache = map;
1624
+ this._clientRegistryCacheAt = Date.now();
1625
+ return new Map(map);
1626
+ } catch (err) {
1627
+ if (err.code === "ENOENT") {
1628
+ const empty = /* @__PURE__ */ new Map();
1629
+ this._clientRegistryCache = empty;
1630
+ this._clientRegistryCacheAt = Date.now();
1631
+ return empty;
1632
+ }
1633
+ throw err;
1634
+ }
1635
+ }
1636
+ _pruneStaleClientsInPlace(registry) {
1637
+ const cutoff = Date.now() - CLIENT_STALE_MS;
1638
+ for (const client of registry.values()) {
1639
+ if (new Date(client.lastSeenAt).getTime() < cutoff) {
1640
+ client.lastSeenAt = new Date(cutoff).toISOString();
1641
+ }
1642
+ }
1643
+ }
1644
+ async _writeClientRegistry(registry) {
1645
+ const obj = {};
1646
+ for (const [id, client] of registry) {
1647
+ obj[id] = client;
1648
+ }
1649
+ const tmp = `${this.clientRegistryPath}.${randomUUID().slice(0, 8)}.tmp`;
1650
+ await fsp.writeFile(tmp, JSON.stringify(obj, null, 2), "utf8");
1651
+ await fsp.rename(tmp, this.clientRegistryPath);
1652
+ }
1653
+ };
1654
+ var HQ_AUTH_FILE_VERSION = 1;
1655
+ function defaultHqDataDir() {
1656
+ return path3.join(wstackGlobalRoot(), "hq");
1657
+ }
1658
+ function resolveHqDataDir(override, env = process.env) {
1659
+ const raw = override ?? env["WRONGSTACK_HQ_DATA_DIR"]?.trim();
1660
+ if (!raw) return defaultHqDataDir();
1661
+ return path3.isAbsolute(raw) ? path3.resolve(raw) : path3.resolve(process.cwd(), raw);
1662
+ }
1663
+ function emptyHqAuthFile() {
1664
+ return {
1665
+ version: HQ_AUTH_FILE_VERSION,
1666
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1667
+ };
1668
+ }
1669
+ function hqAuthFilePath(dataDir) {
1670
+ return path3.join(dataDir, "auth.json");
1671
+ }
1672
+ async function readHqAuthFile(dataDir, opts = {}) {
1673
+ const file = hqAuthFilePath(dataDir);
1674
+ let raw;
1675
+ try {
1676
+ raw = await fsp.readFile(file, "utf8");
1677
+ } catch (err) {
1678
+ if (err.code === "ENOENT") return emptyHqAuthFile();
1679
+ opts.warn?.(`HQ auth file read failed at ${file}: ${err.message}`);
1680
+ return emptyHqAuthFile();
1681
+ }
1682
+ try {
1683
+ const parsed = JSON.parse(raw);
1684
+ if (parsed.version !== HQ_AUTH_FILE_VERSION) {
1685
+ opts.warn?.(
1686
+ `HQ auth file at ${file} has unsupported version ${String(parsed.version)} (expected ${String(HQ_AUTH_FILE_VERSION)}); ignoring stored policy/tokens.`
1687
+ );
1688
+ return emptyHqAuthFile();
1689
+ }
1690
+ return parsed;
1691
+ } catch (err) {
1692
+ opts.warn?.(`HQ auth file at ${file} is not valid JSON; ignoring stored policy/tokens: ${err.message}`);
1693
+ return emptyHqAuthFile();
1694
+ }
1695
+ }
1696
+ async function writeHqAuthFile(dataDir, file) {
1697
+ const target = hqAuthFilePath(dataDir);
1698
+ const payload = {
1699
+ ...file,
1700
+ version: HQ_AUTH_FILE_VERSION,
1701
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1702
+ };
1703
+ await atomicWrite(target, `${JSON.stringify(payload, null, 2)}
1704
+ `, { mode: 384 });
1705
+ }
1706
+ async function ensureHqFirstRunAuthFile(dataDir, opts = {}) {
1707
+ const file = hqAuthFilePath(dataDir);
1708
+ try {
1709
+ await fsp.access(file);
1710
+ return { authFile: await readHqAuthFile(dataDir, opts), created: false };
1711
+ } catch (err) {
1712
+ if (err.code !== "ENOENT") {
1713
+ opts.warn?.(`HQ auth file access failed at ${file}: ${err.message}`);
1714
+ return { authFile: await readHqAuthFile(dataDir, opts), created: false };
1715
+ }
1716
+ }
1717
+ const browserToken = mintHqToken("first-run browser");
1718
+ const clientToken = mintHqToken("first-run client");
1719
+ const authFile = {
1720
+ version: HQ_AUTH_FILE_VERSION,
1721
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1722
+ browserTokens: [browserToken],
1723
+ clientTokens: [clientToken]
1724
+ };
1725
+ await writeHqAuthFile(dataDir, authFile);
1726
+ return { authFile: await readHqAuthFile(dataDir, opts), created: true, browserToken, clientToken };
1727
+ }
1728
+ async function mutateHqAuthFile(dataDir, mutator, opts = {}) {
1729
+ const current = await readHqAuthFile(dataDir, opts);
1730
+ const next = await mutator(current);
1731
+ await writeHqAuthFile(dataDir, next);
1732
+ return next;
1733
+ }
1734
+ function mintHqToken(label) {
1735
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1736
+ return {
1737
+ id: randomUUID(),
1738
+ token: randomUUID().replace(/-/g, "") + randomUUID().replace(/-/g, ""),
1739
+ ...label ? { label } : {},
1740
+ createdAt: now
1741
+ };
1742
+ }
1743
+ var mintHqBrowserToken = mintHqToken;
1744
+ function watchHqAuthFile(dataDir, onChange, opts = {}) {
1745
+ const file = hqAuthFilePath(dataDir);
1746
+ const debounceMs = opts.debounceMs ?? 200;
1747
+ let timer;
1748
+ let closed = false;
1749
+ let watcher;
1750
+ try {
1751
+ watcher = syncFs.watch(path3.dirname(file), { recursive: false });
1752
+ } catch (err) {
1753
+ opts.warn?.(`HQ auth watcher could not start: ${err.message}`);
1754
+ return { close: () => {
1755
+ } };
1756
+ }
1757
+ const trigger = () => {
1758
+ if (closed) return;
1759
+ if (timer) clearTimeout(timer);
1760
+ timer = setTimeout(() => {
1761
+ timer = void 0;
1762
+ const readOpts = opts.warn ? { warn: opts.warn } : {};
1763
+ void readHqAuthFile(dataDir, readOpts).then(
1764
+ (next) => {
1765
+ if (!closed) onChange(next);
1766
+ },
1767
+ () => {
1768
+ }
1769
+ );
1770
+ }, debounceMs);
1771
+ };
1772
+ watcher.on("change", (eventType, filename) => {
1773
+ const name = typeof filename === "string" ? filename : "";
1774
+ if (eventType === "rename" || eventType === "change") {
1775
+ if (!name || name === "auth.json" || name === path3.basename(file)) {
1776
+ trigger();
1777
+ }
1778
+ }
1779
+ });
1780
+ watcher.on("error", (err) => {
1781
+ opts.warn?.(`HQ auth watcher error: ${err.message}`);
1782
+ });
1783
+ return {
1784
+ close: () => {
1785
+ closed = true;
1786
+ if (timer) clearTimeout(timer);
1787
+ watcher.close();
1788
+ }
1789
+ };
1790
+ }
1791
+
1792
+ // src/hq/factory.ts
1793
+ function readFirstClientTokenFromAuthFile(dataDir) {
1794
+ try {
1795
+ const raw = syncFs.readFileSync(hqAuthFilePath(dataDir), "utf8");
1796
+ const parsed = JSON.parse(raw);
1797
+ return parsed.clientTokens?.find((t) => t.token.trim().length > 0)?.token;
1798
+ } catch {
1799
+ return void 0;
1800
+ }
1801
+ }
1802
+ function resolveHqConfigFromEnv(env = process.env) {
1803
+ return resolveHqConfig({ env });
1804
+ }
1805
+ function resolveHqConfig(options = {}) {
1806
+ const env = options.env ?? process.env;
1807
+ const fileConfig = options.config;
1808
+ const envUrl = env["WRONGSTACK_HQ_URL"]?.trim();
1809
+ const envToken = env["WRONGSTACK_HQ_TOKEN"]?.trim();
1810
+ const configUrl = fileConfig?.url?.trim();
1811
+ const configToken = fileConfig?.token?.trim();
1812
+ const envEnabledRaw = env["WRONGSTACK_HQ_ENABLED"]?.trim();
1813
+ const enabled = envEnabledRaw !== void 0 && envEnabledRaw.length > 0 ? envEnabledRaw !== "0" : fileConfig?.enabled;
1814
+ const dataDir = resolveHqDataDir(fileConfig?.dataDir, env);
1815
+ const token = envToken || configToken || readFirstClientTokenFromAuthFile(dataDir);
1816
+ const url = envUrl || configUrl;
1817
+ if (!url) {
1818
+ if (enabled === false) return void 0;
1819
+ if (enabled === true || token) {
1820
+ return {
1821
+ url: "http://localhost:3499",
1822
+ enabled: true,
1823
+ ...token ? { token } : {}
1824
+ };
1825
+ }
1826
+ return void 0;
1827
+ }
1828
+ const rawContentEnv = env["WRONGSTACK_HQ_RAW_CONTENT"]?.trim();
1829
+ const projectAliasEnv = env["WRONGSTACK_HQ_PROJECT_ALIAS"]?.trim();
1830
+ return {
1831
+ url,
1832
+ ...token ? { token } : {},
1833
+ ...enabled !== void 0 ? { enabled } : {},
1834
+ ...rawContentEnv ? { rawContent: rawContentEnv === "1" } : fileConfig?.rawContent !== void 0 ? { rawContent: fileConfig.rawContent } : {},
1835
+ ...projectAliasEnv ? { projectAlias: projectAliasEnv } : fileConfig?.projectAlias ? { projectAlias: fileConfig.projectAlias } : {}
1836
+ };
1837
+ }
1838
+ function stableMachineId() {
1839
+ return createHash("sha256").update(`${hostname()}:${process.pid}`).digest("hex").slice(0, 12);
1840
+ }
1841
+ function deriveProjectId(projectRoot) {
1842
+ return createHash("sha256").update(projectRoot).digest("hex").slice(0, 12);
1843
+ }
1844
+ function createHqPublisherFromEnv(options) {
1845
+ const config = options.config ?? resolveHqConfig({ config: options.appConfig?.hq });
1846
+ if (!config || config.enabled === false) return void 0;
1847
+ const machineId = options.machineId ?? stableMachineId();
1848
+ const host = options.hostnameOverride ?? hostname();
1849
+ const projectName = options.projectName ?? config.projectAlias ?? (basename(options.projectRoot) || "unknown");
1850
+ const client = {
1851
+ clientId: `${machineId}:${options.clientKind}:${process.pid}`,
1852
+ kind: options.clientKind,
1853
+ machineId,
1854
+ ...host ? { hostname: host } : {},
1855
+ pid: process.pid,
1856
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
1857
+ };
1858
+ const project = {
1859
+ projectId: deriveProjectId(options.projectRoot),
1860
+ projectRoot: options.projectRoot,
1861
+ projectName,
1862
+ machineId,
1863
+ workspaceKind: "git"
1864
+ };
1865
+ const redactionPolicy = options.redactionPolicy || config.rawContent !== void 0 ? {
1866
+ ...config.rawContent !== void 0 ? { rawContent: config.rawContent } : {},
1867
+ ...options.redactionPolicy ?? {}
1868
+ } : void 0;
1869
+ return new HqPublisher({
1870
+ url: config.url,
1871
+ ...config.token ? { token: config.token } : {},
1872
+ client,
1873
+ project,
1874
+ ...options.socketFactory ? { socketFactory: options.socketFactory } : {},
1875
+ ...redactionPolicy !== void 0 ? { redactionPolicy } : {}
1876
+ });
1877
+ }
1878
+ function createGlobalMailbox(options) {
1879
+ return new GlobalMailbox(options.projectDir, options.events, options.hqPublisher);
1880
+ }
1881
+
1882
+ export { DEFAULT_HQ_REDACTION_POLICY, HQ_AUTH_FILE_VERSION, HQ_PROTOCOL_VERSION, HqPublisher, createGlobalMailbox, createHqEventEnvelope, createHqPublisherFromEnv, createMailboxEventPayload, createMailboxSnapshotPayload, createMailboxSnapshotPayloadFromMailbox, defaultHqDataDir, emptyHqAuthFile, ensureHqFirstRunAuthFile, hqAuthFilePath, mapMailboxAgentToHqSummary, mapMailboxMessageToHqSummary, mintHqBrowserToken, mintHqToken, mutateHqAuthFile, parseHqEventPayload, parseHqFrame, readHqAuthFile, redactHqEvent, redactHqValue, resolveHqConfig, resolveHqConfigFromEnv, resolveHqDataDir, scrubAndTruncateHqPreview, summarizeHqToolArgs, watchHqAuthFile, writeHqAuthFile };
1883
+ //# sourceMappingURL=index.js.map
1884
+ //# sourceMappingURL=index.js.map