@wrongstack/core 0.309.0 → 0.310.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/dist/chronicle/index.js +113 -41
  2. package/dist/chronicle/metrics-ingest.d.ts +7 -0
  3. package/dist/chronicle/metrics-schema.d.ts +4 -1
  4. package/dist/chronicle/project-server.js +94 -38
  5. package/dist/chronicle/query.d.ts +1 -0
  6. package/dist/chronicle/sqlite-journal-schema.d.ts +2 -1
  7. package/dist/chronicle/types.d.ts +2 -0
  8. package/dist/{agent-status-helpers.d.ts → coordination/agent-status-helpers.d.ts} +1 -1
  9. package/dist/{agent-status-tracker.d.ts → coordination/agent-status-tracker.d.ts} +2 -2
  10. package/dist/{middleware → coordination}/collab-pause.d.ts +1 -1
  11. package/dist/coordination/director-kanban-queue-helpers.d.ts +1 -1
  12. package/dist/coordination/director.d.ts +7 -0
  13. package/dist/coordination/explore-companion.d.ts +9 -6
  14. package/dist/coordination/fleet-status-tool.d.ts +1 -1
  15. package/dist/coordination/index.d.ts +5 -1
  16. package/dist/coordination/index.js +2392 -405
  17. package/dist/coordination/kanban-dispatch-port.d.ts +30 -0
  18. package/dist/coordination/kanban-ops-port.d.ts +21 -0
  19. package/dist/coordination/mailbox-hooks.d.ts +1 -1
  20. package/dist/coordination/mailbox-project-server.js +14 -9
  21. package/dist/coordination/mutation-engine.d.ts +5 -3
  22. package/dist/core/context.d.ts +35 -44
  23. package/dist/core/conversation-state.d.ts +16 -52
  24. package/dist/core/index.js +92 -25
  25. package/dist/core/provider-runner.d.ts +2 -2
  26. package/dist/core/run-env.d.ts +7 -28
  27. package/dist/core/streaming-response-builder.d.ts +2 -2
  28. package/dist/execution/index.js +138 -162
  29. package/dist/goal/index.js +100 -22
  30. package/dist/hq/auth-store.d.ts +9 -0
  31. package/dist/hq/cost-bridge.d.ts +1 -1
  32. package/dist/hq/index.js +69 -7
  33. package/dist/index.d.ts +12 -5
  34. package/dist/index.js +21185 -26807
  35. package/dist/infrastructure/index.js +232 -135
  36. package/dist/infrastructure/provider-cache-ledger.d.ts +1 -1
  37. package/dist/infrastructure/token-counter.d.ts +5 -1
  38. package/dist/kernel/events/file-events.d.ts +6 -0
  39. package/dist/kernel/events/provider-events.d.ts +10 -7
  40. package/dist/kernel/events/session-events.d.ts +4 -4
  41. package/dist/kernel/events/tool-events.d.ts +12 -2
  42. package/dist/observability/index.js +1 -1
  43. package/dist/plugin/index.js +218 -47
  44. package/dist/prompts/index.js +360 -3
  45. package/dist/security/auto-approve-policy.d.ts +2 -2
  46. package/dist/security/index.d.ts +1 -0
  47. package/dist/security/index.js +224 -68
  48. package/dist/security/kanban-boundary.d.ts +1 -1
  49. package/dist/security/kanban-governance-port.d.ts +28 -0
  50. package/dist/security/permission-helpers.d.ts +11 -0
  51. package/dist/security/permission-policy.d.ts +10 -1
  52. package/dist/security/yolo-risk.d.ts +17 -0
  53. package/dist/session-catalog/index.js +34 -5
  54. package/dist/session-catalog/project-server.js +34 -5
  55. package/dist/session-catalog/protocol.d.ts +1 -1
  56. package/dist/session-catalog/registry.d.ts +1 -1
  57. package/dist/session-catalog/store-schema.d.ts +1 -1
  58. package/dist/session-catalog/store.d.ts +1 -1
  59. package/dist/skills/index.js +39 -6
  60. package/dist/storage/annotations-store.d.ts +1 -1
  61. package/dist/storage/board-store-port.d.ts +45 -0
  62. package/dist/storage/completed-work-checkpoint.d.ts +1 -1
  63. package/dist/storage/config-loader/types.d.ts +1 -1
  64. package/dist/storage/event-bus-port.d.ts +27 -0
  65. package/dist/storage/file-session-writer.d.ts +47 -5
  66. package/dist/storage/goal-coordination.d.ts +1 -1
  67. package/dist/storage/goal-store.d.ts +1 -1
  68. package/dist/storage/index.d.ts +3 -4
  69. package/dist/storage/index.js +1107 -1462
  70. package/dist/storage/plan-store.d.ts +1 -1
  71. package/dist/storage/queue-store.d.ts +1 -1
  72. package/dist/storage/replay-log-store.d.ts +1 -1
  73. package/dist/storage/session-recovery.d.ts +10 -0
  74. package/dist/storage/session-resume-validation.d.ts +13 -1
  75. package/dist/storage/session-store/events.d.ts +1 -1
  76. package/dist/storage/session-store/load-session-data.d.ts +1 -1
  77. package/dist/storage/session-store/rename-session.d.ts +1 -1
  78. package/dist/storage/session-store/resume-session.d.ts +3 -1
  79. package/dist/storage/session-store/session-store-index.d.ts +2 -1
  80. package/dist/storage/session-store/types.d.ts +1 -1
  81. package/dist/storage/session-store.d.ts +70 -0
  82. package/dist/storage/session-summary-tracker.d.ts +8 -0
  83. package/dist/storage/session-write-buffer.d.ts +31 -2
  84. package/dist/storage/task-store.d.ts +1 -1
  85. package/dist/storage/todos-checkpoint.d.ts +1 -1
  86. package/dist/storage/tool-audit-log.d.ts +1 -1
  87. package/dist/tasking/index.js +100 -22
  88. package/dist/tasking/task-tracker.d.ts +24 -1
  89. package/dist/types/compactor.d.ts +2 -2
  90. package/dist/types/context.d.ts +212 -0
  91. package/dist/types/conversation-state.d.ts +109 -0
  92. package/dist/types/error-handler.d.ts +2 -2
  93. package/dist/types/file-event-record.d.ts +6 -0
  94. package/dist/types/index.d.ts +3 -3
  95. package/dist/types/index.js +17 -1
  96. package/dist/types/permission.d.ts +3 -3
  97. package/dist/types/plugin.d.ts +3 -3
  98. package/dist/types/provider-runner.d.ts +2 -2
  99. package/dist/types/provider.d.ts +10 -0
  100. package/dist/types/run-env.d.ts +32 -0
  101. package/dist/types/session.d.ts +3 -0
  102. package/dist/types/slash-command.d.ts +2 -2
  103. package/dist/types/token-counter.d.ts +30 -1
  104. package/dist/types/tool-executor.d.ts +2 -2
  105. package/dist/types/tool.d.ts +20 -5
  106. package/dist/utils/context-breakdown.d.ts +2 -2
  107. package/dist/utils/context-evidence.d.ts +12 -12
  108. package/dist/utils/crash-shield.d.ts +9 -0
  109. package/dist/utils/heap-watchdog.js +11 -3
  110. package/dist/utils/index.d.ts +2 -1
  111. package/dist/utils/index.js +126 -160
  112. package/dist/utils/regex-guard.d.ts +7 -30
  113. package/dist/utils/terminal-sanitize.d.ts +41 -0
  114. package/dist/utils/todos-format.d.ts +1 -1
  115. package/dist/utils/tool-subject.d.ts +1 -1
  116. package/dist/utils/tree-kill.d.ts +2 -0
  117. package/dist/utils/tree-kill.js +1 -0
  118. package/instructions/agents/chaos-monkey.md +5 -1
  119. package/instructions/coordination/subagent-baseline.md +10 -0
  120. package/instructions/system-lite.md +2 -0
  121. package/instructions/system-pro.md +40 -0
  122. package/instructions/system.md +15 -0
  123. package/package.json +7 -10
  124. package/dist/defaults/index.d.ts +0 -69
  125. package/dist/defaults/index.js +0 -37755
  126. /package/dist/{fleet-notifier.d.ts → coordination/fleet-notifier.d.ts} +0 -0
  127. /package/dist/{session-registry-atomic-file.d.ts → session-catalog/session-registry-atomic-file.d.ts} +0 -0
  128. /package/dist/{session-registry-types.d.ts → session-catalog/session-registry-types.d.ts} +0 -0
  129. /package/dist/{session-registry.d.ts → session-catalog/session-registry.d.ts} +0 -0
@@ -1,867 +1,31 @@
1
- // src/agent-status-helpers.ts
2
- var TOOL_TEXT_CAP = 360;
3
- var TOUCHED_FILE_LIMIT = 200;
4
- var TODO_TEXT_CAP = 360;
5
- var TODO_LIMIT = 32;
6
- function lineCount(value) {
7
- return value.length === 0 ? 0 : value.split(/\r?\n/).length;
8
- }
9
- function patchDelta(value) {
10
- let addedLines = 0;
11
- let removedLines = 0;
12
- const hunkStart = value.indexOf("@@");
13
- if (hunkStart === -1) return { addedLines, removedLines };
14
- for (const line of value.slice(hunkStart).split(/\r?\n/)) {
15
- if (line.startsWith("+++") || line.startsWith("---")) continue;
16
- if (line.startsWith("+")) addedLines += 1;
17
- else if (line.startsWith("-")) removedLines += 1;
18
- }
19
- return { addedLines, removedLines };
20
- }
21
- function compactText(value) {
22
- return value.length <= TOOL_TEXT_CAP ? value : `${value.slice(0, TOOL_TEXT_CAP - 1)}\u2026`;
23
- }
24
- function boundedText(value, cap) {
25
- const trimmed = value.trim();
26
- return trimmed.length <= cap ? trimmed : `${trimmed.slice(0, cap - 1)}\u2026`;
27
- }
28
- function compactTodos(value) {
29
- if (!Array.isArray(value)) return void 0;
30
- const todos = [];
31
- for (const candidate of value) {
32
- if (typeof candidate !== "object" || candidate === null) continue;
33
- const todo = candidate;
34
- const id = typeof todo["id"] === "string" ? todo["id"].trim() : "";
35
- const content = typeof todo["content"] === "string" ? boundedText(todo["content"], TODO_TEXT_CAP) : "";
36
- const status = todo["status"];
37
- if (!id || !content || status !== "pending" && status !== "in_progress" && status !== "completed") {
38
- continue;
39
- }
40
- const activeForm = typeof todo["activeForm"] === "string" ? boundedText(todo["activeForm"], TODO_TEXT_CAP) : void 0;
41
- todos.push({ id, content, status, ...activeForm ? { activeForm } : {} });
42
- if (todos.length >= TODO_LIMIT) break;
43
- }
44
- return todos;
45
- }
46
- function compactToolInput(input) {
47
- if (!input || typeof input !== "object" || Array.isArray(input)) {
48
- return typeof input === "string" ? { input: compactText(input) } : {};
49
- }
50
- const source = input;
51
- const safe = {};
52
- const safeKeys = [
53
- "file_path",
54
- "filePath",
55
- "path",
56
- "filename",
57
- "target_file",
58
- "targetFile",
59
- "line",
60
- "start_line",
61
- "startLine",
62
- "end_line",
63
- "endLine",
64
- "offset",
65
- "limit",
66
- "command",
67
- "cmd",
68
- "url",
69
- "href",
70
- "uri",
71
- "query",
72
- "pattern"
73
- ];
74
- for (const key of safeKeys) {
75
- const value = source[key];
76
- if (typeof value === "string") safe[key] = compactText(value);
77
- else if (typeof value === "number" || typeof value === "boolean") safe[key] = value;
78
- }
79
- const content = [source["content"], source["text"], source["data"]].find(
80
- (value) => typeof value === "string"
81
- );
82
- const oldText = [source["old_string"], source["oldString"], source["search"]].find(
83
- (value) => typeof value === "string"
1
+ // src/storage/board-store-port.ts
2
+ var notWired = () => {
3
+ throw new Error(
4
+ "Kanban BoardStorePort is not wired \u2014 register the implementation at the CLI composition root (see setBoardStorePort)."
84
5
  );
85
- const newText = [source["new_string"], source["newString"], source["replacement"]].find(
86
- (value) => typeof value === "string"
87
- );
88
- const patch = [source["patch"], source["diff"]].find(
89
- (value) => typeof value === "string"
90
- );
91
- const delta = patch ? patchDelta(patch) : void 0;
92
- return {
93
- ...Object.keys(safe).length > 0 ? { input: safe } : {},
94
- ...content !== void 0 ? { inputLines: lineCount(content) } : {},
95
- ...oldText !== void 0 ? { oldLines: lineCount(oldText) } : {},
96
- ...newText !== void 0 ? { newLines: lineCount(newText) } : {},
97
- ...delta && delta.addedLines > 0 ? { addedLines: delta.addedLines } : {},
98
- ...delta && delta.removedLines > 0 ? { removedLines: delta.removedLines } : {}
99
- };
100
- }
101
- function completedToolReceipt(payload, pending) {
102
- const completedAt = Date.now();
103
- const durationMs = Math.max(
104
- 0,
105
- payload.durationMs ?? completedAt - (pending?.startedAt ?? completedAt)
106
- );
107
- const compact = compactToolInput(payload.input ?? pending?.input);
108
- return {
109
- id: payload.id ?? `${payload.name}:${completedAt}`,
110
- name: payload.name,
111
- startedAt: pending?.startedAt ?? Math.max(0, completedAt - durationMs),
112
- completedAt,
113
- durationMs,
114
- ok: payload.ok !== false,
115
- ...compact,
116
- ...payload.output !== void 0 ? { output: compactText(payload.output) } : {},
117
- ...payload.outputLines !== void 0 ? { outputLines: payload.outputLines } : {},
118
- ...payload.outputBytes !== void 0 ? { outputBytes: payload.outputBytes } : {},
119
- ...payload.outputTokens !== void 0 ? { outputTokens: payload.outputTokens } : {}
120
- };
121
- }
122
- function emptyActivityTotals() {
123
- return {
124
- filesTouched: [],
125
- reads: 0,
126
- writes: 0,
127
- edits: 0,
128
- terminalCalls: 0,
129
- webCalls: 0,
130
- searches: 0,
131
- otherCalls: 0,
132
- mailReceived: 0,
133
- mailSent: 0,
134
- linesRead: 0,
135
- linesWritten: 0,
136
- linesAdded: 0,
137
- linesRemoved: 0
138
- };
139
- }
140
- function addMailTotal(current, direction) {
141
- const next = { ...current ?? emptyActivityTotals() };
142
- if (direction === "incoming") next.mailReceived += 1;
143
- else next.mailSent += 1;
144
- return next;
145
- }
146
- function toolActivityKind(name) {
147
- const normalized = name.toLowerCase().replace(/[.:/-]+/g, "_");
148
- if (/^(read|view|open_file|read_file)|file_read/.test(normalized)) return "read";
149
- if (/^(write|create|save)|file_write/.test(normalized)) return "write";
150
- if (/edit|update|patch|replace|apply_patch/.test(normalized)) return "edit";
151
- if (/bash|shell|terminal|exec|command|powershell|cmd/.test(normalized)) return "terminal";
152
- if (/fetch|browser|browse|http|url|web/.test(normalized)) return "web";
153
- if (/search|grep|find|glob|query/.test(normalized)) return "search";
154
- return "other";
155
- }
156
- function receiptFilePath(receipt) {
157
- if (!receipt.input || typeof receipt.input !== "object" || Array.isArray(receipt.input)) {
158
- return void 0;
159
- }
160
- const input = receipt.input;
161
- for (const key of ["file_path", "filePath", "path", "filename", "target_file", "targetFile"]) {
162
- const value = input[key];
163
- if (typeof value === "string" && value.trim()) return value.trim();
164
- }
165
- return void 0;
166
- }
167
- function addToolActivity(current, receipt) {
168
- const next = {
169
- ...current ?? emptyActivityTotals(),
170
- filesTouched: [...current?.filesTouched ?? []]
171
- };
172
- const kind = toolActivityKind(receipt.name);
173
- if (kind === "read") {
174
- next.reads += 1;
175
- next.linesRead += receipt.outputLines ?? 0;
176
- } else if (kind === "write") {
177
- next.writes += 1;
178
- next.linesWritten += receipt.inputLines ?? 0;
179
- } else if (kind === "edit") {
180
- next.edits += 1;
181
- next.linesAdded += receipt.addedLines ?? receipt.newLines ?? 0;
182
- next.linesRemoved += receipt.removedLines ?? receipt.oldLines ?? 0;
183
- } else if (kind === "terminal") next.terminalCalls += 1;
184
- else if (kind === "web") next.webCalls += 1;
185
- else if (kind === "search") next.searches += 1;
186
- else next.otherCalls += 1;
187
- const filePath = receiptFilePath(receipt);
188
- if (filePath && !next.filesTouched.includes(filePath) && next.filesTouched.length < TOUCHED_FILE_LIMIT) {
189
- next.filesTouched.push(filePath);
190
- }
191
- return next;
192
- }
193
- function clampPct(pct) {
194
- if (!Number.isFinite(pct)) return 0;
195
- return Math.max(0, Math.min(100, pct));
196
- }
197
-
198
- // src/agent-status-tracker.ts
199
- var AGENT_REAP_MS = 3e4;
200
- var AGENT_SWEEP_INTERVAL_MS = 1e4;
201
- var PENDING_TOOL_TTL_MS = 3e5;
202
- var PARTIAL_TEXT_CAP = 1200;
203
- var PARTIAL_FLUSH_THROTTLE_MS = 300;
204
- var RECENT_TOOL_LIMIT = 12;
205
- var RECENT_MAIL_LIMIT = 12;
206
- var TASK_TEXT_CAP = 1200;
207
- var PROMPT_TEXT_CAP = 6e3;
208
- var AgentStatusTracker = class _AgentStatusTracker {
209
- events;
210
- registry;
211
- sessionId;
212
- leaderName;
213
- // Live agent map: agentId → AgentEntry
214
- agents = /* @__PURE__ */ new Map();
215
- // Last full agent list flushed (leader + subagents). Lets external consumers
216
- // read the current state synchronously without re-deriving it.
217
- lastAgents = [];
218
- // Leader tracking
219
- leaderStatus = "idle";
220
- leaderCurrentTool;
221
- leaderCurrentTask;
222
- leaderIterations = 0;
223
- leaderToolCalls = 0;
224
- leaderCostUsd = 0;
225
- leaderTokensIn = 0;
226
- leaderTokensOut = 0;
227
- leaderCtxPct;
228
- leaderModel;
229
- leaderPartialText = "";
230
- leaderStartedAt;
231
- leaderRecentTools = [];
232
- leaderRecentMail = [];
233
- leaderTodos = [];
234
- leaderLatestPrompt;
235
- leaderLatestPromptAt;
236
- leaderActivity = emptyActivityTotals();
237
- leaderPendingTools = /* @__PURE__ */ new Map();
238
- subagentPendingTools = /* @__PURE__ */ new Map();
239
- seenIncomingMail = /* @__PURE__ */ new Set();
240
- seenOutgoingMail = /* @__PURE__ */ new Set();
241
- static SEEN_MAIL_LIMIT = 1e4;
242
- rememberMailId(seen, messageId) {
243
- if (seen.has(messageId)) return false;
244
- seen.add(messageId);
245
- if (seen.size > _AgentStatusTracker.SEEN_MAIL_LIMIT) {
246
- const oldest = seen.values().next().value;
247
- if (oldest !== void 0) seen.delete(oldest);
248
- }
249
- return true;
250
- }
251
- unsubscribers = [];
252
- onUpdate;
253
- sweepTimer = null;
254
- partialTimer = null;
255
- /** Serialize registry writes so concurrent flush() calls don't race. */
256
- flushPromise = null;
257
- constructor(opts) {
258
- this.events = opts.events;
259
- this.registry = opts.registry;
260
- this.sessionId = opts.sessionId;
261
- this.leaderName = opts.leaderName ?? "leader";
262
- this.onUpdate = opts.onUpdate;
263
- }
264
- /** Current full agent list (leader + subagents) as of the last flush. */
265
- getAgents() {
266
- return this.lastAgents.length > 0 ? [...this.lastAgents] : [];
267
- }
268
- start() {
269
- this.stop();
270
- const on = (pattern, fn) => this.events.onPattern(pattern, (event, payload) => {
271
- if (!this.acceptsSession(payload)) return;
272
- fn(event, payload);
273
- });
274
- this.unsubscribers.push(
275
- on("agent.run.started", (_event, payload) => {
276
- const p = payload;
277
- this.markLeaderStarted(p?.at);
278
- this.captureLeaderContext(p?.ctx);
279
- if (p?.model) this.leaderModel = p.model;
280
- if (p?.inputText?.trim()) {
281
- const prompt = boundedText(p.inputText, PROMPT_TEXT_CAP);
282
- this.leaderLatestPrompt = prompt;
283
- this.leaderLatestPromptAt = p.at ? Date.parse(p.at) : Date.now();
284
- if (!Number.isFinite(this.leaderLatestPromptAt)) this.leaderLatestPromptAt = Date.now();
285
- this.leaderCurrentTask = boundedText(p.inputText, TASK_TEXT_CAP);
286
- }
287
- this.leaderStatus = "running";
288
- this.leaderIterations++;
289
- this.flush();
290
- })
291
- );
292
- this.unsubscribers.push(
293
- on("iteration.started", (_e, payload) => {
294
- const p = payload;
295
- const ctx = p?.ctx;
296
- this.markLeaderStarted();
297
- this.leaderStatus = "running";
298
- if (typeof p?.index === "number") {
299
- this.leaderIterations = Math.max(this.leaderIterations, p.index + 1);
300
- }
301
- if (!ctx) {
302
- this.flush();
303
- return;
304
- }
305
- this.captureLeaderContext(ctx);
306
- this.flush();
307
- })
308
- );
309
- this.unsubscribers.push(
310
- on("agent.run.completed", (_event, payload) => {
311
- const p = payload;
312
- this.captureLeaderContext(p?.ctx);
313
- this.leaderStatus = p?.status === "failed" ? "error" : "idle";
314
- this.leaderCurrentTool = void 0;
315
- this.leaderCurrentTask = void 0;
316
- this.leaderPartialText = "";
317
- if (this.leaderStatus === "idle") this.leaderStartedAt = void 0;
318
- this.flush();
319
- })
320
- );
321
- this.unsubscribers.push(
322
- on("agent.run.error", (_event, payload) => {
323
- const p = payload;
324
- this.captureLeaderContext(p?.ctx);
325
- this.leaderStatus = "error";
326
- this.leaderCurrentTool = void 0;
327
- this.leaderCurrentTask = void 0;
328
- this.leaderPartialText = "";
329
- this.flush();
330
- })
331
- );
332
- this.unsubscribers.push(
333
- on("iteration.completed", (_event, payload) => {
334
- const p = payload;
335
- this.captureLeaderContext(p?.ctx);
336
- this.flush();
337
- })
338
- );
339
- this.unsubscribers.push(
340
- on("tool.started", (_event, payload) => {
341
- const p = payload;
342
- if (p?.name) {
343
- this.markLeaderStarted();
344
- this.leaderCurrentTool = p.name;
345
- this.leaderToolCalls++;
346
- this.leaderPendingTools.set(p.id ?? p.name, {
347
- name: p.name,
348
- input: p.input,
349
- startedAt: Date.now()
350
- });
351
- }
352
- this.leaderStatus = "running";
353
- this.flush();
354
- })
355
- );
356
- this.unsubscribers.push(
357
- on("tool.executed", (_event, payload) => {
358
- const p = payload;
359
- if (p?.name) {
360
- const key = p.id ?? p.name;
361
- const receipt = completedToolReceipt(p, this.leaderPendingTools.get(key));
362
- this.leaderRecentTools = [receipt, ...this.leaderRecentTools].slice(0, RECENT_TOOL_LIMIT);
363
- this.leaderActivity = addToolActivity(this.leaderActivity, receipt);
364
- this.leaderPendingTools.delete(key);
365
- }
366
- this.leaderCurrentTool = void 0;
367
- this.flush();
368
- })
369
- );
370
- this.unsubscribers.push(
371
- on("brain.ask_human", () => {
372
- this.markLeaderStarted();
373
- this.leaderStatus = "waiting_user";
374
- this.flush();
375
- })
376
- );
377
- const recordMail = (direction, payload) => {
378
- const p = payload;
379
- if (!p?.messageId) return;
380
- const seen = direction === "incoming" ? this.seenIncomingMail : this.seenOutgoingMail;
381
- if (!this.rememberMailId(seen, p.messageId)) return;
382
- const receipt = {
383
- id: p.messageId,
384
- direction,
385
- from: p.from ?? "?",
386
- to: p.to ?? (direction === "incoming" ? "leader" : "?"),
387
- type: p.type ?? "note",
388
- subject: p.subject ?? "Message",
389
- at: Date.now()
390
- };
391
- const directAgentId = direction === "outgoing" ? p.from : p.to;
392
- const entry = directAgentId ? this.agents.get(directAgentId) : void 0;
393
- if (entry) {
394
- entry.recentMail = [receipt, ...entry.recentMail ?? []].slice(0, RECENT_MAIL_LIMIT);
395
- entry.activity = addMailTotal(entry.activity, direction);
396
- } else {
397
- this.leaderRecentMail = [receipt, ...this.leaderRecentMail].slice(0, RECENT_MAIL_LIMIT);
398
- this.leaderActivity = addMailTotal(this.leaderActivity, direction);
399
- }
400
- this.flush();
401
- };
402
- this.unsubscribers.push(
403
- on("mailbox.message_sent", (_event, payload) => recordMail("outgoing", payload)),
404
- on("mailbox.received", (_event, payload) => recordMail("incoming", payload))
405
- );
406
- this.unsubscribers.push(
407
- on("llm.stream_started", () => {
408
- this.markLeaderStarted();
409
- this.leaderStatus = "streaming";
410
- this.leaderPartialText = "";
411
- this.flush();
412
- })
413
- );
414
- this.unsubscribers.push(
415
- on("provider.text_delta", (_e, payload) => {
416
- const p = payload;
417
- const text = p?.text;
418
- if (!text) return;
419
- this.markLeaderStarted();
420
- this.captureLeaderContext(p?.ctx);
421
- this.leaderStatus = "streaming";
422
- const next = this.leaderPartialText + text;
423
- this.leaderPartialText = next.length > PARTIAL_TEXT_CAP ? next.slice(next.length - PARTIAL_TEXT_CAP) : next;
424
- this.schedulePartialFlush();
425
- })
426
- );
427
- this.unsubscribers.push(
428
- on("provider.response", (_e, payload) => {
429
- const p = payload;
430
- this.captureLeaderContext(p?.ctx);
431
- this.flush();
432
- })
433
- );
434
- this.unsubscribers.push(
435
- on("provider.fallback", (_e, payload) => {
436
- const p = payload;
437
- if (p?.to?.model) {
438
- this.leaderModel = p.to.providerId ? `${p.to.providerId}/${p.to.model}` : p.to.model;
439
- this.flush();
440
- }
441
- })
442
- );
443
- this.unsubscribers.push(
444
- on("ctx.pct", (_e, payload) => {
445
- const p = payload;
446
- if (typeof p?.load === "number" && Number.isFinite(p.load)) {
447
- this.leaderCtxPct = clampPct(Math.round(p.load * 100));
448
- this.flush();
449
- }
450
- })
451
- );
452
- this.unsubscribers.push(
453
- on("token.accounted", (_e, payload) => {
454
- const p = payload;
455
- if (!p) return;
456
- this.leaderTokensIn += p.usage?.input ?? 0;
457
- this.leaderTokensOut += p.usage?.output ?? 0;
458
- this.leaderCostUsd += p.cost?.total ?? 0;
459
- this.flush();
460
- })
461
- );
462
- const touch = (id) => {
463
- let entry = this.agents.get(id);
464
- if (!entry) {
465
- const now = (/* @__PURE__ */ new Date()).toISOString();
466
- entry = {
467
- id,
468
- name: id,
469
- status: "idle",
470
- iterations: 0,
471
- toolCalls: 0,
472
- startedAt: now,
473
- lastActivityAt: now
474
- };
475
- this.agents.set(id, entry);
476
- }
477
- entry.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
478
- return entry;
479
- };
480
- this.unsubscribers.push(
481
- on("subagent.spawned", (_e, payload) => {
482
- const p = payload;
483
- if (!p?.subagentId) return;
484
- const entry = touch(p.subagentId);
485
- entry.name = p.name?.trim() || entry.name;
486
- if (p.model) entry.model = p.model;
487
- if (p.taskId) entry.taskId = p.taskId;
488
- if (p.description?.trim()) entry.currentTask = boundedText(p.description, TASK_TEXT_CAP);
489
- if (!entry.startedAt) entry.startedAt = (/* @__PURE__ */ new Date()).toISOString();
490
- entry.status = "running";
491
- this.flush();
492
- })
493
- );
494
- this.unsubscribers.push(
495
- on("subagent.ctx_pct", (_e, payload) => {
496
- const p = payload;
497
- if (!p?.subagentId) return;
498
- const entry = touch(p.subagentId);
499
- if (typeof p.load === "number") entry.ctxPct = clampPct(Math.round(p.load * 100));
500
- this.flush();
501
- })
502
- );
503
- this.unsubscribers.push(
504
- on("subagent.task_started", (_e, payload) => {
505
- const p = payload;
506
- if (!p?.subagentId) return;
507
- const entry = touch(p.subagentId);
508
- entry.status = "running";
509
- if (p.taskId) entry.taskId = p.taskId;
510
- if (p.description?.trim()) entry.currentTask = boundedText(p.description, TASK_TEXT_CAP);
511
- if (!entry.startedAt) entry.startedAt = (/* @__PURE__ */ new Date()).toISOString();
512
- entry.iterations++;
513
- this.flush();
514
- })
515
- );
516
- this.unsubscribers.push(
517
- on("subagent.tool_started", (_e, payload) => {
518
- const p = payload;
519
- if (!p?.subagentId || !p.name) return;
520
- const entry = touch(p.subagentId);
521
- entry.status = "running";
522
- entry.currentTool = p.name;
523
- this.subagentPendingTools.set(`${p.subagentId}:${p.id ?? p.name}`, {
524
- name: p.name,
525
- input: p.input,
526
- startedAt: Date.now()
527
- });
528
- this.flush();
529
- })
530
- );
531
- this.unsubscribers.push(
532
- on("subagent.tool_executed", (_e, payload) => {
533
- const p = payload;
534
- if (!p?.subagentId || !p.name) return;
535
- const entry = touch(p.subagentId);
536
- entry.status = "running";
537
- if (!entry.startedAt) entry.startedAt = (/* @__PURE__ */ new Date()).toISOString();
538
- const key = `${p.subagentId}:${p.id ?? p.name}`;
539
- const receipt = completedToolReceipt(p, this.subagentPendingTools.get(key));
540
- entry.recentTools = [receipt, ...entry.recentTools ?? []].slice(0, RECENT_TOOL_LIMIT);
541
- entry.activity = addToolActivity(entry.activity, receipt);
542
- this.subagentPendingTools.delete(key);
543
- entry.currentTool = void 0;
544
- entry.toolCalls++;
545
- this.flush();
546
- })
547
- );
548
- this.unsubscribers.push(
549
- on("subagent.iteration_summary", (_e, payload) => {
550
- const p = payload;
551
- if (!p?.subagentId) return;
552
- const entry = touch(p.subagentId);
553
- entry.status = "running";
554
- if (!entry.startedAt) entry.startedAt = (/* @__PURE__ */ new Date()).toISOString();
555
- if (typeof p.iteration === "number") entry.iterations = p.iteration;
556
- if (typeof p.toolCalls === "number") entry.toolCalls = p.toolCalls;
557
- if (typeof p.costUsd === "number") entry.costUsd = p.costUsd;
558
- if (p.currentTool) entry.currentTool = p.currentTool;
559
- if (typeof p.partialText === "string") {
560
- entry.partialText = p.partialText.length > PARTIAL_TEXT_CAP ? p.partialText.slice(p.partialText.length - PARTIAL_TEXT_CAP) : p.partialText;
561
- }
562
- this.flush();
563
- })
564
- );
565
- this.unsubscribers.push(
566
- on("subagent.task_completed", (_e, payload) => {
567
- const p = payload;
568
- if (!p?.subagentId) return;
569
- const entry = this.agents.get(p.subagentId);
570
- if (!entry) return;
571
- entry.status = p.status === "failed" || p.status === "timeout" ? "error" : "idle";
572
- entry.currentTool = void 0;
573
- entry.currentTask = void 0;
574
- entry.taskId = void 0;
575
- entry.partialText = void 0;
576
- if (typeof p.iterations === "number") entry.iterations = p.iterations;
577
- if (typeof p.toolCalls === "number") entry.toolCalls = p.toolCalls;
578
- entry.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
579
- this.flush();
580
- })
581
- );
582
- this.unsubscribers.push(
583
- on("subagent.stopped", (_e, payload) => {
584
- const p = payload;
585
- if (!p?.subagentId) return;
586
- if (this.agents.delete(p.subagentId)) this.flush();
587
- })
588
- );
589
- this.unsubscribers.push(
590
- on("subagent.removed", (_e, payload) => {
591
- const p = payload;
592
- if (!p?.subagentId) return;
593
- if (this.agents.delete(p.subagentId)) this.flush();
594
- })
595
- );
596
- this.sweepTimer = setInterval(() => this.sweep(), AGENT_SWEEP_INTERVAL_MS);
597
- if (typeof this.sweepTimer.unref === "function") this.sweepTimer.unref();
598
- }
599
- stop() {
600
- for (const unsub of this.unsubscribers) {
601
- try {
602
- unsub();
603
- } catch {
604
- }
605
- }
606
- this.unsubscribers = [];
607
- if (this.sweepTimer) {
608
- clearInterval(this.sweepTimer);
609
- this.sweepTimer = null;
610
- }
611
- if (this.partialTimer) {
612
- clearTimeout(this.partialTimer);
613
- this.partialTimer = null;
614
- }
615
- this.leaderPendingTools.clear();
616
- this.subagentPendingTools.clear();
617
- }
618
- /**
619
- * Coalesce streamed-text flushes: at most one registry write per
620
- * {@link PARTIAL_FLUSH_THROTTLE_MS} while text streams in, so per-token
621
- * deltas never thrash the cross-process registry file.
622
- */
623
- schedulePartialFlush() {
624
- if (this.partialTimer) return;
625
- this.partialTimer = setTimeout(() => {
626
- this.partialTimer = null;
627
- this.flush();
628
- }, PARTIAL_FLUSH_THROTTLE_MS);
629
- if (typeof this.partialTimer.unref === "function") this.partialTimer.unref();
630
- }
631
- /**
632
- * Remove subagents that have been finished (idle/error) for longer than
633
- * {@link AGENT_REAP_MS}. Running / streaming / waiting_user agents are kept
634
- * regardless of age — only *not-working* agents are reaped.
635
- *
636
- * Also trims orphaned PendingTool entries older than {@link PENDING_TOOL_TTL_MS}
637
- * from both leader and subagent pending-tools Maps. Piggybacks on the same
638
- * 10-second interval to avoid a second timer.
639
- */
640
- sweep() {
641
- const now = Date.now();
642
- let removed = false;
643
- for (const [id, a] of this.agents) {
644
- const finished = a.status !== "running" && a.status !== "streaming" && a.status !== "waiting_user";
645
- const age = now - Date.parse(a.lastActivityAt);
646
- if (finished && Number.isFinite(age) && age > AGENT_REAP_MS) {
647
- this.agents.delete(id);
648
- removed = true;
649
- }
650
- }
651
- this.trimPendingTools(this.leaderPendingTools, now);
652
- this.trimPendingTools(this.subagentPendingTools, now);
653
- if (removed) this.flush();
654
- }
655
- /**
656
- * Evict pending-tool entries whose `startedAt` is older than
657
- * {@link PENDING_TOOL_TTL_MS}. These are tool.started / subagent.tool_started
658
- * events whose matching tool.executed / subagent.tool_executed never arrived
659
- * (provider crash, abort, process kill). Without periodic cleanup the
660
- * Maps retain the full tool-input objects for the process lifetime.
661
- */
662
- trimPendingTools(map, now) {
663
- const cutoff = now - PENDING_TOOL_TTL_MS;
664
- for (const [key, entry] of map) {
665
- if (entry.startedAt < cutoff) map.delete(key);
666
- }
667
- }
668
- flush() {
669
- const leaderEntry = {
670
- id: "leader",
671
- name: this.leaderName,
672
- startedAt: this.leaderStartedAt,
673
- status: this.leaderStatus,
674
- currentTool: this.leaderCurrentTool,
675
- currentTask: this.leaderCurrentTask,
676
- iterations: this.leaderIterations,
677
- toolCalls: this.leaderToolCalls,
678
- costUsd: this.leaderCostUsd,
679
- tokensIn: this.leaderTokensIn,
680
- tokensOut: this.leaderTokensOut,
681
- ctxPct: this.leaderCtxPct,
682
- model: this.leaderModel,
683
- partialText: this.leaderPartialText || void 0,
684
- recentTools: this.leaderRecentTools,
685
- recentMail: this.leaderRecentMail,
686
- todos: this.leaderTodos,
687
- latestPrompt: this.leaderLatestPrompt,
688
- latestPromptAt: this.leaderLatestPromptAt,
689
- activity: this.leaderActivity,
690
- lastActivityAt: (/* @__PURE__ */ new Date()).toISOString()
691
- };
692
- const allAgents = [leaderEntry, ...this.agents.values()];
693
- this.lastAgents = allAgents;
694
- try {
695
- this.events.emit("session.agents_updated", {
696
- sessionId: this.currentSessionId(),
697
- agents: allAgents
698
- });
699
- } catch {
700
- }
701
- const write = this.registry.updateAgents(allAgents);
702
- const chain = write.then(() => {
703
- try {
704
- this.onUpdate?.();
705
- } catch {
706
- }
707
- });
708
- if (this.flushPromise) {
709
- this.flushPromise = this.flushPromise.then(
710
- () => chain,
711
- () => chain
712
- ).catch(() => void 0);
713
- } else {
714
- this.flushPromise = chain.catch(() => void 0);
715
- }
716
- }
717
- currentSessionId() {
718
- return typeof this.sessionId === "function" ? this.sessionId() : this.sessionId;
719
- }
720
- acceptsSession(payload) {
721
- const expected = this.currentSessionId();
722
- if (!expected) return true;
723
- if (typeof payload !== "object" || payload === null) return true;
724
- const actual = payload.sessionId;
725
- return typeof actual !== "string" || actual.length === 0 || actual === expected;
726
- }
727
- markLeaderStarted(startedAt) {
728
- if (this.leaderStartedAt && (this.leaderStatus === "running" || this.leaderStatus === "streaming" || this.leaderStatus === "waiting_user")) {
729
- return;
730
- }
731
- this.leaderStartedAt = startedAt ?? (/* @__PURE__ */ new Date()).toISOString();
732
- }
733
- captureLeaderContext(ctx) {
734
- if (typeof ctx !== "object" || ctx === null) return;
735
- const c = ctx;
736
- if (typeof c.model === "string" && c.model.length > 0) this.leaderModel = c.model;
737
- const todos = compactTodos(c.todos);
738
- if (todos !== void 0) this.leaderTodos = todos;
739
- const metaLimit = c.meta?.["effectiveMaxContext"];
740
- const providerMax = c.provider?.capabilities?.maxContext;
741
- const maxContext = typeof metaLimit === "number" && metaLimit > 0 ? metaLimit : typeof providerMax === "number" && providerMax > 0 ? providerMax : void 0;
742
- if (typeof c.lastRequestTokens === "number" && c.lastRequestTokens > 0 && maxContext !== void 0) {
743
- this.leaderCtxPct = clampPct(Math.round(c.lastRequestTokens / maxContext * 100));
744
- }
745
- }
746
6
  };
747
-
748
- // src/fleet-notifier.ts
749
- import * as fs from "node:fs/promises";
750
- import * as path from "node:path";
751
-
752
- // src/utils/pid.ts
753
- function isPidAlive(pid) {
754
- if (!Number.isInteger(pid) || pid <= 0) return false;
755
- if (pid === process.pid) return true;
756
- try {
757
- process.kill(pid, 0);
758
- return true;
759
- } catch (err) {
760
- const code = err.code;
761
- if (code === "EPERM") return true;
762
- return false;
763
- }
7
+ var port = void 0;
8
+ function setBoardStorePort(impl) {
9
+ port = impl;
764
10
  }
765
-
766
- // src/fleet-notifier.ts
767
- var INSTANCES_FILE = "webui-instances.json";
768
- var DISCOVERY_TTL_MS = 2500;
769
- var COALESCE_MS = 50;
770
- var POST_TIMEOUT_MS = 500;
771
- var pidAlive = isPidAlive;
772
- function normRoot(root) {
773
- const resolved = path.resolve(root);
774
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
11
+ function boardStore() {
12
+ return port ?? notWired();
775
13
  }
776
- var FleetNotifier = class {
777
- baseDir;
778
- projectRoot;
779
- selfPid;
780
- doPost;
781
- cache = null;
782
- timer = null;
783
- disposed = false;
784
- constructor(opts) {
785
- this.baseDir = opts.baseDir;
786
- this.projectRoot = normRoot(opts.projectRoot);
787
- this.selfPid = opts.selfPid ?? process.pid;
788
- this.doPost = opts.post ?? defaultPost;
789
- }
790
- /** Coalesced, best-effort nudge. Safe to call on every status change. */
791
- notify() {
792
- if (this.disposed || this.timer) return;
793
- this.timer = setTimeout(() => {
794
- this.timer = null;
795
- void this.flush();
796
- }, COALESCE_MS);
797
- if (typeof this.timer.unref === "function") this.timer.unref();
798
- }
799
- /** Resolve same-project WebUI ping URLs (cached briefly). Exposed for tests. */
800
- async endpoints() {
801
- return (await this.targets()).map((t) => t.url);
802
- }
803
- /** Ping targets with their tokens (cached briefly). */
804
- async targets() {
805
- const now = Date.now();
806
- if (this.cache && now - this.cache.at < DISCOVERY_TTL_MS) return this.cache.targets;
807
- const targets = await this.discover();
808
- this.cache = { at: now, targets };
809
- return targets;
810
- }
811
- dispose() {
812
- this.disposed = true;
813
- if (this.timer) {
814
- clearTimeout(this.timer);
815
- this.timer = null;
816
- }
817
- }
818
- /** Re-scope notifications after an in-process project switch. */
819
- setProjectRoot(projectRoot) {
820
- this.projectRoot = normRoot(projectRoot);
821
- this.cache = null;
822
- }
823
- async flush() {
824
- const targets = await this.targets();
825
- await Promise.all(targets.map((t) => this.doPost(t.url, t.token).catch(() => void 0)));
826
- }
827
- async discover() {
828
- try {
829
- const raw = await fs.readFile(path.join(this.baseDir, INSTANCES_FILE), "utf8");
830
- const data = JSON.parse(raw);
831
- const list = Array.isArray(data?.instances) ? data.instances : [];
832
- return list.filter((i) => i && typeof i.httpPort === "number").filter((i) => i.pid !== this.selfPid).filter((i) => normRoot(i.projectRoot) === this.projectRoot).filter((i) => pidAlive(i.pid)).map((i) => {
833
- const host = i.host === "0.0.0.0" || i.host === "::" || !i.host ? "127.0.0.1" : i.host;
834
- return {
835
- url: `http://${host}:${i.httpPort}/api/fleet/ping`,
836
- token: typeof i.authToken === "string" ? i.authToken : void 0
837
- };
838
- });
839
- } catch {
840
- return [];
841
- }
842
- }
843
- };
844
- async function defaultPost(url, token) {
845
- await fetch(url, {
846
- method: "POST",
847
- // The API requires a token on every bind (H3). Sent as a header rather
848
- // than a query param so it never lands in an access log or the URL.
849
- ...token ? { headers: { "x-ws-token": token } } : {},
850
- signal: AbortSignal.timeout(POST_TIMEOUT_MS)
851
- });
14
+ function tryBoardStore() {
15
+ return port ?? null;
852
16
  }
853
17
 
854
18
  // src/session-catalog/client.ts
855
19
  import { spawn } from "node:child_process";
856
- import * as fs2 from "node:fs";
20
+ import * as fs from "node:fs";
857
21
  import * as net from "node:net";
858
- import * as path3 from "node:path";
22
+ import * as path2 from "node:path";
859
23
  import { fileURLToPath } from "node:url";
860
24
 
861
25
  // src/session-catalog/endpoint.ts
862
26
  import { createHash } from "node:crypto";
863
27
  import * as os from "node:os";
864
- import * as path2 from "node:path";
28
+ import * as path from "node:path";
865
29
 
866
30
  // src/session-catalog/protocol.ts
867
31
  var SESSION_CATALOG_PROTOCOL_VERSION = 1;
@@ -874,7 +38,7 @@ function encodeSessionCatalogMessage(message) {
874
38
  // src/session-catalog/endpoint.ts
875
39
  var SESSION_CATALOG_METADATA_FILE = ".session-catalog-server.json";
876
40
  function normalizedPath(value) {
877
- const resolved = path2.resolve(value);
41
+ const resolved = path.resolve(value);
878
42
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
879
43
  }
880
44
  function sessionCatalogProjectServerKey(projectDir) {
@@ -885,10 +49,10 @@ function sessionCatalogProjectServerEndpoint(projectDir) {
885
49
  if (process.platform === "win32") {
886
50
  return `\\\\.\\pipe\\wrongstack-session-catalog-v${SESSION_CATALOG_PROTOCOL_VERSION}-${key}`;
887
51
  }
888
- return path2.join(os.tmpdir(), `wssc-v${SESSION_CATALOG_PROTOCOL_VERSION}`, `${key}.sock`);
52
+ return path.join(os.tmpdir(), `wssc-v${SESSION_CATALOG_PROTOCOL_VERSION}`, `${key}.sock`);
889
53
  }
890
54
  function sessionCatalogProjectServerMetadataPath(projectDir) {
891
- return path2.join(projectDir, SESSION_CATALOG_METADATA_FILE);
55
+ return path.join(projectDir, SESSION_CATALOG_METADATA_FILE);
892
56
  }
893
57
 
894
58
  // src/session-catalog/client.ts
@@ -912,22 +76,22 @@ function locateServer(moduleUrl, exists) {
912
76
  }
913
77
  return null;
914
78
  }
915
- function resolveSessionCatalogDaemonAvailability(moduleUrl = import.meta.url, exists = fs2.existsSync) {
79
+ function resolveSessionCatalogDaemonAvailability(moduleUrl = import.meta.url, exists = fs.existsSync) {
916
80
  if (process.env["WRONGSTACK_SESSION_CATALOG_INLINE"] || process.env["WRONGSTACK_SESSION_CATALOG_SERVER"] === "0")
917
81
  return { kind: "inline-requested" };
918
82
  const url = locateServer(moduleUrl, exists);
919
83
  return url ? { kind: "available", url } : { kind: "missing-build" };
920
84
  }
921
- function resolveSessionCatalogProjectServerUrl(moduleUrl = import.meta.url, exists = fs2.existsSync) {
85
+ function resolveSessionCatalogProjectServerUrl(moduleUrl = import.meta.url, exists = fs.existsSync) {
922
86
  const availability = resolveSessionCatalogDaemonAvailability(moduleUrl, exists);
923
87
  return availability.kind === "available" ? availability.url : null;
924
88
  }
925
89
  function normalize(value) {
926
- const resolved = path3.resolve(value);
90
+ const resolved = path2.resolve(value);
927
91
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
928
92
  }
929
93
  function delay(ms) {
930
- return new Promise((resolve14) => setTimeout(resolve14, ms));
94
+ return new Promise((resolve13) => setTimeout(resolve13, ms));
931
95
  }
932
96
  var SessionCatalogProjectClient = class {
933
97
  constructor(options) {
@@ -995,7 +159,7 @@ var SessionCatalogProjectClient = class {
995
159
  if (result.stopped) {
996
160
  const metadataPath = sessionCatalogProjectServerMetadataPath(this.options.projectDir);
997
161
  const deadline = Date.now() + 5e3;
998
- while (fs2.existsSync(metadataPath) && Date.now() < deadline) await delay(20);
162
+ while (fs.existsSync(metadataPath) && Date.now() < deadline) await delay(20);
999
163
  if (result.pid && result.pid !== process.pid) {
1000
164
  while (Date.now() < deadline) {
1001
165
  try {
@@ -1017,8 +181,8 @@ var SessionCatalogProjectClient = class {
1017
181
  this.socket = null;
1018
182
  this.info = null;
1019
183
  if (socket && !socket.destroyed)
1020
- await new Promise((resolve14) => {
1021
- socket.once("close", resolve14);
184
+ await new Promise((resolve13) => {
185
+ socket.once("close", resolve13);
1022
186
  socket.end();
1023
187
  });
1024
188
  }
@@ -1056,7 +220,7 @@ var SessionCatalogProjectClient = class {
1056
220
  this.info = null;
1057
221
  this.authToken = void 0;
1058
222
  this.buffer = "";
1059
- return new Promise((resolve14, reject) => {
223
+ return new Promise((resolve13, reject) => {
1060
224
  const socket = net.createConnection(this.endpoint);
1061
225
  this.socket = socket;
1062
226
  socket.setEncoding("utf8");
@@ -1069,7 +233,7 @@ var SessionCatalogProjectClient = class {
1069
233
  clearTimeout(timer);
1070
234
  this.connectResolve = null;
1071
235
  this.connectReject = null;
1072
- resolve14();
236
+ resolve13();
1073
237
  };
1074
238
  this.connectReject = (error) => {
1075
239
  clearTimeout(timer);
@@ -1088,7 +252,7 @@ var SessionCatalogProjectClient = class {
1088
252
  if (this.authToken === void 0) {
1089
253
  try {
1090
254
  const parsed = JSON.parse(
1091
- fs2.readFileSync(sessionCatalogProjectServerMetadataPath(this.options.projectDir), "utf8")
255
+ fs.readFileSync(sessionCatalogProjectServerMetadataPath(this.options.projectDir), "utf8")
1092
256
  );
1093
257
  if (typeof parsed.authToken === "string" && parsed.authToken)
1094
258
  this.authToken = parsed.authToken;
@@ -1114,7 +278,7 @@ var SessionCatalogProjectClient = class {
1114
278
  });
1115
279
  if (encoded.length > SESSION_CATALOG_MAX_FRAME_CHARS)
1116
280
  return Promise.reject(new Error("Session Catalog request exceeded frame limit"));
1117
- return new Promise((resolve14, reject) => {
281
+ return new Promise((resolve13, reject) => {
1118
282
  const timer = setTimeout(() => {
1119
283
  const pending = this.pending.get(id);
1120
284
  if (!pending) return;
@@ -1126,7 +290,7 @@ var SessionCatalogProjectClient = class {
1126
290
  );
1127
291
  }, timeoutMs);
1128
292
  timer.unref?.();
1129
- this.pending.set(id, { resolve: resolve14, reject, timer });
293
+ this.pending.set(id, { resolve: resolve13, reject, timer });
1130
294
  socket.write(encoded);
1131
295
  });
1132
296
  }
@@ -1235,8 +399,8 @@ var SessionCatalogProjectClient = class {
1235
399
 
1236
400
  // src/session-catalog/registry.ts
1237
401
  import { randomUUID } from "node:crypto";
1238
- import * as fs3 from "node:fs/promises";
1239
- import * as path4 from "node:path";
402
+ import * as fs2 from "node:fs/promises";
403
+ import * as path3 from "node:path";
1240
404
  var HEARTBEAT_INTERVAL_MS = 5e3;
1241
405
  var AGENT_WRITE_THROTTLE_MS = 300;
1242
406
  var MAX_PROJECT_CLIENTS = 128;
@@ -1255,7 +419,7 @@ var ProjectSessionRegistry = class {
1255
419
  agentTimer;
1256
420
  lastAgentWriteAt = 0;
1257
421
  bindingKey(projectDir) {
1258
- const resolved = path4.resolve(projectDir);
422
+ const resolved = path3.resolve(projectDir);
1259
423
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
1260
424
  }
1261
425
  async closeBinding(binding) {
@@ -1265,7 +429,7 @@ var ProjectSessionRegistry = class {
1265
429
  await binding.client.close();
1266
430
  }
1267
431
  binding(projectSlug2, projectRoot) {
1268
- const projectDir = path4.join(this.globalRoot, "projects", projectSlug2);
432
+ const projectDir = path3.join(this.globalRoot, "projects", projectSlug2);
1269
433
  const key = this.bindingKey(projectDir);
1270
434
  let binding = this.clients.get(key);
1271
435
  if (!binding) {
@@ -1280,7 +444,7 @@ var ProjectSessionRegistry = class {
1280
444
  }
1281
445
  binding = {
1282
446
  projectDir,
1283
- projectRoot: path4.resolve(projectRoot),
447
+ projectRoot: path3.resolve(projectRoot),
1284
448
  client: new SessionCatalogProjectClient({ projectDir, projectRoot })
1285
449
  };
1286
450
  this.clients.set(key, binding);
@@ -1441,20 +605,20 @@ var ProjectSessionRegistry = class {
1441
605
  }
1442
606
  }
1443
607
  async list() {
1444
- const projectsDir = path4.join(this.globalRoot, "projects");
608
+ const projectsDir = path3.join(this.globalRoot, "projects");
1445
609
  let directories = [];
1446
610
  try {
1447
- directories = (await fs3.readdir(projectsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).slice(0, 1e3).map((entry) => entry.name);
611
+ directories = (await fs2.readdir(projectsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).slice(0, 1e3).map((entry) => entry.name);
1448
612
  } catch {
1449
613
  return [];
1450
614
  }
1451
615
  const snapshots = await Promise.all(
1452
616
  directories.map(async (slug) => {
1453
- const projectDir = path4.join(projectsDir, slug);
617
+ const projectDir = path3.join(projectsDir, slug);
1454
618
  let client;
1455
619
  try {
1456
620
  const metadata = JSON.parse(
1457
- await fs3.readFile(sessionCatalogProjectServerMetadataPath(projectDir), "utf8")
621
+ await fs2.readFile(sessionCatalogProjectServerMetadataPath(projectDir), "utf8")
1458
622
  );
1459
623
  if (typeof metadata.projectRoot !== "string" || !metadata.projectRoot) return [];
1460
624
  client = new SessionCatalogProjectClient({
@@ -1473,14 +637,14 @@ var ProjectSessionRegistry = class {
1473
637
  }
1474
638
  async listByProject(projectSlug2) {
1475
639
  const current = this.current;
1476
- if (current && path4.basename(current.binding.projectDir) === projectSlug2) {
640
+ if (current && path3.basename(current.binding.projectDir) === projectSlug2) {
1477
641
  return current.binding.client.call("list_live", {}).catch(() => []);
1478
642
  }
1479
- const projectDir = path4.join(this.globalRoot, "projects", projectSlug2);
643
+ const projectDir = path3.join(this.globalRoot, "projects", projectSlug2);
1480
644
  let client;
1481
645
  try {
1482
646
  const metadata = JSON.parse(
1483
- await fs3.readFile(sessionCatalogProjectServerMetadataPath(projectDir), "utf8")
647
+ await fs2.readFile(sessionCatalogProjectServerMetadataPath(projectDir), "utf8")
1484
648
  );
1485
649
  if (typeof metadata.projectRoot !== "string") return [];
1486
650
  client = new SessionCatalogProjectClient({ projectDir, projectRoot: metadata.projectRoot });
@@ -1502,7 +666,7 @@ var ProjectSessionRegistry = class {
1502
666
  });
1503
667
  }
1504
668
  get registryPath() {
1505
- return path4.join(this.globalRoot, "projects");
669
+ return path3.join(this.globalRoot, "projects");
1506
670
  }
1507
671
  async dispose() {
1508
672
  await this.unregister().catch(() => void 0);
@@ -1545,7 +709,7 @@ var ProjectSessionRegistry = class {
1545
709
  var registries = /* @__PURE__ */ new Map();
1546
710
  var lastRegistryKey;
1547
711
  function getProjectSessionRegistry(globalRoot) {
1548
- const key = globalRoot !== void 0 ? path4.resolve(globalRoot) : lastRegistryKey;
712
+ const key = globalRoot !== void 0 ? path3.resolve(globalRoot) : lastRegistryKey;
1549
713
  if (!key)
1550
714
  throw new Error("SessionRegistry not initialized. Call getSessionRegistry(globalRoot) first.");
1551
715
  let registry = registries.get(key);
@@ -1568,31 +732,31 @@ function getProjectSessionRegistry(globalRoot) {
1568
732
  }
1569
733
  function hasProjectSessionRegistry(globalRoot) {
1570
734
  if (globalRoot === void 0) return registries.size > 0;
1571
- return registries.has(path4.resolve(globalRoot));
735
+ return registries.has(path3.resolve(globalRoot));
1572
736
  }
1573
737
 
1574
738
  // src/utils/wstack-paths.ts
1575
739
  import { createHash as createHash2 } from "node:crypto";
1576
- import * as fs4 from "node:fs";
740
+ import * as fs3 from "node:fs";
1577
741
  import * as os2 from "node:os";
1578
- import * as path5 from "node:path";
742
+ import * as path4 from "node:path";
1579
743
  function canonicalProjectRoot(absRoot) {
1580
- const checkoutRoot = path5.resolve(absRoot);
1581
- const dotGit = path5.join(checkoutRoot, ".git");
744
+ const checkoutRoot = path4.resolve(absRoot);
745
+ const dotGit = path4.join(checkoutRoot, ".git");
1582
746
  try {
1583
- if (!fs4.statSync(dotGit).isFile()) return checkoutRoot;
1584
- const gitDirLine = fs4.readFileSync(dotGit, "utf8").trim();
747
+ if (!fs3.statSync(dotGit).isFile()) return checkoutRoot;
748
+ const gitDirLine = fs3.readFileSync(dotGit, "utf8").trim();
1585
749
  const match = /^gitdir:\s*(.+)$/i.exec(gitDirLine);
1586
750
  if (!match?.[1]) return checkoutRoot;
1587
- const gitDir = path5.resolve(checkoutRoot, match[1].trim());
1588
- const commonDirFile = path5.join(gitDir, "commondir");
1589
- if (!fs4.statSync(commonDirFile).isFile()) return checkoutRoot;
1590
- const commonDir = path5.resolve(gitDir, fs4.readFileSync(commonDirFile, "utf8").trim());
1591
- const worktreesDir = path5.dirname(gitDir);
1592
- if (path5.basename(worktreesDir).toLowerCase() !== "worktrees") return checkoutRoot;
1593
- if (path5.resolve(worktreesDir, "..") !== commonDir) return checkoutRoot;
1594
- if (path5.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
1595
- return path5.dirname(commonDir);
751
+ const gitDir = path4.resolve(checkoutRoot, match[1].trim());
752
+ const commonDirFile = path4.join(gitDir, "commondir");
753
+ if (!fs3.statSync(commonDirFile).isFile()) return checkoutRoot;
754
+ const commonDir = path4.resolve(gitDir, fs3.readFileSync(commonDirFile, "utf8").trim());
755
+ const worktreesDir = path4.dirname(gitDir);
756
+ if (path4.basename(worktreesDir).toLowerCase() !== "worktrees") return checkoutRoot;
757
+ if (path4.resolve(worktreesDir, "..") !== commonDir) return checkoutRoot;
758
+ if (path4.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
759
+ return path4.dirname(commonDir);
1596
760
  } catch {
1597
761
  return checkoutRoot;
1598
762
  }
@@ -1602,7 +766,7 @@ function projectHash(absRoot) {
1602
766
  }
1603
767
  function projectSlug(absRoot) {
1604
768
  const identityRoot = canonicalProjectRoot(absRoot);
1605
- const base = slugify(path5.basename(identityRoot));
769
+ const base = slugify(path4.basename(identityRoot));
1606
770
  const hash = createHash2("sha256").update(identityRoot).digest("hex").slice(0, 6);
1607
771
  return `${base}-${hash}`;
1608
772
  }
@@ -1615,7 +779,7 @@ function safeProfileName(name) {
1615
779
  }
1616
780
  function bootstrapProfileName(globalRoot) {
1617
781
  try {
1618
- const parsed = JSON.parse(fs4.readFileSync(path5.join(globalRoot, "config.json"), "utf8"));
782
+ const parsed = JSON.parse(fs3.readFileSync(path4.join(globalRoot, "config.json"), "utf8"));
1619
783
  return safeProfileName(
1620
784
  typeof parsed.activeProfile === "string" ? parsed.activeProfile : void 0
1621
785
  );
@@ -1625,17 +789,17 @@ function bootstrapProfileName(globalRoot) {
1625
789
  }
1626
790
  function wstackGlobalRoot() {
1627
791
  const fromEnv = process.env["WRONGSTACK_HOME"];
1628
- if (fromEnv && fromEnv.trim().length > 0) return path5.resolve(fromEnv);
1629
- return path5.join(os2.homedir(), ".wrongstack");
792
+ if (fromEnv && fromEnv.trim().length > 0) return path4.resolve(fromEnv);
793
+ return path4.join(os2.homedir(), ".wrongstack");
1630
794
  }
1631
795
  function resolveWstackPaths(opts) {
1632
- const globalRoot = opts.globalRoot ?? (opts.userHome ? path5.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
796
+ const globalRoot = opts.globalRoot ?? (opts.userHome ? path4.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
1633
797
  const homeDir = opts.userHome ?? os2.homedir();
1634
798
  const profileName = safeProfileName(opts.profileName ?? bootstrapProfileName(globalRoot));
1635
- const profileDir = path5.join(globalRoot, "profiles", profileName);
799
+ const profileDir = path4.join(globalRoot, "profiles", profileName);
1636
800
  const hash = projectHash(opts.projectRoot);
1637
801
  const slug = projectSlug(opts.projectRoot);
1638
- const projectDir = path5.join(globalRoot, "projects", slug);
802
+ const projectDir = path4.join(globalRoot, "projects", slug);
1639
803
  return {
1640
804
  globalRoot,
1641
805
  profileName,
@@ -1643,69 +807,69 @@ function resolveWstackPaths(opts) {
1643
807
  projectRoot: opts.projectRoot,
1644
808
  homeDir,
1645
809
  configDir: profileDir,
1646
- globalConfig: path5.join(globalRoot, "config.json"),
1647
- profilesDir: path5.join(globalRoot, "profiles"),
810
+ globalConfig: path4.join(globalRoot, "config.json"),
811
+ profilesDir: path4.join(globalRoot, "profiles"),
1648
812
  profileConfig: (name) => {
1649
- return path5.join(globalRoot, "profiles", safeProfileName(name), "config.json");
813
+ return path4.join(globalRoot, "profiles", safeProfileName(name), "config.json");
1650
814
  },
1651
815
  profileStatuslineConfig: (name) => {
1652
- return path5.join(globalRoot, "profiles", safeProfileName(name), "statusline.json");
816
+ return path4.join(globalRoot, "profiles", safeProfileName(name), "statusline.json");
1653
817
  },
1654
818
  profileModeConfig: (name) => {
1655
- return path5.join(globalRoot, "profiles", safeProfileName(name), "mode.json");
819
+ return path4.join(globalRoot, "profiles", safeProfileName(name), "mode.json");
1656
820
  },
1657
821
  profileProviderStatus: (name) => {
1658
- return path5.join(globalRoot, "profiles", safeProfileName(name), "provider-status.json");
822
+ return path4.join(globalRoot, "profiles", safeProfileName(name), "provider-status.json");
1659
823
  },
1660
824
  profileUpdateCache: (name) => {
1661
- return path5.join(globalRoot, "profiles", safeProfileName(name), "update-cache.json");
825
+ return path4.join(globalRoot, "profiles", safeProfileName(name), "update-cache.json");
1662
826
  },
1663
- secretsKey: path5.join(globalRoot, ".key"),
1664
- globalMemory: path5.join(profileDir, "memory.md"),
1665
- globalSkills: path5.join(profileDir, "skills"),
1666
- globalClaudeSkills: path5.join(homeDir, ".claude", "skills"),
1667
- globalDesignKits: path5.join(profileDir, "design-kits"),
1668
- globalPrompts: path5.join(profileDir, "prompts"),
1669
- globalInstructions: path5.join(profileDir, "instructions"),
1670
- promptUsage: path5.join(profileDir, "prompt-usage.json"),
1671
- cacheDir: path5.join(globalRoot, "cache"),
1672
- modelsCache: path5.join(globalRoot, "cache", "models.dev.json"),
1673
- modelsOverlayCache: path5.join(globalRoot, "cache", "models-overlay.json"),
1674
- historyFile: path5.join(profileDir, "history"),
1675
- logFile: path5.join(globalRoot, "logs", "wrongstack.log"),
827
+ secretsKey: path4.join(globalRoot, ".key"),
828
+ globalMemory: path4.join(profileDir, "memory.md"),
829
+ globalSkills: path4.join(profileDir, "skills"),
830
+ globalClaudeSkills: path4.join(homeDir, ".claude", "skills"),
831
+ globalDesignKits: path4.join(profileDir, "design-kits"),
832
+ globalPrompts: path4.join(profileDir, "prompts"),
833
+ globalInstructions: path4.join(profileDir, "instructions"),
834
+ promptUsage: path4.join(profileDir, "prompt-usage.json"),
835
+ cacheDir: path4.join(globalRoot, "cache"),
836
+ modelsCache: path4.join(globalRoot, "cache", "models.dev.json"),
837
+ modelsOverlayCache: path4.join(globalRoot, "cache", "models-overlay.json"),
838
+ historyFile: path4.join(profileDir, "history"),
839
+ logFile: path4.join(globalRoot, "logs", "wrongstack.log"),
1676
840
  projectDir,
1677
- projectCodebaseIndex: path5.join(projectDir, "codebase-index"),
1678
- projectMemory: path5.join(projectDir, "memory.md"),
1679
- projectSessions: path5.join(projectDir, "sessions"),
1680
- projectTrust: path5.join(projectDir, "trust.json"),
1681
- projectMeta: path5.join(projectDir, "meta.json"),
1682
- projectLocalConfig: path5.join(projectDir, "config.local.json"),
1683
- inProjectConfig: path5.join(opts.projectRoot, ".wrongstack", "config.json"),
1684
- inProjectAgentsFile: path5.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
1685
- inProjectSkills: path5.join(opts.projectRoot, ".wrongstack", "skills"),
1686
- inProjectClaudeSkills: path5.join(opts.projectRoot, ".claude", "skills"),
1687
- inProjectPrompts: path5.join(opts.projectRoot, ".wrongstack", "prompts"),
1688
- inProjectInstructions: path5.join(opts.projectRoot, ".wrongstack", "instructions"),
1689
- inProjectDesignKits: path5.join(opts.projectRoot, ".wrongstack", "design-kits"),
1690
- inProjectWorktrees: path5.join(opts.projectRoot, ".wrongstack", "worktrees"),
841
+ projectCodebaseIndex: path4.join(projectDir, "codebase-index"),
842
+ projectMemory: path4.join(projectDir, "memory.md"),
843
+ projectSessions: path4.join(projectDir, "sessions"),
844
+ projectTrust: path4.join(projectDir, "trust.json"),
845
+ projectMeta: path4.join(projectDir, "meta.json"),
846
+ projectLocalConfig: path4.join(projectDir, "config.local.json"),
847
+ inProjectConfig: path4.join(opts.projectRoot, ".wrongstack", "config.json"),
848
+ inProjectAgentsFile: path4.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
849
+ inProjectSkills: path4.join(opts.projectRoot, ".wrongstack", "skills"),
850
+ inProjectClaudeSkills: path4.join(opts.projectRoot, ".claude", "skills"),
851
+ inProjectPrompts: path4.join(opts.projectRoot, ".wrongstack", "prompts"),
852
+ inProjectInstructions: path4.join(opts.projectRoot, ".wrongstack", "instructions"),
853
+ inProjectDesignKits: path4.join(opts.projectRoot, ".wrongstack", "design-kits"),
854
+ inProjectWorktrees: path4.join(opts.projectRoot, ".wrongstack", "worktrees"),
1691
855
  projectHash: hash,
1692
856
  projectSlug: slug,
1693
- projectGoal: path5.join(projectDir, "goal.json"),
1694
- projectInputHistory: path5.join(projectDir, "input-history.json"),
1695
- projectSpecs: path5.join(projectDir, "specs"),
1696
- projectTaskGraphs: path5.join(projectDir, "task-graphs"),
1697
- projectSddSession: path5.join(projectDir, "sdd-session.json"),
1698
- projectPlan: path5.join(projectDir, "plan.json"),
1699
- projectAutophase: path5.join(projectDir, "autophase"),
1700
- projectSddBoards: path5.join(projectDir, "sdd-boards"),
1701
- projectRequirementIntakes: path5.join(projectDir, "requirement-intakes"),
1702
- syncConfig: path5.join(profileDir, "sync.json"),
1703
- configHistoryDir: path5.join(globalRoot, "config-history"),
857
+ projectGoal: path4.join(projectDir, "goal.json"),
858
+ projectInputHistory: path4.join(projectDir, "input-history.json"),
859
+ projectSpecs: path4.join(projectDir, "specs"),
860
+ projectTaskGraphs: path4.join(projectDir, "task-graphs"),
861
+ projectSddSession: path4.join(projectDir, "sdd-session.json"),
862
+ projectPlan: path4.join(projectDir, "plan.json"),
863
+ projectAutophase: path4.join(projectDir, "autophase"),
864
+ projectSddBoards: path4.join(projectDir, "sdd-boards"),
865
+ projectRequirementIntakes: path4.join(projectDir, "requirement-intakes"),
866
+ syncConfig: path4.join(profileDir, "sync.json"),
867
+ configHistoryDir: path4.join(globalRoot, "config-history"),
1704
868
  projectStatus: (statusProjectSlug) => {
1705
869
  if (!/^[a-z0-9](?:[a-z0-9-]{0,39})-[a-f0-9]{6}$/.test(statusProjectSlug)) {
1706
870
  throw new Error(`Invalid project slug: ${statusProjectSlug}`);
1707
871
  }
1708
- return path5.join(globalRoot, "projects", statusProjectSlug, "status.json");
872
+ return path4.join(globalRoot, "projects", statusProjectSlug, "status.json");
1709
873
  }
1710
874
  };
1711
875
  }
@@ -1738,6 +902,14 @@ var PATTERNS = [
1738
902
  anchor: "sk-ant-"
1739
903
  },
1740
904
  { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g, anchor: "sk-" },
905
+ {
906
+ // `xai` is a first-class provider in this codebase, but its key shape was
907
+ // absent here — so the one credential format WrongStack itself hands users
908
+ // was the one the scrubber could not recognize (audit 2026-08-20).
909
+ type: "xai_key",
910
+ regex: /(?<![A-Za-z0-9])xai-[A-Za-z0-9]{20,}(?![A-Za-z0-9])/g,
911
+ anchor: "xai-"
912
+ },
1741
913
  { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g, anchor: "ghp_" },
1742
914
  { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g, anchor: "github_pat_" },
1743
915
  { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g, anchor: "AKIA" },
@@ -1828,8 +1000,8 @@ var PATTERNS = [
1828
1000
  // replacement so the separator between adjacent secrets is preserved
1829
1001
  // rather than collapsed. Capture groups are therefore: 1=leading
1830
1002
  // delimiter, 2=key name, 3=value.
1831
- regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
1832
- anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
1003
+ regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD|PASSPHRASE))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
1004
+ anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD", "PASSPHRASE"]
1833
1005
  },
1834
1006
  {
1835
1007
  type: "json_credential_key",
@@ -1946,6 +1118,27 @@ var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key
1946
1118
  var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
1947
1119
  var SCRUB_CHUNK_BYTES = 64 * 1024;
1948
1120
  var SCRUB_OVERLAP_BYTES = 1024;
1121
+ var PEM_PRIVATE_KEY_BEGIN_RE = /-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----/;
1122
+ var PEM_END_MARKER = "-----END";
1123
+ var MAX_PEM_BLOCK_BYTES = 64 * 1024;
1124
+ var PEM_END_LINE_TOLERANCE = 64;
1125
+ function extendChunkBoundaryPastPem(text, chunkStart, proposedEnd) {
1126
+ const head = text.slice(chunkStart, proposedEnd);
1127
+ const lastBegin = head.lastIndexOf("-----BEGIN ");
1128
+ if (lastBegin === -1) return proposedEnd;
1129
+ const fromBegin = text.slice(chunkStart + lastBegin);
1130
+ const marker = PEM_PRIVATE_KEY_BEGIN_RE.exec(fromBegin);
1131
+ if (!marker || marker.index !== 0) return proposedEnd;
1132
+ const bodyStart = marker[0].length;
1133
+ const cap = Math.min(text.length, chunkStart + lastBegin + MAX_PEM_BLOCK_BYTES);
1134
+ const closeIdx = fromBegin.indexOf(PEM_END_MARKER, bodyStart);
1135
+ if (closeIdx === -1 || chunkStart + lastBegin + closeIdx >= cap + PEM_END_LINE_TOLERANCE) {
1136
+ return proposedEnd;
1137
+ }
1138
+ const lineEnd = fromBegin.indexOf("\n", closeIdx);
1139
+ const end = lineEnd === -1 ? text.length : chunkStart + lastBegin + lineEnd + 1;
1140
+ return Math.max(proposedEnd, end);
1141
+ }
1949
1142
  var PATTERN_ANCHORS = [
1950
1143
  ...new Set(
1951
1144
  PATTERNS.flatMap(
@@ -1982,6 +1175,7 @@ var DefaultSecretScrubber = class {
1982
1175
  }
1983
1176
  }
1984
1177
  end = safe === -1 ? end : safe + 1;
1178
+ end = extendChunkBoundaryPastPem(text, i, end);
1985
1179
  }
1986
1180
  out.push(this.scrubOne(text.slice(i, end)));
1987
1181
  i = end;
@@ -2188,15 +1382,29 @@ var atomicReplaceWithWriter = primitives.atomicReplaceWithWriter;
2188
1382
  var ensureDir = primitives.ensureDir;
2189
1383
  var withFileLock = primitives.withFileLock;
2190
1384
 
1385
+ // src/utils/pid.ts
1386
+ function isPidAlive(pid) {
1387
+ if (!Number.isInteger(pid) || pid <= 0) return false;
1388
+ if (pid === process.pid) return true;
1389
+ try {
1390
+ process.kill(pid, 0);
1391
+ return true;
1392
+ } catch (err) {
1393
+ const code = err.code;
1394
+ if (code === "EPERM") return true;
1395
+ return false;
1396
+ }
1397
+ }
1398
+
2191
1399
  // src/utils/session-scoped-path.ts
2192
- import * as path6 from "node:path";
1400
+ import * as path5 from "node:path";
2193
1401
  function sessionScopedPath(dir, sessionId, suffix) {
2194
1402
  if (!sessionId || sessionId.includes("\\") || sessionId.includes("..")) {
2195
1403
  throw invalid(sessionId);
2196
1404
  }
2197
- const resolved = path6.resolve(dir, `${sessionId}${suffix}`);
2198
- const rel = path6.relative(path6.resolve(dir), resolved);
2199
- if (rel.startsWith("..") || path6.isAbsolute(rel)) {
1405
+ const resolved = path5.resolve(dir, `${sessionId}${suffix}`);
1406
+ const rel = path5.relative(path5.resolve(dir), resolved);
1407
+ if (rel.startsWith("..") || path5.isAbsolute(rel)) {
2200
1408
  throw invalid(sessionId);
2201
1409
  }
2202
1410
  return resolved;
@@ -2224,15 +1432,15 @@ function invalid(sessionId) {
2224
1432
  });
2225
1433
  }
2226
1434
 
2227
- // src/session-registry.ts
2228
- import * as fs6 from "node:fs/promises";
2229
- import * as path8 from "node:path";
1435
+ // src/session-catalog/session-registry.ts
1436
+ import * as fs5 from "node:fs/promises";
1437
+ import * as path7 from "node:path";
2230
1438
 
2231
- // src/session-registry-atomic-file.ts
1439
+ // src/session-catalog/session-registry-atomic-file.ts
2232
1440
  import { randomUUID as randomUUID2 } from "node:crypto";
2233
- import * as fs5 from "node:fs/promises";
1441
+ import * as fs4 from "node:fs/promises";
2234
1442
  import { hostname } from "node:os";
2235
- import * as path7 from "node:path";
1443
+ import * as path6 from "node:path";
2236
1444
  var STALE_LOCK_MS = 1e4;
2237
1445
  var SAME_HOST_STALE_MS = 2 * STALE_LOCK_MS;
2238
1446
  var STALE_TMP_MS = 6e4;
@@ -2242,32 +1450,32 @@ function lockOwnerStamp() {
2242
1450
  return `${HOST_NAME}:${process.pid}`;
2243
1451
  }
2244
1452
  async function maybeUnlinkOwnedLock(lockPath) {
2245
- const owner = await fs5.readFile(lockPath, "utf8").catch(() => null);
1453
+ const owner = await fs4.readFile(lockPath, "utf8").catch(() => null);
2246
1454
  if (owner === null || owner.trim() !== lockOwnerStamp()) return;
2247
1455
  for (let attempt = 0; attempt < 3; attempt++) {
2248
1456
  try {
2249
- await fs5.unlink(lockPath);
1457
+ await fs4.unlink(lockPath);
2250
1458
  return;
2251
1459
  } catch {
2252
- if (attempt < 2) await new Promise((resolve14) => setTimeout(resolve14, 5));
1460
+ if (attempt < 2) await new Promise((resolve13) => setTimeout(resolve13, 5));
2253
1461
  }
2254
1462
  }
2255
1463
  }
2256
1464
  async function breakStaleLock(lockPath) {
2257
1465
  try {
2258
- const content = await fs5.readFile(lockPath, "utf8").catch(() => "");
1466
+ const content = await fs4.readFile(lockPath, "utf8").catch(() => "");
2259
1467
  const trimmed = content.trim();
2260
1468
  const colonIdx = trimmed.indexOf(":");
2261
1469
  if (colonIdx === -1) {
2262
1470
  const barePid = Number.parseInt(trimmed, 10);
2263
1471
  if (Number.isInteger(barePid) && barePid > 0 && !isPidAlive(barePid)) {
2264
1472
  return await breakStaleLockVerified(lockPath, async () => {
2265
- const reread = await fs5.readFile(lockPath, "utf8").catch(() => "");
1473
+ const reread = await fs4.readFile(lockPath, "utf8").catch(() => "");
2266
1474
  return reread.trim() === trimmed;
2267
1475
  });
2268
1476
  }
2269
1477
  return await breakStaleLockVerified(lockPath, async () => {
2270
- const st = await fs5.stat(lockPath);
1478
+ const st = await fs4.stat(lockPath);
2271
1479
  return Date.now() - st.mtimeMs > STALE_LOCK_MS;
2272
1480
  });
2273
1481
  }
@@ -2275,22 +1483,22 @@ async function breakStaleLock(lockPath) {
2275
1483
  const ownerPid = Number.parseInt(trimmed.slice(colonIdx + 1), 10);
2276
1484
  if (ownerHost === HOST_NAME && Number.isInteger(ownerPid) && ownerPid > 0) {
2277
1485
  if (isPidAlive(ownerPid)) {
2278
- const stat19 = await fs5.stat(lockPath);
1486
+ const stat19 = await fs4.stat(lockPath);
2279
1487
  if (Date.now() - stat19.mtimeMs > SAME_HOST_STALE_MS) {
2280
1488
  return await breakStaleLockVerified(lockPath, async () => {
2281
- const st = await fs5.stat(lockPath);
1489
+ const st = await fs4.stat(lockPath);
2282
1490
  return Date.now() - st.mtimeMs > SAME_HOST_STALE_MS;
2283
1491
  });
2284
1492
  }
2285
1493
  return false;
2286
1494
  }
2287
1495
  return await breakStaleLockVerified(lockPath, async () => {
2288
- const reread = await fs5.readFile(lockPath, "utf8").catch(() => "");
1496
+ const reread = await fs4.readFile(lockPath, "utf8").catch(() => "");
2289
1497
  return reread.trim() === trimmed;
2290
1498
  });
2291
1499
  }
2292
1500
  return await breakStaleLockVerified(lockPath, async () => {
2293
- const st = await fs5.stat(lockPath);
1501
+ const st = await fs4.stat(lockPath);
2294
1502
  return Date.now() - st.mtimeMs > STALE_LOCK_MS;
2295
1503
  });
2296
1504
  } catch {
@@ -2304,21 +1512,21 @@ async function breakStaleLockVerified(lockPath, verify) {
2304
1512
  async function breakLockAtomically(lockPath) {
2305
1513
  const tombstone = `${lockPath}.stale-${randomUUID2()}.tmp`;
2306
1514
  try {
2307
- await fs5.rename(lockPath, tombstone);
1515
+ await fs4.rename(lockPath, tombstone);
2308
1516
  } catch {
2309
1517
  return false;
2310
1518
  }
2311
- void fs5.unlink(tombstone).catch(() => void 0);
1519
+ void fs4.unlink(tombstone).catch(() => void 0);
2312
1520
  return true;
2313
1521
  }
2314
1522
  async function writeAtomicFile(filePath, registry) {
2315
- const tmp = path7.join(
2316
- path7.dirname(filePath),
2317
- `.${path7.basename(filePath)}.${randomUUID2().slice(0, 8)}.tmp`
1523
+ const tmp = path6.join(
1524
+ path6.dirname(filePath),
1525
+ `.${path6.basename(filePath)}.${randomUUID2().slice(0, 8)}.tmp`
2318
1526
  );
2319
1527
  let tmpPersisted = false;
2320
1528
  try {
2321
- const handle = await fs5.open(tmp, "w");
1529
+ const handle = await fs4.open(tmp, "w");
2322
1530
  try {
2323
1531
  await handle.writeFile(JSON.stringify(registry, null, 2), "utf8");
2324
1532
  await handle.sync().catch(() => void 0);
@@ -2327,14 +1535,14 @@ async function writeAtomicFile(filePath, registry) {
2327
1535
  }
2328
1536
  tmpPersisted = true;
2329
1537
  try {
2330
- await fs5.rename(tmp, filePath);
1538
+ await fs4.rename(tmp, filePath);
2331
1539
  tmpPersisted = false;
2332
1540
  } catch (renameErr) {
2333
1541
  const code = renameErr?.code;
2334
1542
  if (code === "EPERM" || code === "EBUSY" || code === "EACCES") {
2335
- await fs5.copyFile(tmp, filePath);
1543
+ await fs4.copyFile(tmp, filePath);
2336
1544
  try {
2337
- const destHandle = await fs5.open(filePath, "r+");
1545
+ const destHandle = await fs4.open(filePath, "r+");
2338
1546
  try {
2339
1547
  await destHandle.sync().catch(() => void 0);
2340
1548
  } finally {
@@ -2342,41 +1550,41 @@ async function writeAtomicFile(filePath, registry) {
2342
1550
  }
2343
1551
  } catch {
2344
1552
  }
2345
- await fs5.unlink(tmp).catch(() => void 0);
1553
+ await fs4.unlink(tmp).catch(() => void 0);
2346
1554
  tmpPersisted = false;
2347
1555
  } else {
2348
1556
  throw renameErr;
2349
1557
  }
2350
1558
  }
2351
1559
  } catch (err) {
2352
- if (tmpPersisted) await fs5.unlink(tmp).catch(() => void 0);
1560
+ if (tmpPersisted) await fs4.unlink(tmp).catch(() => void 0);
2353
1561
  throw err;
2354
1562
  }
2355
1563
  }
2356
1564
  async function pruneStaleTempFiles(filePath) {
2357
1565
  try {
2358
- const dir = path7.dirname(filePath);
2359
- const base = path7.basename(filePath);
1566
+ const dir = path6.dirname(filePath);
1567
+ const base = path6.basename(filePath);
2360
1568
  const now = Date.now();
2361
1569
  const stale = [];
2362
- for (const name of await fs5.readdir(dir)) {
1570
+ for (const name of await fs4.readdir(dir)) {
2363
1571
  const isTemp = (name.startsWith(`${base}.`) || name.startsWith(`.${base}.`)) && name.endsWith(".tmp");
2364
1572
  if (!isTemp) continue;
2365
- const stat19 = await fs5.stat(path7.join(dir, name)).catch(() => null);
1573
+ const stat19 = await fs4.stat(path6.join(dir, name)).catch(() => null);
2366
1574
  if (!stat19) continue;
2367
1575
  if (now - stat19.mtimeMs > STALE_TMP_MS) stale.push({ name, mtimeMs: stat19.mtimeMs });
2368
1576
  }
2369
- stale.sort((a, b) => b.mtimeMs - a.mtimeMs);
1577
+ stale.sort((a, b) => a.mtimeMs - b.mtimeMs);
2370
1578
  await Promise.all(
2371
- stale.slice(MAX_STALE_TMP_FILES).map(async ({ name }) => {
2372
- await fs5.unlink(path7.join(dir, name)).catch(() => void 0);
1579
+ stale.slice(0, MAX_STALE_TMP_FILES).map(async ({ name }) => {
1580
+ await fs4.unlink(path6.join(dir, name)).catch(() => void 0);
2373
1581
  })
2374
1582
  );
2375
1583
  } catch {
2376
1584
  }
2377
1585
  }
2378
1586
 
2379
- // src/session-registry.ts
1587
+ // src/session-catalog/session-registry.ts
2380
1588
  var REGISTRY_FILE = "session-registry.json";
2381
1589
  var HEARTBEAT_INTERVAL_MS2 = 5e3;
2382
1590
  var STALE_TIMEOUT_MS = 3e4;
@@ -2392,7 +1600,7 @@ var SessionOwnershipConflictError = class extends Error {
2392
1600
  var SessionRegistryWriteError = class extends Error {
2393
1601
  };
2394
1602
  var TEMP_PRUNE_INTERVAL_MS = STALE_TMP_MS;
2395
- var pidAlive2 = isPidAlive;
1603
+ var pidAlive = isPidAlive;
2396
1604
  function sameOwner(entry, owner) {
2397
1605
  return entry.pid === owner.pid && entry.startedAt === owner.startedAt;
2398
1606
  }
@@ -2433,7 +1641,7 @@ var SessionRegistry = class {
2433
1641
  lastEntry = null;
2434
1642
  ownershipLockWaitMs;
2435
1643
  constructor(globalRoot, options = {}) {
2436
- this.filePath = path8.join(globalRoot, REGISTRY_FILE);
1644
+ this.filePath = path7.join(globalRoot, REGISTRY_FILE);
2437
1645
  this.ownershipLockWaitMs = Math.max(0, options.ownershipLockWaitMs ?? OWNERSHIP_LOCK_WAIT_MS);
2438
1646
  }
2439
1647
  // ── Public API ──────────────────────────────────────────────────────────
@@ -2451,7 +1659,7 @@ var SessionRegistry = class {
2451
1659
  };
2452
1660
  const existingRegistry = await this.readAndPrune();
2453
1661
  const currentOwner = existingRegistry[entry.sessionId];
2454
- if (currentOwner && currentOwner.pid !== entry.pid && pidAlive2(currentOwner.pid)) {
1662
+ if (currentOwner && currentOwner.pid !== entry.pid && pidAlive(currentOwner.pid)) {
2455
1663
  throw new SessionOwnershipConflictError(
2456
1664
  `Session ${entry.sessionId} is already open in another running wstack (pid ${currentOwner.pid}).`
2457
1665
  );
@@ -2464,12 +1672,12 @@ var SessionRegistry = class {
2464
1672
  continue;
2465
1673
  }
2466
1674
  const heartbeatAt = Date.parse(existing.lastHeartbeatAt);
2467
- if (!Number.isFinite(heartbeatAt) || now - heartbeatAt > PID_CHECK_AFTER_MS && !pidAlive2(existing.pid)) {
1675
+ if (!Number.isFinite(heartbeatAt) || now - heartbeatAt > PID_CHECK_AFTER_MS && !pidAlive(existing.pid)) {
2468
1676
  delete registry[id];
2469
1677
  }
2470
1678
  }
2471
1679
  const lockedOwner = registry[entry.sessionId];
2472
- if (lockedOwner && lockedOwner.pid !== entry.pid && pidAlive2(lockedOwner.pid)) {
1680
+ if (lockedOwner && lockedOwner.pid !== entry.pid && pidAlive(lockedOwner.pid)) {
2473
1681
  throw new SessionOwnershipConflictError(
2474
1682
  `Session ${entry.sessionId} is already open in another running wstack (pid ${lockedOwner.pid}).`
2475
1683
  );
@@ -2515,13 +1723,13 @@ var SessionRegistry = class {
2515
1723
  }
2516
1724
  if (!this.agentsFlushTimer) {
2517
1725
  const delay2 = Math.max(0, AGENTS_WRITE_THROTTLE_MS - sinceLastWrite);
2518
- this.pendingAgentsFlush = new Promise((resolve14, reject) => {
2519
- this.pendingAgentsResolve = resolve14;
1726
+ this.pendingAgentsFlush = new Promise((resolve13, reject) => {
1727
+ this.pendingAgentsResolve = resolve13;
2520
1728
  const timer = setTimeout(() => {
2521
1729
  this.agentsFlushTimer = null;
2522
1730
  this.pendingAgentsFlush = null;
2523
1731
  this.pendingAgentsResolve = null;
2524
- this.writeAgentsSnapshot().then(resolve14, reject);
1732
+ this.writeAgentsSnapshot().then(resolve13, reject);
2525
1733
  }, delay2);
2526
1734
  if (typeof timer.unref === "function") timer.unref();
2527
1735
  this.agentsFlushTimer = timer;
@@ -2672,7 +1880,7 @@ var SessionRegistry = class {
2672
1880
  }
2673
1881
  async readAndPrune() {
2674
1882
  try {
2675
- const raw = await fs6.readFile(this.filePath, "utf8");
1883
+ const raw = await fs5.readFile(this.filePath, "utf8");
2676
1884
  const registry = parseRegistry(raw);
2677
1885
  const observed = new Map(
2678
1886
  Object.entries(registry).map(([id, entry]) => [
@@ -2702,13 +1910,13 @@ var SessionRegistry = class {
2702
1910
  pruned = true;
2703
1911
  }
2704
1912
  if (entry.status === "closing" && heartbeatAge > CLOSING_GRACE_MS) {
2705
- if (!pidAlive2(entry.pid)) {
1913
+ if (!pidAlive(entry.pid)) {
2706
1914
  delete registry[id];
2707
1915
  pruned = true;
2708
1916
  }
2709
1917
  continue;
2710
1918
  }
2711
- if (heartbeatAge > PID_CHECK_AFTER_MS && !pidAlive2(entry.pid)) {
1919
+ if (heartbeatAge > PID_CHECK_AFTER_MS && !pidAlive(entry.pid)) {
2712
1920
  delete registry[id];
2713
1921
  pruned = true;
2714
1922
  continue;
@@ -2743,7 +1951,7 @@ var SessionRegistry = class {
2743
1951
  const deadline = Date.now() + waitBudgetMs;
2744
1952
  let attempt = 0;
2745
1953
  try {
2746
- await fs6.mkdir(path8.dirname(this.filePath), { recursive: true });
1954
+ await fs5.mkdir(path7.dirname(this.filePath), { recursive: true });
2747
1955
  await this.maybePruneStaleTempFiles();
2748
1956
  } catch (err) {
2749
1957
  if (required) {
@@ -2755,17 +1963,17 @@ var SessionRegistry = class {
2755
1963
  }
2756
1964
  while (true) {
2757
1965
  try {
2758
- let lockHandle = await fs6.open(lockPath, "wx").catch(() => null);
1966
+ let lockHandle = await fs5.open(lockPath, "wx").catch(() => null);
2759
1967
  if (!lockHandle) {
2760
1968
  if (await breakStaleLock(lockPath)) {
2761
- lockHandle = await fs6.open(lockPath, "wx").catch(() => null);
1969
+ lockHandle = await fs5.open(lockPath, "wx").catch(() => null);
2762
1970
  }
2763
1971
  if (!lockHandle) {
2764
1972
  const remainingMs = deadline - Date.now();
2765
1973
  if (remainingMs <= 0) break;
2766
1974
  const retryDelayMs = Math.min(LOCK_RETRY_MAX_MS, 20 * (attempt + 1));
2767
1975
  await new Promise(
2768
- (resolve14) => setTimeout(resolve14, Math.min(retryDelayMs, remainingMs))
1976
+ (resolve13) => setTimeout(resolve13, Math.min(retryDelayMs, remainingMs))
2769
1977
  );
2770
1978
  attempt += 1;
2771
1979
  continue;
@@ -2779,14 +1987,14 @@ var SessionRegistry = class {
2779
1987
  await lockHandle.writeFile(lockOwnerStamp());
2780
1988
  stamped = true;
2781
1989
  } catch {
2782
- if (stampAttempt < 2) await new Promise((resolve14) => setTimeout(resolve14, 5));
1990
+ if (stampAttempt < 2) await new Promise((resolve13) => setTimeout(resolve13, 5));
2783
1991
  }
2784
1992
  }
2785
1993
  if (!stamped) {
2786
1994
  stampFailed = true;
2787
1995
  throw new Error("failed to stamp session-registry lock owner");
2788
1996
  }
2789
- const raw = await fs6.readFile(this.filePath, "utf8").catch(() => "{}");
1997
+ const raw = await fs5.readFile(this.filePath, "utf8").catch(() => "{}");
2790
1998
  const registry = parseRegistry(raw);
2791
1999
  fn(registry);
2792
2000
  await this.writeAtomicWithRetry(registry);
@@ -2794,7 +2002,7 @@ var SessionRegistry = class {
2794
2002
  } finally {
2795
2003
  await lockHandle.close().catch(() => void 0);
2796
2004
  if (stampFailed) {
2797
- await fs6.unlink(lockPath).catch(() => void 0);
2005
+ await fs5.unlink(lockPath).catch(() => void 0);
2798
2006
  } else {
2799
2007
  await maybeUnlinkOwnedLock(lockPath);
2800
2008
  }
@@ -2845,7 +2053,7 @@ var SessionRegistry = class {
2845
2053
  throw err;
2846
2054
  }
2847
2055
  const delayMs = Math.min(80, 5 * attempt);
2848
- await new Promise((resolve14) => setTimeout(resolve14, delayMs));
2056
+ await new Promise((resolve13) => setTimeout(resolve13, delayMs));
2849
2057
  }
2850
2058
  }
2851
2059
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
@@ -2875,7 +2083,7 @@ var SessionRegistry = class {
2875
2083
 
2876
2084
  // src/storage/annotations-store.ts
2877
2085
  import { randomUUID as randomUUID3 } from "node:crypto";
2878
- import * as fs7 from "node:fs/promises";
2086
+ import * as fs6 from "node:fs/promises";
2879
2087
 
2880
2088
  // src/utils/expect-defined.ts
2881
2089
  function expectDefined(value, label) {
@@ -3114,7 +2322,7 @@ var AnnotationsStore = class {
3114
2322
  const fp = this.filePath(sessionId);
3115
2323
  let raw;
3116
2324
  try {
3117
- raw = await fs7.readFile(fp, "utf8");
2325
+ raw = await fs6.readFile(fp, "utf8");
3118
2326
  } catch (err) {
3119
2327
  if (err.code === "ENOENT") return null;
3120
2328
  throw err;
@@ -3154,7 +2362,7 @@ var AnnotationsStore = class {
3154
2362
  // src/storage/attachment-store.ts
3155
2363
  import { randomBytes } from "node:crypto";
3156
2364
  import * as fsp from "node:fs/promises";
3157
- import * as path9 from "node:path";
2365
+ import * as path8 from "node:path";
3158
2366
  var DEFAULT_SPOOL_THRESHOLD = 256 * 1024;
3159
2367
  var PLACEHOLDER_RE = /\[(pasted|image|file) #(\d+)[^\]]*\]|\[file:([^\]]+)\]/g;
3160
2368
  var DefaultAttachmentStore = class {
@@ -3175,7 +2383,7 @@ var DefaultAttachmentStore = class {
3175
2383
  let data = input.data;
3176
2384
  if (this.spoolDir && bytes >= this.spoolThreshold) {
3177
2385
  await fsp.mkdir(this.spoolDir, { recursive: true });
3178
- spooledPath = path9.join(this.spoolDir, `${id}.bin`);
2386
+ spooledPath = path8.join(this.spoolDir, `${id}.bin`);
3179
2387
  await atomicWrite(spooledPath, input.data, {
3180
2388
  encoding: input.kind === "image" ? "base64" : "utf8"
3181
2389
  });
@@ -3295,8 +2503,8 @@ function mergeAdjacentText(blocks) {
3295
2503
 
3296
2504
  // src/storage/cloud-config-sync.ts
3297
2505
  import { createHash as createHash3, randomUUID as nodeRandomUUID } from "node:crypto";
3298
- import * as fs8 from "node:fs/promises";
3299
- import * as path10 from "node:path";
2506
+ import * as fs7 from "node:fs/promises";
2507
+ import * as path9 from "node:path";
3300
2508
 
3301
2509
  // src/storage/cloud-config-sync/sanitize.ts
3302
2510
  var SAGE_TREE = {
@@ -4017,7 +3225,7 @@ var CloudConfigSync = class {
4017
3225
  async loadState() {
4018
3226
  if (this.state) return this.state;
4019
3227
  try {
4020
- const raw = await fs8.readFile(this.deps.statePath, "utf8");
3228
+ const raw = await fs7.readFile(this.deps.statePath, "utf8");
4021
3229
  const parsed = JSON.parse(raw);
4022
3230
  this.state = {
4023
3231
  ...structuredClone(EMPTY_STATE),
@@ -4031,7 +3239,7 @@ var CloudConfigSync = class {
4031
3239
  }
4032
3240
  async saveState(state) {
4033
3241
  this.state = state;
4034
- await fs8.mkdir(path10.dirname(this.deps.statePath), { recursive: true });
3242
+ await fs7.mkdir(path9.dirname(this.deps.statePath), { recursive: true });
4035
3243
  await atomicWrite(this.deps.statePath, `${JSON.stringify(state, null, 2)}
4036
3244
  `, {
4037
3245
  mode: 384
@@ -4051,8 +3259,8 @@ function summary(partial) {
4051
3259
  }
4052
3260
 
4053
3261
  // src/storage/cloud-sync.ts
4054
- import * as fs9 from "node:fs/promises";
4055
- import * as path11 from "node:path";
3262
+ import * as fs8 from "node:fs/promises";
3263
+ import * as path10 from "node:path";
4056
3264
  import { createHash as createHash4 } from "node:crypto";
4057
3265
  var ALL_SYNC_CATEGORIES = ["settings", "skills", "prompts", "memory", "history"];
4058
3266
  var CloudSync = class {
@@ -4060,8 +3268,8 @@ var CloudSync = class {
4060
3268
  this.paths = paths;
4061
3269
  this.getConfig = getConfig;
4062
3270
  this.setConfig = setConfig;
4063
- this.statePath = path11.join(paths.configDir, "sync-state.json");
4064
- this.getSettingsConfigPath = getSettingsConfigPath ?? (() => path11.join(this.paths.configDir, "config.json"));
3271
+ this.statePath = path10.join(paths.configDir, "sync-state.json");
3272
+ this.getSettingsConfigPath = getSettingsConfigPath ?? (() => path10.join(this.paths.configDir, "config.json"));
4065
3273
  }
4066
3274
  paths;
4067
3275
  getConfig;
@@ -4216,7 +3424,7 @@ var CloudSync = class {
4216
3424
  }
4217
3425
  async loadState() {
4218
3426
  try {
4219
- const raw = await fs9.readFile(this.statePath, "utf8");
3427
+ const raw = await fs8.readFile(this.statePath, "utf8");
4220
3428
  this.state = JSON.parse(raw);
4221
3429
  } catch {
4222
3430
  this.state = null;
@@ -4341,18 +3549,18 @@ var CloudSync = class {
4341
3549
  const localPath = this.categoryToPath(cat);
4342
3550
  if (!localPath) continue;
4343
3551
  try {
4344
- const stat19 = await fs9.lstat(localPath);
3552
+ const stat19 = await fs8.lstat(localPath);
4345
3553
  if (stat19.isSymbolicLink()) continue;
4346
3554
  if (stat19.isDirectory()) {
4347
3555
  const files = await this.walkDir(localPath, localPath);
4348
3556
  for (const file of files) {
4349
- const content = await fs9.readFile(file, "utf8");
4350
- const rel = path11.relative(localPath, file).replace(/\\/g, "/");
3557
+ const content = await fs8.readFile(file, "utf8");
3558
+ const rel = path10.relative(localPath, file).replace(/\\/g, "/");
4351
3559
  entries.push({ path: `data/${cat}/${rel}`, content, mode: "100644" });
4352
3560
  hashes.push(`${cat}/${rel}\0${content}`);
4353
3561
  }
4354
3562
  } else {
4355
- const content = await fs9.readFile(localPath, "utf8");
3563
+ const content = await fs8.readFile(localPath, "utf8");
4356
3564
  entries.push({ path: `data/${cat}`, content, mode: "100644" });
4357
3565
  hashes.push(`${cat}\0${content}`);
4358
3566
  }
@@ -4368,17 +3576,17 @@ var CloudSync = class {
4368
3576
  const localPath = this.categoryToPath(cat);
4369
3577
  if (!localPath) continue;
4370
3578
  try {
4371
- const stat19 = await fs9.lstat(localPath);
3579
+ const stat19 = await fs8.lstat(localPath);
4372
3580
  if (stat19.isSymbolicLink()) continue;
4373
3581
  if (stat19.isDirectory()) {
4374
3582
  const files = await this.walkDir(localPath, localPath);
4375
3583
  for (const file of files) {
4376
- const content = await fs9.readFile(file, "utf8");
4377
- const rel = path11.relative(localPath, file).replace(/\\/g, "/");
3584
+ const content = await fs8.readFile(file, "utf8");
3585
+ const rel = path10.relative(localPath, file).replace(/\\/g, "/");
4378
3586
  hashes.push(`${cat}/${rel}\0${content}`);
4379
3587
  }
4380
3588
  } else {
4381
- const content = await fs9.readFile(localPath, "utf8");
3589
+ const content = await fs8.readFile(localPath, "utf8");
4382
3590
  hashes.push(`${cat}\0${content}`);
4383
3591
  }
4384
3592
  } catch {
@@ -4405,10 +3613,10 @@ var CloudSync = class {
4405
3613
  }
4406
3614
  async walkDir(dir, base) {
4407
3615
  const results = [];
4408
- const entries = await fs9.readdir(dir, { withFileTypes: true });
3616
+ const entries = await fs8.readdir(dir, { withFileTypes: true });
4409
3617
  entries.sort((a, b) => a.name.localeCompare(b.name));
4410
3618
  for (const entry of entries) {
4411
- const full = path11.join(dir, entry.name);
3619
+ const full = path10.join(dir, entry.name);
4412
3620
  if (entry.isSymbolicLink()) continue;
4413
3621
  if (entry.isDirectory()) {
4414
3622
  results.push(...await this.walkDir(full, base));
@@ -4421,17 +3629,17 @@ var CloudSync = class {
4421
3629
  };
4422
3630
  async function preparePulledDestination(cat, localPath, destPath, remotePath) {
4423
3631
  const directoryBacked = cat === "skills" || cat === "prompts";
4424
- const rootPath = directoryBacked ? localPath : path11.dirname(localPath);
3632
+ const rootPath = directoryBacked ? localPath : path10.dirname(localPath);
4425
3633
  const rootStat = await lstatIfExists(rootPath);
4426
3634
  if (rootStat?.isSymbolicLink()) {
4427
3635
  throw unsafePulledSymlinkError(remotePath, rootPath);
4428
3636
  }
4429
3637
  if (directoryBacked) {
4430
- const relativeParent = path11.relative(rootPath, path11.dirname(destPath));
3638
+ const relativeParent = path10.relative(rootPath, path10.dirname(destPath));
4431
3639
  let cursor = rootPath;
4432
3640
  if (relativeParent) {
4433
- for (const segment of relativeParent.split(path11.sep)) {
4434
- cursor = path11.join(cursor, segment);
3641
+ for (const segment of relativeParent.split(path10.sep)) {
3642
+ cursor = path10.join(cursor, segment);
4435
3643
  const stat19 = await lstatIfExists(cursor);
4436
3644
  if (stat19?.isSymbolicLink()) throw unsafePulledSymlinkError(remotePath, cursor);
4437
3645
  }
@@ -4439,11 +3647,11 @@ async function preparePulledDestination(cat, localPath, destPath, remotePath) {
4439
3647
  }
4440
3648
  const destStat = await lstatIfExists(destPath);
4441
3649
  if (destStat?.isSymbolicLink()) throw unsafePulledSymlinkError(remotePath, destPath);
4442
- await fs9.mkdir(path11.dirname(destPath), { recursive: true });
3650
+ await fs8.mkdir(path10.dirname(destPath), { recursive: true });
4443
3651
  }
4444
3652
  async function lstatIfExists(filePath) {
4445
3653
  try {
4446
- return await fs9.lstat(filePath);
3654
+ return await fs8.lstat(filePath);
4447
3655
  } catch (err) {
4448
3656
  if (err.code === "ENOENT") return null;
4449
3657
  throw err;
@@ -4469,9 +3677,9 @@ function resolvePulledCategoryPath(cat, localPath, rel, remotePath) {
4469
3677
  return localPath;
4470
3678
  }
4471
3679
  if (!rel) return localPath;
4472
- const normalizedRel = path11.normalize(rel);
4473
- const traversesUp = normalizedRel === ".." || normalizedRel.startsWith(`..${path11.sep}`);
4474
- if (path11.isAbsolute(normalizedRel) || traversesUp) {
3680
+ const normalizedRel = path10.normalize(rel);
3681
+ const traversesUp = normalizedRel === ".." || normalizedRel.startsWith(`..${path10.sep}`);
3682
+ if (path10.isAbsolute(normalizedRel) || traversesUp) {
4475
3683
  throw new FsError({
4476
3684
  message: `Refusing CloudSync path traversal: ${remotePath}`,
4477
3685
  code: ERROR_CODES.FS_DELETE_FAILED,
@@ -4479,10 +3687,10 @@ function resolvePulledCategoryPath(cat, localPath, rel, remotePath) {
4479
3687
  context: { reason: "path_traversal", normalizedRel }
4480
3688
  });
4481
3689
  }
4482
- const dest = path11.resolve(localPath, normalizedRel);
4483
- const root = path11.resolve(localPath);
4484
- const relative8 = path11.relative(root, dest);
4485
- if (relative8.startsWith("..") || path11.isAbsolute(relative8)) {
3690
+ const dest = path10.resolve(localPath, normalizedRel);
3691
+ const root = path10.resolve(localPath);
3692
+ const relative8 = path10.relative(root, dest);
3693
+ if (relative8.startsWith("..") || path10.isAbsolute(relative8)) {
4486
3694
  throw new FsError({
4487
3695
  message: `Refusing CloudSync path outside category root: ${remotePath}`,
4488
3696
  code: ERROR_CODES.FS_DELETE_FAILED,
@@ -4605,7 +3813,7 @@ function isCompletedWorkEvidence(value) {
4605
3813
  }
4606
3814
 
4607
3815
  // src/storage/config-loader.ts
4608
- import * as fs11 from "node:fs/promises";
3816
+ import * as fs10 from "node:fs/promises";
4609
3817
 
4610
3818
  // src/security/config-secrets.ts
4611
3819
  function decryptConfigSecrets(cfg, vault, opts) {
@@ -4639,7 +3847,7 @@ function walk(node, vault, transform) {
4639
3847
  }
4640
3848
  return out;
4641
3849
  }
4642
- var SECRET_KEY_PATTERN = /(?:apikey|api_key|authtoken|auth_token|bearer|secret|password|passwd|pwd|refreshtoken|refresh_token|sessionkey|session_key|access[_-]?token|private[_-]?key|token\b)/i;
3850
+ var SECRET_KEY_PATTERN = /(?:api[-_]?key|auth[-_]?token|authorization|proxy-authorization|cookie|bearer|secret|password|passwd|pwd|refresh[-_]?token|session[-_]?key|access[_-]?token|private[_-]?key|token\b)/i;
4643
3851
  var NON_SECRET_OVERRIDES = /* @__PURE__ */ new Set(["publickey", "public_key"]);
4644
3852
  function isSecretField(name) {
4645
3853
  const lc = name.toLowerCase();
@@ -4703,20 +3911,20 @@ function isContextWindowModeId(id) {
4703
3911
  }
4704
3912
 
4705
3913
  // src/utils/config-backup.ts
4706
- import * as fs10 from "node:fs/promises";
4707
- import * as path12 from "node:path";
3914
+ import * as fs9 from "node:fs/promises";
3915
+ import * as path11 from "node:path";
4708
3916
  function configHistoryDir(globalRoot) {
4709
- return path12.join(globalRoot, "config-history");
3917
+ return path11.join(globalRoot, "config-history");
4710
3918
  }
4711
3919
  function configSlug(absolutePath, globalRoot) {
4712
- const rel = path12.relative(globalRoot, absolutePath);
3920
+ const rel = path11.relative(globalRoot, absolutePath);
4713
3921
  const normalized = rel.replace(/\\/g, "/").replace(/\.json$/i, "");
4714
3922
  return normalized.replace(/\//g, "-");
4715
3923
  }
4716
3924
  async function backupConfigFile(filePath, paths) {
4717
3925
  let currentContent;
4718
3926
  try {
4719
- currentContent = await fs10.readFile(filePath, "utf8");
3927
+ currentContent = await fs9.readFile(filePath, "utf8");
4720
3928
  if (!currentContent.trim()) return;
4721
3929
  } catch {
4722
3930
  return;
@@ -4725,10 +3933,10 @@ async function backupConfigFile(filePath, paths) {
4725
3933
  const ts = now.toISOString().replace(/[:.]/g, "-").replace(/Z$/, "");
4726
3934
  const slug = configSlug(filePath, paths.globalRoot);
4727
3935
  const backupDir = configHistoryDir(paths.globalRoot);
4728
- const backupFile = path12.join(backupDir, `${slug}-${ts}.json`);
3936
+ const backupFile = path11.join(backupDir, `${slug}-${ts}.json`);
4729
3937
  try {
4730
- await fs10.mkdir(backupDir, { recursive: true });
4731
- await fs10.writeFile(backupFile, currentContent, { mode: 384, encoding: "utf8" });
3938
+ await fs9.mkdir(backupDir, { recursive: true });
3939
+ await fs9.writeFile(backupFile, currentContent, { mode: 384, encoding: "utf8" });
4732
3940
  } catch {
4733
3941
  }
4734
3942
  }
@@ -5052,19 +4260,19 @@ function repairConfigDefaults(input) {
5052
4260
  });
5053
4261
  }
5054
4262
  for (const [key, defaultValue] of Object.entries(defaults)) {
5055
- const path36 = prefix ? `${prefix}.${key}` : key;
4263
+ const path35 = prefix ? `${prefix}.${key}` : key;
5056
4264
  if (!Object.hasOwn(target, key)) {
5057
4265
  target[key] = cloneJsonValue(defaultValue);
5058
- changes.push({ path: path36, action: "added" });
4266
+ changes.push({ path: path35, action: "added" });
5059
4267
  continue;
5060
4268
  }
5061
4269
  const current = target[key];
5062
4270
  if (isPlainRecord(defaultValue)) {
5063
4271
  if (isPlainRecord(current)) {
5064
- repair(current, defaultValue, path36, depth + 1);
4272
+ repair(current, defaultValue, path35, depth + 1);
5065
4273
  } else {
5066
4274
  target[key] = cloneJsonValue(defaultValue);
5067
- changes.push({ path: path36, action: "replaced" });
4275
+ changes.push({ path: path35, action: "replaced" });
5068
4276
  }
5069
4277
  continue;
5070
4278
  }
@@ -5072,7 +4280,7 @@ function repairConfigDefaults(input) {
5072
4280
  const compatible = defaultIsArray ? Array.isArray(current) : !Array.isArray(current) && typeof current === typeof defaultValue;
5073
4281
  if (!compatible) {
5074
4282
  target[key] = cloneJsonValue(defaultValue);
5075
- changes.push({ path: path36, action: "replaced" });
4283
+ changes.push({ path: path35, action: "replaced" });
5076
4284
  }
5077
4285
  }
5078
4286
  };
@@ -5344,10 +4552,21 @@ var IN_PROJECT_DENIED_PATHS = [
5344
4552
  // See discover-mailbox-bridge.ts:findWorkspaceCliEntry.
5345
4553
  path: "features.mailboxBridge",
5346
4554
  reason: "Enables the mailbox bridge, whose CLI-entry resolution walks up from the project root \u2014 a repo-supplied packages/cli/dist/index.js would be spawned on WebUI boot."
4555
+ },
4556
+ {
4557
+ // `plugins` is already denied above, so a repo cannot ADD a plugin. This
4558
+ // closes the other half: a repo could previously ship
4559
+ // `{"features":{"pluginsTrust":false}}` and switch off the integrity gate
4560
+ // for plugins the user had ALREADY installed globally — disarming the
4561
+ // trust-on-first-use pin that exists to catch a supply-chain update
4562
+ // rewriting a plugin's entry file. Same operator-owned class as the
4563
+ // switches above.
4564
+ path: "features.pluginsTrust",
4565
+ reason: "Disables the plugin trust-on-first-use integrity gate, re-trusting changed code in already-installed global plugins."
5347
4566
  }
5348
4567
  ];
5349
- function deleteNestedPath(target, path36) {
5350
- const segments = path36.split(".");
4568
+ function deleteNestedPath(target, path35) {
4569
+ const segments = path35.split(".");
5351
4570
  const last = segments[segments.length - 1];
5352
4571
  if (last === void 0) return false;
5353
4572
  let cursor = target;
@@ -5404,16 +4623,16 @@ function assertInProjectAllowListComplete() {
5404
4623
  );
5405
4624
  }
5406
4625
  const orphanedPaths = IN_PROJECT_DENIED_PATHS.filter(
5407
- ({ path: path36 }) => !IN_PROJECT_ALLOWED_KEYS.has(path36.split(".")[0] ?? "")
5408
- ).map(({ path: path36 }) => path36);
4626
+ ({ path: path35 }) => !IN_PROJECT_ALLOWED_KEYS.has(path35.split(".")[0] ?? "")
4627
+ ).map(({ path: path35 }) => path35);
5409
4628
  if (orphanedPaths.length > 0) {
5410
4629
  problems.push(
5411
4630
  `IN_PROJECT_DENIED_PATHS entr(ies) whose top-level parent is not allowed: ` + orphanedPaths.join(", ") + ". The parent is already stripped wholesale, so the nested denial is dead \u2014 remove it."
5412
4631
  );
5413
4632
  }
5414
4633
  const malformedPaths = IN_PROJECT_DENIED_PATHS.filter(
5415
- ({ path: path36 }) => path36.split(".").length < 2 || path36.split(".").some((s) => s.length === 0)
5416
- ).map(({ path: path36 }) => path36);
4634
+ ({ path: path35 }) => path35.split(".").length < 2 || path35.split(".").some((s) => s.length === 0)
4635
+ ).map(({ path: path35 }) => path35);
5417
4636
  if (malformedPaths.length > 0) {
5418
4637
  problems.push(
5419
4638
  `IN_PROJECT_DENIED_PATHS entr(ies) are not dotted nested paths: ` + malformedPaths.join(", ") + ". Top-level keys belong in KNOWN_DENIED_IN_PROJECT instead."
@@ -5441,8 +4660,8 @@ function stripUnsafeInProjectFields(inProject, sourcePath, warn = (msg) => conso
5441
4660
  }
5442
4661
  stripped.push(k);
5443
4662
  }
5444
- for (const { path: path36 } of IN_PROJECT_DENIED_PATHS) {
5445
- if (deleteNestedPath(out, path36)) stripped.push(path36);
4663
+ for (const { path: path35 } of IN_PROJECT_DENIED_PATHS) {
4664
+ if (deleteNestedPath(out, path35)) stripped.push(path35);
5446
4665
  }
5447
4666
  if (stripped.length > 0) {
5448
4667
  warn(
@@ -5631,10 +4850,10 @@ function removeLegacySageEngine(config) {
5631
4850
  }
5632
4851
 
5633
4852
  // src/storage/config-loader/path-identity.ts
5634
- import * as path13 from "node:path";
4853
+ import * as path12 from "node:path";
5635
4854
  function samePath(a, b) {
5636
- let ra = path13.resolve(a);
5637
- let rb = path13.resolve(b);
4855
+ let ra = path12.resolve(a);
4856
+ let rb = path12.resolve(b);
5638
4857
  if (process.platform === "win32" || process.platform === "darwin") {
5639
4858
  ra = ra.toLowerCase();
5640
4859
  rb = rb.toLowerCase();
@@ -5794,7 +5013,7 @@ var DefaultConfigLoader = class _DefaultConfigLoader {
5794
5013
  let parsed;
5795
5014
  let fileExisted = true;
5796
5015
  try {
5797
- const raw = await fs11.readFile(fp, "utf8");
5016
+ const raw = await fs10.readFile(fp, "utf8");
5798
5017
  const result = safeParse(raw);
5799
5018
  if (!result.ok || !isPlainRecord(result.value)) {
5800
5019
  return;
@@ -5890,7 +5109,7 @@ var DefaultConfigLoader = class _DefaultConfigLoader {
5890
5109
  let parsed;
5891
5110
  let existed = true;
5892
5111
  try {
5893
- const raw = await fs11.readFile(profileFp, "utf8");
5112
+ const raw = await fs10.readFile(profileFp, "utf8");
5894
5113
  const result = safeParse(raw);
5895
5114
  if (!result.ok || !isPlainRecord(result.value)) {
5896
5115
  this.logWarn("Profile config parse failed \u2014 falling back to defaults", {
@@ -5989,7 +5208,7 @@ var DefaultConfigLoader = class _DefaultConfigLoader {
5989
5208
  const fp = this.paths.syncConfig;
5990
5209
  const t0 = Date.now();
5991
5210
  try {
5992
- const raw = await fs11.readFile(fp, "utf8");
5211
+ const raw = await fs10.readFile(fp, "utf8");
5993
5212
  const parsed = safeParse(raw);
5994
5213
  if (!parsed.ok || !parsed.value) {
5995
5214
  this.events?.emit("storage.read", {
@@ -6051,7 +5270,7 @@ var DefaultConfigLoader = class _DefaultConfigLoader {
6051
5270
  const t0 = Date.now();
6052
5271
  let mtimeMs = null;
6053
5272
  try {
6054
- const stat19 = await fs11.stat(file);
5273
+ const stat19 = await fs10.stat(file);
6055
5274
  mtimeMs = stat19.mtimeMs;
6056
5275
  const cached = this.jsonCache.get(file);
6057
5276
  if (cached && cached.mtimeMs === mtimeMs) {
@@ -6081,7 +5300,7 @@ var DefaultConfigLoader = class _DefaultConfigLoader {
6081
5300
  }
6082
5301
  let raw;
6083
5302
  try {
6084
- raw = await fs11.readFile(file, "utf8");
5303
+ raw = await fs10.readFile(file, "utf8");
6085
5304
  } catch (err) {
6086
5305
  if (err.code !== "ENOENT") {
6087
5306
  this.events?.emit("storage.read", {
@@ -6557,12 +5776,6 @@ var DirectorStateCheckpoint = class {
6557
5776
  }
6558
5777
  };
6559
5778
 
6560
- // src/storage/goal-coordination.ts
6561
- import { updateTask } from "@wrongstack/kanban";
6562
-
6563
- // src/storage/goal-kanban.ts
6564
- import { addTask, createBoard, getBoard, listBoards, removeBoard } from "@wrongstack/kanban";
6565
-
6566
5779
  // src/utils/term.ts
6567
5780
  var hasStdout = () => typeof process !== "undefined" && !!process.stdout;
6568
5781
  function isStdoutTTY() {
@@ -6611,14 +5824,16 @@ var color = {
6611
5824
  var GOAL_BOARD_TAG_PREFIX = "goal:";
6612
5825
  async function createGoalKanbanBoard(projectRoot, goalFile) {
6613
5826
  if (!projectRoot) return null;
5827
+ const store = tryBoardStore();
5828
+ if (!store) return null;
6614
5829
  const existingId = goalFile.kanbanBoardId;
6615
5830
  if (existingId) {
6616
- const existing = await getBoard(projectRoot, existingId).catch(() => null);
5831
+ const existing = await store.getBoard(projectRoot, existingId).catch(() => null);
6617
5832
  if (existing) return existingId;
6618
5833
  }
6619
5834
  const displayGoal = (goalFile.refinedGoal || goalFile.goal).replace(/\s+/g, " ").trim();
6620
5835
  const titleSuffix = displayGoal.length > 80 ? displayGoal.slice(0, 77) + "\u2026" : displayGoal;
6621
- const board = await createBoard(projectRoot, {
5836
+ const board = await store.createBoard(projectRoot, {
6622
5837
  title: `\u{1F3AF} ${titleSuffix}`,
6623
5838
  description: `Kanban board auto-created for goal: ${displayGoal}`,
6624
5839
  tags: [goalTag(goalFile)],
@@ -6632,7 +5847,7 @@ async function createGoalKanbanBoard(projectRoot, goalFile) {
6632
5847
  const d = goalFile.deliverables[index];
6633
5848
  const cleaned = d.replace(/^\[[x✓]\]|✅|\(done\)\s*/i, "").trim();
6634
5849
  if (cleaned) {
6635
- await addTask(projectRoot, board.id, {
5850
+ await store.addTask(projectRoot, board.id, {
6636
5851
  title: cleaned,
6637
5852
  columnId: targetColumnId,
6638
5853
  description: `Deliverable for goal: ${displayGoal}`,
@@ -6652,23 +5867,27 @@ async function createGoalKanbanBoard(projectRoot, goalFile) {
6652
5867
  async function findGoalKanbanBoard(projectRoot, boardId) {
6653
5868
  if (!projectRoot || !boardId) return null;
6654
5869
  try {
6655
- return await getBoard(projectRoot, boardId);
5870
+ return await boardStore().getBoard(projectRoot, boardId);
6656
5871
  } catch {
6657
5872
  return null;
6658
5873
  }
6659
5874
  }
6660
5875
  async function deleteGoalKanbanBoard(projectRoot, boardId) {
6661
5876
  if (!projectRoot || !boardId) return;
6662
- await removeBoard(projectRoot, boardId).catch(() => {
5877
+ const store = tryBoardStore();
5878
+ if (!store) return;
5879
+ await store.removeBoard(projectRoot, boardId).catch(() => {
6663
5880
  });
6664
5881
  }
6665
5882
  async function findGoalBoardByTag(projectRoot, goalFile) {
6666
5883
  if (!projectRoot) return null;
5884
+ const store = tryBoardStore();
5885
+ if (!store) return null;
6667
5886
  const tag = goalTag(goalFile);
6668
- const boards = await listBoards(projectRoot);
5887
+ const boards = await store.listBoards(projectRoot);
6669
5888
  const matched = boards.find((b) => b.tags?.includes(tag));
6670
5889
  if (!matched) return null;
6671
- return getBoard(projectRoot, matched.id).catch(() => null);
5890
+ return store.getBoard(projectRoot, matched.id).catch(() => null);
6672
5891
  }
6673
5892
  function formatGoalKanbanPreview(goalFile, boardId, taskCount) {
6674
5893
  const lines = [];
@@ -7184,7 +6403,7 @@ async function refreshGoalKanban(projectRoot, goal, completed, resolvedBoard) {
7184
6403
  const originMatch = task.origin?.system === "goal" && task.origin?.taskId ? originKeys.has(task.origin.taskId) : false;
7185
6404
  const titleMatch = !task.origin?.taskId && completedTitles.has(normalizeTitle(task.title));
7186
6405
  if (!originMatch && !titleMatch) continue;
7187
- const result = await updateTask(
6406
+ const result = await boardStore().updateTask(
7188
6407
  projectRoot,
7189
6408
  board.id,
7190
6409
  task.id,
@@ -7220,8 +6439,8 @@ function normalizeTitle(value) {
7220
6439
  }
7221
6440
 
7222
6441
  // src/storage/input-history-store.ts
7223
- import * as fs12 from "node:fs/promises";
7224
- import * as path14 from "node:path";
6442
+ import * as fs11 from "node:fs/promises";
6443
+ import * as path13 from "node:path";
7225
6444
  var INPUT_HISTORY_DEFAULT_MAX = 100;
7226
6445
  var InputHistoryStore = class {
7227
6446
  /**
@@ -7243,7 +6462,7 @@ var InputHistoryStore = class {
7243
6462
  */
7244
6463
  async load() {
7245
6464
  try {
7246
- const raw = JSON.parse(await fs12.readFile(this.file, "utf8"));
6465
+ const raw = JSON.parse(await fs11.readFile(this.file, "utf8"));
7247
6466
  if (raw && typeof raw === "object" && Array.isArray(raw.entries) && raw.entries.every((e) => typeof e === "string")) {
7248
6467
  return raw.entries.slice(0, this.maxEntries);
7249
6468
  }
@@ -7260,7 +6479,7 @@ var InputHistoryStore = class {
7260
6479
  */
7261
6480
  async save(entries) {
7262
6481
  const cleaned = this.scrubAndFilter(entries);
7263
- await ensureDir(path14.dirname(this.file));
6482
+ await ensureDir(path13.dirname(this.file));
7264
6483
  const payload = {
7265
6484
  version: 1,
7266
6485
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -7270,7 +6489,7 @@ var InputHistoryStore = class {
7270
6489
  }
7271
6490
  /** Truncate the file to an empty entry list (used by /clear). */
7272
6491
  async clear() {
7273
- await ensureDir(path14.dirname(this.file));
6492
+ await ensureDir(path13.dirname(this.file));
7274
6493
  const payload = { version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), entries: [] };
7275
6494
  await atomicWrite(this.file, JSON.stringify(payload, null, 2));
7276
6495
  }
@@ -7301,8 +6520,8 @@ var InputHistoryStore = class {
7301
6520
 
7302
6521
  // src/storage/memory-backend.ts
7303
6522
  import { randomUUID as randomUUID4 } from "node:crypto";
7304
- import * as fs13 from "node:fs/promises";
7305
- import * as path15 from "node:path";
6523
+ import * as fs12 from "node:fs/promises";
6524
+ import * as path14 from "node:path";
7306
6525
 
7307
6526
  // src/security/file-permissions.ts
7308
6527
  import {
@@ -7477,7 +6696,7 @@ var FileMemoryBackend = class {
7477
6696
  }
7478
6697
  async getMtime(file) {
7479
6698
  try {
7480
- const stat19 = await fs13.stat(file);
6699
+ const stat19 = await fs12.stat(file);
7481
6700
  return stat19.mtimeMs;
7482
6701
  } catch {
7483
6702
  return 0;
@@ -7529,7 +6748,7 @@ var FileMemoryBackend = class {
7529
6748
  const line = `- [${entry.ts}] ${id}${meta} ${entry.text.replace(/\n/g, " ")}
7530
6749
  `;
7531
6750
  await withFileLock(file, async () => {
7532
- const handle = await fs13.open(file, "a+", SECRET_FILE_MODE);
6751
+ const handle = await fs12.open(file, "a+", SECRET_FILE_MODE);
7533
6752
  try {
7534
6753
  const stat19 = await handle.stat();
7535
6754
  let prefix = "";
@@ -7553,7 +6772,7 @@ var FileMemoryBackend = class {
7553
6772
  return withFileLock(file, async () => {
7554
6773
  let existing;
7555
6774
  try {
7556
- existing = await fs13.readFile(file, "utf8");
6775
+ existing = await fs12.readFile(file, "utf8");
7557
6776
  } catch {
7558
6777
  return 0;
7559
6778
  }
@@ -7590,7 +6809,7 @@ var FileMemoryBackend = class {
7590
6809
  async readAll(scope, filePath) {
7591
6810
  const file = this.resolveFile(filePath, scope);
7592
6811
  try {
7593
- return await fs13.readFile(file, "utf8");
6812
+ return await fs12.readFile(file, "utf8");
7594
6813
  } catch {
7595
6814
  return "";
7596
6815
  }
@@ -7614,7 +6833,7 @@ var FileMemoryBackend = class {
7614
6833
  const file = this.resolveFile(filePath, scope);
7615
6834
  let existing;
7616
6835
  try {
7617
- existing = await fs13.readFile(file, "utf8");
6836
+ existing = await fs12.readFile(file, "utf8");
7618
6837
  } catch {
7619
6838
  return 0;
7620
6839
  }
@@ -7634,7 +6853,7 @@ var FileMemoryBackend = class {
7634
6853
  const next = lines.join("\n");
7635
6854
  const backup = `${file}.bak.${Date.now()}`;
7636
6855
  try {
7637
- await fs13.copyFile(file, backup);
6856
+ await fs12.copyFile(file, backup);
7638
6857
  await pruneConsolidateBackups(file);
7639
6858
  } catch {
7640
6859
  }
@@ -7648,14 +6867,14 @@ var FileMemoryBackend = class {
7648
6867
  }
7649
6868
  };
7650
6869
  async function pruneConsolidateBackups(file) {
7651
- const dir = path15.dirname(file);
7652
- const base = path15.basename(file);
6870
+ const dir = path14.dirname(file);
6871
+ const base = path14.basename(file);
7653
6872
  const prefix = `${base}.bak.`;
7654
- const backups = (await fs13.readdir(dir)).filter((name) => name.startsWith(prefix)).sort().reverse();
6873
+ const backups = (await fs12.readdir(dir)).filter((name) => name.startsWith(prefix)).sort().reverse();
7655
6874
  await Promise.all(
7656
6875
  backups.slice(MAX_MEMORY_CONSOLIDATE_BACKUPS).map(async (name) => {
7657
6876
  try {
7658
- await fs13.unlink(path15.join(dir, name));
6877
+ await fs12.unlink(path14.join(dir, name));
7659
6878
  } catch {
7660
6879
  }
7661
6880
  })
@@ -7671,11 +6890,11 @@ function parseEntries(raw, scope = "project-memory") {
7671
6890
  }
7672
6891
 
7673
6892
  // src/storage/memory-consolidator.ts
7674
- import * as path17 from "node:path";
6893
+ import * as path16 from "node:path";
7675
6894
 
7676
6895
  // src/utils/instruction-file.ts
7677
6896
  import { readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
7678
- import * as path16 from "node:path";
6897
+ import * as path15 from "node:path";
7679
6898
  import { fileURLToPath as fileURLToPath2 } from "node:url";
7680
6899
  var textCache = /* @__PURE__ */ new Map();
7681
6900
  var rootCandidates;
@@ -7685,7 +6904,7 @@ function readBundledInstructionText(relativePath) {
7685
6904
  let resolved = "";
7686
6905
  for (const root of instructionRootCandidates()) {
7687
6906
  try {
7688
- resolved = readFileSync3(path16.join(root, relativePath), "utf8").trimEnd();
6907
+ resolved = readFileSync3(path15.join(root, relativePath), "utf8").trimEnd();
7689
6908
  break;
7690
6909
  } catch {
7691
6910
  }
@@ -7701,11 +6920,11 @@ function renderInstructionTemplate(template, values) {
7701
6920
  }
7702
6921
  function instructionRootCandidates() {
7703
6922
  if (rootCandidates !== void 0) return rootCandidates;
7704
- const here = path16.dirname(fileURLToPath2(import.meta.url));
6923
+ const here = path15.dirname(fileURLToPath2(import.meta.url));
7705
6924
  const candidates = [
7706
- path16.resolve(here, "../../instructions"),
7707
- path16.resolve(here, "../instructions"),
7708
- path16.resolve(here, "instructions")
6925
+ path15.resolve(here, "../../instructions"),
6926
+ path15.resolve(here, "../instructions"),
6927
+ path15.resolve(here, "instructions")
7709
6928
  ];
7710
6929
  rootCandidates = candidates.sort((a, b) => Number(!isDirectory(a)) - Number(!isDirectory(b)));
7711
6930
  return rootCandidates;
@@ -7775,8 +6994,8 @@ ${existingEntries.map((e) => `- [${e.ts.slice(0, 10)}] ${e.text}`).join("\n")}`
7775
6994
  });
7776
6995
  }
7777
6996
  function relativeEvidencePath(projectRoot, filePath) {
7778
- const relative8 = path17.relative(projectRoot, filePath);
7779
- return relative8 && !relative8.startsWith("..") && !path17.isAbsolute(relative8) ? relative8.replaceAll("\\", "/") : filePath.replaceAll("\\", "/");
6997
+ const relative8 = path16.relative(projectRoot, filePath);
6998
+ return relative8 && !relative8.startsWith("..") && !path16.isAbsolute(relative8) ? relative8.replaceAll("\\", "/") : filePath.replaceAll("\\", "/");
7780
6999
  }
7781
7000
  function safeCommandEvidence(value) {
7782
7001
  if (typeof value !== "string") return void 0;
@@ -7986,7 +7205,7 @@ function buildSessionDigestText(finalText, iterations, factsAdded) {
7986
7205
  }
7987
7206
 
7988
7207
  // src/storage/memory-graph-backend.ts
7989
- import * as fs14 from "node:fs/promises";
7208
+ import * as fs13 from "node:fs/promises";
7990
7209
  var GraphMemoryBackend = class _GraphMemoryBackend {
7991
7210
  kind = "graph";
7992
7211
  file;
@@ -8154,7 +7373,7 @@ var GraphMemoryBackend = class _GraphMemoryBackend {
8154
7373
  this.loadedScope = scope;
8155
7374
  this.loaded = true;
8156
7375
  try {
8157
- await fs14.unlink(this.graphFile);
7376
+ await fs13.unlink(this.graphFile);
8158
7377
  } catch {
8159
7378
  }
8160
7379
  }
@@ -8194,7 +7413,7 @@ var GraphMemoryBackend = class _GraphMemoryBackend {
8194
7413
  async loadGraph(scope) {
8195
7414
  if (this.loaded && this.loadedScope === scope) return;
8196
7415
  try {
8197
- const raw = await fs14.readFile(this.graphFile, "utf8");
7416
+ const raw = await fs13.readFile(this.graphFile, "utf8");
8198
7417
  const data = JSON.parse(raw);
8199
7418
  this.nodes = new Map(data.nodes);
8200
7419
  this.edges = data.edges;
@@ -8216,10 +7435,10 @@ var GraphMemoryBackend = class _GraphMemoryBackend {
8216
7435
  edges: this.edges
8217
7436
  };
8218
7437
  const dir = this.graphFile.substring(0, this.graphFile.lastIndexOf("/"));
8219
- await fs14.mkdir(dir, { recursive: true });
7438
+ await fs13.mkdir(dir, { recursive: true });
8220
7439
  const tmp = `${this.graphFile}.tmp`;
8221
- await fs14.writeFile(tmp, JSON.stringify(data));
8222
- await fs14.rename(tmp, this.graphFile);
7440
+ await fs13.writeFile(tmp, JSON.stringify(data));
7441
+ await fs13.rename(tmp, this.graphFile);
8223
7442
  } catch {
8224
7443
  }
8225
7444
  }
@@ -8677,8 +7896,8 @@ ${cat}:`);
8677
7896
 
8678
7897
  // src/storage/prompt-store.ts
8679
7898
  import { createHash as createHash5 } from "node:crypto";
8680
- import * as fs15 from "node:fs/promises";
8681
- import * as path18 from "node:path";
7899
+ import * as fs14 from "node:fs/promises";
7900
+ import * as path17 from "node:path";
8682
7901
 
8683
7902
  // src/utils/slug.ts
8684
7903
  function slugify2(name, fallback = "prompt", maxLen = 64) {
@@ -8777,12 +7996,12 @@ var DefaultPromptStore = class {
8777
7996
  await ensureDir(this.dir);
8778
7997
  const entries = [];
8779
7998
  try {
8780
- const files = await fs15.readdir(this.dir);
7999
+ const files = await fs14.readdir(this.dir);
8781
8000
  for (const file of files) {
8782
8001
  if (!file.endsWith(".json")) continue;
8783
8002
  try {
8784
8003
  const raw = JSON.parse(
8785
- await fs15.readFile(path18.join(this.dir, file), "utf8")
8004
+ await fs14.readFile(path17.join(this.dir, file), "utf8")
8786
8005
  );
8787
8006
  const migrated = migratePromptEntry(raw.entry);
8788
8007
  if (migrated) entries.push(migrated);
@@ -8796,9 +8015,9 @@ var DefaultPromptStore = class {
8796
8015
  );
8797
8016
  }
8798
8017
  async get(id) {
8799
- const file = path18.join(this.dir, `${id}.json`);
8018
+ const file = path17.join(this.dir, `${id}.json`);
8800
8019
  try {
8801
- const raw = JSON.parse(await fs15.readFile(file, "utf8"));
8020
+ const raw = JSON.parse(await fs14.readFile(file, "utf8"));
8802
8021
  return migratePromptEntry(raw.entry);
8803
8022
  } catch {
8804
8023
  return null;
@@ -8806,14 +8025,14 @@ var DefaultPromptStore = class {
8806
8025
  }
8807
8026
  async save(entry) {
8808
8027
  await ensureDir(this.dir);
8809
- const file = path18.join(this.dir, `${entry.id}.json`);
8028
+ const file = path17.join(this.dir, `${entry.id}.json`);
8810
8029
  const raw = { version: SCHEMA_VERSION, entry };
8811
8030
  await atomicWrite(file, JSON.stringify(raw, null, 2));
8812
8031
  }
8813
8032
  async delete(id) {
8814
- const file = path18.join(this.dir, `${id}.json`);
8033
+ const file = path17.join(this.dir, `${id}.json`);
8815
8034
  try {
8816
- await fs15.unlink(file);
8035
+ await fs14.unlink(file);
8817
8036
  return true;
8818
8037
  } catch {
8819
8038
  return false;
@@ -8852,7 +8071,7 @@ var DefaultPromptStore = class {
8852
8071
  };
8853
8072
 
8854
8073
  // src/storage/prompt-usage-store.ts
8855
- import * as fs16 from "node:fs/promises";
8074
+ import * as fs15 from "node:fs/promises";
8856
8075
  var PromptUsageStore = class {
8857
8076
  constructor(file) {
8858
8077
  this.file = file;
@@ -8872,7 +8091,7 @@ var PromptUsageStore = class {
8872
8091
  return this.cachedUsage;
8873
8092
  }
8874
8093
  try {
8875
- const raw = JSON.parse(await fs16.readFile(this.file, "utf8"));
8094
+ const raw = JSON.parse(await fs15.readFile(this.file, "utf8"));
8876
8095
  if (raw && typeof raw === "object" && raw.usage && typeof raw.usage === "object") {
8877
8096
  this.cachedUsage = raw.usage;
8878
8097
  this.cachedSignature = signature;
@@ -8885,8 +8104,8 @@ var PromptUsageStore = class {
8885
8104
  return this.cachedUsage;
8886
8105
  }
8887
8106
  async record(slug, at = (/* @__PURE__ */ new Date()).toISOString()) {
8888
- return await new Promise((resolve14, reject) => {
8889
- this.pendingRecords.push({ slug, at, resolve: resolve14, reject });
8107
+ return await new Promise((resolve13, reject) => {
8108
+ this.pendingRecords.push({ slug, at, resolve: resolve13, reject });
8890
8109
  this.scheduleDrain();
8891
8110
  });
8892
8111
  }
@@ -8952,7 +8171,7 @@ var PromptUsageStore = class {
8952
8171
  }
8953
8172
  async fileSignature() {
8954
8173
  try {
8955
- const stat19 = await fs16.stat(this.file);
8174
+ const stat19 = await fs15.stat(this.file);
8956
8175
  return { size: stat19.size, mtimeMs: stat19.mtimeMs, ctimeMs: stat19.ctimeMs };
8957
8176
  } catch {
8958
8177
  return null;
@@ -8969,12 +8188,12 @@ function cloneUsage(usage) {
8969
8188
 
8970
8189
  // src/storage/provider-config-watcher.ts
8971
8190
  import * as syncFs from "node:fs";
8972
- import * as fs17 from "node:fs/promises";
8973
- import * as path19 from "node:path";
8191
+ import * as fs16 from "node:fs/promises";
8192
+ import * as path18 from "node:path";
8974
8193
  async function readProviderSnapshot(configPath, vault, warn) {
8975
8194
  let raw;
8976
8195
  try {
8977
- raw = await fs17.readFile(configPath, "utf8");
8196
+ raw = await fs16.readFile(configPath, "utf8");
8978
8197
  } catch (err) {
8979
8198
  if (err.code !== "ENOENT") {
8980
8199
  warn?.(`Could not read ${configPath}: ${err.message}`);
@@ -9046,7 +8265,7 @@ function serializeSnapshot(s) {
9046
8265
  function watchProviderConfig(configPath, vault, onChange, opts = {}) {
9047
8266
  const debounceMs = opts.debounceMs ?? 200;
9048
8267
  const warn = opts.warn;
9049
- const base = path19.basename(configPath);
8268
+ const base = path18.basename(configPath);
9050
8269
  let timer;
9051
8270
  let closed = false;
9052
8271
  let lastSerialized;
@@ -9057,7 +8276,7 @@ function watchProviderConfig(configPath, vault, onChange, opts = {}) {
9057
8276
  });
9058
8277
  let watcher;
9059
8278
  try {
9060
- watcher = syncFs.watch(path19.dirname(configPath), { recursive: false });
8279
+ watcher = syncFs.watch(path18.dirname(configPath), { recursive: false });
9061
8280
  } catch (err) {
9062
8281
  warn?.(`Provider config watcher could not start: ${err.message}`);
9063
8282
  return { close: () => {
@@ -9101,7 +8320,7 @@ function watchProviderConfig(configPath, vault, onChange, opts = {}) {
9101
8320
 
9102
8321
  // src/storage/queue-store.ts
9103
8322
  import * as fsp6 from "node:fs/promises";
9104
- import * as path20 from "node:path";
8323
+ import * as path19 from "node:path";
9105
8324
  var QUEUE_MAX_ITEMS = 100;
9106
8325
  var QUEUE_MAX_BYTES = 16 * 1024 * 1024;
9107
8326
  var QUEUE_MAX_ITEM_BYTES = 8 * 1024 * 1024;
@@ -9134,7 +8353,7 @@ var QueueStore = class {
9134
8353
  traceId;
9135
8354
  logger;
9136
8355
  constructor(opts) {
9137
- this.file = path20.join(opts.dir, "queue.json");
8356
+ this.file = path19.join(opts.dir, "queue.json");
9138
8357
  this.events = opts.events;
9139
8358
  this.traceId = opts.traceId;
9140
8359
  this.logger = opts.logger;
@@ -9350,7 +8569,7 @@ function isPersistedQueueItem(v) {
9350
8569
  // src/storage/recovery-lock.ts
9351
8570
  import * as fsp7 from "node:fs/promises";
9352
8571
  import * as os3 from "node:os";
9353
- import * as path21 from "node:path";
8572
+ import * as path20 from "node:path";
9354
8573
 
9355
8574
  // src/storage/session-workspace-checkpoints.ts
9356
8575
  function requiredCheckpointCas(checkpointCas, operation) {
@@ -9435,7 +8654,7 @@ var RecoveryLock = class {
9435
8654
  sessionStore;
9436
8655
  probe;
9437
8656
  constructor(opts) {
9438
- this.file = path21.join(opts.dir, LOCK_FILE);
8657
+ this.file = path20.join(opts.dir, LOCK_FILE);
9439
8658
  this.pid = opts.pid ?? process.pid;
9440
8659
  this.hostname = opts.hostname ?? os3.hostname();
9441
8660
  this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
@@ -9509,7 +8728,7 @@ var RecoveryLock = class {
9509
8728
  * null return before calling this.
9510
8729
  */
9511
8730
  async write(sessionId) {
9512
- await ensureDir(path21.dirname(this.file));
8731
+ await ensureDir(path20.dirname(this.file));
9513
8732
  const lock = {
9514
8733
  v: 1,
9515
8734
  sessionId,
@@ -9568,30 +8787,30 @@ var defaultIsPidAlive = isPidAlive;
9568
8787
 
9569
8788
  // src/storage/orphan-lock-cleaner.ts
9570
8789
  import * as fsp8 from "node:fs/promises";
9571
- import * as path22 from "node:path";
8790
+ import * as path21 from "node:path";
9572
8791
  async function cleanOrphanLocks(projectRoot, options) {
9573
8792
  const result = {
9574
8793
  cleanedGitLocks: [],
9575
8794
  cleanedWorktrees: [],
9576
8795
  errors: []
9577
8796
  };
9578
- const pidAlive3 = options?.isPidAlive ?? isPidAlive;
8797
+ const pidAlive2 = options?.isPidAlive ?? isPidAlive;
9579
8798
  const maxAgeMs = options?.maxLockAgeMs ?? 10 * 60 * 1e3;
9580
8799
  const now = Date.now();
9581
- const worktreesDir = path22.join(projectRoot, ".wrongstack", "worktrees");
8800
+ const worktreesDir = path21.join(projectRoot, ".wrongstack", "worktrees");
9582
8801
  try {
9583
8802
  const entries = await fsp8.readdir(worktreesDir, { withFileTypes: true });
9584
8803
  for (const entry of entries) {
9585
8804
  if (!entry.isDirectory()) continue;
9586
- const wtPath = path22.join(worktreesDir, entry.name);
9587
- const lockPath = path22.join(wtPath, ".lock");
8805
+ const wtPath = path21.join(worktreesDir, entry.name);
8806
+ const lockPath = path21.join(wtPath, ".lock");
9588
8807
  try {
9589
8808
  const lockStat = await fsp8.stat(lockPath).catch(() => null);
9590
8809
  if (lockStat) {
9591
8810
  const raw = await fsp8.readFile(lockPath, "utf8").catch(() => "");
9592
8811
  const pid = parseInt(raw.trim(), 10);
9593
8812
  if (Number.isInteger(pid) && pid > 0) {
9594
- if (!pidAlive3(pid)) {
8813
+ if (!pidAlive2(pid)) {
9595
8814
  await fsp8.unlink(lockPath).catch(() => void 0);
9596
8815
  result.cleanedWorktrees.push(wtPath);
9597
8816
  }
@@ -9613,8 +8832,8 @@ async function cleanOrphanLocks(projectRoot, options) {
9613
8832
  }
9614
8833
 
9615
8834
  // src/storage/replay-log-store.ts
9616
- import * as fs18 from "node:fs/promises";
9617
- import * as path23 from "node:path";
8835
+ import * as fs17 from "node:fs/promises";
8836
+ import * as path22 from "node:path";
9618
8837
 
9619
8838
  // src/replay/hash.ts
9620
8839
  import { createHash as createHash6 } from "node:crypto";
@@ -9708,12 +8927,12 @@ var ReplayLogStore = class _ReplayLogStore {
9708
8927
  const line = JSON.stringify(entry) + "\n";
9709
8928
  let offset2 = 0;
9710
8929
  try {
9711
- const stat19 = await fs18.stat(fp);
8930
+ const stat19 = await fs17.stat(fp);
9712
8931
  offset2 = stat19.size;
9713
8932
  } catch (err) {
9714
8933
  if (err.code !== "ENOENT") throw err;
9715
8934
  }
9716
- await fs18.appendFile(fp, line, { encoding: "utf8", mode: SECRET_FILE_MODE });
8935
+ await fs17.appendFile(fp, line, { encoding: "utf8", mode: SECRET_FILE_MODE });
9717
8936
  cache.set(hash, { entry, offset: offset2, length: Buffer.byteLength(line, "utf8") });
9718
8937
  this.diskCount.set(input.sessionId, currentCount + 1);
9719
8938
  this.events?.emit("storage.write", {
@@ -9841,7 +9060,7 @@ var ReplayLogStore = class _ReplayLogStore {
9841
9060
  const scan = async (dir, prefix, depth) => {
9842
9061
  let entries;
9843
9062
  try {
9844
- entries = await fs18.readdir(dir, { withFileTypes: true });
9063
+ entries = await fs17.readdir(dir, { withFileTypes: true });
9845
9064
  } catch (err) {
9846
9065
  if (depth === 0 && err.code !== "ENOENT") {
9847
9066
  console.warn(
@@ -9859,13 +9078,13 @@ var ReplayLogStore = class _ReplayLogStore {
9859
9078
  for (const entry of entries) {
9860
9079
  if (entry.name.startsWith(".")) continue;
9861
9080
  if (entry.isDirectory()) {
9862
- if (depth === 0) await scan(path23.join(dir, entry.name), entry.name, depth + 1);
9081
+ if (depth === 0) await scan(path22.join(dir, entry.name), entry.name, depth + 1);
9863
9082
  continue;
9864
9083
  }
9865
9084
  if (!entry.isFile() || !entry.name.endsWith(".replay.jsonl")) continue;
9866
9085
  const base = entry.name.slice(0, -".replay.jsonl".length);
9867
9086
  const sessionId = prefix ? `${prefix}/${base}` : base;
9868
- const fp = path23.join(dir, entry.name);
9087
+ const fp = path22.join(dir, entry.name);
9869
9088
  out.push({
9870
9089
  sessionId,
9871
9090
  entryCount: await this.countEntries(fp),
@@ -9881,7 +9100,7 @@ var ReplayLogStore = class _ReplayLogStore {
9881
9100
  return sessionScopedPath(this.dir, sessionId, ".replay.jsonl");
9882
9101
  }
9883
9102
  async countEntries(filePath) {
9884
- const handle = await fs18.open(filePath, "r");
9103
+ const handle = await fs17.open(filePath, "r");
9885
9104
  const buffer = Buffer.allocUnsafe(64 * 1024);
9886
9105
  let count = 0;
9887
9106
  let hasNonWhitespace2 = false;
@@ -9922,7 +9141,7 @@ var ReplayLogStore = class _ReplayLogStore {
9922
9141
  async readAll(sessionId) {
9923
9142
  const fp = this.filePath(sessionId);
9924
9143
  try {
9925
- const raw = await fs18.readFile(fp, "utf8");
9144
+ const raw = await fs17.readFile(fp, "utf8");
9926
9145
  const out = [];
9927
9146
  for (const line of raw.split("\n")) {
9928
9147
  if (!line.trim()) continue;
@@ -9962,7 +9181,7 @@ var ReplayLogStore = class _ReplayLogStore {
9962
9181
  const fp = this.filePath(sessionId);
9963
9182
  cache = /* @__PURE__ */ new Map();
9964
9183
  try {
9965
- const handle = await fs18.open(fp, "r");
9184
+ const handle = await fs17.open(fp, "r");
9966
9185
  const CHUNK = 64 * 1024;
9967
9186
  const buffer = Buffer.alloc(CHUNK);
9968
9187
  let leftover = "";
@@ -10048,7 +9267,7 @@ var ReplayLogStore = class _ReplayLogStore {
10048
9267
  const fp = this.filePath(sessionId);
10049
9268
  let handle;
10050
9269
  try {
10051
- handle = await fs18.open(fp, "r");
9270
+ handle = await fs17.open(fp, "r");
10052
9271
  } catch {
10053
9272
  return null;
10054
9273
  }
@@ -10178,7 +9397,7 @@ var SessionAnalyzer = class {
10178
9397
  import { spawn as spawn2 } from "node:child_process";
10179
9398
  import { createHash as createHash7, randomUUID as randomUUID6 } from "node:crypto";
10180
9399
  import * as fsp9 from "node:fs/promises";
10181
- import * as path24 from "node:path";
9400
+ import * as path23 from "node:path";
10182
9401
 
10183
9402
  // src/utils/child-env.ts
10184
9403
  var ALLOWED_KEYS = /* @__PURE__ */ new Set([
@@ -10345,13 +9564,13 @@ function sha256(content) {
10345
9564
  return createHash7("sha256").update(content).digest("hex");
10346
9565
  }
10347
9566
  function isInside(root, target) {
10348
- const relative8 = path24.relative(root, target);
10349
- return relative8 === "" || !relative8.startsWith("..") && !path24.isAbsolute(relative8);
9567
+ const relative8 = path23.relative(root, target);
9568
+ return relative8 === "" || !relative8.startsWith("..") && !path23.isAbsolute(relative8);
10350
9569
  }
10351
9570
  function normalizeRelative(input) {
10352
- if (!input || path24.isAbsolute(input)) return null;
9571
+ if (!input || path23.isAbsolute(input)) return null;
10353
9572
  const normalized = input.replace(/\\/g, "/").replace(/^\.\//, "");
10354
- const resolved = path24.posix.normalize(normalized);
9573
+ const resolved = path23.posix.normalize(normalized);
10355
9574
  if (!resolved || resolved === "." || resolved === ".." || resolved.startsWith("../")) return null;
10356
9575
  return resolved;
10357
9576
  }
@@ -10366,8 +9585,8 @@ var SessionCheckpointCas = class {
10366
9585
  projectRoot;
10367
9586
  runGit;
10368
9587
  constructor(opts) {
10369
- this.rootDir = path24.resolve(opts.rootDir);
10370
- this.projectRoot = path24.resolve(opts.projectRoot);
9588
+ this.rootDir = path23.resolve(opts.rootDir);
9589
+ this.projectRoot = path23.resolve(opts.projectRoot);
10371
9590
  this.runGit = opts.runGit ?? defaultRunGit;
10372
9591
  }
10373
9592
  async capture(_sessionId, _promptIndex) {
@@ -10392,7 +9611,7 @@ var SessionCheckpointCas = class {
10392
9611
  relativePaths,
10393
9612
  CAPTURE_CONCURRENCY,
10394
9613
  async (relative8) => {
10395
- const absolute = path24.resolve(this.projectRoot, ...relative8.split("/"));
9614
+ const absolute = path23.resolve(this.projectRoot, ...relative8.split("/"));
10396
9615
  if (!isInside(this.projectRoot, absolute)) {
10397
9616
  unresolved.push({ path: relative8, reason: "path escapes project root" });
10398
9617
  return null;
@@ -10401,8 +9620,8 @@ var SessionCheckpointCas = class {
10401
9620
  const stat19 = await fsp9.lstat(absolute);
10402
9621
  if (stat19.isSymbolicLink()) {
10403
9622
  const linkTarget = await fsp9.readlink(absolute);
10404
- const resolvedLink = path24.resolve(path24.dirname(absolute), linkTarget);
10405
- if (path24.isAbsolute(linkTarget) || !isInside(this.projectRoot, resolvedLink)) {
9623
+ const resolvedLink = path23.resolve(path23.dirname(absolute), linkTarget);
9624
+ if (path23.isAbsolute(linkTarget) || !isInside(this.projectRoot, resolvedLink)) {
10406
9625
  unresolved.push({
10407
9626
  path: relative8,
10408
9627
  reason: "symlink target escapes project root"
@@ -10464,7 +9683,7 @@ var SessionCheckpointCas = class {
10464
9683
  };
10465
9684
  }
10466
9685
  async materialize(checkpoint, targetRoot) {
10467
- const target = path24.resolve(targetRoot);
9686
+ const target = path23.resolve(targetRoot);
10468
9687
  if (target === this.projectRoot) {
10469
9688
  throw new Error("Refusing to materialize a workspace checkpoint over the parent project root");
10470
9689
  }
@@ -10504,10 +9723,10 @@ var SessionCheckpointCas = class {
10504
9723
  try {
10505
9724
  const output = await this.safeOutputPath(target, realTarget, entry.path);
10506
9725
  if (entry.state === "symlink") {
10507
- if (path24.isAbsolute(entry.linkTarget)) {
9726
+ if (path23.isAbsolute(entry.linkTarget)) {
10508
9727
  throw new Error("absolute symlink target refused");
10509
9728
  }
10510
- const resolvedLink = path24.resolve(path24.dirname(output), entry.linkTarget);
9729
+ const resolvedLink = path23.resolve(path23.dirname(output), entry.linkTarget);
10511
9730
  if (!isInside(target, resolvedLink)) throw new Error("symlink target escapes checkpoint root");
10512
9731
  }
10513
9732
  prepared.push({
@@ -10535,7 +9754,7 @@ var SessionCheckpointCas = class {
10535
9754
  await fsp9.unlink(output).catch((err) => {
10536
9755
  if (err.code !== "ENOENT") throw err;
10537
9756
  });
10538
- await fsp9.mkdir(path24.dirname(output), { recursive: true });
9757
+ await fsp9.mkdir(path23.dirname(output), { recursive: true });
10539
9758
  await fsp9.symlink(entry.linkTarget, output);
10540
9759
  writtenFiles.push(entry.path);
10541
9760
  continue;
@@ -10552,15 +9771,15 @@ var SessionCheckpointCas = class {
10552
9771
  }
10553
9772
  objectPath(hash) {
10554
9773
  if (!HASH_RE.test(hash)) throw new Error(`Invalid CAS object hash: ${hash}`);
10555
- return path24.join(this.rootDir, "objects", hash.slice(0, 2), hash.slice(2));
9774
+ return path23.join(this.rootDir, "objects", hash.slice(0, 2), hash.slice(2));
10556
9775
  }
10557
9776
  manifestPath(hash) {
10558
9777
  if (!HASH_RE.test(hash)) throw new Error(`Invalid checkpoint manifest hash: ${hash}`);
10559
- return path24.join(this.rootDir, "manifests", `${hash}.json`);
9778
+ return path23.join(this.rootDir, "manifests", `${hash}.json`);
10560
9779
  }
10561
9780
  async putBlob(hash, content) {
10562
9781
  const target = this.objectPath(hash);
10563
- await fsp9.mkdir(path24.dirname(target), { recursive: true });
9782
+ await fsp9.mkdir(path23.dirname(target), { recursive: true });
10564
9783
  try {
10565
9784
  const existing = await fsp9.readFile(target);
10566
9785
  if (sha256(existing) !== hash) throw new Error(`Corrupt CAS object collision: ${hash}`);
@@ -10568,9 +9787,9 @@ var SessionCheckpointCas = class {
10568
9787
  } catch (err) {
10569
9788
  if (err.code !== "ENOENT") throw err;
10570
9789
  }
10571
- const temp = path24.join(
10572
- path24.dirname(target),
10573
- `.${path24.basename(target)}.${process.pid}.${randomUUID6()}.tmp`
9790
+ const temp = path23.join(
9791
+ path23.dirname(target),
9792
+ `.${path23.basename(target)}.${process.pid}.${randomUUID6()}.tmp`
10574
9793
  );
10575
9794
  let handle;
10576
9795
  try {
@@ -10631,7 +9850,7 @@ var SessionCheckpointCas = class {
10631
9850
  async safeOutputPath(target, realTarget, relative8) {
10632
9851
  const normalized = normalizeRelative(relative8);
10633
9852
  if (!normalized) throw new Error("invalid relative path");
10634
- const output = path24.resolve(target, ...normalized.split("/"));
9853
+ const output = path23.resolve(target, ...normalized.split("/"));
10635
9854
  if (!isInside(target, output)) throw new Error("path escapes checkpoint target");
10636
9855
  let probe = output;
10637
9856
  for (; ; ) {
@@ -10641,7 +9860,7 @@ var SessionCheckpointCas = class {
10641
9860
  return output;
10642
9861
  } catch (err) {
10643
9862
  if (err.code !== "ENOENT") throw err;
10644
- const parent = path24.dirname(probe);
9863
+ const parent = path23.dirname(probe);
10645
9864
  if (parent === probe) throw err;
10646
9865
  probe = parent;
10647
9866
  }
@@ -10649,7 +9868,7 @@ var SessionCheckpointCas = class {
10649
9868
  }
10650
9869
  };
10651
9870
  function defaultRunGit(args, cwd) {
10652
- return new Promise((resolve14) => {
9871
+ return new Promise((resolve13) => {
10653
9872
  const stdoutChunks = [];
10654
9873
  const stderrChunks = [];
10655
9874
  let stdoutBytes = 0;
@@ -10692,8 +9911,8 @@ function defaultRunGit(args, cwd) {
10692
9911
  stdoutTruncated,
10693
9912
  stderrTruncated
10694
9913
  });
10695
- child.on("error", (err) => resolve14(result(1, err.message)));
10696
- child.on("close", (code) => resolve14(result(code ?? 1)));
9914
+ child.on("error", (err) => resolve13(result(1, err.message)));
9915
+ child.on("close", (code) => resolve13(result(code ?? 1)));
10697
9916
  });
10698
9917
  }
10699
9918
 
@@ -10846,13 +10065,13 @@ function generateSessionId(startedAt, _model) {
10846
10065
  }
10847
10066
 
10848
10067
  // src/storage/session-id-resolver.ts
10849
- import * as path25 from "node:path";
10068
+ import * as path24 from "node:path";
10850
10069
  function resolveSessionId(query, candidateIds) {
10851
10070
  const normalized = query.trim();
10852
10071
  if (!normalized) return { status: "missing", query: normalized };
10853
10072
  const uniqueIds = [...new Set(candidateIds)];
10854
10073
  if (uniqueIds.includes(normalized)) return { status: "resolved", id: normalized };
10855
- const leaf = (id) => path25.posix.basename(id.replace(/\\/g, "/"));
10074
+ const leaf = (id) => path24.posix.basename(id.replace(/\\/g, "/"));
10856
10075
  const exactLeafMatches = uniqueIds.filter((id) => leaf(id) === normalized);
10857
10076
  if (exactLeafMatches.length === 1) {
10858
10077
  return { status: "resolved", id: exactLeafMatches[0] };
@@ -10904,127 +10123,11 @@ function scrubPersistedSessionSummary(summary2, scrubber) {
10904
10123
  }
10905
10124
 
10906
10125
  // src/utils/regex-guard.ts
10907
- var MAX_PATTERN_LEN = 256;
10908
- var DANGEROUS_PATTERNS = [
10909
- // (a+)+, (.*)+, etc — nested quantifier on a group with internal quantifier
10910
- /(\([^)]*[+*][^)]*\))[+*]/,
10911
- /(\(\?:[^)]*[+*][^)]*\))[+*]/,
10912
- // Adjacent quantifiers: a++ a*+
10913
- /[+*]{2,}/,
10914
- // Quantifier on alternation with length 2+
10915
- /\([^|)]+\|[^)]+\)[+*][+*]/,
10916
- // Greedy quantifier inside lookahead/lookbehind — (?!.*a+)
10917
- /[([][^)\]]*[+*][^)\]]*[)\]][^)]*\?\??/
10918
- ];
10919
- function hasAmbiguousQuantifiedAlternation(pattern) {
10920
- for (let i = 0; i < pattern.length; i++) {
10921
- if (pattern[i] !== "(") continue;
10922
- if (i > 0 && pattern[i - 1] === "\\") continue;
10923
- let depth = 0;
10924
- let inClass = false;
10925
- let j = i;
10926
- for (; j < pattern.length; j++) {
10927
- const ch = pattern[j];
10928
- if (ch === "\\") {
10929
- j++;
10930
- continue;
10931
- }
10932
- if (inClass) {
10933
- if (ch === "]") inClass = false;
10934
- continue;
10935
- }
10936
- if (ch === "[") {
10937
- inClass = true;
10938
- continue;
10939
- }
10940
- if (ch === "(") depth++;
10941
- else if (ch === ")") {
10942
- depth--;
10943
- if (depth === 0) break;
10944
- }
10945
- }
10946
- if (j >= pattern.length) return false;
10947
- const next = pattern[j + 1];
10948
- if (next !== "+" && next !== "*" && next !== "{") continue;
10949
- let inner = pattern.slice(i + 1, j);
10950
- inner = inner.replace(/^\?(?::|<?[=!])/u, "");
10951
- const branches = [];
10952
- let current = "";
10953
- let d = 0;
10954
- let cls = false;
10955
- for (let k = 0; k < inner.length; k++) {
10956
- const ch = inner[k];
10957
- if (ch === "\\") {
10958
- current += ch + (inner[k + 1] ?? "");
10959
- k++;
10960
- continue;
10961
- }
10962
- if (cls) {
10963
- if (ch === "]") cls = false;
10964
- current += ch;
10965
- continue;
10966
- }
10967
- if (ch === "[") {
10968
- cls = true;
10969
- current += ch;
10970
- continue;
10971
- }
10972
- if (ch === "(") d++;
10973
- if (ch === ")") d--;
10974
- if (ch === "|" && d === 0) {
10975
- branches.push(current);
10976
- current = "";
10977
- continue;
10978
- }
10979
- current += ch;
10980
- }
10981
- branches.push(current);
10982
- if (branches.length < 2) continue;
10983
- for (let a = 0; a < branches.length; a++) {
10984
- for (let b = a + 1; b < branches.length; b++) {
10985
- const x = branches[a];
10986
- const y = branches[b];
10987
- if (x === "" || y === "") return true;
10988
- if (x === y || x.startsWith(y) || y.startsWith(x)) return true;
10989
- }
10990
- }
10991
- }
10992
- return false;
10993
- }
10994
- function compileUserRegex(pattern, flags) {
10995
- if (typeof pattern !== "string") {
10996
- return { ok: false, reason: "pattern must be a string" };
10997
- }
10998
- if (pattern.length === 0) {
10999
- return { ok: false, reason: "pattern is empty" };
11000
- }
11001
- if (pattern.length > MAX_PATTERN_LEN) {
11002
- return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
11003
- }
11004
- for (const rx of DANGEROUS_PATTERNS) {
11005
- if (rx.test(pattern)) {
11006
- return {
11007
- ok: false,
11008
- reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
11009
- };
11010
- }
11011
- }
11012
- if (hasAmbiguousQuantifiedAlternation(pattern)) {
11013
- return {
11014
- ok: false,
11015
- reason: "pattern quantifies an alternation with overlapping branches \u2014 rewrite so no two branches can match the same text"
11016
- };
11017
- }
11018
- try {
11019
- return { ok: true, regex: new RegExp(pattern, flags) };
11020
- } catch (err) {
11021
- return {
11022
- ok: false,
11023
- reason: err instanceof Error ? err.message : "invalid regex"
11024
- };
11025
- }
11026
- }
11027
- var MAX_SUBJECT_LEN = 64 * 1024;
10126
+ import {
10127
+ capSubject,
10128
+ compileUserRegex,
10129
+ MAX_SUBJECT_LEN
10130
+ } from "@wrongstack/primitives";
11028
10131
 
11029
10132
  // src/storage/session-reader.ts
11030
10133
  var DefaultSessionReader = class {
@@ -11380,13 +10483,13 @@ function renderPlainText(meta, events) {
11380
10483
 
11381
10484
  // src/storage/session-recovery.ts
11382
10485
  import { createReadStream } from "node:fs";
11383
- import * as fs19 from "node:fs/promises";
11384
- import * as path27 from "node:path";
10486
+ import * as fs18 from "node:fs/promises";
10487
+ import * as path26 from "node:path";
11385
10488
  import { createInterface } from "node:readline";
11386
10489
 
11387
10490
  // src/storage/session-store/directory-session-files.ts
11388
10491
  import * as fsp10 from "node:fs/promises";
11389
- import * as path26 from "node:path";
10492
+ import * as path25 from "node:path";
11390
10493
 
11391
10494
  // src/storage/session-store/directory-scan.ts
11392
10495
  function shouldSkipSessionDirectoryEntry(name) {
@@ -11415,12 +10518,12 @@ async function collectSessionFiles(dir, prefix = "", depth = 0) {
11415
10518
  if (entry.isDirectory()) {
11416
10519
  dirEntries.push(entry);
11417
10520
  } else if (entry.isFile() && isSessionTranscriptFileName(entry.name)) {
11418
- files.push({ id: sessionIdForFile(prefix, entry.name), filePath: path26.join(dir, entry.name) });
10521
+ files.push({ id: sessionIdForFile(prefix, entry.name), filePath: path25.join(dir, entry.name) });
11419
10522
  }
11420
10523
  }
11421
10524
  const childFileArrays = await Promise.all(
11422
10525
  dirEntries.map(
11423
- (entry) => collectSessionFiles(path26.join(dir, entry.name), childPrefixFor(entry, prefix, depth), depth + 1)
10526
+ (entry) => collectSessionFiles(path25.join(dir, entry.name), childPrefixFor(entry, prefix, depth), depth + 1)
11424
10527
  )
11425
10528
  );
11426
10529
  return [...childFileArrays.flat(), ...files];
@@ -11444,7 +10547,7 @@ async function collectSessionIds(dir, prefix = "", depth = 0) {
11444
10547
  }
11445
10548
  const childIdArrays = await Promise.all(
11446
10549
  dirEntries.map(
11447
- (entry) => collectSessionIds(path26.join(dir, entry.name), childPrefixFor(entry, prefix, depth), depth + 1)
10550
+ (entry) => collectSessionIds(path25.join(dir, entry.name), childPrefixFor(entry, prefix, depth), depth + 1)
11448
10551
  )
11449
10552
  );
11450
10553
  return [...childIdArrays.flat(), ...fileIds];
@@ -11454,11 +10557,14 @@ async function collectSessionIds(dir, prefix = "", depth = 0) {
11454
10557
  function extractInterruptedTools(plan) {
11455
10558
  const tools = [];
11456
10559
  const openCalls = /* @__PURE__ */ new Map();
10560
+ let anonymousSeq = 0;
11457
10561
  for (const ev of plan.pendingEvents) {
11458
10562
  if ((ev.type === "tool_use" || ev.type === "tool_call_start") && typeof ev.name === "string") {
11459
10563
  const toolName = ev.name;
11460
- const callId = ev.id ?? toolName;
10564
+ const rawId = ev.id;
10565
+ const callId = rawId ?? toolName;
11461
10566
  openCalls.set(callId, {
10567
+ id: rawId,
11462
10568
  name: toolName,
11463
10569
  args: ev.input ?? ev.args,
11464
10570
  ts: ev.ts
@@ -11469,8 +10575,10 @@ function extractInterruptedTools(plan) {
11469
10575
  } else if (ev.type === "llm_response" && Array.isArray(ev.content)) {
11470
10576
  for (const block of ev.content) {
11471
10577
  if (block && block.type === "tool_use" && typeof block.name === "string") {
11472
- const callId = block.id ?? block.name;
10578
+ const rawId = block.id;
10579
+ const callId = rawId ?? `${block.name}#${++anonymousSeq}`;
11473
10580
  openCalls.set(callId, {
10581
+ id: rawId,
11474
10582
  name: block.name,
11475
10583
  args: block.input,
11476
10584
  ts: ev.ts
@@ -11482,8 +10590,10 @@ function extractInterruptedTools(plan) {
11482
10590
  if (Array.isArray(msg.content)) {
11483
10591
  for (const block of msg.content) {
11484
10592
  if (block && block.type === "tool_use" && typeof block.name === "string") {
11485
- const callId = block.id ?? block.name;
10593
+ const rawId = block.id;
10594
+ const callId = rawId ?? `${block.name}#${++anonymousSeq}`;
11486
10595
  openCalls.set(callId, {
10596
+ id: rawId,
11487
10597
  name: block.name,
11488
10598
  args: block.input,
11489
10599
  ts: ev.ts
@@ -11506,6 +10616,7 @@ function extractInterruptedTools(plan) {
11506
10616
  }
11507
10617
  }
11508
10618
  tools.push({
10619
+ id: call.id,
11509
10620
  name: call.name,
11510
10621
  argsSummary,
11511
10622
  ts: call.ts
@@ -11520,6 +10631,47 @@ var SessionRecovery = class _SessionRecovery {
11520
10631
  dir;
11521
10632
  static MAX_PENDING_EVENTS = 1e4;
11522
10633
  static MAX_PENDING_BYTES = 16 * 1024 * 1024;
10634
+ /**
10635
+ * Build a recovery plan from ALREADY-LOADED events without touching disk.
10636
+ * executeResumeSession uses this because load() has already paid for the
10637
+ * transcript read; recover()'s file scan would duplicate it.
10638
+ */
10639
+ static buildRecoveryPlan(events, sessionId) {
10640
+ const pendingEvents = [];
10641
+ const pendingSizes = [];
10642
+ let pendingBytes = 0;
10643
+ let lastCheckpoint = null;
10644
+ let latestBoundary = null;
10645
+ for (const event of events) {
10646
+ if (!event || typeof event !== "object" || typeof event.type !== "string") continue;
10647
+ if (event.type === "checkpoint") {
10648
+ lastCheckpoint = event;
10649
+ pendingEvents.length = 0;
10650
+ pendingSizes.length = 0;
10651
+ pendingBytes = 0;
10652
+ continue;
10653
+ }
10654
+ if (isLifecycleBoundary(event)) latestBoundary = event;
10655
+ const bytes = Buffer.byteLength(JSON.stringify(event), "utf8");
10656
+ if (bytes > _SessionRecovery.MAX_PENDING_BYTES) continue;
10657
+ while (pendingEvents.length >= _SessionRecovery.MAX_PENDING_EVENTS || pendingBytes + bytes > _SessionRecovery.MAX_PENDING_BYTES) {
10658
+ pendingBytes -= pendingSizes.shift();
10659
+ pendingEvents.shift();
10660
+ }
10661
+ pendingEvents.push(event);
10662
+ pendingSizes.push(bytes);
10663
+ pendingBytes += bytes;
10664
+ }
10665
+ const inFlightStart = latestBoundary?.type === "in_flight_start" ? latestBoundary : null;
10666
+ return {
10667
+ sessionId,
10668
+ stale: inFlightStart !== null,
10669
+ lastCheckpoint,
10670
+ pendingEvents,
10671
+ inFlightStart,
10672
+ context: inFlightStart?.context ?? null
10673
+ };
10674
+ }
11523
10675
  /**
11524
10676
  * Scan a session log and return a `StaleSession` if and only if the newest
11525
10677
  * lifecycle boundary is an `in_flight_start` without a later
@@ -11550,9 +10702,8 @@ var SessionRecovery = class _SessionRecovery {
11550
10702
  const fp = this.filePath(sessionId);
11551
10703
  let stat19;
11552
10704
  try {
11553
- stat19 = await fs19.stat(fp);
11554
- } catch (err) {
11555
- if (err.code === "ENOENT") return null;
10705
+ stat19 = await fs18.stat(fp);
10706
+ } catch {
11556
10707
  return null;
11557
10708
  }
11558
10709
  if (stat19.size === 0) return null;
@@ -11612,7 +10763,7 @@ var SessionRecovery = class _SessionRecovery {
11612
10763
  if (bytes <= _SessionRecovery.MAX_PENDING_BYTES) {
11613
10764
  while (pendingEvents.length >= _SessionRecovery.MAX_PENDING_EVENTS || pendingBytes + bytes > _SessionRecovery.MAX_PENDING_BYTES) {
11614
10765
  pendingEvents.shift();
11615
- pendingBytes = Math.max(0, pendingBytes - (pendingSizes.shift() ?? 0));
10766
+ pendingBytes = Math.max(0, pendingBytes - pendingSizes.shift());
11616
10767
  }
11617
10768
  pendingEvents.push(event);
11618
10769
  pendingSizes.push(bytes);
@@ -11621,8 +10772,7 @@ var SessionRecovery = class _SessionRecovery {
11621
10772
  }
11622
10773
  if (isLifecycleBoundary(event)) latestBoundary = event;
11623
10774
  }
11624
- } catch (err) {
11625
- if (err.code === "ENOENT") return null;
10775
+ } catch {
11626
10776
  return null;
11627
10777
  } finally {
11628
10778
  lines.close();
@@ -11650,7 +10800,7 @@ var SessionRecovery = class _SessionRecovery {
11650
10800
  const collect = async (dir, prefix, depth) => {
11651
10801
  let entries;
11652
10802
  try {
11653
- entries = await fs19.readdir(dir, { withFileTypes: true });
10803
+ entries = await fs18.readdir(dir, { withFileTypes: true });
11654
10804
  } catch {
11655
10805
  return;
11656
10806
  }
@@ -11660,7 +10810,7 @@ var SessionRecovery = class _SessionRecovery {
11660
10810
  continue;
11661
10811
  if (entry.isDirectory()) {
11662
10812
  if (depth === 0) {
11663
- await collect(path27.join(dir, entry.name), entry.name, depth + 1);
10813
+ await collect(path26.join(dir, entry.name), entry.name, depth + 1);
11664
10814
  }
11665
10815
  continue;
11666
10816
  }
@@ -11698,7 +10848,7 @@ function hasNonWhitespace(line) {
11698
10848
  }
11699
10849
  async function scanLatestLifecycleBoundary(filePath, size) {
11700
10850
  const CHUNK_SIZE2 = 64 * 1024;
11701
- const handle = await fs19.open(filePath, "r");
10851
+ const handle = await fs18.open(filePath, "r");
11702
10852
  let position = size;
11703
10853
  let laterLineFragment = Buffer.alloc(0);
11704
10854
  let latestBoundary = null;
@@ -11738,7 +10888,7 @@ async function scanLatestLifecycleBoundary(filePath, size) {
11738
10888
  // src/storage/session-store.ts
11739
10889
  import { randomUUID as randomUUID7 } from "node:crypto";
11740
10890
  import * as fsp26 from "node:fs/promises";
11741
- import * as path34 from "node:path";
10891
+ import * as path33 from "node:path";
11742
10892
 
11743
10893
  // src/utils/message-invariants.ts
11744
10894
  function repairToolUseAdjacency(messages) {
@@ -11848,7 +10998,7 @@ function isEmptyMessage(msg) {
11848
10998
 
11849
10999
  // src/storage/file-session-writer.ts
11850
11000
  import * as fsp14 from "node:fs/promises";
11851
- import * as path28 from "node:path";
11001
+ import * as path27 from "node:path";
11852
11002
 
11853
11003
  // src/storage/session-summary-tracker.ts
11854
11004
  import { createReadStream as createReadStream2 } from "node:fs";
@@ -11904,6 +11054,29 @@ var SessionSummaryTracker = class {
11904
11054
  get currentSummary() {
11905
11055
  return this.summary;
11906
11056
  }
11057
+ /**
11058
+ * Non-destructive materialization of every live counter into a summary
11059
+ * snapshot. Unlike finalize(), it stamps no endedAt, applies no final
11060
+ * outcome, and resolves no name — mid-session metadata checkpoints use it
11061
+ * so a killed process leaves accurate listing metadata behind without
11062
+ * pretending the session ended cleanly.
11063
+ */
11064
+ snapshot() {
11065
+ const { lastUserMessage: _lastUserMessage, ...rest } = this.summary;
11066
+ return {
11067
+ ...rest,
11068
+ messageCount: this.messageCount,
11069
+ ...this.lastUserMessage !== void 0 ? { lastUserMessage: this.lastUserMessage } : {},
11070
+ iterationCount: this.iterationCount,
11071
+ toolCallCount: this.toolCallCount,
11072
+ toolErrorCount: this.toolErrorCount,
11073
+ fileChangeCount: this.fileChangeCount,
11074
+ compactionCount: this.compactionCount > 0 ? this.compactionCount : void 0,
11075
+ toolBreakdown: { ...this.toolBreakdown },
11076
+ lastActivityAt: this.lastActivityAt,
11077
+ ...this.outcome !== void 0 ? { outcome: this.outcome } : {}
11078
+ };
11079
+ }
11907
11080
  get pendingToolUses() {
11908
11081
  return Array.from(this.openToolUses);
11909
11082
  }
@@ -12096,6 +11269,12 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
12096
11269
  lastAppendWarnAt = 0;
12097
11270
  writeChain = Promise.resolve();
12098
11271
  flushPromise = null;
11272
+ /**
11273
+ * Batch currently inside enqueueWrite. `flushSync` may steal it if the
11274
+ * async append has not started, so a dying process writes in-flight +
11275
+ * remaining buffer as one ordered append instead of racing a second fd.
11276
+ */
11277
+ inFlight = null;
12099
11278
  get isClosed() {
12100
11279
  return false;
12101
11280
  }
@@ -12136,14 +11315,20 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
12136
11315
  return true;
12137
11316
  }
12138
11317
  enqueueWrite(data) {
11318
+ return this.enqueueFlight({ data, stolen: false, started: false });
11319
+ }
11320
+ enqueueFlight(flight) {
12139
11321
  const write = this.writeChain.then(async () => {
11322
+ if (flight.stolen) return;
11323
+ flight.started = true;
11324
+ if (flight.stolen) return;
12140
11325
  try {
12141
- return await this.opts.getHandle().appendFile(data, "utf8");
11326
+ return await this.opts.getHandle().appendFile(flight.data, "utf8");
12142
11327
  } catch (err) {
12143
11328
  if (isClosedHandleError(err)) {
12144
11329
  const reloaded = await fsp12.open(this.opts.filePath, "a", 384);
12145
11330
  this.opts.setHandle(reloaded);
12146
- return await reloaded.appendFile(data, "utf8");
11331
+ return await reloaded.appendFile(flight.data, "utf8");
12147
11332
  }
12148
11333
  throw err;
12149
11334
  }
@@ -12168,17 +11353,29 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
12168
11353
  this.flushTimer = null;
12169
11354
  }
12170
11355
  }
12171
- async flushBuffer(isClosed = false) {
12172
- if (this.flushPromise) return this.flushPromise;
11356
+ async flushBuffer(isClosed = false, opts = {}) {
11357
+ if (this.flushPromise) {
11358
+ const joined = this.flushPromise;
11359
+ const continueIfDirty = () => {
11360
+ if (this.writeBuffer.length === 0) return Promise.resolve();
11361
+ return this.flushBuffer(isClosed, opts);
11362
+ };
11363
+ if (opts.datasync === true) {
11364
+ return joined.then(
11365
+ () => this.opts.getHandle().datasync().catch(() => void 0).then(continueIfDirty)
11366
+ );
11367
+ }
11368
+ return joined.then(continueIfDirty);
11369
+ }
12173
11370
  const flush = (async () => {
12174
- while (this.writeBuffer.length > 0) await this.flushBufferOnce(isClosed);
11371
+ while (this.writeBuffer.length > 0) await this.flushBufferOnce(isClosed, opts);
12175
11372
  })().finally(() => {
12176
11373
  if (this.flushPromise === flush) this.flushPromise = null;
12177
11374
  });
12178
11375
  this.flushPromise = flush;
12179
11376
  return flush;
12180
11377
  }
12181
- async flushBufferOnce(isClosed) {
11378
+ async flushBufferOnce(isClosed, opts) {
12182
11379
  if (this.writeBuffer.length === 0) return;
12183
11380
  const events = this.writeBuffer;
12184
11381
  const eventCount = events.length;
@@ -12186,12 +11383,21 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
12186
11383
  const batch = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
12187
11384
  this.writeBuffer = [];
12188
11385
  this.writeBufferBytes = 0;
11386
+ const flight = { data: batch, stolen: false, started: false };
11387
+ this.inFlight = flight;
12189
11388
  const t0 = Date.now();
12190
11389
  let outcome = "success";
12191
11390
  let errorMsg;
12192
11391
  try {
12193
- await this.enqueueWrite(batch);
11392
+ await this.enqueueFlight(flight);
11393
+ if (flight.stolen) {
11394
+ return;
11395
+ }
11396
+ if (opts?.datasync === true) {
11397
+ await this.opts.getHandle().datasync().catch(() => void 0);
11398
+ }
12194
11399
  } catch (err) {
11400
+ if (flight.stolen) return;
12195
11401
  outcome = "failure";
12196
11402
  errorMsg = toErrorMessage(err);
12197
11403
  const newer = this.writeBuffer;
@@ -12229,6 +11435,7 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
12229
11435
  ...eventCount !== void 0 ? { eventCount } : {},
12230
11436
  ...this.opts.getTraceId?.() ? { traceId: this.opts.getTraceId() } : {}
12231
11437
  });
11438
+ if (this.inFlight === flight) this.inFlight = null;
12232
11439
  }
12233
11440
  }
12234
11441
  async drainWriteChain() {
@@ -12237,18 +11444,56 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
12237
11444
  async drainFlushPromise() {
12238
11445
  await this.flushPromise?.catch(() => void 0);
12239
11446
  }
11447
+ /**
11448
+ * Last-gasp synchronous append (SIGKILL/SIGTERM traps, `process.on('exit')`).
11449
+ *
11450
+ * Failure contract: nothing is discarded before the write is known to have
11451
+ * landed. Buffered events stay in `writeBuffer` — it is cleared only after
11452
+ * `fsyncSync` returns — and a stolen in-flight batch is handed back to the
11453
+ * async write chain, so a survivable failure (EACCES, ENOSPC, EMFILE) loses
11454
+ * nothing. Failure is never silent: a structured `session.flush_sync_failed`
11455
+ * warning names exactly what was still pending, which is what SIGKILL-trap
11456
+ * tests assert against.
11457
+ */
12240
11458
  flushSync() {
12241
- if (this.writeBuffer.length === 0 || !this.opts.filePath) return;
11459
+ if (!this.opts.filePath) return;
12242
11460
  this.cancelTimer();
12243
- const batch = this.writeBuffer.map((e) => JSON.stringify(e)).join("\n") + "\n";
12244
- this.writeBuffer = [];
12245
- this.writeBufferBytes = 0;
11461
+ const chunks = [];
11462
+ const flight = this.inFlight;
11463
+ const stole = flight !== null && !flight.started && !flight.stolen;
11464
+ if (stole && flight) {
11465
+ flight.stolen = true;
11466
+ chunks.push(flight.data);
11467
+ }
11468
+ const events = this.writeBuffer;
11469
+ if (events.length > 0) {
11470
+ chunks.push(events.map((e) => JSON.stringify(e)).join("\n") + "\n");
11471
+ }
11472
+ if (chunks.length === 0) return;
12246
11473
  let fd;
12247
11474
  try {
12248
11475
  fd = openSync(this.opts.filePath, "a");
12249
- writeSync(fd, batch, null, "utf8");
11476
+ writeSync(fd, chunks.join(""), null, "utf8");
12250
11477
  fsyncSync(fd);
12251
- } catch {
11478
+ if (this.writeBuffer === events) {
11479
+ this.writeBuffer = [];
11480
+ this.writeBufferBytes = 0;
11481
+ }
11482
+ } catch (err) {
11483
+ if (stole && flight) flight.stolen = false;
11484
+ console.warn(
11485
+ JSON.stringify({
11486
+ level: "error",
11487
+ event: "session.flush_sync_failed",
11488
+ sessionId: this.opts.sessionId,
11489
+ filePath: this.opts.filePath,
11490
+ message: toErrorMessage(err),
11491
+ pendingEvents: events.length,
11492
+ pendingBytes: this.writeBufferBytes,
11493
+ hadInFlightBatch: stole,
11494
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
11495
+ })
11496
+ );
12252
11497
  } finally {
12253
11498
  if (fd !== void 0) {
12254
11499
  try {
@@ -12416,6 +11661,17 @@ function isClosedHandleError2(err) {
12416
11661
  const code = err?.code;
12417
11662
  return code === "EBADF" || code === "ERR_CLOSED_RESOURCE" || code === "ERR_INVALID_HANDLE";
12418
11663
  }
11664
+ var CRITICAL_EVENT_TYPES = /* @__PURE__ */ new Set([
11665
+ "user_input",
11666
+ "llm_response",
11667
+ "checkpoint",
11668
+ "in_flight_start",
11669
+ "in_flight_end"
11670
+ ]);
11671
+ function isCriticalEvent(event) {
11672
+ return CRITICAL_EVENT_TYPES.has(event.type);
11673
+ }
11674
+ var METADATA_CHECKPOINT_INTERVAL_MS = 1e4;
12419
11675
  var FileSessionWriter = class _FileSessionWriter {
12420
11676
  constructor(id, handle, startedAt, meta, events, opts = {}, traceId) {
12421
11677
  this.id = id;
@@ -12424,13 +11680,15 @@ var FileSessionWriter = class _FileSessionWriter {
12424
11680
  this.meta = meta;
12425
11681
  this.events = events;
12426
11682
  this.resumed = opts.resumed ?? false;
12427
- this.manifestFile = opts.dir ? path28.join(opts.dir, `${path28.basename(id)}.summary.json`) : "";
11683
+ this.manifestFile = opts.dir ? path27.join(opts.dir, `${path27.basename(id)}.summary.json`) : "";
12428
11684
  this.filePath = opts.filePath ?? "";
12429
11685
  this.secretScrubber = opts.secretScrubber;
12430
11686
  this.checkpointCas = opts.checkpointCas;
12431
11687
  this._onAppend = opts.onAppend;
12432
11688
  this._onAppendBatch = opts.onAppendBatch;
12433
11689
  this.onCloseCb = opts.onClose;
11690
+ this.onMetadataCheckpointCb = opts.onMetadataCheckpoint;
11691
+ this.metadataCheckpointMs = opts.metadataCheckpointMs ?? METADATA_CHECKPOINT_INTERVAL_MS;
12434
11692
  this.summaryTracker = new SessionSummaryTracker({
12435
11693
  id,
12436
11694
  startedAt,
@@ -12498,6 +11756,15 @@ var FileSessionWriter = class _FileSessionWriter {
12498
11756
  this._onAppendBatch = cb;
12499
11757
  }
12500
11758
  onCloseCb;
11759
+ /** Mid-session metadata checkpoint throttle. 0 disables checkpointing. */
11760
+ metadataCheckpointMs;
11761
+ /** One-shot guard for the "interval set but no sink" warning below. */
11762
+ _checkpointNoSinkWarned = false;
11763
+ onMetadataCheckpointCb;
11764
+ /** Set whenever summary counters changed since the last metadata checkpoint. */
11765
+ metadataDirty = false;
11766
+ metadataTimer = null;
11767
+ metadataCheckpointInFlight = null;
12501
11768
  /** Implements SessionWriter.traceId — propagated from ContextInit.traceId. */
12502
11769
  traceId;
12503
11770
  /**
@@ -12529,18 +11796,31 @@ var FileSessionWriter = class _FileSessionWriter {
12529
11796
  void this.ensureInit();
12530
11797
  const appendEvent = scrubSessionWriterEvent(event, this.secretScrubber);
12531
11798
  this.summaryTracker.observe(appendEvent);
11799
+ this.metadataDirty = true;
11800
+ this.scheduleMetadataCheckpoint();
12532
11801
  try {
12533
11802
  this._onAppend?.(appendEvent);
12534
11803
  } catch {
12535
11804
  }
11805
+ const critical = isCriticalEvent(appendEvent);
12536
11806
  if (!this.buffer.push(appendEvent)) {
12537
11807
  this.buffer.cancelTimer();
12538
- void this.buffer.flushBuffer(this.closed).catch(() => void 0).then(() => {
12539
- this.buffer.push(appendEvent);
11808
+ void this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => void 0).then(() => {
11809
+ if (this.buffer.push(appendEvent)) {
11810
+ if (!critical) return;
11811
+ this.buffer.cancelTimer();
11812
+ void this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => void 0);
11813
+ return;
11814
+ }
11815
+ void this.buffer.drainWriteChain().then(() => this.buffer.enqueueWrite(`${JSON.stringify(appendEvent)}
11816
+ `)).then(() => {
11817
+ if (!critical) return;
11818
+ return this.handle.datasync().catch(() => void 0);
11819
+ }).catch(() => void 0);
12540
11820
  });
12541
- } else if (this.buffer.shouldFlushNow()) {
11821
+ } else if (critical || this.buffer.shouldFlushNow()) {
12542
11822
  this.buffer.cancelTimer();
12543
- void this.buffer.flushBuffer(this.closed).catch(() => {
11823
+ void this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => {
12544
11824
  });
12545
11825
  } else {
12546
11826
  this.buffer.scheduleFlush(this.closed);
@@ -12611,23 +11891,112 @@ var FileSessionWriter = class _FileSessionWriter {
12611
11891
  provider: this.meta.provider ?? "unknown"
12612
11892
  });
12613
11893
  }
11894
+ /**
11895
+ * Arm the mid-session metadata checkpoint timer if it is not already armed.
11896
+ * Called after every observed event; the timer itself is unref'd so an idle
11897
+ * session never keeps the process alive for a cosmetic sidecar refresh.
11898
+ */
11899
+ scheduleMetadataCheckpoint() {
11900
+ if (this.closed || this.metadataTimer) return;
11901
+ if (this.metadataCheckpointMs <= 0) return;
11902
+ if (!this.onMetadataCheckpointCb && !this.manifestFile) {
11903
+ if (!this._checkpointNoSinkWarned) {
11904
+ this._checkpointNoSinkWarned = true;
11905
+ console.warn(
11906
+ JSON.stringify({
11907
+ level: "warn",
11908
+ event: "session.metadata_checkpoint_no_sink",
11909
+ sessionId: this.id,
11910
+ message: "metadataCheckpointMs set but neither onMetadataCheckpoint nor manifestFile configured; mid-session checkpoints disabled.",
11911
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
11912
+ })
11913
+ );
11914
+ }
11915
+ return;
11916
+ }
11917
+ this.metadataTimer = setTimeout(() => {
11918
+ this.metadataTimer = null;
11919
+ void this.runMetadataCheckpoint();
11920
+ }, this.metadataCheckpointMs);
11921
+ this.metadataTimer.unref?.();
11922
+ }
11923
+ /**
11924
+ * Persist a mid-session summary snapshot: the `.summary.json` sidecar under
11925
+ * the manifest lock, then the store-level index row / catalog upsert via
11926
+ * `onMetadataCheckpoint`. Runs at most once per throttle interval and only
11927
+ * when summary counters changed since the last checkpoint; a failed
11928
+ * checkpoint stays dirty and retries on the next armed tick.
11929
+ */
11930
+ runMetadataCheckpoint() {
11931
+ if (this.closed || !this.metadataDirty) return Promise.resolve();
11932
+ if (this.metadataCheckpointInFlight) return this.metadataCheckpointInFlight;
11933
+ const {
11934
+ endedAt: _priorEndedAt,
11935
+ outcome: _priorOutcome,
11936
+ ...snapshot
11937
+ } = this.summaryTracker.snapshot();
11938
+ const run = (async () => {
11939
+ const t0 = Date.now();
11940
+ let outcome = "success";
11941
+ let errorMsg;
11942
+ try {
11943
+ if (this.manifestFile) {
11944
+ await withFileLock(this.manifestFile, async () => {
11945
+ await atomicWrite(this.manifestFile, JSON.stringify(snapshot), { mode: 384 });
11946
+ });
11947
+ }
11948
+ this.metadataDirty = false;
11949
+ await this.onMetadataCheckpointCb?.(snapshot);
11950
+ if (this.metadataDirty && !this.closed) this.scheduleMetadataCheckpoint();
11951
+ } catch (err) {
11952
+ outcome = "failure";
11953
+ errorMsg = toErrorMessage(err);
11954
+ this.metadataDirty = true;
11955
+ this.scheduleMetadataCheckpoint();
11956
+ } finally {
11957
+ this.metadataCheckpointInFlight = null;
11958
+ this.events?.emit("storage.write", {
11959
+ sessionId: this.id,
11960
+ store: "session",
11961
+ filePath: this.manifestFile || this.filePath,
11962
+ operation: "metadata_checkpoint",
11963
+ outcome,
11964
+ durationMs: Date.now() - t0,
11965
+ ...errorMsg !== void 0 ? { error: errorMsg } : {},
11966
+ ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
11967
+ });
11968
+ }
11969
+ })();
11970
+ this.metadataCheckpointInFlight = run;
11971
+ return run;
11972
+ }
12614
11973
  async append(event) {
12615
11974
  if (this.closed) return;
12616
11975
  await this.ensureInit();
12617
11976
  const scrubbed = scrubSessionWriterEvent(event, this.secretScrubber);
12618
11977
  this.summaryTracker.observe(scrubbed);
11978
+ this.metadataDirty = true;
11979
+ this.scheduleMetadataCheckpoint();
12619
11980
  try {
12620
11981
  this._onAppend?.(scrubbed);
12621
11982
  } catch {
12622
11983
  }
12623
- if (!this.buffer.push(scrubbed)) {
11984
+ let pushed = this.buffer.push(scrubbed);
11985
+ if (!pushed) {
12624
11986
  this.buffer.cancelTimer();
12625
- await this.buffer.flushBuffer(this.closed).catch(() => void 0);
12626
- this.buffer.push(scrubbed);
11987
+ await this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => void 0);
11988
+ pushed = this.buffer.push(scrubbed);
11989
+ if (!pushed) {
11990
+ await this.buffer.drainWriteChain().then(() => this.buffer.enqueueWrite(`${JSON.stringify(scrubbed)}
11991
+ `)).then(() => {
11992
+ if (!isCriticalEvent(scrubbed)) return;
11993
+ return this.handle.datasync().catch(() => void 0);
11994
+ }).catch(() => void 0);
11995
+ }
12627
11996
  }
12628
- if (this.buffer.shouldFlushNow()) {
11997
+ if (isCriticalEvent(scrubbed) || this.buffer.shouldFlushNow()) {
12629
11998
  this.buffer.cancelTimer();
12630
- await this.buffer.flushBuffer(this.closed).catch(() => {
11999
+ await this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => {
12631
12000
  });
12632
12001
  } else {
12633
12002
  this.buffer.scheduleFlush(this.closed);
@@ -12644,20 +12013,33 @@ var FileSessionWriter = class _FileSessionWriter {
12644
12013
  this._onAppend?.(scrubbed);
12645
12014
  } catch {
12646
12015
  }
12647
- if (!this.buffer.push(scrubbed)) {
12016
+ let pushed = this.buffer.push(scrubbed);
12017
+ if (!pushed) {
12648
12018
  this.buffer.cancelTimer();
12649
- await this.buffer.flushBuffer(this.closed).catch(() => void 0);
12650
- this.buffer.push(scrubbed);
12019
+ await this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => void 0);
12020
+ pushed = this.buffer.push(scrubbed);
12021
+ if (!pushed) {
12022
+ await this.buffer.drainWriteChain().then(() => this.buffer.enqueueWrite(`${JSON.stringify(scrubbed)}
12023
+ `)).then(() => {
12024
+ if (!isCriticalEvent(scrubbed)) return;
12025
+ return this.handle.datasync().catch(() => void 0);
12026
+ }).catch(() => void 0);
12027
+ }
12651
12028
  }
12652
12029
  scrubbedBatch.push(scrubbed);
12653
12030
  }
12031
+ if (scrubbedBatch.length > 0) {
12032
+ this.metadataDirty = true;
12033
+ this.scheduleMetadataCheckpoint();
12034
+ }
12654
12035
  try {
12655
12036
  this._onAppendBatch?.(scrubbedBatch);
12656
12037
  } catch {
12657
12038
  }
12658
- if (this.buffer.shouldFlushNow()) {
12039
+ const hasCritical = scrubbedBatch.some(isCriticalEvent);
12040
+ if (hasCritical || this.buffer.shouldFlushNow()) {
12659
12041
  this.buffer.cancelTimer();
12660
- await this.buffer.flushBuffer(this.closed).catch(() => {
12042
+ await this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => {
12661
12043
  });
12662
12044
  } else {
12663
12045
  this.buffer.scheduleFlush(this.closed);
@@ -12665,8 +12047,10 @@ var FileSessionWriter = class _FileSessionWriter {
12665
12047
  }
12666
12048
  /**
12667
12049
  * Flush buffered events to disk immediately. Critical events
12668
- * (user_input, llm_response) call this so they survive SIGKILL/crash
12669
- * instead of sitting in the in-memory buffer for up to 500ms.
12050
+ * (user_input, llm_response, checkpoint, in_flight_*) already flush
12051
+ * themselves inside append()/appendBatch(), so calling this matters for
12052
+ * non-critical tails that would otherwise sit in the in-memory buffer
12053
+ * for up to 500ms.
12670
12054
  *
12671
12055
  * Idempotent — cancels any pending timer, writes whatever has accumulated,
12672
12056
  * then asks the OS to synchronize the file data before resolving. Even an
@@ -12675,7 +12059,7 @@ var FileSessionWriter = class _FileSessionWriter {
12675
12059
  async flush() {
12676
12060
  if (this.closed) return;
12677
12061
  this.buffer.cancelTimer();
12678
- await this.buffer.flushBuffer(this.closed);
12062
+ await this.buffer.flushBuffer(this.closed, { datasync: true });
12679
12063
  await this.buffer.drainWriteChain();
12680
12064
  try {
12681
12065
  await this.handle.datasync();
@@ -12691,23 +12075,35 @@ var FileSessionWriter = class _FileSessionWriter {
12691
12075
  * Last-gasp synchronous drain for hard-exit paths (process.exit after
12692
12076
  * rapid Ctrl+C). The async write chain cannot be awaited when the process
12693
12077
  * is about to die, but whatever still sits in the in-memory buffer CAN be
12694
- * saved with a blocking append. Best-effort: an in-flight async write may
12695
- * be cut off by the exit regardless; errors here are swallowed.
12078
+ * saved with a blocking append. A failed sync append leaves the buffer
12079
+ * intact so a subsequent close()/flush() can retry. An in-flight async
12080
+ * write may still be cut off by a hard exit.
12696
12081
  */
12697
12082
  flushSync() {
12698
12083
  this.buffer.flushSync();
12699
12084
  }
12700
12085
  async close() {
12701
12086
  if (this.closePromise) return this.closePromise;
12702
- this.closePromise = this.doClose().catch((err) => {
12087
+ this.closePromise = this.doClose().catch(async (err) => {
12088
+ if (this.metadataTimer) {
12089
+ clearTimeout(this.metadataTimer);
12090
+ this.metadataTimer = null;
12091
+ }
12092
+ await this.metadataCheckpointInFlight?.catch(() => void 0);
12703
12093
  this.closed = false;
12704
12094
  this.closePromise = null;
12705
12095
  if (this.buffer.length > 0) this.buffer.scheduleFlush(this.closed);
12096
+ if (this.metadataDirty) this.scheduleMetadataCheckpoint();
12706
12097
  throw err;
12707
12098
  });
12708
12099
  return this.closePromise;
12709
12100
  }
12710
12101
  async doClose() {
12102
+ if (this.metadataTimer) {
12103
+ clearTimeout(this.metadataTimer);
12104
+ this.metadataTimer = null;
12105
+ }
12106
+ await this.metadataCheckpointInFlight?.catch(() => void 0);
12711
12107
  await this.ensureInit();
12712
12108
  if (this.pendingFileSnapshots.length > 0) {
12713
12109
  await this.writeFileSnapshot(this.activePromptIndex ?? 0, [...this.pendingFileSnapshots]);
@@ -12715,8 +12111,13 @@ var FileSessionWriter = class _FileSessionWriter {
12715
12111
  this.pendingFileSnapshotBytes = 0;
12716
12112
  }
12717
12113
  this.closed = true;
12114
+ if (this.metadataTimer) {
12115
+ clearTimeout(this.metadataTimer);
12116
+ this.metadataTimer = null;
12117
+ }
12118
+ await this.metadataCheckpointInFlight?.catch(() => void 0);
12718
12119
  this.buffer.cancelTimer();
12719
- await this.buffer.flushBuffer(this.closed);
12120
+ await this.buffer.flushBuffer(this.closed, { datasync: true });
12720
12121
  await this.buffer.drainWriteChain();
12721
12122
  try {
12722
12123
  await this.handle.datasync();
@@ -12831,10 +12232,23 @@ var FileSessionWriter = class _FileSessionWriter {
12831
12232
  async truncateToCheckpoint(targetPromptIndex, revertedFiles = []) {
12832
12233
  if (!this.filePath) return 0;
12833
12234
  this.buffer.cancelTimer();
12834
- await this.buffer.flushBuffer(this.closed);
12235
+ await this.buffer.flushBuffer(this.closed, { datasync: true });
12835
12236
  await this.buffer.drainWriteChain();
12836
- const plan = await findSessionCheckpointTruncatePlan(this.filePath, targetPromptIndex);
12837
- if (!plan) return 0;
12237
+ if (this.metadataTimer) {
12238
+ clearTimeout(this.metadataTimer);
12239
+ this.metadataTimer = null;
12240
+ }
12241
+ await this.metadataCheckpointInFlight?.catch(() => void 0);
12242
+ const plan = await findSessionCheckpointTruncatePlan(this.filePath, targetPromptIndex).catch(
12243
+ (err) => {
12244
+ this.scheduleMetadataCheckpoint();
12245
+ throw err;
12246
+ }
12247
+ );
12248
+ if (!plan) {
12249
+ this.scheduleMetadataCheckpoint();
12250
+ return 0;
12251
+ }
12838
12252
  await this.buffer.drainWriteChain();
12839
12253
  try {
12840
12254
  await this.handle.close();
@@ -12845,6 +12259,7 @@ var FileSessionWriter = class _FileSessionWriter {
12845
12259
  this.handle = await fsp14.open(this.filePath, "a", 384);
12846
12260
  } catch (err) {
12847
12261
  this.handle = await fsp14.open(this.filePath, "a", 384).catch(() => this.handle);
12262
+ this.scheduleMetadataCheckpoint();
12848
12263
  throw err;
12849
12264
  }
12850
12265
  await this.summaryTracker.recomputeFromDisk(this.filePath);
@@ -12870,6 +12285,11 @@ var FileSessionWriter = class _FileSessionWriter {
12870
12285
  await this.buffer.drainFlushPromise();
12871
12286
  this.buffer.clear();
12872
12287
  await this.buffer.drainWriteChain();
12288
+ if (this.metadataTimer) {
12289
+ clearTimeout(this.metadataTimer);
12290
+ this.metadataTimer = null;
12291
+ }
12292
+ await this.metadataCheckpointInFlight?.catch(() => void 0);
12873
12293
  const resetAt = (/* @__PURE__ */ new Date()).toISOString();
12874
12294
  const record = `${JSON.stringify({
12875
12295
  type: "session_start",
@@ -12880,8 +12300,10 @@ var FileSessionWriter = class _FileSessionWriter {
12880
12300
  })}
12881
12301
  `;
12882
12302
  await this.handle.close();
12883
- await fsp14.writeFile(this.filePath, record, "utf8");
12303
+ await atomicWrite(this.filePath, record, { mode: 384 });
12884
12304
  this.summaryTracker.reset(resetAt);
12305
+ this.metadataDirty = true;
12306
+ this.scheduleMetadataCheckpoint();
12885
12307
  this.activePromptIndex = null;
12886
12308
  this.pendingFileSnapshots = [];
12887
12309
  this.pendingFileSnapshotBytes = 0;
@@ -12935,22 +12357,22 @@ var FileSessionWriter = class _FileSessionWriter {
12935
12357
 
12936
12358
  // src/storage/session-store/delete-session-artifacts.ts
12937
12359
  import * as fsp15 from "node:fs/promises";
12938
- import * as path30 from "node:path";
12360
+ import * as path29 from "node:path";
12939
12361
 
12940
12362
  // src/storage/session-store/paths.ts
12941
- import * as path29 from "node:path";
12363
+ import * as path28 from "node:path";
12942
12364
  function sessionPath(storeDir, id, ext) {
12943
12365
  return sessionScopedPath(storeDir, id, ext);
12944
12366
  }
12945
12367
  function shardManifestPath(storeDir, shardKey) {
12946
- return shardKey ? path29.join(storeDir, shardKey, "_manifest.json") : path29.join(storeDir, "_manifest.json");
12368
+ return shardKey ? path28.join(storeDir, shardKey, "_manifest.json") : path28.join(storeDir, "_manifest.json");
12947
12369
  }
12948
12370
  function shardKeyForSessionId(id) {
12949
- const dirName = path29.dirname(id);
12371
+ const dirName = path28.dirname(id);
12950
12372
  return dirName === "." ? "" : dirName;
12951
12373
  }
12952
12374
  async function ensureShardDir(storeDir, id) {
12953
- const dirPath = path29.dirname(sessionScopedPath(storeDir, id, ""));
12375
+ const dirPath = path28.dirname(sessionScopedPath(storeDir, id, ""));
12954
12376
  await ensureDir(dirPath);
12955
12377
  return dirPath;
12956
12378
  }
@@ -12961,9 +12383,9 @@ async function deleteSessionArtifacts({
12961
12383
  id,
12962
12384
  jsonlPath
12963
12385
  }) {
12964
- const shardDir = path30.dirname(jsonlPath);
12965
- const base = path30.basename(id);
12966
- const sessDir = path30.join(shardDir, base);
12386
+ const shardDir = path29.dirname(jsonlPath);
12387
+ const base = path29.basename(id);
12388
+ const sessDir = path29.join(shardDir, base);
12967
12389
  const deletions = [
12968
12390
  fsp15.unlink(jsonlPath),
12969
12391
  fsp15.unlink(sessionPath(rootDir, id, ".summary.json")),
@@ -13505,7 +12927,7 @@ function emitDamaged(params, detail) {
13505
12927
 
13506
12928
  // src/storage/session-store/prune-helpers.ts
13507
12929
  import * as fsp16 from "node:fs/promises";
13508
- import * as path31 from "node:path";
12930
+ import * as path30 from "node:path";
13509
12931
  function isPrunableSessionJsonl(name) {
13510
12932
  return isSessionTranscriptFileName(name);
13511
12933
  }
@@ -13513,7 +12935,7 @@ async function pruneSessionFiles(storeDir, maxAgeDays, deleteSession) {
13513
12935
  const cutoff = Date.now() - maxAgeDays * 864e5;
13514
12936
  let deleted = 0;
13515
12937
  const pruneFile = async (dir, name, prefix) => {
13516
- const jsonlPath = path31.join(dir, name);
12938
+ const jsonlPath = path30.join(dir, name);
13517
12939
  try {
13518
12940
  const stat19 = await fsp16.stat(jsonlPath);
13519
12941
  if (stat19.mtimeMs >= cutoff) return;
@@ -13532,7 +12954,7 @@ async function pruneSessionFiles(storeDir, maxAgeDays, deleteSession) {
13532
12954
  continue;
13533
12955
  }
13534
12956
  if (!entry.isDirectory()) continue;
13535
- const dateDir = path31.join(storeDir, entry.name);
12957
+ const dateDir = path30.join(storeDir, entry.name);
13536
12958
  const files = await fsp16.readdir(dateDir, { withFileTypes: true }).catch(() => []);
13537
12959
  for (const file of files) {
13538
12960
  if (!file.isFile() || !isPrunableSessionJsonl(file.name)) continue;
@@ -13541,7 +12963,7 @@ async function pruneSessionFiles(storeDir, maxAgeDays, deleteSession) {
13541
12963
  }
13542
12964
  for (const entry of entries) {
13543
12965
  if (!entry.isDirectory()) continue;
13544
- const dateDir = path31.join(storeDir, entry.name);
12966
+ const dateDir = path30.join(storeDir, entry.name);
13545
12967
  try {
13546
12968
  const remaining = await fsp16.readdir(dateDir);
13547
12969
  if (remaining.length === 0) {
@@ -13617,18 +13039,18 @@ async function executeRenameSession(params) {
13617
13039
 
13618
13040
  // src/storage/session-store/resume-session.ts
13619
13041
  import * as fsp19 from "node:fs/promises";
13620
- import * as path33 from "node:path";
13042
+ import * as path32 from "node:path";
13621
13043
 
13622
13044
  // src/storage/session-resume-validation.ts
13623
13045
  import { createHash as createHash9 } from "node:crypto";
13624
13046
  import * as fsp18 from "node:fs/promises";
13625
- import * as path32 from "node:path";
13047
+ import * as path31 from "node:path";
13626
13048
  var MAX_REVALIDATE_BYTES = 5 * 1024 * 1024;
13627
13049
  var VALIDATION_CONCURRENCY = 8;
13628
13050
  var NOTICE_PATH_LIMIT = 20;
13629
13051
  function isInside2(root, target) {
13630
- const relative8 = path32.relative(root, target);
13631
- return relative8 === "" || !relative8.startsWith("..") && !path32.isAbsolute(relative8);
13052
+ const relative8 = path31.relative(root, target);
13053
+ return relative8 === "" || !relative8.startsWith("..") && !path31.isAbsolute(relative8);
13632
13054
  }
13633
13055
  function errno(err) {
13634
13056
  return err && typeof err === "object" && "code" in err ? String(err.code) : void 0;
@@ -13639,7 +13061,7 @@ function latestObservations(events, projectRoot) {
13639
13061
  if (event.type !== "file_observation" || typeof event.path !== "string" || event.path.length === 0 || typeof event.hash !== "string" || !/^[a-f\d]{64}$/i.test(event.hash)) {
13640
13062
  continue;
13641
13063
  }
13642
- const normalized = path32.resolve(projectRoot, event.path);
13064
+ const normalized = path31.resolve(projectRoot, event.path);
13643
13065
  latest.set(normalized, {
13644
13066
  path: normalized,
13645
13067
  hash: event.hash.toLowerCase(),
@@ -13697,7 +13119,7 @@ async function validateOne(observation, lexicalRoot, realRoot) {
13697
13119
  }
13698
13120
  }
13699
13121
  async function validateResumeFileObservations(events, projectRoot) {
13700
- const lexicalRoot = path32.resolve(projectRoot);
13122
+ const lexicalRoot = path31.resolve(projectRoot);
13701
13123
  const realRoot = await fsp18.realpath(lexicalRoot).catch(() => lexicalRoot);
13702
13124
  const observations = latestObservations(events, lexicalRoot);
13703
13125
  const results = await mapWithConcurrency(
@@ -13713,8 +13135,22 @@ async function validateResumeFileObservations(events, projectRoot) {
13713
13135
  }
13714
13136
  var RESUME_NOTICE_HEADERS = [
13715
13137
  "[SESSION RESUME FILE VALIDATION]",
13716
- "[SESSION RESUME INTERRUPTED WORK]"
13138
+ "[SESSION RESUME INTERRUPTED WORK]",
13139
+ "[SESSION RESUME CRASH RECOVERY]"
13717
13140
  ];
13141
+ function formatCrashRecoveryNotice(interruptedTools, lastContext) {
13142
+ if (interruptedTools.length === 0) return null;
13143
+ const plural = interruptedTools.length === 1 ? "call was" : "calls were";
13144
+ const lines = [
13145
+ "[SESSION RESUME CRASH RECOVERY]",
13146
+ `The previous run stopped mid-iteration${lastContext ? ` while: ${lastContext}` : ""} \u2014 ${interruptedTools.length} tool ${plural} left without a recorded result.`,
13147
+ "Those interrupted tool calls were removed from the restored conversation and were NOT re-executed; their workspace side effects were NOT rolled back."
13148
+ ];
13149
+ for (const tool of interruptedTools.slice(0, NOTICE_PATH_LIMIT)) {
13150
+ lines.push(`- ${tool.name}${tool.argsSummary ? ` (${tool.argsSummary})` : ""}`);
13151
+ }
13152
+ return lines.join("\n");
13153
+ }
13718
13154
  function isResumeNoticeMessage(message) {
13719
13155
  if (message.role !== "system" || typeof message.content !== "string") return false;
13720
13156
  return RESUME_NOTICE_HEADERS.some((header) => message.content === header || message.content.startsWith(`${header}
@@ -13722,9 +13158,9 @@ function isResumeNoticeMessage(message) {
13722
13158
  }
13723
13159
  function formatResumeValidationNotice(validation, projectRoot) {
13724
13160
  if (validation.staleFiles.length === 0) return null;
13725
- const root = path32.resolve(projectRoot);
13161
+ const root = path31.resolve(projectRoot);
13726
13162
  const shown = validation.staleFiles.slice(0, NOTICE_PATH_LIMIT).map((entry) => {
13727
- const relative8 = path32.relative(root, entry.path);
13163
+ const relative8 = path31.relative(root, entry.path);
13728
13164
  const display = isInside2(root, entry.path) ? relative8 || "." : entry.path;
13729
13165
  return `- ${JSON.stringify(display)} [${entry.status}]`;
13730
13166
  });
@@ -13917,7 +13353,8 @@ async function executeResumeSession(params) {
13917
13353
  readSummaryManifest,
13918
13354
  searchEvents,
13919
13355
  persistCatalogSummary,
13920
- logWarn
13356
+ logWarn,
13357
+ sessionsDir
13921
13358
  } = params;
13922
13359
  const t0 = Date.now();
13923
13360
  const data = await load(canonicalId);
@@ -13938,6 +13375,24 @@ async function executeResumeSession(params) {
13938
13375
  ...derivedSummary,
13939
13376
  ...persistedSummary?.name !== void 0 ? { name: persistedSummary.name } : {}
13940
13377
  };
13378
+ let recoveryPlan = SessionRecovery.buildRecoveryPlan(data.events, canonicalId);
13379
+ if (eventsDropped > 0 && sessionsDir) {
13380
+ const fromDisk = await new SessionRecovery(sessionsDir).recover(canonicalId);
13381
+ if (fromDisk) recoveryPlan = fromDisk;
13382
+ }
13383
+ const interruptedTools = recoveryPlan.stale ? extractInterruptedTools(recoveryPlan) : [];
13384
+ const synthesizedResults = interruptedTools.flatMap(
13385
+ (tool) => typeof tool.id === "string" ? [
13386
+ {
13387
+ type: "tool_result",
13388
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
13389
+ id: tool.id,
13390
+ content: "[interrupted] No result was recorded \u2014 the previous process stopped before this call completed. Re-run it if still needed.",
13391
+ isError: true
13392
+ }
13393
+ ] : []
13394
+ );
13395
+ if (synthesizedResults.length > 0) data.events.push(...synthesizedResults);
13941
13396
  const noticeMessages = [];
13942
13397
  let resumeValidation;
13943
13398
  if (projectRoot) {
@@ -13966,7 +13421,7 @@ async function executeResumeSession(params) {
13966
13421
  );
13967
13422
  }
13968
13423
  }
13969
- const interruptedNotice = formatInterruptedToolNotice(data.pendingToolUseCount ?? 0);
13424
+ const interruptedNotice = recoveryPlan.stale ? null : formatInterruptedToolNotice(data.pendingToolUseCount ?? 0);
13970
13425
  if (interruptedNotice) {
13971
13426
  noticeMessages.push({
13972
13427
  role: "system",
@@ -13974,6 +13429,14 @@ async function executeResumeSession(params) {
13974
13429
  ts: (/* @__PURE__ */ new Date()).toISOString()
13975
13430
  });
13976
13431
  }
13432
+ const crashNotice = formatCrashRecoveryNotice(interruptedTools, recoveryPlan.context);
13433
+ if (crashNotice) {
13434
+ noticeMessages.push({
13435
+ role: "system",
13436
+ content: crashNotice,
13437
+ ts: (/* @__PURE__ */ new Date()).toISOString()
13438
+ });
13439
+ }
13977
13440
  const carriedMessages = data.messages.filter((message) => !isResumeNoticeMessage(message));
13978
13441
  const resumedData = {
13979
13442
  ...data,
@@ -14004,7 +13467,7 @@ async function executeResumeSession(params) {
14004
13467
  {
14005
13468
  resumed: true,
14006
13469
  initialSummary,
14007
- dir: path33.dirname(file),
13470
+ dir: path32.dirname(file),
14008
13471
  filePath: file,
14009
13472
  secretScrubber,
14010
13473
  checkpointCas,
@@ -14015,9 +13478,21 @@ async function executeResumeSession(params) {
14015
13478
  if (!current) return null;
14016
13479
  return current.name === void 0 ? {} : { name: sessionContentText(secretScrubber.scrub(current.name)) };
14017
13480
  },
14018
- onClose: (s) => persistCatalogSummary(s)
13481
+ onClose: (s) => persistCatalogSummary(s),
13482
+ // Resumed sessions checkpoint their index/catalog metadata mid-flight
13483
+ // too, so a kill during a resumed session still leaves fresh listing
13484
+ // state behind.
13485
+ onMetadataCheckpoint: (s) => persistCatalogSummary(s)
14019
13486
  }
14020
13487
  );
13488
+ if (synthesizedResults.length > 0) {
13489
+ await writer.appendBatch(synthesizedResults);
13490
+ await writer.flush();
13491
+ }
13492
+ if (recoveryPlan.stale) {
13493
+ await writer.clearInFlightMarker("recovered");
13494
+ await writer.flush();
13495
+ }
14021
13496
  emitSessionStoreWrite(events, canonicalId, file, "resume", "success", Date.now() - t0);
14022
13497
  return { writer, data: resumedData };
14023
13498
  } catch (err) {
@@ -14236,6 +13711,11 @@ function applySessionIndexLines(raw, byId, deleted) {
14236
13711
  byId.delete(entry.id);
14237
13712
  continue;
14238
13713
  }
13714
+ if (entry.action === "create" && entry.id) {
13715
+ deleted.delete(entry.id);
13716
+ byId.delete(entry.id);
13717
+ continue;
13718
+ }
14239
13719
  if (entry.id && !deleted.has(entry.id)) {
14240
13720
  byId.set(entry.id, entry);
14241
13721
  }
@@ -14280,6 +13760,7 @@ async function readFileRange(file, start, end) {
14280
13760
 
14281
13761
  // src/storage/session-store/session-store-index.ts
14282
13762
  var COMPACT_EVERY = 30;
13763
+ var NO_DELETED_IDS = /* @__PURE__ */ new Set();
14283
13764
  async function appendToIndexStrict(dir, indexFile, summary2, invalidateShard, onAppended, compactInner) {
14284
13765
  await ensureDir(dir);
14285
13766
  let shouldCompact = false;
@@ -14312,10 +13793,14 @@ async function readIndexFile(indexFile, currentCache) {
14312
13793
  const s = await fsp23.stat(indexFile);
14313
13794
  stat19 = { mtimeMs: s.mtimeMs, size: s.size, ino: s.ino, birthtimeMs: s.birthtimeMs };
14314
13795
  } catch {
14315
- return { summaries: [], cache: null };
13796
+ return { summaries: [], deletedIds: NO_DELETED_IDS, cache: null };
14316
13797
  }
14317
13798
  if (currentCache !== null && currentCache.mtimeMs === stat19.mtimeMs && currentCache.size === stat19.size && currentCache.ino === stat19.ino && currentCache.birthtimeMs === stat19.birthtimeMs) {
14318
- return { summaries: currentCache.summaries, cache: currentCache };
13799
+ return {
13800
+ summaries: currentCache.summaries,
13801
+ deletedIds: currentCache.deleted,
13802
+ cache: currentCache
13803
+ };
14319
13804
  }
14320
13805
  const cached = currentCache;
14321
13806
  const sameFile = cached !== null && cached.ino === stat19.ino && cached.birthtimeMs === stat19.birthtimeMs;
@@ -14331,14 +13816,14 @@ async function readIndexFile(indexFile, currentCache) {
14331
13816
  byId: cached.byId,
14332
13817
  deleted: cached.deleted
14333
13818
  };
14334
- return { summaries: summaries2, cache: nextCache2 };
13819
+ return { summaries: summaries2, deletedIds: cached.deleted, cache: nextCache2 };
14335
13820
  }
14336
13821
  }
14337
13822
  let raw;
14338
13823
  try {
14339
13824
  raw = await fsp23.readFile(indexFile, "utf8");
14340
13825
  } catch {
14341
- return { summaries: [], cache: null };
13826
+ return { summaries: [], deletedIds: NO_DELETED_IDS, cache: null };
14342
13827
  }
14343
13828
  const deleted = /* @__PURE__ */ new Set();
14344
13829
  const byId = /* @__PURE__ */ new Map();
@@ -14346,7 +13831,18 @@ async function readIndexFile(indexFile, currentCache) {
14346
13831
  const summaries = Array.from(byId.values());
14347
13832
  summaries.sort(compareSessionSummaries);
14348
13833
  const nextCache = { ...stat19, summaries, byId, deleted };
14349
- return { summaries, cache: nextCache };
13834
+ return { summaries, deletedIds: deleted, cache: nextCache };
13835
+ }
13836
+ async function compactIndexInner(indexFile, entries, deletedIds) {
13837
+ const parts = entries.map((s) => JSON.stringify(s));
13838
+ if (deletedIds) {
13839
+ for (const id of deletedIds) {
13840
+ parts.push(JSON.stringify({ action: "delete", id }));
13841
+ }
13842
+ }
13843
+ if (parts.length === 0) return;
13844
+ const lines = parts.join("\n") + "\n";
13845
+ await atomicWrite(indexFile, lines, { mode: 384 });
14350
13846
  }
14351
13847
 
14352
13848
  // src/storage/session-store/shard-manifest.ts
@@ -14465,6 +13961,7 @@ function damagedSummary(id, startedAt) {
14465
13961
  }
14466
13962
 
14467
13963
  // src/storage/session-store.ts
13964
+ var SESSION_FILTER_POOL_LIMIT = 1e4;
14468
13965
  var DefaultSessionStore = class _DefaultSessionStore {
14469
13966
  dir;
14470
13967
  events;
@@ -14480,14 +13977,36 @@ var DefaultSessionStore = class _DefaultSessionStore {
14480
13977
  _loadCache = /* @__PURE__ */ new Map();
14481
13978
  loadCache = new SessionLoadCache(this._loadCache);
14482
13979
  _indexCache = null;
13980
+ /**
13981
+ * Tombstoned ids — hidden even if their JSONL remains on disk.
13982
+ * Convention: readIndex() REASSIGNS this set from the parsed index file
13983
+ * MERGED with _manualTombstones; writeTombstone() adds in-place immediately
13984
+ * so an incremental cache rebuild can never resurrect a just-deleted
13985
+ * session.
13986
+ */
13987
+ _indexDeletedIds = /* @__PURE__ */ new Set();
13988
+ /**
13989
+ * Tombstones added by THIS store between reads. Merged into every fresh
13990
+ * snapshot so a read racing writeTombstone cannot drop an in-flight
13991
+ * deletion; entries are pruned once the parsed index file itself carries
13992
+ * them.
13993
+ */
13994
+ _manualTombstones = /* @__PURE__ */ new Set();
13995
+ /**
13996
+ * File-truth tombstones from the last readIndex() parse (EXCLUDES
13997
+ * _manualTombstones additions). compactIndexInner persists THIS snapshot so
13998
+ * concurrent writeTombstones that landed after the parse are not written
13999
+ * prematurely — they persist through their own append path instead.
14000
+ */
14001
+ _indexFileDeletedIds = /* @__PURE__ */ new Set();
14483
14002
  shardManifestCache = /* @__PURE__ */ new Map();
14484
14003
  static LIST_SCAN_CONCURRENCY = 32;
14485
14004
  indexAppendCount = 0;
14486
14005
  constructor(opts) {
14487
14006
  this.dir = opts.dir;
14488
- this.projectRoot = opts.projectRoot ? path34.resolve(opts.projectRoot) : void 0;
14007
+ this.projectRoot = opts.projectRoot ? path33.resolve(opts.projectRoot) : void 0;
14489
14008
  this.checkpointCas = this.projectRoot ? new SessionCheckpointCas({
14490
- rootDir: path34.join(this.dir, "_cas"),
14009
+ rootDir: path33.join(this.dir, "_cas"),
14491
14010
  projectRoot: this.projectRoot
14492
14011
  }) : void 0;
14493
14012
  this.events = opts.events;
@@ -14498,7 +14017,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
14498
14017
  this.onAppendBatch = opts.onAppendBatch;
14499
14018
  const builtRuntime = import.meta.url.includes("/dist/");
14500
14019
  this.catalogClient = this.projectRoot && (builtRuntime || process.env["WRONGSTACK_SESSION_CATALOG_FORCE"] === "1") && resolveSessionCatalogProjectServerUrl() ? new SessionCatalogProjectClient({
14501
- projectDir: path34.dirname(this.dir),
14020
+ projectDir: path33.dirname(this.dir),
14502
14021
  projectRoot: this.projectRoot
14503
14022
  }) : void 0;
14504
14023
  }
@@ -14520,7 +14039,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
14520
14039
  this.clearLoadCache();
14521
14040
  }
14522
14041
  get indexFile() {
14523
- return path34.join(this.dir, "_index.jsonl");
14042
+ return path33.join(this.dir, "_index.jsonl");
14524
14043
  }
14525
14044
  sessionPath(id, ext) {
14526
14045
  return sessionPath(this.dir, id, ext);
@@ -14546,19 +14065,94 @@ var DefaultSessionStore = class _DefaultSessionStore {
14546
14065
  async ensureShardDir(id) {
14547
14066
  return ensureShardDir(this.dir, id);
14548
14067
  }
14068
+ /**
14069
+ * Create a fresh session writer.
14070
+ *
14071
+ * @threadSafety Failure-prone steps (manifest invalidation, sidecar
14072
+ * removal, catalog upsert, the durable `{action:'create'}` index row)
14073
+ * run BEFORE the truncating `'w'` open, so no rejection path can destroy
14074
+ * prior bytes. Ordinary index summary rows never undelete a tombstone
14075
+ * (the parser only honors `{action:'create'}`), so a swallowed create-row
14076
+ * would leave a live writer whose id stays hidden forever — that append
14077
+ * is therefore required, not best-effort.
14078
+ */
14549
14079
  async create(meta) {
14550
14080
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
14551
14081
  const id = meta.id && meta.id.length > 0 ? meta.id : generateSessionId(startedAt);
14552
14082
  const shardDir = await this.ensureShardDir(id);
14553
14083
  const file = this.sessionPath(id, ".jsonl");
14084
+ const inUseBy = this.isSessionInUse ? await this.isSessionInUse(id) : null;
14085
+ if (inUseBy) {
14086
+ throw new Error(`Refusing to create session ${id}: in use (${inUseBy}).`);
14087
+ }
14554
14088
  const t0 = Date.now();
14089
+ try {
14090
+ await this.invalidateShardManifestBySessionId(id);
14091
+ } catch (cause) {
14092
+ throw new Error(
14093
+ `Failed to invalidate stale shard manifest for ${id}: ${toErrorMessage(cause)}`,
14094
+ { cause }
14095
+ );
14096
+ }
14097
+ const sidecar = path33.join(shardDir, `${path33.basename(id)}.summary.json`);
14098
+ try {
14099
+ await fsp26.rm(sidecar, { force: true });
14100
+ } catch (cause) {
14101
+ emitSessionStoreError(this.events, id, sidecar, "create", toErrorMessage(cause), true);
14102
+ try {
14103
+ await fsp26.access(sidecar);
14104
+ throw new Error(
14105
+ `Failed to remove stale session sidecar for ${id}: ${toErrorMessage(cause)}`,
14106
+ { cause }
14107
+ );
14108
+ } catch (accessErr) {
14109
+ const code = accessErr.code;
14110
+ if (code !== "ENOENT" && code !== "ENAMETOOLONG") throw accessErr;
14111
+ }
14112
+ }
14113
+ if (this.catalogClient) {
14114
+ await this.catalogClient.call("upsert_summary", {
14115
+ summary: {
14116
+ id,
14117
+ title: meta.title ?? "",
14118
+ startedAt,
14119
+ model: meta.model ?? "",
14120
+ provider: meta.provider ?? "",
14121
+ tokenTotal: 0,
14122
+ lastActivityAt: startedAt
14123
+ },
14124
+ transcriptRelativePath: `${id}.jsonl`,
14125
+ summaryRelativePath: `${id}.summary.json`
14126
+ });
14127
+ }
14128
+ try {
14129
+ await withFileLock(this.indexFile, async () => {
14130
+ try {
14131
+ await fsp26.appendFile(this.indexFile, `${JSON.stringify({ action: "create", id })}
14132
+ `, {
14133
+ encoding: "utf8",
14134
+ mode: 384
14135
+ });
14136
+ } finally {
14137
+ this._indexCache = null;
14138
+ }
14139
+ this._manualTombstones.delete(id);
14140
+ this._indexDeletedIds.delete(id);
14141
+ });
14142
+ } catch (cause) {
14143
+ throw new Error(
14144
+ `Failed to record session create in the index for ${id}: ${toErrorMessage(cause)}`,
14145
+ { cause }
14146
+ );
14147
+ }
14555
14148
  let handle;
14556
14149
  try {
14557
- handle = await fsp26.open(file, "a", 384);
14150
+ handle = await fsp26.open(file, "w", 384);
14558
14151
  } catch (err) {
14559
14152
  emitSessionStoreError(this.events, id, file, "create", toErrorMessage(err), false);
14560
14153
  throw new Error(`Failed to open session file: ${toErrorMessage(err)}`, { cause: err });
14561
14154
  }
14155
+ await this.invalidateShardManifestBySessionId(id).catch(() => void 0);
14562
14156
  try {
14563
14157
  const writer = new FileSessionWriter(id, handle, startedAt, meta, this.events, {
14564
14158
  dir: shardDir,
@@ -14572,23 +14166,11 @@ var DefaultSessionStore = class _DefaultSessionStore {
14572
14166
  if (!current) return null;
14573
14167
  return current.name === void 0 ? {} : { name: sessionContentText(this.secretScrubber.scrub(current.name)) };
14574
14168
  },
14575
- onClose: (s) => this.persistCatalogSummary(s)
14169
+ onClose: (s) => this.persistCatalogSummary(s),
14170
+ // Mid-session metadata checkpoints reuse the same sink as close so
14171
+ // killed sessions leave accurate index rows / catalog entries behind.
14172
+ onMetadataCheckpoint: (s) => this.persistCatalogSummary(s)
14576
14173
  });
14577
- if (this.catalogClient) {
14578
- await this.catalogClient.call("upsert_summary", {
14579
- summary: {
14580
- id,
14581
- title: meta.title ?? "",
14582
- startedAt,
14583
- model: meta.model ?? "",
14584
- provider: meta.provider ?? "",
14585
- tokenTotal: 0,
14586
- lastActivityAt: startedAt
14587
- },
14588
- transcriptRelativePath: `${id}.jsonl`,
14589
- summaryRelativePath: `${id}.summary.json`
14590
- });
14591
- }
14592
14174
  emitSessionStoreWrite(this.events, id, file, "create", "success", Date.now() - t0);
14593
14175
  return writer;
14594
14176
  } catch (err) {
@@ -14650,7 +14232,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
14650
14232
  readSummaryManifest: (summaryId) => this.readSummaryManifest(summaryId),
14651
14233
  searchEvents: (searchId, pred) => this.searchEvents(searchId, pred),
14652
14234
  persistCatalogSummary: (sum) => this.persistCatalogSummary(sum),
14653
- logWarn: (msg, ctx) => this.logWarn(msg, ctx)
14235
+ logWarn: (msg, ctx) => this.logWarn(msg, ctx),
14236
+ sessionsDir: this.dir
14654
14237
  });
14655
14238
  }
14656
14239
  async load(id) {
@@ -14724,11 +14307,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
14724
14307
  return this.scrubSummaries(records);
14725
14308
  }
14726
14309
  try {
14727
- const indexed = await this.readIndex();
14728
- if (indexed.length > 0) {
14729
- return this.scrubSummaries(indexed.slice(0, limit));
14730
- }
14731
- return this.scrubSummaries(await this.listFromDirectoryScan(limit));
14310
+ const [indexed, scanned] = await Promise.all([
14311
+ this.readIndex(),
14312
+ // Wide scan bound: mergeIndexWithScan slices to `limit`, so killed
14313
+ // sessions deep in history stay visible instead of being dropped by
14314
+ // the user-facing page size before the union runs.
14315
+ this.listFromDirectoryScan(SESSION_FILTER_POOL_LIMIT).catch(() => [])
14316
+ ]);
14317
+ return this.scrubSummaries(this.mergeIndexWithScan(indexed, scanned, limit));
14732
14318
  } catch {
14733
14319
  return [];
14734
14320
  }
@@ -14743,15 +14329,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
14743
14329
  return this.scrubSummaries(records);
14744
14330
  }
14745
14331
  try {
14746
- const indexed = await this.readIndex();
14747
- if (indexed.length === 0) {
14748
- const raw = await this.list(Math.max(limit, 100));
14749
- return raw.filter((s) => matchesSessionFilter(s, criteria)).slice(0, limit);
14750
- }
14751
- const filtered = this.scrubSummaries(indexed).filter(
14752
- (s) => matchesSessionFilter(s, criteria)
14753
- );
14754
- return filtered.slice(0, limit);
14332
+ const [indexed, scanned] = await Promise.all([
14333
+ this.readIndex(),
14334
+ // Same best-effort contract as list(): scan failures enrich nothing
14335
+ // but must not blank the filtered result set.
14336
+ this.listFromDirectoryScan(SESSION_FILTER_POOL_LIMIT).catch(() => [])
14337
+ ]);
14338
+ const pool = this.mergeIndexWithScan(indexed, scanned, SESSION_FILTER_POOL_LIMIT);
14339
+ return this.scrubSummaries(pool).filter((s) => matchesSessionFilter(s, criteria)).slice(0, limit);
14755
14340
  } catch {
14756
14341
  return [];
14757
14342
  }
@@ -14793,6 +14378,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
14793
14378
  id,
14794
14379
  (sid) => this.invalidateShardManifestBySessionId(sid),
14795
14380
  () => {
14381
+ this._manualTombstones.add(id);
14382
+ this._indexDeletedIds.add(id);
14796
14383
  this._indexCache = null;
14797
14384
  this.indexAppendCount++;
14798
14385
  }
@@ -14820,34 +14407,92 @@ var DefaultSessionStore = class _DefaultSessionStore {
14820
14407
  );
14821
14408
  }
14822
14409
  }
14410
+ /**
14411
+ * Compact the local index in place.
14412
+ *
14413
+ * Contract carried into the shared compactIndexInner helper
14414
+ * (session-store-index.ts): `entries` MUST already exclude tombstoned ids,
14415
+ * and the deleted-set argument is persisted VERBATIM — neither the helper
14416
+ * nor its callers may resurrect filtered rows or invent deletions.
14417
+ * Locking: callers MUST already hold the indexFile lock (both do:
14418
+ * compactIndex() below and the appendToIndexStrict compaction hook);
14419
+ * readIndex() inside reads that same locked file, so no second lock may
14420
+ * be taken here (non-reentrant → deadlock).
14421
+ *
14422
+ * That same lock is what makes the _indexFileDeletedIds snapshot safe to
14423
+ * pass across the await below: writeTombstone() appends under the identical
14424
+ * non-reentrant indexFile lock, so no tombstone can land between our
14425
+ * readIndex() and the snapshot handed to the helper. Compaction would
14426
+ * otherwise be racing a delete it cannot see.
14427
+ */
14823
14428
  async compactIndexInner() {
14824
14429
  const entries = await this.readIndex();
14825
- if (entries.length === 0) return;
14826
- const lines = entries.map((s) => JSON.stringify(s)).join("\n") + "\n";
14827
- await atomicWrite(this.indexFile, lines, { mode: 384 });
14430
+ await compactIndexInner(this.indexFile, entries, this._indexFileDeletedIds);
14828
14431
  this._indexCache = null;
14829
14432
  }
14830
14433
  async readIndex() {
14831
- const { summaries, cache } = await readIndexFile(this.indexFile, this._indexCache);
14434
+ const { summaries, deletedIds, cache } = await readIndexFile(this.indexFile, this._indexCache);
14832
14435
  this._indexCache = cache;
14436
+ const merged = new Set(deletedIds);
14437
+ for (const manual of this._manualTombstones) {
14438
+ merged.add(manual);
14439
+ if (deletedIds.has(manual)) this._manualTombstones.delete(manual);
14440
+ }
14441
+ this._indexFileDeletedIds = deletedIds;
14442
+ this._indexDeletedIds = merged;
14833
14443
  return summaries;
14834
14444
  }
14445
+ /**
14446
+ * Merge close-time index rows with directory-scan results, keyed by id.
14447
+ * Scanned entries win — their metadata is re-derived from the transcript,
14448
+ * so it reflects mid-session activity that index rows (written on close)
14449
+ * cannot know about. Indexed-only ids fill gaps; duplicates within the
14450
+ * index resolve last-wins, matching append order.
14451
+ */
14452
+ mergeIndexWithScan(indexed, scanned, limit) {
14453
+ const byId = /* @__PURE__ */ new Map();
14454
+ for (const row of indexed) byId.set(row.id, row);
14455
+ for (const row of scanned) byId.set(row.id, row);
14456
+ return [...byId.values()].filter((row) => !this._indexDeletedIds.has(row.id)).sort(compareSessionSummaries).slice(0, limit);
14457
+ }
14458
+ /**
14459
+ * Rebuild the index from what is actually on disk.
14460
+ *
14461
+ * @returns the number of healthy, live entries in the rebuilt index. Both
14462
+ * backends report that same quantity: ids whose summary could not be derived
14463
+ * are excluded (the catalog counts them as `damaged`; the local scan drops
14464
+ * them when `summaryFor` rejects), and ids carrying a surviving tombstone are
14465
+ * excluded (the catalog rebuilds only from live files; the local branch skips
14466
+ * them explicitly). It is NOT a count of rows written to the file — tombstone
14467
+ * rows are persisted but never counted.
14468
+ */
14835
14469
  async rebuildIndex() {
14836
14470
  if (this.catalogClient) {
14837
14471
  const result = await this.catalogClient.call("rebuild_catalog", {}, { timeoutMs: 12e4 });
14838
14472
  return result.indexed;
14839
14473
  }
14840
- const ids = await this.collectSessionIds(this.dir);
14841
- const summaries = await Promise.all(
14842
- ids.map((id) => this.summaryFor(id).catch(() => null))
14843
- );
14844
- const valid = summaries.filter((s) => s !== null);
14845
- const lines = valid.map((s) => JSON.stringify(s)).join("\n") + "\n";
14846
- await withFileLock(this.indexFile, async () => {
14474
+ return withFileLock(this.indexFile, async () => {
14475
+ await this.readIndex();
14476
+ const ids = await this.collectSessionIds(this.dir);
14477
+ const summaries = await Promise.all(ids.map((id) => this.summaryFor(id).catch(() => null)));
14478
+ const valid = summaries.filter((s) => s !== null);
14479
+ const parts = [];
14480
+ let tombstoned = 0;
14481
+ for (const s of valid) {
14482
+ if (this._indexDeletedIds.has(s.id)) {
14483
+ tombstoned++;
14484
+ continue;
14485
+ }
14486
+ parts.push(JSON.stringify(s));
14487
+ }
14488
+ for (const id of this._indexDeletedIds) {
14489
+ parts.push(JSON.stringify({ action: "delete", id }));
14490
+ }
14491
+ const lines = parts.join("\n") + "\n";
14847
14492
  await atomicWrite(this.indexFile, lines, { mode: 384 });
14848
14493
  this._indexCache = null;
14494
+ return valid.length - tombstoned;
14849
14495
  });
14850
- return valid.length;
14851
14496
  }
14852
14497
  async listFromDirectoryScan(limit) {
14853
14498
  const shardKeys = await this.collectShardKeys();
@@ -14929,7 +14574,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
14929
14574
  return void 0;
14930
14575
  }
14931
14576
  async collectSessionFilesInShard(shardKey) {
14932
- const dir = shardKey ? path34.join(this.dir, shardKey) : this.dir;
14577
+ const dir = shardKey ? path33.join(this.dir, shardKey) : this.dir;
14933
14578
  const entries = await this.collectSessionFiles(dir, shardKey);
14934
14579
  return shardKey ? entries.filter((entry) => entry.id.startsWith(`${shardKey}/`)) : entries.filter((entry) => !entry.id.includes("/"));
14935
14580
  }
@@ -15110,7 +14755,7 @@ async function applyRewindToConversation(opts) {
15110
14755
  // src/storage/session-rewinder.ts
15111
14756
  import { createReadStream as createReadStream6 } from "node:fs";
15112
14757
  import * as fsp27 from "node:fs/promises";
15113
- import * as path35 from "node:path";
14758
+ import * as path34 from "node:path";
15114
14759
  import { createInterface as createInterface6 } from "node:readline";
15115
14760
  var DefaultSessionRewinder = class {
15116
14761
  constructor(sessionsDir, projectRoot) {
@@ -15244,10 +14889,10 @@ async function revertSnapshots(snapshots, projectRoot) {
15244
14889
  for (const snapshot of [...snapshots].reverse()) {
15245
14890
  for (const file of [...snapshot.files].reverse()) {
15246
14891
  try {
15247
- const absPath = path35.resolve(file.path);
15248
- const root = path35.resolve(projectRoot);
15249
- const rel = path35.relative(root, absPath);
15250
- if (rel.startsWith("..") || path35.isAbsolute(rel)) {
14892
+ const absPath = path34.resolve(file.path);
14893
+ const root = path34.resolve(projectRoot);
14894
+ const rel = path34.relative(root, absPath);
14895
+ if (rel.startsWith("..") || path34.isAbsolute(rel)) {
15251
14896
  errors.push(`${file.path}: path resolves outside project root \u2014 skipping`);
15252
14897
  continue;
15253
14898
  }
@@ -15614,7 +15259,7 @@ function attachTodosCheckpoint(state, filePath, sessionId, events, traceId, warn
15614
15259
 
15615
15260
  // src/storage/tool-audit-log.ts
15616
15261
  import { createHash as createHash10, randomUUID as randomUUID8 } from "node:crypto";
15617
- import * as fs20 from "node:fs/promises";
15262
+ import * as fs19 from "node:fs/promises";
15618
15263
  var GENESIS_PREV = "0".repeat(64);
15619
15264
  var DEFAULT_FSYNC_EVERY = 100;
15620
15265
  var ToolAuditLog = class {
@@ -15684,7 +15329,7 @@ var ToolAuditLog = class {
15684
15329
  isError: input.isError,
15685
15330
  index
15686
15331
  };
15687
- await fs20.appendFile(fp, JSON.stringify(entry) + "\n", {
15332
+ await fs19.appendFile(fp, JSON.stringify(entry) + "\n", {
15688
15333
  encoding: "utf8",
15689
15334
  // WS-035: this file records raw tool INPUT and OUTPUT. Created
15690
15335
  // without a mode it lands 0644 — world-readable — while its
@@ -15692,7 +15337,7 @@ var ToolAuditLog = class {
15692
15337
  mode: SECRET_FILE_MODE
15693
15338
  });
15694
15339
  try {
15695
- const st = await fs20.stat(fp);
15340
+ const st = await fs19.stat(fp);
15696
15341
  this.tailStat.set(input.sessionId, { mtimeMs: st.mtimeMs, size: st.size });
15697
15342
  } catch {
15698
15343
  }
@@ -15739,7 +15384,7 @@ var ToolAuditLog = class {
15739
15384
  const cachedStat = this.tailStat.get(sessionId);
15740
15385
  if (cachedHash !== void 0 && cachedIndex !== void 0 && cachedStat) {
15741
15386
  try {
15742
- const st = await fs20.stat(fp);
15387
+ const st = await fs19.stat(fp);
15743
15388
  if (st.mtimeMs === cachedStat.mtimeMs && st.size === cachedStat.size) {
15744
15389
  return { prevHash: cachedHash, nextIndex: cachedIndex };
15745
15390
  }
@@ -15760,7 +15405,7 @@ var ToolAuditLog = class {
15760
15405
  this.tailHash.set(sessionId, prevHash);
15761
15406
  this.tailIndex.set(sessionId, nextIndex);
15762
15407
  try {
15763
- const st = await fs20.stat(fp);
15408
+ const st = await fs19.stat(fp);
15764
15409
  this.tailStat.set(sessionId, { mtimeMs: st.mtimeMs, size: st.size });
15765
15410
  } catch {
15766
15411
  }
@@ -15881,7 +15526,7 @@ var ToolAuditLog = class {
15881
15526
  async readAll(sessionId) {
15882
15527
  const fp = this.filePath(sessionId);
15883
15528
  try {
15884
- const raw = await fs20.readFile(fp, "utf8");
15529
+ const raw = await fs19.readFile(fp, "utf8");
15885
15530
  const out = [];
15886
15531
  for (const line of raw.split("\n")) {
15887
15532
  if (!line.trim()) continue;
@@ -15919,7 +15564,7 @@ var ToolAuditLog = class {
15919
15564
  }
15920
15565
  async sync(sessionId, fp) {
15921
15566
  try {
15922
- const fh = await fs20.open(fp, "r+");
15567
+ const fh = await fs19.open(fp, "r+");
15923
15568
  try {
15924
15569
  await fh.sync();
15925
15570
  } finally {
@@ -15958,7 +15603,6 @@ function sortKeys2(value) {
15958
15603
  }
15959
15604
  export {
15960
15605
  ALL_SYNC_CATEGORIES,
15961
- AgentStatusTracker,
15962
15606
  AnnotationsStore,
15963
15607
  CLOUD_SYNC_CONTRACT,
15964
15608
  CLOUD_SYNC_NAMESPACES,
@@ -15977,7 +15621,6 @@ export {
15977
15621
  DefaultSessionStore,
15978
15622
  DirectorStateCheckpoint,
15979
15623
  FileMemoryBackend,
15980
- FleetNotifier,
15981
15624
  GraphMemoryBackend,
15982
15625
  INPUT_HISTORY_DEFAULT_MAX,
15983
15626
  InputHistoryStore,
@@ -16008,6 +15651,7 @@ export {
16008
15651
  applyRewindToConversation,
16009
15652
  attachPlanCheckpoint,
16010
15653
  attachTodosCheckpoint,
15654
+ boardStore,
16011
15655
  buildNamespacePayloads,
16012
15656
  cleanOrphanLocks,
16013
15657
  clearPlan,
@@ -16069,6 +15713,7 @@ export {
16069
15713
  scrubPersistedSessionEvent,
16070
15714
  scrubPersistedSessionSummary,
16071
15715
  sessionIdResolutionError,
15716
+ setBoardStorePort,
16072
15717
  setPlanItemStatus,
16073
15718
  setProgress,
16074
15719
  stripGoalDeliverableMarker,