@wrongstack/core 0.309.1 → 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 (116) 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/fleet-status-tool.d.ts +1 -1
  13. package/dist/coordination/index.d.ts +5 -1
  14. package/dist/coordination/index.js +2061 -346
  15. package/dist/coordination/kanban-dispatch-port.d.ts +30 -0
  16. package/dist/coordination/kanban-ops-port.d.ts +21 -0
  17. package/dist/coordination/mailbox-hooks.d.ts +1 -1
  18. package/dist/coordination/mailbox-project-server.js +14 -9
  19. package/dist/core/context.d.ts +35 -44
  20. package/dist/core/conversation-state.d.ts +16 -52
  21. package/dist/core/index.js +53 -16
  22. package/dist/core/provider-runner.d.ts +2 -2
  23. package/dist/core/run-env.d.ts +7 -28
  24. package/dist/core/streaming-response-builder.d.ts +2 -2
  25. package/dist/execution/index.js +111 -153
  26. package/dist/goal/index.js +100 -22
  27. package/dist/hq/auth-store.d.ts +9 -0
  28. package/dist/hq/cost-bridge.d.ts +1 -1
  29. package/dist/hq/index.js +24 -2
  30. package/dist/index.d.ts +12 -5
  31. package/dist/index.js +27806 -33937
  32. package/dist/infrastructure/index.js +210 -132
  33. package/dist/infrastructure/provider-cache-ledger.d.ts +1 -1
  34. package/dist/infrastructure/token-counter.d.ts +5 -1
  35. package/dist/kernel/events/file-events.d.ts +6 -0
  36. package/dist/kernel/events/provider-events.d.ts +10 -7
  37. package/dist/kernel/events/session-events.d.ts +4 -4
  38. package/dist/kernel/events/tool-events.d.ts +12 -2
  39. package/dist/plugin/index.js +105 -35
  40. package/dist/security/index.d.ts +1 -0
  41. package/dist/security/index.js +47 -18
  42. package/dist/security/kanban-boundary.d.ts +1 -1
  43. package/dist/security/kanban-governance-port.d.ts +28 -0
  44. package/dist/session-catalog/index.js +2 -3
  45. package/dist/session-catalog/project-server.js +2 -3
  46. package/dist/session-catalog/protocol.d.ts +1 -1
  47. package/dist/session-catalog/registry.d.ts +1 -1
  48. package/dist/session-catalog/store-schema.d.ts +1 -1
  49. package/dist/session-catalog/store.d.ts +1 -1
  50. package/dist/storage/annotations-store.d.ts +1 -1
  51. package/dist/storage/board-store-port.d.ts +45 -0
  52. package/dist/storage/completed-work-checkpoint.d.ts +1 -1
  53. package/dist/storage/config-loader/types.d.ts +1 -1
  54. package/dist/storage/event-bus-port.d.ts +27 -0
  55. package/dist/storage/file-session-writer.d.ts +47 -5
  56. package/dist/storage/goal-coordination.d.ts +1 -1
  57. package/dist/storage/goal-store.d.ts +1 -1
  58. package/dist/storage/index.d.ts +3 -4
  59. package/dist/storage/index.js +1063 -1459
  60. package/dist/storage/plan-store.d.ts +1 -1
  61. package/dist/storage/queue-store.d.ts +1 -1
  62. package/dist/storage/replay-log-store.d.ts +1 -1
  63. package/dist/storage/session-recovery.d.ts +10 -0
  64. package/dist/storage/session-resume-validation.d.ts +13 -1
  65. package/dist/storage/session-store/events.d.ts +1 -1
  66. package/dist/storage/session-store/load-session-data.d.ts +1 -1
  67. package/dist/storage/session-store/rename-session.d.ts +1 -1
  68. package/dist/storage/session-store/resume-session.d.ts +3 -1
  69. package/dist/storage/session-store/session-store-index.d.ts +2 -1
  70. package/dist/storage/session-store/types.d.ts +1 -1
  71. package/dist/storage/session-store.d.ts +70 -0
  72. package/dist/storage/session-summary-tracker.d.ts +8 -0
  73. package/dist/storage/session-write-buffer.d.ts +31 -2
  74. package/dist/storage/task-store.d.ts +1 -1
  75. package/dist/storage/todos-checkpoint.d.ts +1 -1
  76. package/dist/storage/tool-audit-log.d.ts +1 -1
  77. package/dist/tasking/index.js +100 -22
  78. package/dist/tasking/task-tracker.d.ts +24 -1
  79. package/dist/types/compactor.d.ts +2 -2
  80. package/dist/types/context.d.ts +212 -0
  81. package/dist/types/conversation-state.d.ts +109 -0
  82. package/dist/types/error-handler.d.ts +2 -2
  83. package/dist/types/file-event-record.d.ts +6 -0
  84. package/dist/types/index.d.ts +3 -3
  85. package/dist/types/index.js +17 -1
  86. package/dist/types/permission.d.ts +3 -3
  87. package/dist/types/plugin.d.ts +3 -3
  88. package/dist/types/provider-runner.d.ts +2 -2
  89. package/dist/types/provider.d.ts +10 -0
  90. package/dist/types/run-env.d.ts +32 -0
  91. package/dist/types/session.d.ts +3 -0
  92. package/dist/types/slash-command.d.ts +2 -2
  93. package/dist/types/token-counter.d.ts +30 -1
  94. package/dist/types/tool-executor.d.ts +2 -2
  95. package/dist/types/tool.d.ts +5 -5
  96. package/dist/utils/context-breakdown.d.ts +2 -2
  97. package/dist/utils/context-evidence.d.ts +12 -12
  98. package/dist/utils/crash-shield.d.ts +9 -0
  99. package/dist/utils/heap-watchdog.js +11 -3
  100. package/dist/utils/index.d.ts +1 -1
  101. package/dist/utils/index.js +72 -156
  102. package/dist/utils/regex-guard.d.ts +7 -30
  103. package/dist/utils/todos-format.d.ts +1 -1
  104. package/dist/utils/tree-kill.d.ts +2 -0
  105. package/dist/utils/tree-kill.js +1 -0
  106. package/instructions/coordination/subagent-baseline.md +10 -0
  107. package/instructions/system-lite.md +2 -0
  108. package/instructions/system-pro.md +40 -0
  109. package/instructions/system.md +15 -0
  110. package/package.json +6 -9
  111. package/dist/defaults/index.d.ts +0 -69
  112. package/dist/defaults/index.js +0 -38133
  113. /package/dist/{fleet-notifier.d.ts → coordination/fleet-notifier.d.ts} +0 -0
  114. /package/dist/{session-registry-atomic-file.d.ts → session-catalog/session-registry-atomic-file.d.ts} +0 -0
  115. /package/dist/{session-registry-types.d.ts → session-catalog/session-registry-types.d.ts} +0 -0
  116. /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
  }
@@ -2218,15 +1382,29 @@ var atomicReplaceWithWriter = primitives.atomicReplaceWithWriter;
2218
1382
  var ensureDir = primitives.ensureDir;
2219
1383
  var withFileLock = primitives.withFileLock;
2220
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
+
2221
1399
  // src/utils/session-scoped-path.ts
2222
- import * as path6 from "node:path";
1400
+ import * as path5 from "node:path";
2223
1401
  function sessionScopedPath(dir, sessionId, suffix) {
2224
1402
  if (!sessionId || sessionId.includes("\\") || sessionId.includes("..")) {
2225
1403
  throw invalid(sessionId);
2226
1404
  }
2227
- const resolved = path6.resolve(dir, `${sessionId}${suffix}`);
2228
- const rel = path6.relative(path6.resolve(dir), resolved);
2229
- 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)) {
2230
1408
  throw invalid(sessionId);
2231
1409
  }
2232
1410
  return resolved;
@@ -2254,15 +1432,15 @@ function invalid(sessionId) {
2254
1432
  });
2255
1433
  }
2256
1434
 
2257
- // src/session-registry.ts
2258
- import * as fs6 from "node:fs/promises";
2259
- 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";
2260
1438
 
2261
- // src/session-registry-atomic-file.ts
1439
+ // src/session-catalog/session-registry-atomic-file.ts
2262
1440
  import { randomUUID as randomUUID2 } from "node:crypto";
2263
- import * as fs5 from "node:fs/promises";
1441
+ import * as fs4 from "node:fs/promises";
2264
1442
  import { hostname } from "node:os";
2265
- import * as path7 from "node:path";
1443
+ import * as path6 from "node:path";
2266
1444
  var STALE_LOCK_MS = 1e4;
2267
1445
  var SAME_HOST_STALE_MS = 2 * STALE_LOCK_MS;
2268
1446
  var STALE_TMP_MS = 6e4;
@@ -2272,32 +1450,32 @@ function lockOwnerStamp() {
2272
1450
  return `${HOST_NAME}:${process.pid}`;
2273
1451
  }
2274
1452
  async function maybeUnlinkOwnedLock(lockPath) {
2275
- const owner = await fs5.readFile(lockPath, "utf8").catch(() => null);
1453
+ const owner = await fs4.readFile(lockPath, "utf8").catch(() => null);
2276
1454
  if (owner === null || owner.trim() !== lockOwnerStamp()) return;
2277
1455
  for (let attempt = 0; attempt < 3; attempt++) {
2278
1456
  try {
2279
- await fs5.unlink(lockPath);
1457
+ await fs4.unlink(lockPath);
2280
1458
  return;
2281
1459
  } catch {
2282
- if (attempt < 2) await new Promise((resolve14) => setTimeout(resolve14, 5));
1460
+ if (attempt < 2) await new Promise((resolve13) => setTimeout(resolve13, 5));
2283
1461
  }
2284
1462
  }
2285
1463
  }
2286
1464
  async function breakStaleLock(lockPath) {
2287
1465
  try {
2288
- const content = await fs5.readFile(lockPath, "utf8").catch(() => "");
1466
+ const content = await fs4.readFile(lockPath, "utf8").catch(() => "");
2289
1467
  const trimmed = content.trim();
2290
1468
  const colonIdx = trimmed.indexOf(":");
2291
1469
  if (colonIdx === -1) {
2292
1470
  const barePid = Number.parseInt(trimmed, 10);
2293
1471
  if (Number.isInteger(barePid) && barePid > 0 && !isPidAlive(barePid)) {
2294
1472
  return await breakStaleLockVerified(lockPath, async () => {
2295
- const reread = await fs5.readFile(lockPath, "utf8").catch(() => "");
1473
+ const reread = await fs4.readFile(lockPath, "utf8").catch(() => "");
2296
1474
  return reread.trim() === trimmed;
2297
1475
  });
2298
1476
  }
2299
1477
  return await breakStaleLockVerified(lockPath, async () => {
2300
- const st = await fs5.stat(lockPath);
1478
+ const st = await fs4.stat(lockPath);
2301
1479
  return Date.now() - st.mtimeMs > STALE_LOCK_MS;
2302
1480
  });
2303
1481
  }
@@ -2305,22 +1483,22 @@ async function breakStaleLock(lockPath) {
2305
1483
  const ownerPid = Number.parseInt(trimmed.slice(colonIdx + 1), 10);
2306
1484
  if (ownerHost === HOST_NAME && Number.isInteger(ownerPid) && ownerPid > 0) {
2307
1485
  if (isPidAlive(ownerPid)) {
2308
- const stat19 = await fs5.stat(lockPath);
1486
+ const stat19 = await fs4.stat(lockPath);
2309
1487
  if (Date.now() - stat19.mtimeMs > SAME_HOST_STALE_MS) {
2310
1488
  return await breakStaleLockVerified(lockPath, async () => {
2311
- const st = await fs5.stat(lockPath);
1489
+ const st = await fs4.stat(lockPath);
2312
1490
  return Date.now() - st.mtimeMs > SAME_HOST_STALE_MS;
2313
1491
  });
2314
1492
  }
2315
1493
  return false;
2316
1494
  }
2317
1495
  return await breakStaleLockVerified(lockPath, async () => {
2318
- const reread = await fs5.readFile(lockPath, "utf8").catch(() => "");
1496
+ const reread = await fs4.readFile(lockPath, "utf8").catch(() => "");
2319
1497
  return reread.trim() === trimmed;
2320
1498
  });
2321
1499
  }
2322
1500
  return await breakStaleLockVerified(lockPath, async () => {
2323
- const st = await fs5.stat(lockPath);
1501
+ const st = await fs4.stat(lockPath);
2324
1502
  return Date.now() - st.mtimeMs > STALE_LOCK_MS;
2325
1503
  });
2326
1504
  } catch {
@@ -2334,21 +1512,21 @@ async function breakStaleLockVerified(lockPath, verify) {
2334
1512
  async function breakLockAtomically(lockPath) {
2335
1513
  const tombstone = `${lockPath}.stale-${randomUUID2()}.tmp`;
2336
1514
  try {
2337
- await fs5.rename(lockPath, tombstone);
1515
+ await fs4.rename(lockPath, tombstone);
2338
1516
  } catch {
2339
1517
  return false;
2340
1518
  }
2341
- void fs5.unlink(tombstone).catch(() => void 0);
1519
+ void fs4.unlink(tombstone).catch(() => void 0);
2342
1520
  return true;
2343
1521
  }
2344
1522
  async function writeAtomicFile(filePath, registry) {
2345
- const tmp = path7.join(
2346
- path7.dirname(filePath),
2347
- `.${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`
2348
1526
  );
2349
1527
  let tmpPersisted = false;
2350
1528
  try {
2351
- const handle = await fs5.open(tmp, "w");
1529
+ const handle = await fs4.open(tmp, "w");
2352
1530
  try {
2353
1531
  await handle.writeFile(JSON.stringify(registry, null, 2), "utf8");
2354
1532
  await handle.sync().catch(() => void 0);
@@ -2357,14 +1535,14 @@ async function writeAtomicFile(filePath, registry) {
2357
1535
  }
2358
1536
  tmpPersisted = true;
2359
1537
  try {
2360
- await fs5.rename(tmp, filePath);
1538
+ await fs4.rename(tmp, filePath);
2361
1539
  tmpPersisted = false;
2362
1540
  } catch (renameErr) {
2363
1541
  const code = renameErr?.code;
2364
1542
  if (code === "EPERM" || code === "EBUSY" || code === "EACCES") {
2365
- await fs5.copyFile(tmp, filePath);
1543
+ await fs4.copyFile(tmp, filePath);
2366
1544
  try {
2367
- const destHandle = await fs5.open(filePath, "r+");
1545
+ const destHandle = await fs4.open(filePath, "r+");
2368
1546
  try {
2369
1547
  await destHandle.sync().catch(() => void 0);
2370
1548
  } finally {
@@ -2372,41 +1550,41 @@ async function writeAtomicFile(filePath, registry) {
2372
1550
  }
2373
1551
  } catch {
2374
1552
  }
2375
- await fs5.unlink(tmp).catch(() => void 0);
1553
+ await fs4.unlink(tmp).catch(() => void 0);
2376
1554
  tmpPersisted = false;
2377
1555
  } else {
2378
1556
  throw renameErr;
2379
1557
  }
2380
1558
  }
2381
1559
  } catch (err) {
2382
- if (tmpPersisted) await fs5.unlink(tmp).catch(() => void 0);
1560
+ if (tmpPersisted) await fs4.unlink(tmp).catch(() => void 0);
2383
1561
  throw err;
2384
1562
  }
2385
1563
  }
2386
1564
  async function pruneStaleTempFiles(filePath) {
2387
1565
  try {
2388
- const dir = path7.dirname(filePath);
2389
- const base = path7.basename(filePath);
1566
+ const dir = path6.dirname(filePath);
1567
+ const base = path6.basename(filePath);
2390
1568
  const now = Date.now();
2391
1569
  const stale = [];
2392
- for (const name of await fs5.readdir(dir)) {
1570
+ for (const name of await fs4.readdir(dir)) {
2393
1571
  const isTemp = (name.startsWith(`${base}.`) || name.startsWith(`.${base}.`)) && name.endsWith(".tmp");
2394
1572
  if (!isTemp) continue;
2395
- const stat19 = await fs5.stat(path7.join(dir, name)).catch(() => null);
1573
+ const stat19 = await fs4.stat(path6.join(dir, name)).catch(() => null);
2396
1574
  if (!stat19) continue;
2397
1575
  if (now - stat19.mtimeMs > STALE_TMP_MS) stale.push({ name, mtimeMs: stat19.mtimeMs });
2398
1576
  }
2399
- stale.sort((a, b) => b.mtimeMs - a.mtimeMs);
1577
+ stale.sort((a, b) => a.mtimeMs - b.mtimeMs);
2400
1578
  await Promise.all(
2401
- stale.slice(MAX_STALE_TMP_FILES).map(async ({ name }) => {
2402
- 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);
2403
1581
  })
2404
1582
  );
2405
1583
  } catch {
2406
1584
  }
2407
1585
  }
2408
1586
 
2409
- // src/session-registry.ts
1587
+ // src/session-catalog/session-registry.ts
2410
1588
  var REGISTRY_FILE = "session-registry.json";
2411
1589
  var HEARTBEAT_INTERVAL_MS2 = 5e3;
2412
1590
  var STALE_TIMEOUT_MS = 3e4;
@@ -2422,7 +1600,7 @@ var SessionOwnershipConflictError = class extends Error {
2422
1600
  var SessionRegistryWriteError = class extends Error {
2423
1601
  };
2424
1602
  var TEMP_PRUNE_INTERVAL_MS = STALE_TMP_MS;
2425
- var pidAlive2 = isPidAlive;
1603
+ var pidAlive = isPidAlive;
2426
1604
  function sameOwner(entry, owner) {
2427
1605
  return entry.pid === owner.pid && entry.startedAt === owner.startedAt;
2428
1606
  }
@@ -2463,7 +1641,7 @@ var SessionRegistry = class {
2463
1641
  lastEntry = null;
2464
1642
  ownershipLockWaitMs;
2465
1643
  constructor(globalRoot, options = {}) {
2466
- this.filePath = path8.join(globalRoot, REGISTRY_FILE);
1644
+ this.filePath = path7.join(globalRoot, REGISTRY_FILE);
2467
1645
  this.ownershipLockWaitMs = Math.max(0, options.ownershipLockWaitMs ?? OWNERSHIP_LOCK_WAIT_MS);
2468
1646
  }
2469
1647
  // ── Public API ──────────────────────────────────────────────────────────
@@ -2481,7 +1659,7 @@ var SessionRegistry = class {
2481
1659
  };
2482
1660
  const existingRegistry = await this.readAndPrune();
2483
1661
  const currentOwner = existingRegistry[entry.sessionId];
2484
- if (currentOwner && currentOwner.pid !== entry.pid && pidAlive2(currentOwner.pid)) {
1662
+ if (currentOwner && currentOwner.pid !== entry.pid && pidAlive(currentOwner.pid)) {
2485
1663
  throw new SessionOwnershipConflictError(
2486
1664
  `Session ${entry.sessionId} is already open in another running wstack (pid ${currentOwner.pid}).`
2487
1665
  );
@@ -2494,12 +1672,12 @@ var SessionRegistry = class {
2494
1672
  continue;
2495
1673
  }
2496
1674
  const heartbeatAt = Date.parse(existing.lastHeartbeatAt);
2497
- 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)) {
2498
1676
  delete registry[id];
2499
1677
  }
2500
1678
  }
2501
1679
  const lockedOwner = registry[entry.sessionId];
2502
- if (lockedOwner && lockedOwner.pid !== entry.pid && pidAlive2(lockedOwner.pid)) {
1680
+ if (lockedOwner && lockedOwner.pid !== entry.pid && pidAlive(lockedOwner.pid)) {
2503
1681
  throw new SessionOwnershipConflictError(
2504
1682
  `Session ${entry.sessionId} is already open in another running wstack (pid ${lockedOwner.pid}).`
2505
1683
  );
@@ -2545,13 +1723,13 @@ var SessionRegistry = class {
2545
1723
  }
2546
1724
  if (!this.agentsFlushTimer) {
2547
1725
  const delay2 = Math.max(0, AGENTS_WRITE_THROTTLE_MS - sinceLastWrite);
2548
- this.pendingAgentsFlush = new Promise((resolve14, reject) => {
2549
- this.pendingAgentsResolve = resolve14;
1726
+ this.pendingAgentsFlush = new Promise((resolve13, reject) => {
1727
+ this.pendingAgentsResolve = resolve13;
2550
1728
  const timer = setTimeout(() => {
2551
1729
  this.agentsFlushTimer = null;
2552
1730
  this.pendingAgentsFlush = null;
2553
1731
  this.pendingAgentsResolve = null;
2554
- this.writeAgentsSnapshot().then(resolve14, reject);
1732
+ this.writeAgentsSnapshot().then(resolve13, reject);
2555
1733
  }, delay2);
2556
1734
  if (typeof timer.unref === "function") timer.unref();
2557
1735
  this.agentsFlushTimer = timer;
@@ -2702,7 +1880,7 @@ var SessionRegistry = class {
2702
1880
  }
2703
1881
  async readAndPrune() {
2704
1882
  try {
2705
- const raw = await fs6.readFile(this.filePath, "utf8");
1883
+ const raw = await fs5.readFile(this.filePath, "utf8");
2706
1884
  const registry = parseRegistry(raw);
2707
1885
  const observed = new Map(
2708
1886
  Object.entries(registry).map(([id, entry]) => [
@@ -2732,13 +1910,13 @@ var SessionRegistry = class {
2732
1910
  pruned = true;
2733
1911
  }
2734
1912
  if (entry.status === "closing" && heartbeatAge > CLOSING_GRACE_MS) {
2735
- if (!pidAlive2(entry.pid)) {
1913
+ if (!pidAlive(entry.pid)) {
2736
1914
  delete registry[id];
2737
1915
  pruned = true;
2738
1916
  }
2739
1917
  continue;
2740
1918
  }
2741
- if (heartbeatAge > PID_CHECK_AFTER_MS && !pidAlive2(entry.pid)) {
1919
+ if (heartbeatAge > PID_CHECK_AFTER_MS && !pidAlive(entry.pid)) {
2742
1920
  delete registry[id];
2743
1921
  pruned = true;
2744
1922
  continue;
@@ -2773,7 +1951,7 @@ var SessionRegistry = class {
2773
1951
  const deadline = Date.now() + waitBudgetMs;
2774
1952
  let attempt = 0;
2775
1953
  try {
2776
- await fs6.mkdir(path8.dirname(this.filePath), { recursive: true });
1954
+ await fs5.mkdir(path7.dirname(this.filePath), { recursive: true });
2777
1955
  await this.maybePruneStaleTempFiles();
2778
1956
  } catch (err) {
2779
1957
  if (required) {
@@ -2785,17 +1963,17 @@ var SessionRegistry = class {
2785
1963
  }
2786
1964
  while (true) {
2787
1965
  try {
2788
- let lockHandle = await fs6.open(lockPath, "wx").catch(() => null);
1966
+ let lockHandle = await fs5.open(lockPath, "wx").catch(() => null);
2789
1967
  if (!lockHandle) {
2790
1968
  if (await breakStaleLock(lockPath)) {
2791
- lockHandle = await fs6.open(lockPath, "wx").catch(() => null);
1969
+ lockHandle = await fs5.open(lockPath, "wx").catch(() => null);
2792
1970
  }
2793
1971
  if (!lockHandle) {
2794
1972
  const remainingMs = deadline - Date.now();
2795
1973
  if (remainingMs <= 0) break;
2796
1974
  const retryDelayMs = Math.min(LOCK_RETRY_MAX_MS, 20 * (attempt + 1));
2797
1975
  await new Promise(
2798
- (resolve14) => setTimeout(resolve14, Math.min(retryDelayMs, remainingMs))
1976
+ (resolve13) => setTimeout(resolve13, Math.min(retryDelayMs, remainingMs))
2799
1977
  );
2800
1978
  attempt += 1;
2801
1979
  continue;
@@ -2809,14 +1987,14 @@ var SessionRegistry = class {
2809
1987
  await lockHandle.writeFile(lockOwnerStamp());
2810
1988
  stamped = true;
2811
1989
  } catch {
2812
- if (stampAttempt < 2) await new Promise((resolve14) => setTimeout(resolve14, 5));
1990
+ if (stampAttempt < 2) await new Promise((resolve13) => setTimeout(resolve13, 5));
2813
1991
  }
2814
1992
  }
2815
1993
  if (!stamped) {
2816
1994
  stampFailed = true;
2817
1995
  throw new Error("failed to stamp session-registry lock owner");
2818
1996
  }
2819
- const raw = await fs6.readFile(this.filePath, "utf8").catch(() => "{}");
1997
+ const raw = await fs5.readFile(this.filePath, "utf8").catch(() => "{}");
2820
1998
  const registry = parseRegistry(raw);
2821
1999
  fn(registry);
2822
2000
  await this.writeAtomicWithRetry(registry);
@@ -2824,7 +2002,7 @@ var SessionRegistry = class {
2824
2002
  } finally {
2825
2003
  await lockHandle.close().catch(() => void 0);
2826
2004
  if (stampFailed) {
2827
- await fs6.unlink(lockPath).catch(() => void 0);
2005
+ await fs5.unlink(lockPath).catch(() => void 0);
2828
2006
  } else {
2829
2007
  await maybeUnlinkOwnedLock(lockPath);
2830
2008
  }
@@ -2875,7 +2053,7 @@ var SessionRegistry = class {
2875
2053
  throw err;
2876
2054
  }
2877
2055
  const delayMs = Math.min(80, 5 * attempt);
2878
- await new Promise((resolve14) => setTimeout(resolve14, delayMs));
2056
+ await new Promise((resolve13) => setTimeout(resolve13, delayMs));
2879
2057
  }
2880
2058
  }
2881
2059
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
@@ -2905,7 +2083,7 @@ var SessionRegistry = class {
2905
2083
 
2906
2084
  // src/storage/annotations-store.ts
2907
2085
  import { randomUUID as randomUUID3 } from "node:crypto";
2908
- import * as fs7 from "node:fs/promises";
2086
+ import * as fs6 from "node:fs/promises";
2909
2087
 
2910
2088
  // src/utils/expect-defined.ts
2911
2089
  function expectDefined(value, label) {
@@ -3144,7 +2322,7 @@ var AnnotationsStore = class {
3144
2322
  const fp = this.filePath(sessionId);
3145
2323
  let raw;
3146
2324
  try {
3147
- raw = await fs7.readFile(fp, "utf8");
2325
+ raw = await fs6.readFile(fp, "utf8");
3148
2326
  } catch (err) {
3149
2327
  if (err.code === "ENOENT") return null;
3150
2328
  throw err;
@@ -3184,7 +2362,7 @@ var AnnotationsStore = class {
3184
2362
  // src/storage/attachment-store.ts
3185
2363
  import { randomBytes } from "node:crypto";
3186
2364
  import * as fsp from "node:fs/promises";
3187
- import * as path9 from "node:path";
2365
+ import * as path8 from "node:path";
3188
2366
  var DEFAULT_SPOOL_THRESHOLD = 256 * 1024;
3189
2367
  var PLACEHOLDER_RE = /\[(pasted|image|file) #(\d+)[^\]]*\]|\[file:([^\]]+)\]/g;
3190
2368
  var DefaultAttachmentStore = class {
@@ -3205,7 +2383,7 @@ var DefaultAttachmentStore = class {
3205
2383
  let data = input.data;
3206
2384
  if (this.spoolDir && bytes >= this.spoolThreshold) {
3207
2385
  await fsp.mkdir(this.spoolDir, { recursive: true });
3208
- spooledPath = path9.join(this.spoolDir, `${id}.bin`);
2386
+ spooledPath = path8.join(this.spoolDir, `${id}.bin`);
3209
2387
  await atomicWrite(spooledPath, input.data, {
3210
2388
  encoding: input.kind === "image" ? "base64" : "utf8"
3211
2389
  });
@@ -3325,8 +2503,8 @@ function mergeAdjacentText(blocks) {
3325
2503
 
3326
2504
  // src/storage/cloud-config-sync.ts
3327
2505
  import { createHash as createHash3, randomUUID as nodeRandomUUID } from "node:crypto";
3328
- import * as fs8 from "node:fs/promises";
3329
- import * as path10 from "node:path";
2506
+ import * as fs7 from "node:fs/promises";
2507
+ import * as path9 from "node:path";
3330
2508
 
3331
2509
  // src/storage/cloud-config-sync/sanitize.ts
3332
2510
  var SAGE_TREE = {
@@ -4047,7 +3225,7 @@ var CloudConfigSync = class {
4047
3225
  async loadState() {
4048
3226
  if (this.state) return this.state;
4049
3227
  try {
4050
- const raw = await fs8.readFile(this.deps.statePath, "utf8");
3228
+ const raw = await fs7.readFile(this.deps.statePath, "utf8");
4051
3229
  const parsed = JSON.parse(raw);
4052
3230
  this.state = {
4053
3231
  ...structuredClone(EMPTY_STATE),
@@ -4061,7 +3239,7 @@ var CloudConfigSync = class {
4061
3239
  }
4062
3240
  async saveState(state) {
4063
3241
  this.state = state;
4064
- await fs8.mkdir(path10.dirname(this.deps.statePath), { recursive: true });
3242
+ await fs7.mkdir(path9.dirname(this.deps.statePath), { recursive: true });
4065
3243
  await atomicWrite(this.deps.statePath, `${JSON.stringify(state, null, 2)}
4066
3244
  `, {
4067
3245
  mode: 384
@@ -4081,8 +3259,8 @@ function summary(partial) {
4081
3259
  }
4082
3260
 
4083
3261
  // src/storage/cloud-sync.ts
4084
- import * as fs9 from "node:fs/promises";
4085
- import * as path11 from "node:path";
3262
+ import * as fs8 from "node:fs/promises";
3263
+ import * as path10 from "node:path";
4086
3264
  import { createHash as createHash4 } from "node:crypto";
4087
3265
  var ALL_SYNC_CATEGORIES = ["settings", "skills", "prompts", "memory", "history"];
4088
3266
  var CloudSync = class {
@@ -4090,8 +3268,8 @@ var CloudSync = class {
4090
3268
  this.paths = paths;
4091
3269
  this.getConfig = getConfig;
4092
3270
  this.setConfig = setConfig;
4093
- this.statePath = path11.join(paths.configDir, "sync-state.json");
4094
- 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"));
4095
3273
  }
4096
3274
  paths;
4097
3275
  getConfig;
@@ -4246,7 +3424,7 @@ var CloudSync = class {
4246
3424
  }
4247
3425
  async loadState() {
4248
3426
  try {
4249
- const raw = await fs9.readFile(this.statePath, "utf8");
3427
+ const raw = await fs8.readFile(this.statePath, "utf8");
4250
3428
  this.state = JSON.parse(raw);
4251
3429
  } catch {
4252
3430
  this.state = null;
@@ -4371,18 +3549,18 @@ var CloudSync = class {
4371
3549
  const localPath = this.categoryToPath(cat);
4372
3550
  if (!localPath) continue;
4373
3551
  try {
4374
- const stat19 = await fs9.lstat(localPath);
3552
+ const stat19 = await fs8.lstat(localPath);
4375
3553
  if (stat19.isSymbolicLink()) continue;
4376
3554
  if (stat19.isDirectory()) {
4377
3555
  const files = await this.walkDir(localPath, localPath);
4378
3556
  for (const file of files) {
4379
- const content = await fs9.readFile(file, "utf8");
4380
- 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, "/");
4381
3559
  entries.push({ path: `data/${cat}/${rel}`, content, mode: "100644" });
4382
3560
  hashes.push(`${cat}/${rel}\0${content}`);
4383
3561
  }
4384
3562
  } else {
4385
- const content = await fs9.readFile(localPath, "utf8");
3563
+ const content = await fs8.readFile(localPath, "utf8");
4386
3564
  entries.push({ path: `data/${cat}`, content, mode: "100644" });
4387
3565
  hashes.push(`${cat}\0${content}`);
4388
3566
  }
@@ -4398,17 +3576,17 @@ var CloudSync = class {
4398
3576
  const localPath = this.categoryToPath(cat);
4399
3577
  if (!localPath) continue;
4400
3578
  try {
4401
- const stat19 = await fs9.lstat(localPath);
3579
+ const stat19 = await fs8.lstat(localPath);
4402
3580
  if (stat19.isSymbolicLink()) continue;
4403
3581
  if (stat19.isDirectory()) {
4404
3582
  const files = await this.walkDir(localPath, localPath);
4405
3583
  for (const file of files) {
4406
- const content = await fs9.readFile(file, "utf8");
4407
- 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, "/");
4408
3586
  hashes.push(`${cat}/${rel}\0${content}`);
4409
3587
  }
4410
3588
  } else {
4411
- const content = await fs9.readFile(localPath, "utf8");
3589
+ const content = await fs8.readFile(localPath, "utf8");
4412
3590
  hashes.push(`${cat}\0${content}`);
4413
3591
  }
4414
3592
  } catch {
@@ -4435,10 +3613,10 @@ var CloudSync = class {
4435
3613
  }
4436
3614
  async walkDir(dir, base) {
4437
3615
  const results = [];
4438
- const entries = await fs9.readdir(dir, { withFileTypes: true });
3616
+ const entries = await fs8.readdir(dir, { withFileTypes: true });
4439
3617
  entries.sort((a, b) => a.name.localeCompare(b.name));
4440
3618
  for (const entry of entries) {
4441
- const full = path11.join(dir, entry.name);
3619
+ const full = path10.join(dir, entry.name);
4442
3620
  if (entry.isSymbolicLink()) continue;
4443
3621
  if (entry.isDirectory()) {
4444
3622
  results.push(...await this.walkDir(full, base));
@@ -4451,17 +3629,17 @@ var CloudSync = class {
4451
3629
  };
4452
3630
  async function preparePulledDestination(cat, localPath, destPath, remotePath) {
4453
3631
  const directoryBacked = cat === "skills" || cat === "prompts";
4454
- const rootPath = directoryBacked ? localPath : path11.dirname(localPath);
3632
+ const rootPath = directoryBacked ? localPath : path10.dirname(localPath);
4455
3633
  const rootStat = await lstatIfExists(rootPath);
4456
3634
  if (rootStat?.isSymbolicLink()) {
4457
3635
  throw unsafePulledSymlinkError(remotePath, rootPath);
4458
3636
  }
4459
3637
  if (directoryBacked) {
4460
- const relativeParent = path11.relative(rootPath, path11.dirname(destPath));
3638
+ const relativeParent = path10.relative(rootPath, path10.dirname(destPath));
4461
3639
  let cursor = rootPath;
4462
3640
  if (relativeParent) {
4463
- for (const segment of relativeParent.split(path11.sep)) {
4464
- cursor = path11.join(cursor, segment);
3641
+ for (const segment of relativeParent.split(path10.sep)) {
3642
+ cursor = path10.join(cursor, segment);
4465
3643
  const stat19 = await lstatIfExists(cursor);
4466
3644
  if (stat19?.isSymbolicLink()) throw unsafePulledSymlinkError(remotePath, cursor);
4467
3645
  }
@@ -4469,11 +3647,11 @@ async function preparePulledDestination(cat, localPath, destPath, remotePath) {
4469
3647
  }
4470
3648
  const destStat = await lstatIfExists(destPath);
4471
3649
  if (destStat?.isSymbolicLink()) throw unsafePulledSymlinkError(remotePath, destPath);
4472
- await fs9.mkdir(path11.dirname(destPath), { recursive: true });
3650
+ await fs8.mkdir(path10.dirname(destPath), { recursive: true });
4473
3651
  }
4474
3652
  async function lstatIfExists(filePath) {
4475
3653
  try {
4476
- return await fs9.lstat(filePath);
3654
+ return await fs8.lstat(filePath);
4477
3655
  } catch (err) {
4478
3656
  if (err.code === "ENOENT") return null;
4479
3657
  throw err;
@@ -4499,9 +3677,9 @@ function resolvePulledCategoryPath(cat, localPath, rel, remotePath) {
4499
3677
  return localPath;
4500
3678
  }
4501
3679
  if (!rel) return localPath;
4502
- const normalizedRel = path11.normalize(rel);
4503
- const traversesUp = normalizedRel === ".." || normalizedRel.startsWith(`..${path11.sep}`);
4504
- 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) {
4505
3683
  throw new FsError({
4506
3684
  message: `Refusing CloudSync path traversal: ${remotePath}`,
4507
3685
  code: ERROR_CODES.FS_DELETE_FAILED,
@@ -4509,10 +3687,10 @@ function resolvePulledCategoryPath(cat, localPath, rel, remotePath) {
4509
3687
  context: { reason: "path_traversal", normalizedRel }
4510
3688
  });
4511
3689
  }
4512
- const dest = path11.resolve(localPath, normalizedRel);
4513
- const root = path11.resolve(localPath);
4514
- const relative8 = path11.relative(root, dest);
4515
- 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)) {
4516
3694
  throw new FsError({
4517
3695
  message: `Refusing CloudSync path outside category root: ${remotePath}`,
4518
3696
  code: ERROR_CODES.FS_DELETE_FAILED,
@@ -4635,7 +3813,7 @@ function isCompletedWorkEvidence(value) {
4635
3813
  }
4636
3814
 
4637
3815
  // src/storage/config-loader.ts
4638
- import * as fs11 from "node:fs/promises";
3816
+ import * as fs10 from "node:fs/promises";
4639
3817
 
4640
3818
  // src/security/config-secrets.ts
4641
3819
  function decryptConfigSecrets(cfg, vault, opts) {
@@ -4733,20 +3911,20 @@ function isContextWindowModeId(id) {
4733
3911
  }
4734
3912
 
4735
3913
  // src/utils/config-backup.ts
4736
- import * as fs10 from "node:fs/promises";
4737
- import * as path12 from "node:path";
3914
+ import * as fs9 from "node:fs/promises";
3915
+ import * as path11 from "node:path";
4738
3916
  function configHistoryDir(globalRoot) {
4739
- return path12.join(globalRoot, "config-history");
3917
+ return path11.join(globalRoot, "config-history");
4740
3918
  }
4741
3919
  function configSlug(absolutePath, globalRoot) {
4742
- const rel = path12.relative(globalRoot, absolutePath);
3920
+ const rel = path11.relative(globalRoot, absolutePath);
4743
3921
  const normalized = rel.replace(/\\/g, "/").replace(/\.json$/i, "");
4744
3922
  return normalized.replace(/\//g, "-");
4745
3923
  }
4746
3924
  async function backupConfigFile(filePath, paths) {
4747
3925
  let currentContent;
4748
3926
  try {
4749
- currentContent = await fs10.readFile(filePath, "utf8");
3927
+ currentContent = await fs9.readFile(filePath, "utf8");
4750
3928
  if (!currentContent.trim()) return;
4751
3929
  } catch {
4752
3930
  return;
@@ -4755,10 +3933,10 @@ async function backupConfigFile(filePath, paths) {
4755
3933
  const ts = now.toISOString().replace(/[:.]/g, "-").replace(/Z$/, "");
4756
3934
  const slug = configSlug(filePath, paths.globalRoot);
4757
3935
  const backupDir = configHistoryDir(paths.globalRoot);
4758
- const backupFile = path12.join(backupDir, `${slug}-${ts}.json`);
3936
+ const backupFile = path11.join(backupDir, `${slug}-${ts}.json`);
4759
3937
  try {
4760
- await fs10.mkdir(backupDir, { recursive: true });
4761
- 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" });
4762
3940
  } catch {
4763
3941
  }
4764
3942
  }
@@ -5082,19 +4260,19 @@ function repairConfigDefaults(input) {
5082
4260
  });
5083
4261
  }
5084
4262
  for (const [key, defaultValue] of Object.entries(defaults)) {
5085
- const path36 = prefix ? `${prefix}.${key}` : key;
4263
+ const path35 = prefix ? `${prefix}.${key}` : key;
5086
4264
  if (!Object.hasOwn(target, key)) {
5087
4265
  target[key] = cloneJsonValue(defaultValue);
5088
- changes.push({ path: path36, action: "added" });
4266
+ changes.push({ path: path35, action: "added" });
5089
4267
  continue;
5090
4268
  }
5091
4269
  const current = target[key];
5092
4270
  if (isPlainRecord(defaultValue)) {
5093
4271
  if (isPlainRecord(current)) {
5094
- repair(current, defaultValue, path36, depth + 1);
4272
+ repair(current, defaultValue, path35, depth + 1);
5095
4273
  } else {
5096
4274
  target[key] = cloneJsonValue(defaultValue);
5097
- changes.push({ path: path36, action: "replaced" });
4275
+ changes.push({ path: path35, action: "replaced" });
5098
4276
  }
5099
4277
  continue;
5100
4278
  }
@@ -5102,7 +4280,7 @@ function repairConfigDefaults(input) {
5102
4280
  const compatible = defaultIsArray ? Array.isArray(current) : !Array.isArray(current) && typeof current === typeof defaultValue;
5103
4281
  if (!compatible) {
5104
4282
  target[key] = cloneJsonValue(defaultValue);
5105
- changes.push({ path: path36, action: "replaced" });
4283
+ changes.push({ path: path35, action: "replaced" });
5106
4284
  }
5107
4285
  }
5108
4286
  };
@@ -5387,8 +4565,8 @@ var IN_PROJECT_DENIED_PATHS = [
5387
4565
  reason: "Disables the plugin trust-on-first-use integrity gate, re-trusting changed code in already-installed global plugins."
5388
4566
  }
5389
4567
  ];
5390
- function deleteNestedPath(target, path36) {
5391
- const segments = path36.split(".");
4568
+ function deleteNestedPath(target, path35) {
4569
+ const segments = path35.split(".");
5392
4570
  const last = segments[segments.length - 1];
5393
4571
  if (last === void 0) return false;
5394
4572
  let cursor = target;
@@ -5445,16 +4623,16 @@ function assertInProjectAllowListComplete() {
5445
4623
  );
5446
4624
  }
5447
4625
  const orphanedPaths = IN_PROJECT_DENIED_PATHS.filter(
5448
- ({ path: path36 }) => !IN_PROJECT_ALLOWED_KEYS.has(path36.split(".")[0] ?? "")
5449
- ).map(({ path: path36 }) => path36);
4626
+ ({ path: path35 }) => !IN_PROJECT_ALLOWED_KEYS.has(path35.split(".")[0] ?? "")
4627
+ ).map(({ path: path35 }) => path35);
5450
4628
  if (orphanedPaths.length > 0) {
5451
4629
  problems.push(
5452
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."
5453
4631
  );
5454
4632
  }
5455
4633
  const malformedPaths = IN_PROJECT_DENIED_PATHS.filter(
5456
- ({ path: path36 }) => path36.split(".").length < 2 || path36.split(".").some((s) => s.length === 0)
5457
- ).map(({ path: path36 }) => path36);
4634
+ ({ path: path35 }) => path35.split(".").length < 2 || path35.split(".").some((s) => s.length === 0)
4635
+ ).map(({ path: path35 }) => path35);
5458
4636
  if (malformedPaths.length > 0) {
5459
4637
  problems.push(
5460
4638
  `IN_PROJECT_DENIED_PATHS entr(ies) are not dotted nested paths: ` + malformedPaths.join(", ") + ". Top-level keys belong in KNOWN_DENIED_IN_PROJECT instead."
@@ -5482,8 +4660,8 @@ function stripUnsafeInProjectFields(inProject, sourcePath, warn = (msg) => conso
5482
4660
  }
5483
4661
  stripped.push(k);
5484
4662
  }
5485
- for (const { path: path36 } of IN_PROJECT_DENIED_PATHS) {
5486
- 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);
5487
4665
  }
5488
4666
  if (stripped.length > 0) {
5489
4667
  warn(
@@ -5672,10 +4850,10 @@ function removeLegacySageEngine(config) {
5672
4850
  }
5673
4851
 
5674
4852
  // src/storage/config-loader/path-identity.ts
5675
- import * as path13 from "node:path";
4853
+ import * as path12 from "node:path";
5676
4854
  function samePath(a, b) {
5677
- let ra = path13.resolve(a);
5678
- let rb = path13.resolve(b);
4855
+ let ra = path12.resolve(a);
4856
+ let rb = path12.resolve(b);
5679
4857
  if (process.platform === "win32" || process.platform === "darwin") {
5680
4858
  ra = ra.toLowerCase();
5681
4859
  rb = rb.toLowerCase();
@@ -5835,7 +5013,7 @@ var DefaultConfigLoader = class _DefaultConfigLoader {
5835
5013
  let parsed;
5836
5014
  let fileExisted = true;
5837
5015
  try {
5838
- const raw = await fs11.readFile(fp, "utf8");
5016
+ const raw = await fs10.readFile(fp, "utf8");
5839
5017
  const result = safeParse(raw);
5840
5018
  if (!result.ok || !isPlainRecord(result.value)) {
5841
5019
  return;
@@ -5931,7 +5109,7 @@ var DefaultConfigLoader = class _DefaultConfigLoader {
5931
5109
  let parsed;
5932
5110
  let existed = true;
5933
5111
  try {
5934
- const raw = await fs11.readFile(profileFp, "utf8");
5112
+ const raw = await fs10.readFile(profileFp, "utf8");
5935
5113
  const result = safeParse(raw);
5936
5114
  if (!result.ok || !isPlainRecord(result.value)) {
5937
5115
  this.logWarn("Profile config parse failed \u2014 falling back to defaults", {
@@ -6030,7 +5208,7 @@ var DefaultConfigLoader = class _DefaultConfigLoader {
6030
5208
  const fp = this.paths.syncConfig;
6031
5209
  const t0 = Date.now();
6032
5210
  try {
6033
- const raw = await fs11.readFile(fp, "utf8");
5211
+ const raw = await fs10.readFile(fp, "utf8");
6034
5212
  const parsed = safeParse(raw);
6035
5213
  if (!parsed.ok || !parsed.value) {
6036
5214
  this.events?.emit("storage.read", {
@@ -6092,7 +5270,7 @@ var DefaultConfigLoader = class _DefaultConfigLoader {
6092
5270
  const t0 = Date.now();
6093
5271
  let mtimeMs = null;
6094
5272
  try {
6095
- const stat19 = await fs11.stat(file);
5273
+ const stat19 = await fs10.stat(file);
6096
5274
  mtimeMs = stat19.mtimeMs;
6097
5275
  const cached = this.jsonCache.get(file);
6098
5276
  if (cached && cached.mtimeMs === mtimeMs) {
@@ -6122,7 +5300,7 @@ var DefaultConfigLoader = class _DefaultConfigLoader {
6122
5300
  }
6123
5301
  let raw;
6124
5302
  try {
6125
- raw = await fs11.readFile(file, "utf8");
5303
+ raw = await fs10.readFile(file, "utf8");
6126
5304
  } catch (err) {
6127
5305
  if (err.code !== "ENOENT") {
6128
5306
  this.events?.emit("storage.read", {
@@ -6598,12 +5776,6 @@ var DirectorStateCheckpoint = class {
6598
5776
  }
6599
5777
  };
6600
5778
 
6601
- // src/storage/goal-coordination.ts
6602
- import { updateTask } from "@wrongstack/kanban";
6603
-
6604
- // src/storage/goal-kanban.ts
6605
- import { addTask, createBoard, getBoard, listBoards, removeBoard } from "@wrongstack/kanban";
6606
-
6607
5779
  // src/utils/term.ts
6608
5780
  var hasStdout = () => typeof process !== "undefined" && !!process.stdout;
6609
5781
  function isStdoutTTY() {
@@ -6652,14 +5824,16 @@ var color = {
6652
5824
  var GOAL_BOARD_TAG_PREFIX = "goal:";
6653
5825
  async function createGoalKanbanBoard(projectRoot, goalFile) {
6654
5826
  if (!projectRoot) return null;
5827
+ const store = tryBoardStore();
5828
+ if (!store) return null;
6655
5829
  const existingId = goalFile.kanbanBoardId;
6656
5830
  if (existingId) {
6657
- const existing = await getBoard(projectRoot, existingId).catch(() => null);
5831
+ const existing = await store.getBoard(projectRoot, existingId).catch(() => null);
6658
5832
  if (existing) return existingId;
6659
5833
  }
6660
5834
  const displayGoal = (goalFile.refinedGoal || goalFile.goal).replace(/\s+/g, " ").trim();
6661
5835
  const titleSuffix = displayGoal.length > 80 ? displayGoal.slice(0, 77) + "\u2026" : displayGoal;
6662
- const board = await createBoard(projectRoot, {
5836
+ const board = await store.createBoard(projectRoot, {
6663
5837
  title: `\u{1F3AF} ${titleSuffix}`,
6664
5838
  description: `Kanban board auto-created for goal: ${displayGoal}`,
6665
5839
  tags: [goalTag(goalFile)],
@@ -6673,7 +5847,7 @@ async function createGoalKanbanBoard(projectRoot, goalFile) {
6673
5847
  const d = goalFile.deliverables[index];
6674
5848
  const cleaned = d.replace(/^\[[x✓]\]|✅|\(done\)\s*/i, "").trim();
6675
5849
  if (cleaned) {
6676
- await addTask(projectRoot, board.id, {
5850
+ await store.addTask(projectRoot, board.id, {
6677
5851
  title: cleaned,
6678
5852
  columnId: targetColumnId,
6679
5853
  description: `Deliverable for goal: ${displayGoal}`,
@@ -6693,23 +5867,27 @@ async function createGoalKanbanBoard(projectRoot, goalFile) {
6693
5867
  async function findGoalKanbanBoard(projectRoot, boardId) {
6694
5868
  if (!projectRoot || !boardId) return null;
6695
5869
  try {
6696
- return await getBoard(projectRoot, boardId);
5870
+ return await boardStore().getBoard(projectRoot, boardId);
6697
5871
  } catch {
6698
5872
  return null;
6699
5873
  }
6700
5874
  }
6701
5875
  async function deleteGoalKanbanBoard(projectRoot, boardId) {
6702
5876
  if (!projectRoot || !boardId) return;
6703
- await removeBoard(projectRoot, boardId).catch(() => {
5877
+ const store = tryBoardStore();
5878
+ if (!store) return;
5879
+ await store.removeBoard(projectRoot, boardId).catch(() => {
6704
5880
  });
6705
5881
  }
6706
5882
  async function findGoalBoardByTag(projectRoot, goalFile) {
6707
5883
  if (!projectRoot) return null;
5884
+ const store = tryBoardStore();
5885
+ if (!store) return null;
6708
5886
  const tag = goalTag(goalFile);
6709
- const boards = await listBoards(projectRoot);
5887
+ const boards = await store.listBoards(projectRoot);
6710
5888
  const matched = boards.find((b) => b.tags?.includes(tag));
6711
5889
  if (!matched) return null;
6712
- return getBoard(projectRoot, matched.id).catch(() => null);
5890
+ return store.getBoard(projectRoot, matched.id).catch(() => null);
6713
5891
  }
6714
5892
  function formatGoalKanbanPreview(goalFile, boardId, taskCount) {
6715
5893
  const lines = [];
@@ -7225,7 +6403,7 @@ async function refreshGoalKanban(projectRoot, goal, completed, resolvedBoard) {
7225
6403
  const originMatch = task.origin?.system === "goal" && task.origin?.taskId ? originKeys.has(task.origin.taskId) : false;
7226
6404
  const titleMatch = !task.origin?.taskId && completedTitles.has(normalizeTitle(task.title));
7227
6405
  if (!originMatch && !titleMatch) continue;
7228
- const result = await updateTask(
6406
+ const result = await boardStore().updateTask(
7229
6407
  projectRoot,
7230
6408
  board.id,
7231
6409
  task.id,
@@ -7261,8 +6439,8 @@ function normalizeTitle(value) {
7261
6439
  }
7262
6440
 
7263
6441
  // src/storage/input-history-store.ts
7264
- import * as fs12 from "node:fs/promises";
7265
- import * as path14 from "node:path";
6442
+ import * as fs11 from "node:fs/promises";
6443
+ import * as path13 from "node:path";
7266
6444
  var INPUT_HISTORY_DEFAULT_MAX = 100;
7267
6445
  var InputHistoryStore = class {
7268
6446
  /**
@@ -7284,7 +6462,7 @@ var InputHistoryStore = class {
7284
6462
  */
7285
6463
  async load() {
7286
6464
  try {
7287
- const raw = JSON.parse(await fs12.readFile(this.file, "utf8"));
6465
+ const raw = JSON.parse(await fs11.readFile(this.file, "utf8"));
7288
6466
  if (raw && typeof raw === "object" && Array.isArray(raw.entries) && raw.entries.every((e) => typeof e === "string")) {
7289
6467
  return raw.entries.slice(0, this.maxEntries);
7290
6468
  }
@@ -7301,7 +6479,7 @@ var InputHistoryStore = class {
7301
6479
  */
7302
6480
  async save(entries) {
7303
6481
  const cleaned = this.scrubAndFilter(entries);
7304
- await ensureDir(path14.dirname(this.file));
6482
+ await ensureDir(path13.dirname(this.file));
7305
6483
  const payload = {
7306
6484
  version: 1,
7307
6485
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -7311,7 +6489,7 @@ var InputHistoryStore = class {
7311
6489
  }
7312
6490
  /** Truncate the file to an empty entry list (used by /clear). */
7313
6491
  async clear() {
7314
- await ensureDir(path14.dirname(this.file));
6492
+ await ensureDir(path13.dirname(this.file));
7315
6493
  const payload = { version: 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), entries: [] };
7316
6494
  await atomicWrite(this.file, JSON.stringify(payload, null, 2));
7317
6495
  }
@@ -7342,8 +6520,8 @@ var InputHistoryStore = class {
7342
6520
 
7343
6521
  // src/storage/memory-backend.ts
7344
6522
  import { randomUUID as randomUUID4 } from "node:crypto";
7345
- import * as fs13 from "node:fs/promises";
7346
- import * as path15 from "node:path";
6523
+ import * as fs12 from "node:fs/promises";
6524
+ import * as path14 from "node:path";
7347
6525
 
7348
6526
  // src/security/file-permissions.ts
7349
6527
  import {
@@ -7518,7 +6696,7 @@ var FileMemoryBackend = class {
7518
6696
  }
7519
6697
  async getMtime(file) {
7520
6698
  try {
7521
- const stat19 = await fs13.stat(file);
6699
+ const stat19 = await fs12.stat(file);
7522
6700
  return stat19.mtimeMs;
7523
6701
  } catch {
7524
6702
  return 0;
@@ -7570,7 +6748,7 @@ var FileMemoryBackend = class {
7570
6748
  const line = `- [${entry.ts}] ${id}${meta} ${entry.text.replace(/\n/g, " ")}
7571
6749
  `;
7572
6750
  await withFileLock(file, async () => {
7573
- const handle = await fs13.open(file, "a+", SECRET_FILE_MODE);
6751
+ const handle = await fs12.open(file, "a+", SECRET_FILE_MODE);
7574
6752
  try {
7575
6753
  const stat19 = await handle.stat();
7576
6754
  let prefix = "";
@@ -7594,7 +6772,7 @@ var FileMemoryBackend = class {
7594
6772
  return withFileLock(file, async () => {
7595
6773
  let existing;
7596
6774
  try {
7597
- existing = await fs13.readFile(file, "utf8");
6775
+ existing = await fs12.readFile(file, "utf8");
7598
6776
  } catch {
7599
6777
  return 0;
7600
6778
  }
@@ -7631,7 +6809,7 @@ var FileMemoryBackend = class {
7631
6809
  async readAll(scope, filePath) {
7632
6810
  const file = this.resolveFile(filePath, scope);
7633
6811
  try {
7634
- return await fs13.readFile(file, "utf8");
6812
+ return await fs12.readFile(file, "utf8");
7635
6813
  } catch {
7636
6814
  return "";
7637
6815
  }
@@ -7655,7 +6833,7 @@ var FileMemoryBackend = class {
7655
6833
  const file = this.resolveFile(filePath, scope);
7656
6834
  let existing;
7657
6835
  try {
7658
- existing = await fs13.readFile(file, "utf8");
6836
+ existing = await fs12.readFile(file, "utf8");
7659
6837
  } catch {
7660
6838
  return 0;
7661
6839
  }
@@ -7675,7 +6853,7 @@ var FileMemoryBackend = class {
7675
6853
  const next = lines.join("\n");
7676
6854
  const backup = `${file}.bak.${Date.now()}`;
7677
6855
  try {
7678
- await fs13.copyFile(file, backup);
6856
+ await fs12.copyFile(file, backup);
7679
6857
  await pruneConsolidateBackups(file);
7680
6858
  } catch {
7681
6859
  }
@@ -7689,14 +6867,14 @@ var FileMemoryBackend = class {
7689
6867
  }
7690
6868
  };
7691
6869
  async function pruneConsolidateBackups(file) {
7692
- const dir = path15.dirname(file);
7693
- const base = path15.basename(file);
6870
+ const dir = path14.dirname(file);
6871
+ const base = path14.basename(file);
7694
6872
  const prefix = `${base}.bak.`;
7695
- 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();
7696
6874
  await Promise.all(
7697
6875
  backups.slice(MAX_MEMORY_CONSOLIDATE_BACKUPS).map(async (name) => {
7698
6876
  try {
7699
- await fs13.unlink(path15.join(dir, name));
6877
+ await fs12.unlink(path14.join(dir, name));
7700
6878
  } catch {
7701
6879
  }
7702
6880
  })
@@ -7712,11 +6890,11 @@ function parseEntries(raw, scope = "project-memory") {
7712
6890
  }
7713
6891
 
7714
6892
  // src/storage/memory-consolidator.ts
7715
- import * as path17 from "node:path";
6893
+ import * as path16 from "node:path";
7716
6894
 
7717
6895
  // src/utils/instruction-file.ts
7718
6896
  import { readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
7719
- import * as path16 from "node:path";
6897
+ import * as path15 from "node:path";
7720
6898
  import { fileURLToPath as fileURLToPath2 } from "node:url";
7721
6899
  var textCache = /* @__PURE__ */ new Map();
7722
6900
  var rootCandidates;
@@ -7726,7 +6904,7 @@ function readBundledInstructionText(relativePath) {
7726
6904
  let resolved = "";
7727
6905
  for (const root of instructionRootCandidates()) {
7728
6906
  try {
7729
- resolved = readFileSync3(path16.join(root, relativePath), "utf8").trimEnd();
6907
+ resolved = readFileSync3(path15.join(root, relativePath), "utf8").trimEnd();
7730
6908
  break;
7731
6909
  } catch {
7732
6910
  }
@@ -7742,11 +6920,11 @@ function renderInstructionTemplate(template, values) {
7742
6920
  }
7743
6921
  function instructionRootCandidates() {
7744
6922
  if (rootCandidates !== void 0) return rootCandidates;
7745
- const here = path16.dirname(fileURLToPath2(import.meta.url));
6923
+ const here = path15.dirname(fileURLToPath2(import.meta.url));
7746
6924
  const candidates = [
7747
- path16.resolve(here, "../../instructions"),
7748
- path16.resolve(here, "../instructions"),
7749
- path16.resolve(here, "instructions")
6925
+ path15.resolve(here, "../../instructions"),
6926
+ path15.resolve(here, "../instructions"),
6927
+ path15.resolve(here, "instructions")
7750
6928
  ];
7751
6929
  rootCandidates = candidates.sort((a, b) => Number(!isDirectory(a)) - Number(!isDirectory(b)));
7752
6930
  return rootCandidates;
@@ -7816,8 +6994,8 @@ ${existingEntries.map((e) => `- [${e.ts.slice(0, 10)}] ${e.text}`).join("\n")}`
7816
6994
  });
7817
6995
  }
7818
6996
  function relativeEvidencePath(projectRoot, filePath) {
7819
- const relative8 = path17.relative(projectRoot, filePath);
7820
- 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("\\", "/");
7821
6999
  }
7822
7000
  function safeCommandEvidence(value) {
7823
7001
  if (typeof value !== "string") return void 0;
@@ -8027,7 +7205,7 @@ function buildSessionDigestText(finalText, iterations, factsAdded) {
8027
7205
  }
8028
7206
 
8029
7207
  // src/storage/memory-graph-backend.ts
8030
- import * as fs14 from "node:fs/promises";
7208
+ import * as fs13 from "node:fs/promises";
8031
7209
  var GraphMemoryBackend = class _GraphMemoryBackend {
8032
7210
  kind = "graph";
8033
7211
  file;
@@ -8195,7 +7373,7 @@ var GraphMemoryBackend = class _GraphMemoryBackend {
8195
7373
  this.loadedScope = scope;
8196
7374
  this.loaded = true;
8197
7375
  try {
8198
- await fs14.unlink(this.graphFile);
7376
+ await fs13.unlink(this.graphFile);
8199
7377
  } catch {
8200
7378
  }
8201
7379
  }
@@ -8235,7 +7413,7 @@ var GraphMemoryBackend = class _GraphMemoryBackend {
8235
7413
  async loadGraph(scope) {
8236
7414
  if (this.loaded && this.loadedScope === scope) return;
8237
7415
  try {
8238
- const raw = await fs14.readFile(this.graphFile, "utf8");
7416
+ const raw = await fs13.readFile(this.graphFile, "utf8");
8239
7417
  const data = JSON.parse(raw);
8240
7418
  this.nodes = new Map(data.nodes);
8241
7419
  this.edges = data.edges;
@@ -8257,10 +7435,10 @@ var GraphMemoryBackend = class _GraphMemoryBackend {
8257
7435
  edges: this.edges
8258
7436
  };
8259
7437
  const dir = this.graphFile.substring(0, this.graphFile.lastIndexOf("/"));
8260
- await fs14.mkdir(dir, { recursive: true });
7438
+ await fs13.mkdir(dir, { recursive: true });
8261
7439
  const tmp = `${this.graphFile}.tmp`;
8262
- await fs14.writeFile(tmp, JSON.stringify(data));
8263
- await fs14.rename(tmp, this.graphFile);
7440
+ await fs13.writeFile(tmp, JSON.stringify(data));
7441
+ await fs13.rename(tmp, this.graphFile);
8264
7442
  } catch {
8265
7443
  }
8266
7444
  }
@@ -8718,8 +7896,8 @@ ${cat}:`);
8718
7896
 
8719
7897
  // src/storage/prompt-store.ts
8720
7898
  import { createHash as createHash5 } from "node:crypto";
8721
- import * as fs15 from "node:fs/promises";
8722
- import * as path18 from "node:path";
7899
+ import * as fs14 from "node:fs/promises";
7900
+ import * as path17 from "node:path";
8723
7901
 
8724
7902
  // src/utils/slug.ts
8725
7903
  function slugify2(name, fallback = "prompt", maxLen = 64) {
@@ -8818,12 +7996,12 @@ var DefaultPromptStore = class {
8818
7996
  await ensureDir(this.dir);
8819
7997
  const entries = [];
8820
7998
  try {
8821
- const files = await fs15.readdir(this.dir);
7999
+ const files = await fs14.readdir(this.dir);
8822
8000
  for (const file of files) {
8823
8001
  if (!file.endsWith(".json")) continue;
8824
8002
  try {
8825
8003
  const raw = JSON.parse(
8826
- await fs15.readFile(path18.join(this.dir, file), "utf8")
8004
+ await fs14.readFile(path17.join(this.dir, file), "utf8")
8827
8005
  );
8828
8006
  const migrated = migratePromptEntry(raw.entry);
8829
8007
  if (migrated) entries.push(migrated);
@@ -8837,9 +8015,9 @@ var DefaultPromptStore = class {
8837
8015
  );
8838
8016
  }
8839
8017
  async get(id) {
8840
- const file = path18.join(this.dir, `${id}.json`);
8018
+ const file = path17.join(this.dir, `${id}.json`);
8841
8019
  try {
8842
- const raw = JSON.parse(await fs15.readFile(file, "utf8"));
8020
+ const raw = JSON.parse(await fs14.readFile(file, "utf8"));
8843
8021
  return migratePromptEntry(raw.entry);
8844
8022
  } catch {
8845
8023
  return null;
@@ -8847,14 +8025,14 @@ var DefaultPromptStore = class {
8847
8025
  }
8848
8026
  async save(entry) {
8849
8027
  await ensureDir(this.dir);
8850
- const file = path18.join(this.dir, `${entry.id}.json`);
8028
+ const file = path17.join(this.dir, `${entry.id}.json`);
8851
8029
  const raw = { version: SCHEMA_VERSION, entry };
8852
8030
  await atomicWrite(file, JSON.stringify(raw, null, 2));
8853
8031
  }
8854
8032
  async delete(id) {
8855
- const file = path18.join(this.dir, `${id}.json`);
8033
+ const file = path17.join(this.dir, `${id}.json`);
8856
8034
  try {
8857
- await fs15.unlink(file);
8035
+ await fs14.unlink(file);
8858
8036
  return true;
8859
8037
  } catch {
8860
8038
  return false;
@@ -8893,7 +8071,7 @@ var DefaultPromptStore = class {
8893
8071
  };
8894
8072
 
8895
8073
  // src/storage/prompt-usage-store.ts
8896
- import * as fs16 from "node:fs/promises";
8074
+ import * as fs15 from "node:fs/promises";
8897
8075
  var PromptUsageStore = class {
8898
8076
  constructor(file) {
8899
8077
  this.file = file;
@@ -8913,7 +8091,7 @@ var PromptUsageStore = class {
8913
8091
  return this.cachedUsage;
8914
8092
  }
8915
8093
  try {
8916
- const raw = JSON.parse(await fs16.readFile(this.file, "utf8"));
8094
+ const raw = JSON.parse(await fs15.readFile(this.file, "utf8"));
8917
8095
  if (raw && typeof raw === "object" && raw.usage && typeof raw.usage === "object") {
8918
8096
  this.cachedUsage = raw.usage;
8919
8097
  this.cachedSignature = signature;
@@ -8926,8 +8104,8 @@ var PromptUsageStore = class {
8926
8104
  return this.cachedUsage;
8927
8105
  }
8928
8106
  async record(slug, at = (/* @__PURE__ */ new Date()).toISOString()) {
8929
- return await new Promise((resolve14, reject) => {
8930
- this.pendingRecords.push({ slug, at, resolve: resolve14, reject });
8107
+ return await new Promise((resolve13, reject) => {
8108
+ this.pendingRecords.push({ slug, at, resolve: resolve13, reject });
8931
8109
  this.scheduleDrain();
8932
8110
  });
8933
8111
  }
@@ -8993,7 +8171,7 @@ var PromptUsageStore = class {
8993
8171
  }
8994
8172
  async fileSignature() {
8995
8173
  try {
8996
- const stat19 = await fs16.stat(this.file);
8174
+ const stat19 = await fs15.stat(this.file);
8997
8175
  return { size: stat19.size, mtimeMs: stat19.mtimeMs, ctimeMs: stat19.ctimeMs };
8998
8176
  } catch {
8999
8177
  return null;
@@ -9010,12 +8188,12 @@ function cloneUsage(usage) {
9010
8188
 
9011
8189
  // src/storage/provider-config-watcher.ts
9012
8190
  import * as syncFs from "node:fs";
9013
- import * as fs17 from "node:fs/promises";
9014
- import * as path19 from "node:path";
8191
+ import * as fs16 from "node:fs/promises";
8192
+ import * as path18 from "node:path";
9015
8193
  async function readProviderSnapshot(configPath, vault, warn) {
9016
8194
  let raw;
9017
8195
  try {
9018
- raw = await fs17.readFile(configPath, "utf8");
8196
+ raw = await fs16.readFile(configPath, "utf8");
9019
8197
  } catch (err) {
9020
8198
  if (err.code !== "ENOENT") {
9021
8199
  warn?.(`Could not read ${configPath}: ${err.message}`);
@@ -9087,7 +8265,7 @@ function serializeSnapshot(s) {
9087
8265
  function watchProviderConfig(configPath, vault, onChange, opts = {}) {
9088
8266
  const debounceMs = opts.debounceMs ?? 200;
9089
8267
  const warn = opts.warn;
9090
- const base = path19.basename(configPath);
8268
+ const base = path18.basename(configPath);
9091
8269
  let timer;
9092
8270
  let closed = false;
9093
8271
  let lastSerialized;
@@ -9098,7 +8276,7 @@ function watchProviderConfig(configPath, vault, onChange, opts = {}) {
9098
8276
  });
9099
8277
  let watcher;
9100
8278
  try {
9101
- watcher = syncFs.watch(path19.dirname(configPath), { recursive: false });
8279
+ watcher = syncFs.watch(path18.dirname(configPath), { recursive: false });
9102
8280
  } catch (err) {
9103
8281
  warn?.(`Provider config watcher could not start: ${err.message}`);
9104
8282
  return { close: () => {
@@ -9142,7 +8320,7 @@ function watchProviderConfig(configPath, vault, onChange, opts = {}) {
9142
8320
 
9143
8321
  // src/storage/queue-store.ts
9144
8322
  import * as fsp6 from "node:fs/promises";
9145
- import * as path20 from "node:path";
8323
+ import * as path19 from "node:path";
9146
8324
  var QUEUE_MAX_ITEMS = 100;
9147
8325
  var QUEUE_MAX_BYTES = 16 * 1024 * 1024;
9148
8326
  var QUEUE_MAX_ITEM_BYTES = 8 * 1024 * 1024;
@@ -9175,7 +8353,7 @@ var QueueStore = class {
9175
8353
  traceId;
9176
8354
  logger;
9177
8355
  constructor(opts) {
9178
- this.file = path20.join(opts.dir, "queue.json");
8356
+ this.file = path19.join(opts.dir, "queue.json");
9179
8357
  this.events = opts.events;
9180
8358
  this.traceId = opts.traceId;
9181
8359
  this.logger = opts.logger;
@@ -9391,7 +8569,7 @@ function isPersistedQueueItem(v) {
9391
8569
  // src/storage/recovery-lock.ts
9392
8570
  import * as fsp7 from "node:fs/promises";
9393
8571
  import * as os3 from "node:os";
9394
- import * as path21 from "node:path";
8572
+ import * as path20 from "node:path";
9395
8573
 
9396
8574
  // src/storage/session-workspace-checkpoints.ts
9397
8575
  function requiredCheckpointCas(checkpointCas, operation) {
@@ -9476,7 +8654,7 @@ var RecoveryLock = class {
9476
8654
  sessionStore;
9477
8655
  probe;
9478
8656
  constructor(opts) {
9479
- this.file = path21.join(opts.dir, LOCK_FILE);
8657
+ this.file = path20.join(opts.dir, LOCK_FILE);
9480
8658
  this.pid = opts.pid ?? process.pid;
9481
8659
  this.hostname = opts.hostname ?? os3.hostname();
9482
8660
  this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
@@ -9550,7 +8728,7 @@ var RecoveryLock = class {
9550
8728
  * null return before calling this.
9551
8729
  */
9552
8730
  async write(sessionId) {
9553
- await ensureDir(path21.dirname(this.file));
8731
+ await ensureDir(path20.dirname(this.file));
9554
8732
  const lock = {
9555
8733
  v: 1,
9556
8734
  sessionId,
@@ -9609,30 +8787,30 @@ var defaultIsPidAlive = isPidAlive;
9609
8787
 
9610
8788
  // src/storage/orphan-lock-cleaner.ts
9611
8789
  import * as fsp8 from "node:fs/promises";
9612
- import * as path22 from "node:path";
8790
+ import * as path21 from "node:path";
9613
8791
  async function cleanOrphanLocks(projectRoot, options) {
9614
8792
  const result = {
9615
8793
  cleanedGitLocks: [],
9616
8794
  cleanedWorktrees: [],
9617
8795
  errors: []
9618
8796
  };
9619
- const pidAlive3 = options?.isPidAlive ?? isPidAlive;
8797
+ const pidAlive2 = options?.isPidAlive ?? isPidAlive;
9620
8798
  const maxAgeMs = options?.maxLockAgeMs ?? 10 * 60 * 1e3;
9621
8799
  const now = Date.now();
9622
- const worktreesDir = path22.join(projectRoot, ".wrongstack", "worktrees");
8800
+ const worktreesDir = path21.join(projectRoot, ".wrongstack", "worktrees");
9623
8801
  try {
9624
8802
  const entries = await fsp8.readdir(worktreesDir, { withFileTypes: true });
9625
8803
  for (const entry of entries) {
9626
8804
  if (!entry.isDirectory()) continue;
9627
- const wtPath = path22.join(worktreesDir, entry.name);
9628
- const lockPath = path22.join(wtPath, ".lock");
8805
+ const wtPath = path21.join(worktreesDir, entry.name);
8806
+ const lockPath = path21.join(wtPath, ".lock");
9629
8807
  try {
9630
8808
  const lockStat = await fsp8.stat(lockPath).catch(() => null);
9631
8809
  if (lockStat) {
9632
8810
  const raw = await fsp8.readFile(lockPath, "utf8").catch(() => "");
9633
8811
  const pid = parseInt(raw.trim(), 10);
9634
8812
  if (Number.isInteger(pid) && pid > 0) {
9635
- if (!pidAlive3(pid)) {
8813
+ if (!pidAlive2(pid)) {
9636
8814
  await fsp8.unlink(lockPath).catch(() => void 0);
9637
8815
  result.cleanedWorktrees.push(wtPath);
9638
8816
  }
@@ -9654,8 +8832,8 @@ async function cleanOrphanLocks(projectRoot, options) {
9654
8832
  }
9655
8833
 
9656
8834
  // src/storage/replay-log-store.ts
9657
- import * as fs18 from "node:fs/promises";
9658
- import * as path23 from "node:path";
8835
+ import * as fs17 from "node:fs/promises";
8836
+ import * as path22 from "node:path";
9659
8837
 
9660
8838
  // src/replay/hash.ts
9661
8839
  import { createHash as createHash6 } from "node:crypto";
@@ -9749,12 +8927,12 @@ var ReplayLogStore = class _ReplayLogStore {
9749
8927
  const line = JSON.stringify(entry) + "\n";
9750
8928
  let offset2 = 0;
9751
8929
  try {
9752
- const stat19 = await fs18.stat(fp);
8930
+ const stat19 = await fs17.stat(fp);
9753
8931
  offset2 = stat19.size;
9754
8932
  } catch (err) {
9755
8933
  if (err.code !== "ENOENT") throw err;
9756
8934
  }
9757
- await fs18.appendFile(fp, line, { encoding: "utf8", mode: SECRET_FILE_MODE });
8935
+ await fs17.appendFile(fp, line, { encoding: "utf8", mode: SECRET_FILE_MODE });
9758
8936
  cache.set(hash, { entry, offset: offset2, length: Buffer.byteLength(line, "utf8") });
9759
8937
  this.diskCount.set(input.sessionId, currentCount + 1);
9760
8938
  this.events?.emit("storage.write", {
@@ -9882,7 +9060,7 @@ var ReplayLogStore = class _ReplayLogStore {
9882
9060
  const scan = async (dir, prefix, depth) => {
9883
9061
  let entries;
9884
9062
  try {
9885
- entries = await fs18.readdir(dir, { withFileTypes: true });
9063
+ entries = await fs17.readdir(dir, { withFileTypes: true });
9886
9064
  } catch (err) {
9887
9065
  if (depth === 0 && err.code !== "ENOENT") {
9888
9066
  console.warn(
@@ -9900,13 +9078,13 @@ var ReplayLogStore = class _ReplayLogStore {
9900
9078
  for (const entry of entries) {
9901
9079
  if (entry.name.startsWith(".")) continue;
9902
9080
  if (entry.isDirectory()) {
9903
- 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);
9904
9082
  continue;
9905
9083
  }
9906
9084
  if (!entry.isFile() || !entry.name.endsWith(".replay.jsonl")) continue;
9907
9085
  const base = entry.name.slice(0, -".replay.jsonl".length);
9908
9086
  const sessionId = prefix ? `${prefix}/${base}` : base;
9909
- const fp = path23.join(dir, entry.name);
9087
+ const fp = path22.join(dir, entry.name);
9910
9088
  out.push({
9911
9089
  sessionId,
9912
9090
  entryCount: await this.countEntries(fp),
@@ -9922,7 +9100,7 @@ var ReplayLogStore = class _ReplayLogStore {
9922
9100
  return sessionScopedPath(this.dir, sessionId, ".replay.jsonl");
9923
9101
  }
9924
9102
  async countEntries(filePath) {
9925
- const handle = await fs18.open(filePath, "r");
9103
+ const handle = await fs17.open(filePath, "r");
9926
9104
  const buffer = Buffer.allocUnsafe(64 * 1024);
9927
9105
  let count = 0;
9928
9106
  let hasNonWhitespace2 = false;
@@ -9963,7 +9141,7 @@ var ReplayLogStore = class _ReplayLogStore {
9963
9141
  async readAll(sessionId) {
9964
9142
  const fp = this.filePath(sessionId);
9965
9143
  try {
9966
- const raw = await fs18.readFile(fp, "utf8");
9144
+ const raw = await fs17.readFile(fp, "utf8");
9967
9145
  const out = [];
9968
9146
  for (const line of raw.split("\n")) {
9969
9147
  if (!line.trim()) continue;
@@ -10003,7 +9181,7 @@ var ReplayLogStore = class _ReplayLogStore {
10003
9181
  const fp = this.filePath(sessionId);
10004
9182
  cache = /* @__PURE__ */ new Map();
10005
9183
  try {
10006
- const handle = await fs18.open(fp, "r");
9184
+ const handle = await fs17.open(fp, "r");
10007
9185
  const CHUNK = 64 * 1024;
10008
9186
  const buffer = Buffer.alloc(CHUNK);
10009
9187
  let leftover = "";
@@ -10089,7 +9267,7 @@ var ReplayLogStore = class _ReplayLogStore {
10089
9267
  const fp = this.filePath(sessionId);
10090
9268
  let handle;
10091
9269
  try {
10092
- handle = await fs18.open(fp, "r");
9270
+ handle = await fs17.open(fp, "r");
10093
9271
  } catch {
10094
9272
  return null;
10095
9273
  }
@@ -10219,7 +9397,7 @@ var SessionAnalyzer = class {
10219
9397
  import { spawn as spawn2 } from "node:child_process";
10220
9398
  import { createHash as createHash7, randomUUID as randomUUID6 } from "node:crypto";
10221
9399
  import * as fsp9 from "node:fs/promises";
10222
- import * as path24 from "node:path";
9400
+ import * as path23 from "node:path";
10223
9401
 
10224
9402
  // src/utils/child-env.ts
10225
9403
  var ALLOWED_KEYS = /* @__PURE__ */ new Set([
@@ -10386,13 +9564,13 @@ function sha256(content) {
10386
9564
  return createHash7("sha256").update(content).digest("hex");
10387
9565
  }
10388
9566
  function isInside(root, target) {
10389
- const relative8 = path24.relative(root, target);
10390
- return relative8 === "" || !relative8.startsWith("..") && !path24.isAbsolute(relative8);
9567
+ const relative8 = path23.relative(root, target);
9568
+ return relative8 === "" || !relative8.startsWith("..") && !path23.isAbsolute(relative8);
10391
9569
  }
10392
9570
  function normalizeRelative(input) {
10393
- if (!input || path24.isAbsolute(input)) return null;
9571
+ if (!input || path23.isAbsolute(input)) return null;
10394
9572
  const normalized = input.replace(/\\/g, "/").replace(/^\.\//, "");
10395
- const resolved = path24.posix.normalize(normalized);
9573
+ const resolved = path23.posix.normalize(normalized);
10396
9574
  if (!resolved || resolved === "." || resolved === ".." || resolved.startsWith("../")) return null;
10397
9575
  return resolved;
10398
9576
  }
@@ -10407,8 +9585,8 @@ var SessionCheckpointCas = class {
10407
9585
  projectRoot;
10408
9586
  runGit;
10409
9587
  constructor(opts) {
10410
- this.rootDir = path24.resolve(opts.rootDir);
10411
- this.projectRoot = path24.resolve(opts.projectRoot);
9588
+ this.rootDir = path23.resolve(opts.rootDir);
9589
+ this.projectRoot = path23.resolve(opts.projectRoot);
10412
9590
  this.runGit = opts.runGit ?? defaultRunGit;
10413
9591
  }
10414
9592
  async capture(_sessionId, _promptIndex) {
@@ -10433,7 +9611,7 @@ var SessionCheckpointCas = class {
10433
9611
  relativePaths,
10434
9612
  CAPTURE_CONCURRENCY,
10435
9613
  async (relative8) => {
10436
- const absolute = path24.resolve(this.projectRoot, ...relative8.split("/"));
9614
+ const absolute = path23.resolve(this.projectRoot, ...relative8.split("/"));
10437
9615
  if (!isInside(this.projectRoot, absolute)) {
10438
9616
  unresolved.push({ path: relative8, reason: "path escapes project root" });
10439
9617
  return null;
@@ -10442,8 +9620,8 @@ var SessionCheckpointCas = class {
10442
9620
  const stat19 = await fsp9.lstat(absolute);
10443
9621
  if (stat19.isSymbolicLink()) {
10444
9622
  const linkTarget = await fsp9.readlink(absolute);
10445
- const resolvedLink = path24.resolve(path24.dirname(absolute), linkTarget);
10446
- 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)) {
10447
9625
  unresolved.push({
10448
9626
  path: relative8,
10449
9627
  reason: "symlink target escapes project root"
@@ -10505,7 +9683,7 @@ var SessionCheckpointCas = class {
10505
9683
  };
10506
9684
  }
10507
9685
  async materialize(checkpoint, targetRoot) {
10508
- const target = path24.resolve(targetRoot);
9686
+ const target = path23.resolve(targetRoot);
10509
9687
  if (target === this.projectRoot) {
10510
9688
  throw new Error("Refusing to materialize a workspace checkpoint over the parent project root");
10511
9689
  }
@@ -10545,10 +9723,10 @@ var SessionCheckpointCas = class {
10545
9723
  try {
10546
9724
  const output = await this.safeOutputPath(target, realTarget, entry.path);
10547
9725
  if (entry.state === "symlink") {
10548
- if (path24.isAbsolute(entry.linkTarget)) {
9726
+ if (path23.isAbsolute(entry.linkTarget)) {
10549
9727
  throw new Error("absolute symlink target refused");
10550
9728
  }
10551
- const resolvedLink = path24.resolve(path24.dirname(output), entry.linkTarget);
9729
+ const resolvedLink = path23.resolve(path23.dirname(output), entry.linkTarget);
10552
9730
  if (!isInside(target, resolvedLink)) throw new Error("symlink target escapes checkpoint root");
10553
9731
  }
10554
9732
  prepared.push({
@@ -10576,7 +9754,7 @@ var SessionCheckpointCas = class {
10576
9754
  await fsp9.unlink(output).catch((err) => {
10577
9755
  if (err.code !== "ENOENT") throw err;
10578
9756
  });
10579
- await fsp9.mkdir(path24.dirname(output), { recursive: true });
9757
+ await fsp9.mkdir(path23.dirname(output), { recursive: true });
10580
9758
  await fsp9.symlink(entry.linkTarget, output);
10581
9759
  writtenFiles.push(entry.path);
10582
9760
  continue;
@@ -10593,15 +9771,15 @@ var SessionCheckpointCas = class {
10593
9771
  }
10594
9772
  objectPath(hash) {
10595
9773
  if (!HASH_RE.test(hash)) throw new Error(`Invalid CAS object hash: ${hash}`);
10596
- 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));
10597
9775
  }
10598
9776
  manifestPath(hash) {
10599
9777
  if (!HASH_RE.test(hash)) throw new Error(`Invalid checkpoint manifest hash: ${hash}`);
10600
- return path24.join(this.rootDir, "manifests", `${hash}.json`);
9778
+ return path23.join(this.rootDir, "manifests", `${hash}.json`);
10601
9779
  }
10602
9780
  async putBlob(hash, content) {
10603
9781
  const target = this.objectPath(hash);
10604
- await fsp9.mkdir(path24.dirname(target), { recursive: true });
9782
+ await fsp9.mkdir(path23.dirname(target), { recursive: true });
10605
9783
  try {
10606
9784
  const existing = await fsp9.readFile(target);
10607
9785
  if (sha256(existing) !== hash) throw new Error(`Corrupt CAS object collision: ${hash}`);
@@ -10609,9 +9787,9 @@ var SessionCheckpointCas = class {
10609
9787
  } catch (err) {
10610
9788
  if (err.code !== "ENOENT") throw err;
10611
9789
  }
10612
- const temp = path24.join(
10613
- path24.dirname(target),
10614
- `.${path24.basename(target)}.${process.pid}.${randomUUID6()}.tmp`
9790
+ const temp = path23.join(
9791
+ path23.dirname(target),
9792
+ `.${path23.basename(target)}.${process.pid}.${randomUUID6()}.tmp`
10615
9793
  );
10616
9794
  let handle;
10617
9795
  try {
@@ -10672,7 +9850,7 @@ var SessionCheckpointCas = class {
10672
9850
  async safeOutputPath(target, realTarget, relative8) {
10673
9851
  const normalized = normalizeRelative(relative8);
10674
9852
  if (!normalized) throw new Error("invalid relative path");
10675
- const output = path24.resolve(target, ...normalized.split("/"));
9853
+ const output = path23.resolve(target, ...normalized.split("/"));
10676
9854
  if (!isInside(target, output)) throw new Error("path escapes checkpoint target");
10677
9855
  let probe = output;
10678
9856
  for (; ; ) {
@@ -10682,7 +9860,7 @@ var SessionCheckpointCas = class {
10682
9860
  return output;
10683
9861
  } catch (err) {
10684
9862
  if (err.code !== "ENOENT") throw err;
10685
- const parent = path24.dirname(probe);
9863
+ const parent = path23.dirname(probe);
10686
9864
  if (parent === probe) throw err;
10687
9865
  probe = parent;
10688
9866
  }
@@ -10690,7 +9868,7 @@ var SessionCheckpointCas = class {
10690
9868
  }
10691
9869
  };
10692
9870
  function defaultRunGit(args, cwd) {
10693
- return new Promise((resolve14) => {
9871
+ return new Promise((resolve13) => {
10694
9872
  const stdoutChunks = [];
10695
9873
  const stderrChunks = [];
10696
9874
  let stdoutBytes = 0;
@@ -10733,8 +9911,8 @@ function defaultRunGit(args, cwd) {
10733
9911
  stdoutTruncated,
10734
9912
  stderrTruncated
10735
9913
  });
10736
- child.on("error", (err) => resolve14(result(1, err.message)));
10737
- 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)));
10738
9916
  });
10739
9917
  }
10740
9918
 
@@ -10887,13 +10065,13 @@ function generateSessionId(startedAt, _model) {
10887
10065
  }
10888
10066
 
10889
10067
  // src/storage/session-id-resolver.ts
10890
- import * as path25 from "node:path";
10068
+ import * as path24 from "node:path";
10891
10069
  function resolveSessionId(query, candidateIds) {
10892
10070
  const normalized = query.trim();
10893
10071
  if (!normalized) return { status: "missing", query: normalized };
10894
10072
  const uniqueIds = [...new Set(candidateIds)];
10895
10073
  if (uniqueIds.includes(normalized)) return { status: "resolved", id: normalized };
10896
- const leaf = (id) => path25.posix.basename(id.replace(/\\/g, "/"));
10074
+ const leaf = (id) => path24.posix.basename(id.replace(/\\/g, "/"));
10897
10075
  const exactLeafMatches = uniqueIds.filter((id) => leaf(id) === normalized);
10898
10076
  if (exactLeafMatches.length === 1) {
10899
10077
  return { status: "resolved", id: exactLeafMatches[0] };
@@ -10945,127 +10123,11 @@ function scrubPersistedSessionSummary(summary2, scrubber) {
10945
10123
  }
10946
10124
 
10947
10125
  // src/utils/regex-guard.ts
10948
- var MAX_PATTERN_LEN = 256;
10949
- var DANGEROUS_PATTERNS = [
10950
- // (a+)+, (.*)+, etc — nested quantifier on a group with internal quantifier
10951
- /(\([^)]*[+*][^)]*\))[+*]/,
10952
- /(\(\?:[^)]*[+*][^)]*\))[+*]/,
10953
- // Adjacent quantifiers: a++ a*+
10954
- /[+*]{2,}/,
10955
- // Quantifier on alternation with length 2+
10956
- /\([^|)]+\|[^)]+\)[+*][+*]/,
10957
- // Greedy quantifier inside lookahead/lookbehind — (?!.*a+)
10958
- /[([][^)\]]*[+*][^)\]]*[)\]][^)]*\?\??/
10959
- ];
10960
- function hasAmbiguousQuantifiedAlternation(pattern) {
10961
- for (let i = 0; i < pattern.length; i++) {
10962
- if (pattern[i] !== "(") continue;
10963
- if (i > 0 && pattern[i - 1] === "\\") continue;
10964
- let depth = 0;
10965
- let inClass = false;
10966
- let j = i;
10967
- for (; j < pattern.length; j++) {
10968
- const ch = pattern[j];
10969
- if (ch === "\\") {
10970
- j++;
10971
- continue;
10972
- }
10973
- if (inClass) {
10974
- if (ch === "]") inClass = false;
10975
- continue;
10976
- }
10977
- if (ch === "[") {
10978
- inClass = true;
10979
- continue;
10980
- }
10981
- if (ch === "(") depth++;
10982
- else if (ch === ")") {
10983
- depth--;
10984
- if (depth === 0) break;
10985
- }
10986
- }
10987
- if (j >= pattern.length) return false;
10988
- const next = pattern[j + 1];
10989
- if (next !== "+" && next !== "*" && next !== "{") continue;
10990
- let inner = pattern.slice(i + 1, j);
10991
- inner = inner.replace(/^\?(?::|<?[=!])/u, "");
10992
- const branches = [];
10993
- let current = "";
10994
- let d = 0;
10995
- let cls = false;
10996
- for (let k = 0; k < inner.length; k++) {
10997
- const ch = inner[k];
10998
- if (ch === "\\") {
10999
- current += ch + (inner[k + 1] ?? "");
11000
- k++;
11001
- continue;
11002
- }
11003
- if (cls) {
11004
- if (ch === "]") cls = false;
11005
- current += ch;
11006
- continue;
11007
- }
11008
- if (ch === "[") {
11009
- cls = true;
11010
- current += ch;
11011
- continue;
11012
- }
11013
- if (ch === "(") d++;
11014
- if (ch === ")") d--;
11015
- if (ch === "|" && d === 0) {
11016
- branches.push(current);
11017
- current = "";
11018
- continue;
11019
- }
11020
- current += ch;
11021
- }
11022
- branches.push(current);
11023
- if (branches.length < 2) continue;
11024
- for (let a = 0; a < branches.length; a++) {
11025
- for (let b = a + 1; b < branches.length; b++) {
11026
- const x = branches[a];
11027
- const y = branches[b];
11028
- if (x === "" || y === "") return true;
11029
- if (x === y || x.startsWith(y) || y.startsWith(x)) return true;
11030
- }
11031
- }
11032
- }
11033
- return false;
11034
- }
11035
- function compileUserRegex(pattern, flags) {
11036
- if (typeof pattern !== "string") {
11037
- return { ok: false, reason: "pattern must be a string" };
11038
- }
11039
- if (pattern.length === 0) {
11040
- return { ok: false, reason: "pattern is empty" };
11041
- }
11042
- if (pattern.length > MAX_PATTERN_LEN) {
11043
- return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
11044
- }
11045
- for (const rx of DANGEROUS_PATTERNS) {
11046
- if (rx.test(pattern)) {
11047
- return {
11048
- ok: false,
11049
- reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
11050
- };
11051
- }
11052
- }
11053
- if (hasAmbiguousQuantifiedAlternation(pattern)) {
11054
- return {
11055
- ok: false,
11056
- reason: "pattern quantifies an alternation with overlapping branches \u2014 rewrite so no two branches can match the same text"
11057
- };
11058
- }
11059
- try {
11060
- return { ok: true, regex: new RegExp(pattern, flags) };
11061
- } catch (err) {
11062
- return {
11063
- ok: false,
11064
- reason: err instanceof Error ? err.message : "invalid regex"
11065
- };
11066
- }
11067
- }
11068
- var MAX_SUBJECT_LEN = 64 * 1024;
10126
+ import {
10127
+ capSubject,
10128
+ compileUserRegex,
10129
+ MAX_SUBJECT_LEN
10130
+ } from "@wrongstack/primitives";
11069
10131
 
11070
10132
  // src/storage/session-reader.ts
11071
10133
  var DefaultSessionReader = class {
@@ -11421,13 +10483,13 @@ function renderPlainText(meta, events) {
11421
10483
 
11422
10484
  // src/storage/session-recovery.ts
11423
10485
  import { createReadStream } from "node:fs";
11424
- import * as fs19 from "node:fs/promises";
11425
- import * as path27 from "node:path";
10486
+ import * as fs18 from "node:fs/promises";
10487
+ import * as path26 from "node:path";
11426
10488
  import { createInterface } from "node:readline";
11427
10489
 
11428
10490
  // src/storage/session-store/directory-session-files.ts
11429
10491
  import * as fsp10 from "node:fs/promises";
11430
- import * as path26 from "node:path";
10492
+ import * as path25 from "node:path";
11431
10493
 
11432
10494
  // src/storage/session-store/directory-scan.ts
11433
10495
  function shouldSkipSessionDirectoryEntry(name) {
@@ -11456,12 +10518,12 @@ async function collectSessionFiles(dir, prefix = "", depth = 0) {
11456
10518
  if (entry.isDirectory()) {
11457
10519
  dirEntries.push(entry);
11458
10520
  } else if (entry.isFile() && isSessionTranscriptFileName(entry.name)) {
11459
- 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) });
11460
10522
  }
11461
10523
  }
11462
10524
  const childFileArrays = await Promise.all(
11463
10525
  dirEntries.map(
11464
- (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)
11465
10527
  )
11466
10528
  );
11467
10529
  return [...childFileArrays.flat(), ...files];
@@ -11485,7 +10547,7 @@ async function collectSessionIds(dir, prefix = "", depth = 0) {
11485
10547
  }
11486
10548
  const childIdArrays = await Promise.all(
11487
10549
  dirEntries.map(
11488
- (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)
11489
10551
  )
11490
10552
  );
11491
10553
  return [...childIdArrays.flat(), ...fileIds];
@@ -11495,11 +10557,14 @@ async function collectSessionIds(dir, prefix = "", depth = 0) {
11495
10557
  function extractInterruptedTools(plan) {
11496
10558
  const tools = [];
11497
10559
  const openCalls = /* @__PURE__ */ new Map();
10560
+ let anonymousSeq = 0;
11498
10561
  for (const ev of plan.pendingEvents) {
11499
10562
  if ((ev.type === "tool_use" || ev.type === "tool_call_start") && typeof ev.name === "string") {
11500
10563
  const toolName = ev.name;
11501
- const callId = ev.id ?? toolName;
10564
+ const rawId = ev.id;
10565
+ const callId = rawId ?? toolName;
11502
10566
  openCalls.set(callId, {
10567
+ id: rawId,
11503
10568
  name: toolName,
11504
10569
  args: ev.input ?? ev.args,
11505
10570
  ts: ev.ts
@@ -11510,8 +10575,10 @@ function extractInterruptedTools(plan) {
11510
10575
  } else if (ev.type === "llm_response" && Array.isArray(ev.content)) {
11511
10576
  for (const block of ev.content) {
11512
10577
  if (block && block.type === "tool_use" && typeof block.name === "string") {
11513
- const callId = block.id ?? block.name;
10578
+ const rawId = block.id;
10579
+ const callId = rawId ?? `${block.name}#${++anonymousSeq}`;
11514
10580
  openCalls.set(callId, {
10581
+ id: rawId,
11515
10582
  name: block.name,
11516
10583
  args: block.input,
11517
10584
  ts: ev.ts
@@ -11523,8 +10590,10 @@ function extractInterruptedTools(plan) {
11523
10590
  if (Array.isArray(msg.content)) {
11524
10591
  for (const block of msg.content) {
11525
10592
  if (block && block.type === "tool_use" && typeof block.name === "string") {
11526
- const callId = block.id ?? block.name;
10593
+ const rawId = block.id;
10594
+ const callId = rawId ?? `${block.name}#${++anonymousSeq}`;
11527
10595
  openCalls.set(callId, {
10596
+ id: rawId,
11528
10597
  name: block.name,
11529
10598
  args: block.input,
11530
10599
  ts: ev.ts
@@ -11547,6 +10616,7 @@ function extractInterruptedTools(plan) {
11547
10616
  }
11548
10617
  }
11549
10618
  tools.push({
10619
+ id: call.id,
11550
10620
  name: call.name,
11551
10621
  argsSummary,
11552
10622
  ts: call.ts
@@ -11561,6 +10631,47 @@ var SessionRecovery = class _SessionRecovery {
11561
10631
  dir;
11562
10632
  static MAX_PENDING_EVENTS = 1e4;
11563
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
+ }
11564
10675
  /**
11565
10676
  * Scan a session log and return a `StaleSession` if and only if the newest
11566
10677
  * lifecycle boundary is an `in_flight_start` without a later
@@ -11591,9 +10702,8 @@ var SessionRecovery = class _SessionRecovery {
11591
10702
  const fp = this.filePath(sessionId);
11592
10703
  let stat19;
11593
10704
  try {
11594
- stat19 = await fs19.stat(fp);
11595
- } catch (err) {
11596
- if (err.code === "ENOENT") return null;
10705
+ stat19 = await fs18.stat(fp);
10706
+ } catch {
11597
10707
  return null;
11598
10708
  }
11599
10709
  if (stat19.size === 0) return null;
@@ -11653,7 +10763,7 @@ var SessionRecovery = class _SessionRecovery {
11653
10763
  if (bytes <= _SessionRecovery.MAX_PENDING_BYTES) {
11654
10764
  while (pendingEvents.length >= _SessionRecovery.MAX_PENDING_EVENTS || pendingBytes + bytes > _SessionRecovery.MAX_PENDING_BYTES) {
11655
10765
  pendingEvents.shift();
11656
- pendingBytes = Math.max(0, pendingBytes - (pendingSizes.shift() ?? 0));
10766
+ pendingBytes = Math.max(0, pendingBytes - pendingSizes.shift());
11657
10767
  }
11658
10768
  pendingEvents.push(event);
11659
10769
  pendingSizes.push(bytes);
@@ -11662,8 +10772,7 @@ var SessionRecovery = class _SessionRecovery {
11662
10772
  }
11663
10773
  if (isLifecycleBoundary(event)) latestBoundary = event;
11664
10774
  }
11665
- } catch (err) {
11666
- if (err.code === "ENOENT") return null;
10775
+ } catch {
11667
10776
  return null;
11668
10777
  } finally {
11669
10778
  lines.close();
@@ -11691,7 +10800,7 @@ var SessionRecovery = class _SessionRecovery {
11691
10800
  const collect = async (dir, prefix, depth) => {
11692
10801
  let entries;
11693
10802
  try {
11694
- entries = await fs19.readdir(dir, { withFileTypes: true });
10803
+ entries = await fs18.readdir(dir, { withFileTypes: true });
11695
10804
  } catch {
11696
10805
  return;
11697
10806
  }
@@ -11701,7 +10810,7 @@ var SessionRecovery = class _SessionRecovery {
11701
10810
  continue;
11702
10811
  if (entry.isDirectory()) {
11703
10812
  if (depth === 0) {
11704
- await collect(path27.join(dir, entry.name), entry.name, depth + 1);
10813
+ await collect(path26.join(dir, entry.name), entry.name, depth + 1);
11705
10814
  }
11706
10815
  continue;
11707
10816
  }
@@ -11739,7 +10848,7 @@ function hasNonWhitespace(line) {
11739
10848
  }
11740
10849
  async function scanLatestLifecycleBoundary(filePath, size) {
11741
10850
  const CHUNK_SIZE2 = 64 * 1024;
11742
- const handle = await fs19.open(filePath, "r");
10851
+ const handle = await fs18.open(filePath, "r");
11743
10852
  let position = size;
11744
10853
  let laterLineFragment = Buffer.alloc(0);
11745
10854
  let latestBoundary = null;
@@ -11779,7 +10888,7 @@ async function scanLatestLifecycleBoundary(filePath, size) {
11779
10888
  // src/storage/session-store.ts
11780
10889
  import { randomUUID as randomUUID7 } from "node:crypto";
11781
10890
  import * as fsp26 from "node:fs/promises";
11782
- import * as path34 from "node:path";
10891
+ import * as path33 from "node:path";
11783
10892
 
11784
10893
  // src/utils/message-invariants.ts
11785
10894
  function repairToolUseAdjacency(messages) {
@@ -11889,7 +10998,7 @@ function isEmptyMessage(msg) {
11889
10998
 
11890
10999
  // src/storage/file-session-writer.ts
11891
11000
  import * as fsp14 from "node:fs/promises";
11892
- import * as path28 from "node:path";
11001
+ import * as path27 from "node:path";
11893
11002
 
11894
11003
  // src/storage/session-summary-tracker.ts
11895
11004
  import { createReadStream as createReadStream2 } from "node:fs";
@@ -11945,6 +11054,29 @@ var SessionSummaryTracker = class {
11945
11054
  get currentSummary() {
11946
11055
  return this.summary;
11947
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
+ }
11948
11080
  get pendingToolUses() {
11949
11081
  return Array.from(this.openToolUses);
11950
11082
  }
@@ -12137,6 +11269,12 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
12137
11269
  lastAppendWarnAt = 0;
12138
11270
  writeChain = Promise.resolve();
12139
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;
12140
11278
  get isClosed() {
12141
11279
  return false;
12142
11280
  }
@@ -12177,14 +11315,20 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
12177
11315
  return true;
12178
11316
  }
12179
11317
  enqueueWrite(data) {
11318
+ return this.enqueueFlight({ data, stolen: false, started: false });
11319
+ }
11320
+ enqueueFlight(flight) {
12180
11321
  const write = this.writeChain.then(async () => {
11322
+ if (flight.stolen) return;
11323
+ flight.started = true;
11324
+ if (flight.stolen) return;
12181
11325
  try {
12182
- return await this.opts.getHandle().appendFile(data, "utf8");
11326
+ return await this.opts.getHandle().appendFile(flight.data, "utf8");
12183
11327
  } catch (err) {
12184
11328
  if (isClosedHandleError(err)) {
12185
11329
  const reloaded = await fsp12.open(this.opts.filePath, "a", 384);
12186
11330
  this.opts.setHandle(reloaded);
12187
- return await reloaded.appendFile(data, "utf8");
11331
+ return await reloaded.appendFile(flight.data, "utf8");
12188
11332
  }
12189
11333
  throw err;
12190
11334
  }
@@ -12209,17 +11353,29 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
12209
11353
  this.flushTimer = null;
12210
11354
  }
12211
11355
  }
12212
- async flushBuffer(isClosed = false) {
12213
- 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
+ }
12214
11370
  const flush = (async () => {
12215
- while (this.writeBuffer.length > 0) await this.flushBufferOnce(isClosed);
11371
+ while (this.writeBuffer.length > 0) await this.flushBufferOnce(isClosed, opts);
12216
11372
  })().finally(() => {
12217
11373
  if (this.flushPromise === flush) this.flushPromise = null;
12218
11374
  });
12219
11375
  this.flushPromise = flush;
12220
11376
  return flush;
12221
11377
  }
12222
- async flushBufferOnce(isClosed) {
11378
+ async flushBufferOnce(isClosed, opts) {
12223
11379
  if (this.writeBuffer.length === 0) return;
12224
11380
  const events = this.writeBuffer;
12225
11381
  const eventCount = events.length;
@@ -12227,12 +11383,21 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
12227
11383
  const batch = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
12228
11384
  this.writeBuffer = [];
12229
11385
  this.writeBufferBytes = 0;
11386
+ const flight = { data: batch, stolen: false, started: false };
11387
+ this.inFlight = flight;
12230
11388
  const t0 = Date.now();
12231
11389
  let outcome = "success";
12232
11390
  let errorMsg;
12233
11391
  try {
12234
- 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
+ }
12235
11399
  } catch (err) {
11400
+ if (flight.stolen) return;
12236
11401
  outcome = "failure";
12237
11402
  errorMsg = toErrorMessage(err);
12238
11403
  const newer = this.writeBuffer;
@@ -12270,6 +11435,7 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
12270
11435
  ...eventCount !== void 0 ? { eventCount } : {},
12271
11436
  ...this.opts.getTraceId?.() ? { traceId: this.opts.getTraceId() } : {}
12272
11437
  });
11438
+ if (this.inFlight === flight) this.inFlight = null;
12273
11439
  }
12274
11440
  }
12275
11441
  async drainWriteChain() {
@@ -12278,18 +11444,56 @@ var SessionWriteBuffer = class _SessionWriteBuffer {
12278
11444
  async drainFlushPromise() {
12279
11445
  await this.flushPromise?.catch(() => void 0);
12280
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
+ */
12281
11458
  flushSync() {
12282
- if (this.writeBuffer.length === 0 || !this.opts.filePath) return;
11459
+ if (!this.opts.filePath) return;
12283
11460
  this.cancelTimer();
12284
- const batch = this.writeBuffer.map((e) => JSON.stringify(e)).join("\n") + "\n";
12285
- this.writeBuffer = [];
12286
- 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;
12287
11473
  let fd;
12288
11474
  try {
12289
11475
  fd = openSync(this.opts.filePath, "a");
12290
- writeSync(fd, batch, null, "utf8");
11476
+ writeSync(fd, chunks.join(""), null, "utf8");
12291
11477
  fsyncSync(fd);
12292
- } 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
+ );
12293
11497
  } finally {
12294
11498
  if (fd !== void 0) {
12295
11499
  try {
@@ -12457,6 +11661,17 @@ function isClosedHandleError2(err) {
12457
11661
  const code = err?.code;
12458
11662
  return code === "EBADF" || code === "ERR_CLOSED_RESOURCE" || code === "ERR_INVALID_HANDLE";
12459
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;
12460
11675
  var FileSessionWriter = class _FileSessionWriter {
12461
11676
  constructor(id, handle, startedAt, meta, events, opts = {}, traceId) {
12462
11677
  this.id = id;
@@ -12465,13 +11680,15 @@ var FileSessionWriter = class _FileSessionWriter {
12465
11680
  this.meta = meta;
12466
11681
  this.events = events;
12467
11682
  this.resumed = opts.resumed ?? false;
12468
- 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`) : "";
12469
11684
  this.filePath = opts.filePath ?? "";
12470
11685
  this.secretScrubber = opts.secretScrubber;
12471
11686
  this.checkpointCas = opts.checkpointCas;
12472
11687
  this._onAppend = opts.onAppend;
12473
11688
  this._onAppendBatch = opts.onAppendBatch;
12474
11689
  this.onCloseCb = opts.onClose;
11690
+ this.onMetadataCheckpointCb = opts.onMetadataCheckpoint;
11691
+ this.metadataCheckpointMs = opts.metadataCheckpointMs ?? METADATA_CHECKPOINT_INTERVAL_MS;
12475
11692
  this.summaryTracker = new SessionSummaryTracker({
12476
11693
  id,
12477
11694
  startedAt,
@@ -12539,6 +11756,15 @@ var FileSessionWriter = class _FileSessionWriter {
12539
11756
  this._onAppendBatch = cb;
12540
11757
  }
12541
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;
12542
11768
  /** Implements SessionWriter.traceId — propagated from ContextInit.traceId. */
12543
11769
  traceId;
12544
11770
  /**
@@ -12570,18 +11796,31 @@ var FileSessionWriter = class _FileSessionWriter {
12570
11796
  void this.ensureInit();
12571
11797
  const appendEvent = scrubSessionWriterEvent(event, this.secretScrubber);
12572
11798
  this.summaryTracker.observe(appendEvent);
11799
+ this.metadataDirty = true;
11800
+ this.scheduleMetadataCheckpoint();
12573
11801
  try {
12574
11802
  this._onAppend?.(appendEvent);
12575
11803
  } catch {
12576
11804
  }
11805
+ const critical = isCriticalEvent(appendEvent);
12577
11806
  if (!this.buffer.push(appendEvent)) {
12578
11807
  this.buffer.cancelTimer();
12579
- void this.buffer.flushBuffer(this.closed).catch(() => void 0).then(() => {
12580
- 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);
12581
11820
  });
12582
- } else if (this.buffer.shouldFlushNow()) {
11821
+ } else if (critical || this.buffer.shouldFlushNow()) {
12583
11822
  this.buffer.cancelTimer();
12584
- void this.buffer.flushBuffer(this.closed).catch(() => {
11823
+ void this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => {
12585
11824
  });
12586
11825
  } else {
12587
11826
  this.buffer.scheduleFlush(this.closed);
@@ -12652,23 +11891,112 @@ var FileSessionWriter = class _FileSessionWriter {
12652
11891
  provider: this.meta.provider ?? "unknown"
12653
11892
  });
12654
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
+ }
12655
11973
  async append(event) {
12656
11974
  if (this.closed) return;
12657
11975
  await this.ensureInit();
12658
11976
  const scrubbed = scrubSessionWriterEvent(event, this.secretScrubber);
12659
11977
  this.summaryTracker.observe(scrubbed);
11978
+ this.metadataDirty = true;
11979
+ this.scheduleMetadataCheckpoint();
12660
11980
  try {
12661
11981
  this._onAppend?.(scrubbed);
12662
11982
  } catch {
12663
11983
  }
12664
- if (!this.buffer.push(scrubbed)) {
11984
+ let pushed = this.buffer.push(scrubbed);
11985
+ if (!pushed) {
12665
11986
  this.buffer.cancelTimer();
12666
- await this.buffer.flushBuffer(this.closed).catch(() => void 0);
12667
- 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
+ }
12668
11996
  }
12669
- if (this.buffer.shouldFlushNow()) {
11997
+ if (isCriticalEvent(scrubbed) || this.buffer.shouldFlushNow()) {
12670
11998
  this.buffer.cancelTimer();
12671
- await this.buffer.flushBuffer(this.closed).catch(() => {
11999
+ await this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => {
12672
12000
  });
12673
12001
  } else {
12674
12002
  this.buffer.scheduleFlush(this.closed);
@@ -12685,20 +12013,33 @@ var FileSessionWriter = class _FileSessionWriter {
12685
12013
  this._onAppend?.(scrubbed);
12686
12014
  } catch {
12687
12015
  }
12688
- if (!this.buffer.push(scrubbed)) {
12016
+ let pushed = this.buffer.push(scrubbed);
12017
+ if (!pushed) {
12689
12018
  this.buffer.cancelTimer();
12690
- await this.buffer.flushBuffer(this.closed).catch(() => void 0);
12691
- 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
+ }
12692
12028
  }
12693
12029
  scrubbedBatch.push(scrubbed);
12694
12030
  }
12031
+ if (scrubbedBatch.length > 0) {
12032
+ this.metadataDirty = true;
12033
+ this.scheduleMetadataCheckpoint();
12034
+ }
12695
12035
  try {
12696
12036
  this._onAppendBatch?.(scrubbedBatch);
12697
12037
  } catch {
12698
12038
  }
12699
- if (this.buffer.shouldFlushNow()) {
12039
+ const hasCritical = scrubbedBatch.some(isCriticalEvent);
12040
+ if (hasCritical || this.buffer.shouldFlushNow()) {
12700
12041
  this.buffer.cancelTimer();
12701
- await this.buffer.flushBuffer(this.closed).catch(() => {
12042
+ await this.buffer.flushBuffer(this.closed, { datasync: true }).catch(() => {
12702
12043
  });
12703
12044
  } else {
12704
12045
  this.buffer.scheduleFlush(this.closed);
@@ -12706,8 +12047,10 @@ var FileSessionWriter = class _FileSessionWriter {
12706
12047
  }
12707
12048
  /**
12708
12049
  * Flush buffered events to disk immediately. Critical events
12709
- * (user_input, llm_response) call this so they survive SIGKILL/crash
12710
- * 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.
12711
12054
  *
12712
12055
  * Idempotent — cancels any pending timer, writes whatever has accumulated,
12713
12056
  * then asks the OS to synchronize the file data before resolving. Even an
@@ -12716,7 +12059,7 @@ var FileSessionWriter = class _FileSessionWriter {
12716
12059
  async flush() {
12717
12060
  if (this.closed) return;
12718
12061
  this.buffer.cancelTimer();
12719
- await this.buffer.flushBuffer(this.closed);
12062
+ await this.buffer.flushBuffer(this.closed, { datasync: true });
12720
12063
  await this.buffer.drainWriteChain();
12721
12064
  try {
12722
12065
  await this.handle.datasync();
@@ -12732,23 +12075,35 @@ var FileSessionWriter = class _FileSessionWriter {
12732
12075
  * Last-gasp synchronous drain for hard-exit paths (process.exit after
12733
12076
  * rapid Ctrl+C). The async write chain cannot be awaited when the process
12734
12077
  * is about to die, but whatever still sits in the in-memory buffer CAN be
12735
- * saved with a blocking append. Best-effort: an in-flight async write may
12736
- * 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.
12737
12081
  */
12738
12082
  flushSync() {
12739
12083
  this.buffer.flushSync();
12740
12084
  }
12741
12085
  async close() {
12742
12086
  if (this.closePromise) return this.closePromise;
12743
- 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);
12744
12093
  this.closed = false;
12745
12094
  this.closePromise = null;
12746
12095
  if (this.buffer.length > 0) this.buffer.scheduleFlush(this.closed);
12096
+ if (this.metadataDirty) this.scheduleMetadataCheckpoint();
12747
12097
  throw err;
12748
12098
  });
12749
12099
  return this.closePromise;
12750
12100
  }
12751
12101
  async doClose() {
12102
+ if (this.metadataTimer) {
12103
+ clearTimeout(this.metadataTimer);
12104
+ this.metadataTimer = null;
12105
+ }
12106
+ await this.metadataCheckpointInFlight?.catch(() => void 0);
12752
12107
  await this.ensureInit();
12753
12108
  if (this.pendingFileSnapshots.length > 0) {
12754
12109
  await this.writeFileSnapshot(this.activePromptIndex ?? 0, [...this.pendingFileSnapshots]);
@@ -12756,8 +12111,13 @@ var FileSessionWriter = class _FileSessionWriter {
12756
12111
  this.pendingFileSnapshotBytes = 0;
12757
12112
  }
12758
12113
  this.closed = true;
12114
+ if (this.metadataTimer) {
12115
+ clearTimeout(this.metadataTimer);
12116
+ this.metadataTimer = null;
12117
+ }
12118
+ await this.metadataCheckpointInFlight?.catch(() => void 0);
12759
12119
  this.buffer.cancelTimer();
12760
- await this.buffer.flushBuffer(this.closed);
12120
+ await this.buffer.flushBuffer(this.closed, { datasync: true });
12761
12121
  await this.buffer.drainWriteChain();
12762
12122
  try {
12763
12123
  await this.handle.datasync();
@@ -12872,10 +12232,23 @@ var FileSessionWriter = class _FileSessionWriter {
12872
12232
  async truncateToCheckpoint(targetPromptIndex, revertedFiles = []) {
12873
12233
  if (!this.filePath) return 0;
12874
12234
  this.buffer.cancelTimer();
12875
- await this.buffer.flushBuffer(this.closed);
12235
+ await this.buffer.flushBuffer(this.closed, { datasync: true });
12876
12236
  await this.buffer.drainWriteChain();
12877
- const plan = await findSessionCheckpointTruncatePlan(this.filePath, targetPromptIndex);
12878
- 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
+ }
12879
12252
  await this.buffer.drainWriteChain();
12880
12253
  try {
12881
12254
  await this.handle.close();
@@ -12886,6 +12259,7 @@ var FileSessionWriter = class _FileSessionWriter {
12886
12259
  this.handle = await fsp14.open(this.filePath, "a", 384);
12887
12260
  } catch (err) {
12888
12261
  this.handle = await fsp14.open(this.filePath, "a", 384).catch(() => this.handle);
12262
+ this.scheduleMetadataCheckpoint();
12889
12263
  throw err;
12890
12264
  }
12891
12265
  await this.summaryTracker.recomputeFromDisk(this.filePath);
@@ -12911,6 +12285,11 @@ var FileSessionWriter = class _FileSessionWriter {
12911
12285
  await this.buffer.drainFlushPromise();
12912
12286
  this.buffer.clear();
12913
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);
12914
12293
  const resetAt = (/* @__PURE__ */ new Date()).toISOString();
12915
12294
  const record = `${JSON.stringify({
12916
12295
  type: "session_start",
@@ -12921,8 +12300,10 @@ var FileSessionWriter = class _FileSessionWriter {
12921
12300
  })}
12922
12301
  `;
12923
12302
  await this.handle.close();
12924
- await fsp14.writeFile(this.filePath, record, "utf8");
12303
+ await atomicWrite(this.filePath, record, { mode: 384 });
12925
12304
  this.summaryTracker.reset(resetAt);
12305
+ this.metadataDirty = true;
12306
+ this.scheduleMetadataCheckpoint();
12926
12307
  this.activePromptIndex = null;
12927
12308
  this.pendingFileSnapshots = [];
12928
12309
  this.pendingFileSnapshotBytes = 0;
@@ -12976,22 +12357,22 @@ var FileSessionWriter = class _FileSessionWriter {
12976
12357
 
12977
12358
  // src/storage/session-store/delete-session-artifacts.ts
12978
12359
  import * as fsp15 from "node:fs/promises";
12979
- import * as path30 from "node:path";
12360
+ import * as path29 from "node:path";
12980
12361
 
12981
12362
  // src/storage/session-store/paths.ts
12982
- import * as path29 from "node:path";
12363
+ import * as path28 from "node:path";
12983
12364
  function sessionPath(storeDir, id, ext) {
12984
12365
  return sessionScopedPath(storeDir, id, ext);
12985
12366
  }
12986
12367
  function shardManifestPath(storeDir, shardKey) {
12987
- 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");
12988
12369
  }
12989
12370
  function shardKeyForSessionId(id) {
12990
- const dirName = path29.dirname(id);
12371
+ const dirName = path28.dirname(id);
12991
12372
  return dirName === "." ? "" : dirName;
12992
12373
  }
12993
12374
  async function ensureShardDir(storeDir, id) {
12994
- const dirPath = path29.dirname(sessionScopedPath(storeDir, id, ""));
12375
+ const dirPath = path28.dirname(sessionScopedPath(storeDir, id, ""));
12995
12376
  await ensureDir(dirPath);
12996
12377
  return dirPath;
12997
12378
  }
@@ -13002,9 +12383,9 @@ async function deleteSessionArtifacts({
13002
12383
  id,
13003
12384
  jsonlPath
13004
12385
  }) {
13005
- const shardDir = path30.dirname(jsonlPath);
13006
- const base = path30.basename(id);
13007
- 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);
13008
12389
  const deletions = [
13009
12390
  fsp15.unlink(jsonlPath),
13010
12391
  fsp15.unlink(sessionPath(rootDir, id, ".summary.json")),
@@ -13546,7 +12927,7 @@ function emitDamaged(params, detail) {
13546
12927
 
13547
12928
  // src/storage/session-store/prune-helpers.ts
13548
12929
  import * as fsp16 from "node:fs/promises";
13549
- import * as path31 from "node:path";
12930
+ import * as path30 from "node:path";
13550
12931
  function isPrunableSessionJsonl(name) {
13551
12932
  return isSessionTranscriptFileName(name);
13552
12933
  }
@@ -13554,7 +12935,7 @@ async function pruneSessionFiles(storeDir, maxAgeDays, deleteSession) {
13554
12935
  const cutoff = Date.now() - maxAgeDays * 864e5;
13555
12936
  let deleted = 0;
13556
12937
  const pruneFile = async (dir, name, prefix) => {
13557
- const jsonlPath = path31.join(dir, name);
12938
+ const jsonlPath = path30.join(dir, name);
13558
12939
  try {
13559
12940
  const stat19 = await fsp16.stat(jsonlPath);
13560
12941
  if (stat19.mtimeMs >= cutoff) return;
@@ -13573,7 +12954,7 @@ async function pruneSessionFiles(storeDir, maxAgeDays, deleteSession) {
13573
12954
  continue;
13574
12955
  }
13575
12956
  if (!entry.isDirectory()) continue;
13576
- const dateDir = path31.join(storeDir, entry.name);
12957
+ const dateDir = path30.join(storeDir, entry.name);
13577
12958
  const files = await fsp16.readdir(dateDir, { withFileTypes: true }).catch(() => []);
13578
12959
  for (const file of files) {
13579
12960
  if (!file.isFile() || !isPrunableSessionJsonl(file.name)) continue;
@@ -13582,7 +12963,7 @@ async function pruneSessionFiles(storeDir, maxAgeDays, deleteSession) {
13582
12963
  }
13583
12964
  for (const entry of entries) {
13584
12965
  if (!entry.isDirectory()) continue;
13585
- const dateDir = path31.join(storeDir, entry.name);
12966
+ const dateDir = path30.join(storeDir, entry.name);
13586
12967
  try {
13587
12968
  const remaining = await fsp16.readdir(dateDir);
13588
12969
  if (remaining.length === 0) {
@@ -13658,18 +13039,18 @@ async function executeRenameSession(params) {
13658
13039
 
13659
13040
  // src/storage/session-store/resume-session.ts
13660
13041
  import * as fsp19 from "node:fs/promises";
13661
- import * as path33 from "node:path";
13042
+ import * as path32 from "node:path";
13662
13043
 
13663
13044
  // src/storage/session-resume-validation.ts
13664
13045
  import { createHash as createHash9 } from "node:crypto";
13665
13046
  import * as fsp18 from "node:fs/promises";
13666
- import * as path32 from "node:path";
13047
+ import * as path31 from "node:path";
13667
13048
  var MAX_REVALIDATE_BYTES = 5 * 1024 * 1024;
13668
13049
  var VALIDATION_CONCURRENCY = 8;
13669
13050
  var NOTICE_PATH_LIMIT = 20;
13670
13051
  function isInside2(root, target) {
13671
- const relative8 = path32.relative(root, target);
13672
- return relative8 === "" || !relative8.startsWith("..") && !path32.isAbsolute(relative8);
13052
+ const relative8 = path31.relative(root, target);
13053
+ return relative8 === "" || !relative8.startsWith("..") && !path31.isAbsolute(relative8);
13673
13054
  }
13674
13055
  function errno(err) {
13675
13056
  return err && typeof err === "object" && "code" in err ? String(err.code) : void 0;
@@ -13680,7 +13061,7 @@ function latestObservations(events, projectRoot) {
13680
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)) {
13681
13062
  continue;
13682
13063
  }
13683
- const normalized = path32.resolve(projectRoot, event.path);
13064
+ const normalized = path31.resolve(projectRoot, event.path);
13684
13065
  latest.set(normalized, {
13685
13066
  path: normalized,
13686
13067
  hash: event.hash.toLowerCase(),
@@ -13738,7 +13119,7 @@ async function validateOne(observation, lexicalRoot, realRoot) {
13738
13119
  }
13739
13120
  }
13740
13121
  async function validateResumeFileObservations(events, projectRoot) {
13741
- const lexicalRoot = path32.resolve(projectRoot);
13122
+ const lexicalRoot = path31.resolve(projectRoot);
13742
13123
  const realRoot = await fsp18.realpath(lexicalRoot).catch(() => lexicalRoot);
13743
13124
  const observations = latestObservations(events, lexicalRoot);
13744
13125
  const results = await mapWithConcurrency(
@@ -13754,8 +13135,22 @@ async function validateResumeFileObservations(events, projectRoot) {
13754
13135
  }
13755
13136
  var RESUME_NOTICE_HEADERS = [
13756
13137
  "[SESSION RESUME FILE VALIDATION]",
13757
- "[SESSION RESUME INTERRUPTED WORK]"
13138
+ "[SESSION RESUME INTERRUPTED WORK]",
13139
+ "[SESSION RESUME CRASH RECOVERY]"
13758
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
+ }
13759
13154
  function isResumeNoticeMessage(message) {
13760
13155
  if (message.role !== "system" || typeof message.content !== "string") return false;
13761
13156
  return RESUME_NOTICE_HEADERS.some((header) => message.content === header || message.content.startsWith(`${header}
@@ -13763,9 +13158,9 @@ function isResumeNoticeMessage(message) {
13763
13158
  }
13764
13159
  function formatResumeValidationNotice(validation, projectRoot) {
13765
13160
  if (validation.staleFiles.length === 0) return null;
13766
- const root = path32.resolve(projectRoot);
13161
+ const root = path31.resolve(projectRoot);
13767
13162
  const shown = validation.staleFiles.slice(0, NOTICE_PATH_LIMIT).map((entry) => {
13768
- const relative8 = path32.relative(root, entry.path);
13163
+ const relative8 = path31.relative(root, entry.path);
13769
13164
  const display = isInside2(root, entry.path) ? relative8 || "." : entry.path;
13770
13165
  return `- ${JSON.stringify(display)} [${entry.status}]`;
13771
13166
  });
@@ -13958,7 +13353,8 @@ async function executeResumeSession(params) {
13958
13353
  readSummaryManifest,
13959
13354
  searchEvents,
13960
13355
  persistCatalogSummary,
13961
- logWarn
13356
+ logWarn,
13357
+ sessionsDir
13962
13358
  } = params;
13963
13359
  const t0 = Date.now();
13964
13360
  const data = await load(canonicalId);
@@ -13979,6 +13375,24 @@ async function executeResumeSession(params) {
13979
13375
  ...derivedSummary,
13980
13376
  ...persistedSummary?.name !== void 0 ? { name: persistedSummary.name } : {}
13981
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);
13982
13396
  const noticeMessages = [];
13983
13397
  let resumeValidation;
13984
13398
  if (projectRoot) {
@@ -14007,7 +13421,7 @@ async function executeResumeSession(params) {
14007
13421
  );
14008
13422
  }
14009
13423
  }
14010
- const interruptedNotice = formatInterruptedToolNotice(data.pendingToolUseCount ?? 0);
13424
+ const interruptedNotice = recoveryPlan.stale ? null : formatInterruptedToolNotice(data.pendingToolUseCount ?? 0);
14011
13425
  if (interruptedNotice) {
14012
13426
  noticeMessages.push({
14013
13427
  role: "system",
@@ -14015,6 +13429,14 @@ async function executeResumeSession(params) {
14015
13429
  ts: (/* @__PURE__ */ new Date()).toISOString()
14016
13430
  });
14017
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
+ }
14018
13440
  const carriedMessages = data.messages.filter((message) => !isResumeNoticeMessage(message));
14019
13441
  const resumedData = {
14020
13442
  ...data,
@@ -14045,7 +13467,7 @@ async function executeResumeSession(params) {
14045
13467
  {
14046
13468
  resumed: true,
14047
13469
  initialSummary,
14048
- dir: path33.dirname(file),
13470
+ dir: path32.dirname(file),
14049
13471
  filePath: file,
14050
13472
  secretScrubber,
14051
13473
  checkpointCas,
@@ -14056,9 +13478,21 @@ async function executeResumeSession(params) {
14056
13478
  if (!current) return null;
14057
13479
  return current.name === void 0 ? {} : { name: sessionContentText(secretScrubber.scrub(current.name)) };
14058
13480
  },
14059
- 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)
14060
13486
  }
14061
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
+ }
14062
13496
  emitSessionStoreWrite(events, canonicalId, file, "resume", "success", Date.now() - t0);
14063
13497
  return { writer, data: resumedData };
14064
13498
  } catch (err) {
@@ -14277,6 +13711,11 @@ function applySessionIndexLines(raw, byId, deleted) {
14277
13711
  byId.delete(entry.id);
14278
13712
  continue;
14279
13713
  }
13714
+ if (entry.action === "create" && entry.id) {
13715
+ deleted.delete(entry.id);
13716
+ byId.delete(entry.id);
13717
+ continue;
13718
+ }
14280
13719
  if (entry.id && !deleted.has(entry.id)) {
14281
13720
  byId.set(entry.id, entry);
14282
13721
  }
@@ -14321,6 +13760,7 @@ async function readFileRange(file, start, end) {
14321
13760
 
14322
13761
  // src/storage/session-store/session-store-index.ts
14323
13762
  var COMPACT_EVERY = 30;
13763
+ var NO_DELETED_IDS = /* @__PURE__ */ new Set();
14324
13764
  async function appendToIndexStrict(dir, indexFile, summary2, invalidateShard, onAppended, compactInner) {
14325
13765
  await ensureDir(dir);
14326
13766
  let shouldCompact = false;
@@ -14353,10 +13793,14 @@ async function readIndexFile(indexFile, currentCache) {
14353
13793
  const s = await fsp23.stat(indexFile);
14354
13794
  stat19 = { mtimeMs: s.mtimeMs, size: s.size, ino: s.ino, birthtimeMs: s.birthtimeMs };
14355
13795
  } catch {
14356
- return { summaries: [], cache: null };
13796
+ return { summaries: [], deletedIds: NO_DELETED_IDS, cache: null };
14357
13797
  }
14358
13798
  if (currentCache !== null && currentCache.mtimeMs === stat19.mtimeMs && currentCache.size === stat19.size && currentCache.ino === stat19.ino && currentCache.birthtimeMs === stat19.birthtimeMs) {
14359
- return { summaries: currentCache.summaries, cache: currentCache };
13799
+ return {
13800
+ summaries: currentCache.summaries,
13801
+ deletedIds: currentCache.deleted,
13802
+ cache: currentCache
13803
+ };
14360
13804
  }
14361
13805
  const cached = currentCache;
14362
13806
  const sameFile = cached !== null && cached.ino === stat19.ino && cached.birthtimeMs === stat19.birthtimeMs;
@@ -14372,14 +13816,14 @@ async function readIndexFile(indexFile, currentCache) {
14372
13816
  byId: cached.byId,
14373
13817
  deleted: cached.deleted
14374
13818
  };
14375
- return { summaries: summaries2, cache: nextCache2 };
13819
+ return { summaries: summaries2, deletedIds: cached.deleted, cache: nextCache2 };
14376
13820
  }
14377
13821
  }
14378
13822
  let raw;
14379
13823
  try {
14380
13824
  raw = await fsp23.readFile(indexFile, "utf8");
14381
13825
  } catch {
14382
- return { summaries: [], cache: null };
13826
+ return { summaries: [], deletedIds: NO_DELETED_IDS, cache: null };
14383
13827
  }
14384
13828
  const deleted = /* @__PURE__ */ new Set();
14385
13829
  const byId = /* @__PURE__ */ new Map();
@@ -14387,7 +13831,18 @@ async function readIndexFile(indexFile, currentCache) {
14387
13831
  const summaries = Array.from(byId.values());
14388
13832
  summaries.sort(compareSessionSummaries);
14389
13833
  const nextCache = { ...stat19, summaries, byId, deleted };
14390
- 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 });
14391
13846
  }
14392
13847
 
14393
13848
  // src/storage/session-store/shard-manifest.ts
@@ -14506,6 +13961,7 @@ function damagedSummary(id, startedAt) {
14506
13961
  }
14507
13962
 
14508
13963
  // src/storage/session-store.ts
13964
+ var SESSION_FILTER_POOL_LIMIT = 1e4;
14509
13965
  var DefaultSessionStore = class _DefaultSessionStore {
14510
13966
  dir;
14511
13967
  events;
@@ -14521,14 +13977,36 @@ var DefaultSessionStore = class _DefaultSessionStore {
14521
13977
  _loadCache = /* @__PURE__ */ new Map();
14522
13978
  loadCache = new SessionLoadCache(this._loadCache);
14523
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();
14524
14002
  shardManifestCache = /* @__PURE__ */ new Map();
14525
14003
  static LIST_SCAN_CONCURRENCY = 32;
14526
14004
  indexAppendCount = 0;
14527
14005
  constructor(opts) {
14528
14006
  this.dir = opts.dir;
14529
- this.projectRoot = opts.projectRoot ? path34.resolve(opts.projectRoot) : void 0;
14007
+ this.projectRoot = opts.projectRoot ? path33.resolve(opts.projectRoot) : void 0;
14530
14008
  this.checkpointCas = this.projectRoot ? new SessionCheckpointCas({
14531
- rootDir: path34.join(this.dir, "_cas"),
14009
+ rootDir: path33.join(this.dir, "_cas"),
14532
14010
  projectRoot: this.projectRoot
14533
14011
  }) : void 0;
14534
14012
  this.events = opts.events;
@@ -14539,7 +14017,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
14539
14017
  this.onAppendBatch = opts.onAppendBatch;
14540
14018
  const builtRuntime = import.meta.url.includes("/dist/");
14541
14019
  this.catalogClient = this.projectRoot && (builtRuntime || process.env["WRONGSTACK_SESSION_CATALOG_FORCE"] === "1") && resolveSessionCatalogProjectServerUrl() ? new SessionCatalogProjectClient({
14542
- projectDir: path34.dirname(this.dir),
14020
+ projectDir: path33.dirname(this.dir),
14543
14021
  projectRoot: this.projectRoot
14544
14022
  }) : void 0;
14545
14023
  }
@@ -14561,7 +14039,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
14561
14039
  this.clearLoadCache();
14562
14040
  }
14563
14041
  get indexFile() {
14564
- return path34.join(this.dir, "_index.jsonl");
14042
+ return path33.join(this.dir, "_index.jsonl");
14565
14043
  }
14566
14044
  sessionPath(id, ext) {
14567
14045
  return sessionPath(this.dir, id, ext);
@@ -14587,19 +14065,94 @@ var DefaultSessionStore = class _DefaultSessionStore {
14587
14065
  async ensureShardDir(id) {
14588
14066
  return ensureShardDir(this.dir, id);
14589
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
+ */
14590
14079
  async create(meta) {
14591
14080
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
14592
14081
  const id = meta.id && meta.id.length > 0 ? meta.id : generateSessionId(startedAt);
14593
14082
  const shardDir = await this.ensureShardDir(id);
14594
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
+ }
14595
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
+ }
14596
14148
  let handle;
14597
14149
  try {
14598
- handle = await fsp26.open(file, "a", 384);
14150
+ handle = await fsp26.open(file, "w", 384);
14599
14151
  } catch (err) {
14600
14152
  emitSessionStoreError(this.events, id, file, "create", toErrorMessage(err), false);
14601
14153
  throw new Error(`Failed to open session file: ${toErrorMessage(err)}`, { cause: err });
14602
14154
  }
14155
+ await this.invalidateShardManifestBySessionId(id).catch(() => void 0);
14603
14156
  try {
14604
14157
  const writer = new FileSessionWriter(id, handle, startedAt, meta, this.events, {
14605
14158
  dir: shardDir,
@@ -14613,23 +14166,11 @@ var DefaultSessionStore = class _DefaultSessionStore {
14613
14166
  if (!current) return null;
14614
14167
  return current.name === void 0 ? {} : { name: sessionContentText(this.secretScrubber.scrub(current.name)) };
14615
14168
  },
14616
- 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)
14617
14173
  });
14618
- if (this.catalogClient) {
14619
- await this.catalogClient.call("upsert_summary", {
14620
- summary: {
14621
- id,
14622
- title: meta.title ?? "",
14623
- startedAt,
14624
- model: meta.model ?? "",
14625
- provider: meta.provider ?? "",
14626
- tokenTotal: 0,
14627
- lastActivityAt: startedAt
14628
- },
14629
- transcriptRelativePath: `${id}.jsonl`,
14630
- summaryRelativePath: `${id}.summary.json`
14631
- });
14632
- }
14633
14174
  emitSessionStoreWrite(this.events, id, file, "create", "success", Date.now() - t0);
14634
14175
  return writer;
14635
14176
  } catch (err) {
@@ -14691,7 +14232,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
14691
14232
  readSummaryManifest: (summaryId) => this.readSummaryManifest(summaryId),
14692
14233
  searchEvents: (searchId, pred) => this.searchEvents(searchId, pred),
14693
14234
  persistCatalogSummary: (sum) => this.persistCatalogSummary(sum),
14694
- logWarn: (msg, ctx) => this.logWarn(msg, ctx)
14235
+ logWarn: (msg, ctx) => this.logWarn(msg, ctx),
14236
+ sessionsDir: this.dir
14695
14237
  });
14696
14238
  }
14697
14239
  async load(id) {
@@ -14765,11 +14307,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
14765
14307
  return this.scrubSummaries(records);
14766
14308
  }
14767
14309
  try {
14768
- const indexed = await this.readIndex();
14769
- if (indexed.length > 0) {
14770
- return this.scrubSummaries(indexed.slice(0, limit));
14771
- }
14772
- 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));
14773
14318
  } catch {
14774
14319
  return [];
14775
14320
  }
@@ -14784,15 +14329,14 @@ var DefaultSessionStore = class _DefaultSessionStore {
14784
14329
  return this.scrubSummaries(records);
14785
14330
  }
14786
14331
  try {
14787
- const indexed = await this.readIndex();
14788
- if (indexed.length === 0) {
14789
- const raw = await this.list(Math.max(limit, 100));
14790
- return raw.filter((s) => matchesSessionFilter(s, criteria)).slice(0, limit);
14791
- }
14792
- const filtered = this.scrubSummaries(indexed).filter(
14793
- (s) => matchesSessionFilter(s, criteria)
14794
- );
14795
- 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);
14796
14340
  } catch {
14797
14341
  return [];
14798
14342
  }
@@ -14834,6 +14378,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
14834
14378
  id,
14835
14379
  (sid) => this.invalidateShardManifestBySessionId(sid),
14836
14380
  () => {
14381
+ this._manualTombstones.add(id);
14382
+ this._indexDeletedIds.add(id);
14837
14383
  this._indexCache = null;
14838
14384
  this.indexAppendCount++;
14839
14385
  }
@@ -14861,34 +14407,92 @@ var DefaultSessionStore = class _DefaultSessionStore {
14861
14407
  );
14862
14408
  }
14863
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
+ */
14864
14428
  async compactIndexInner() {
14865
14429
  const entries = await this.readIndex();
14866
- if (entries.length === 0) return;
14867
- const lines = entries.map((s) => JSON.stringify(s)).join("\n") + "\n";
14868
- await atomicWrite(this.indexFile, lines, { mode: 384 });
14430
+ await compactIndexInner(this.indexFile, entries, this._indexFileDeletedIds);
14869
14431
  this._indexCache = null;
14870
14432
  }
14871
14433
  async readIndex() {
14872
- const { summaries, cache } = await readIndexFile(this.indexFile, this._indexCache);
14434
+ const { summaries, deletedIds, cache } = await readIndexFile(this.indexFile, this._indexCache);
14873
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;
14874
14443
  return summaries;
14875
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
+ */
14876
14469
  async rebuildIndex() {
14877
14470
  if (this.catalogClient) {
14878
14471
  const result = await this.catalogClient.call("rebuild_catalog", {}, { timeoutMs: 12e4 });
14879
14472
  return result.indexed;
14880
14473
  }
14881
- const ids = await this.collectSessionIds(this.dir);
14882
- const summaries = await Promise.all(
14883
- ids.map((id) => this.summaryFor(id).catch(() => null))
14884
- );
14885
- const valid = summaries.filter((s) => s !== null);
14886
- const lines = valid.map((s) => JSON.stringify(s)).join("\n") + "\n";
14887
- 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";
14888
14492
  await atomicWrite(this.indexFile, lines, { mode: 384 });
14889
14493
  this._indexCache = null;
14494
+ return valid.length - tombstoned;
14890
14495
  });
14891
- return valid.length;
14892
14496
  }
14893
14497
  async listFromDirectoryScan(limit) {
14894
14498
  const shardKeys = await this.collectShardKeys();
@@ -14970,7 +14574,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
14970
14574
  return void 0;
14971
14575
  }
14972
14576
  async collectSessionFilesInShard(shardKey) {
14973
- const dir = shardKey ? path34.join(this.dir, shardKey) : this.dir;
14577
+ const dir = shardKey ? path33.join(this.dir, shardKey) : this.dir;
14974
14578
  const entries = await this.collectSessionFiles(dir, shardKey);
14975
14579
  return shardKey ? entries.filter((entry) => entry.id.startsWith(`${shardKey}/`)) : entries.filter((entry) => !entry.id.includes("/"));
14976
14580
  }
@@ -15151,7 +14755,7 @@ async function applyRewindToConversation(opts) {
15151
14755
  // src/storage/session-rewinder.ts
15152
14756
  import { createReadStream as createReadStream6 } from "node:fs";
15153
14757
  import * as fsp27 from "node:fs/promises";
15154
- import * as path35 from "node:path";
14758
+ import * as path34 from "node:path";
15155
14759
  import { createInterface as createInterface6 } from "node:readline";
15156
14760
  var DefaultSessionRewinder = class {
15157
14761
  constructor(sessionsDir, projectRoot) {
@@ -15285,10 +14889,10 @@ async function revertSnapshots(snapshots, projectRoot) {
15285
14889
  for (const snapshot of [...snapshots].reverse()) {
15286
14890
  for (const file of [...snapshot.files].reverse()) {
15287
14891
  try {
15288
- const absPath = path35.resolve(file.path);
15289
- const root = path35.resolve(projectRoot);
15290
- const rel = path35.relative(root, absPath);
15291
- 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)) {
15292
14896
  errors.push(`${file.path}: path resolves outside project root \u2014 skipping`);
15293
14897
  continue;
15294
14898
  }
@@ -15655,7 +15259,7 @@ function attachTodosCheckpoint(state, filePath, sessionId, events, traceId, warn
15655
15259
 
15656
15260
  // src/storage/tool-audit-log.ts
15657
15261
  import { createHash as createHash10, randomUUID as randomUUID8 } from "node:crypto";
15658
- import * as fs20 from "node:fs/promises";
15262
+ import * as fs19 from "node:fs/promises";
15659
15263
  var GENESIS_PREV = "0".repeat(64);
15660
15264
  var DEFAULT_FSYNC_EVERY = 100;
15661
15265
  var ToolAuditLog = class {
@@ -15725,7 +15329,7 @@ var ToolAuditLog = class {
15725
15329
  isError: input.isError,
15726
15330
  index
15727
15331
  };
15728
- await fs20.appendFile(fp, JSON.stringify(entry) + "\n", {
15332
+ await fs19.appendFile(fp, JSON.stringify(entry) + "\n", {
15729
15333
  encoding: "utf8",
15730
15334
  // WS-035: this file records raw tool INPUT and OUTPUT. Created
15731
15335
  // without a mode it lands 0644 — world-readable — while its
@@ -15733,7 +15337,7 @@ var ToolAuditLog = class {
15733
15337
  mode: SECRET_FILE_MODE
15734
15338
  });
15735
15339
  try {
15736
- const st = await fs20.stat(fp);
15340
+ const st = await fs19.stat(fp);
15737
15341
  this.tailStat.set(input.sessionId, { mtimeMs: st.mtimeMs, size: st.size });
15738
15342
  } catch {
15739
15343
  }
@@ -15780,7 +15384,7 @@ var ToolAuditLog = class {
15780
15384
  const cachedStat = this.tailStat.get(sessionId);
15781
15385
  if (cachedHash !== void 0 && cachedIndex !== void 0 && cachedStat) {
15782
15386
  try {
15783
- const st = await fs20.stat(fp);
15387
+ const st = await fs19.stat(fp);
15784
15388
  if (st.mtimeMs === cachedStat.mtimeMs && st.size === cachedStat.size) {
15785
15389
  return { prevHash: cachedHash, nextIndex: cachedIndex };
15786
15390
  }
@@ -15801,7 +15405,7 @@ var ToolAuditLog = class {
15801
15405
  this.tailHash.set(sessionId, prevHash);
15802
15406
  this.tailIndex.set(sessionId, nextIndex);
15803
15407
  try {
15804
- const st = await fs20.stat(fp);
15408
+ const st = await fs19.stat(fp);
15805
15409
  this.tailStat.set(sessionId, { mtimeMs: st.mtimeMs, size: st.size });
15806
15410
  } catch {
15807
15411
  }
@@ -15922,7 +15526,7 @@ var ToolAuditLog = class {
15922
15526
  async readAll(sessionId) {
15923
15527
  const fp = this.filePath(sessionId);
15924
15528
  try {
15925
- const raw = await fs20.readFile(fp, "utf8");
15529
+ const raw = await fs19.readFile(fp, "utf8");
15926
15530
  const out = [];
15927
15531
  for (const line of raw.split("\n")) {
15928
15532
  if (!line.trim()) continue;
@@ -15960,7 +15564,7 @@ var ToolAuditLog = class {
15960
15564
  }
15961
15565
  async sync(sessionId, fp) {
15962
15566
  try {
15963
- const fh = await fs20.open(fp, "r+");
15567
+ const fh = await fs19.open(fp, "r+");
15964
15568
  try {
15965
15569
  await fh.sync();
15966
15570
  } finally {
@@ -15999,7 +15603,6 @@ function sortKeys2(value) {
15999
15603
  }
16000
15604
  export {
16001
15605
  ALL_SYNC_CATEGORIES,
16002
- AgentStatusTracker,
16003
15606
  AnnotationsStore,
16004
15607
  CLOUD_SYNC_CONTRACT,
16005
15608
  CLOUD_SYNC_NAMESPACES,
@@ -16018,7 +15621,6 @@ export {
16018
15621
  DefaultSessionStore,
16019
15622
  DirectorStateCheckpoint,
16020
15623
  FileMemoryBackend,
16021
- FleetNotifier,
16022
15624
  GraphMemoryBackend,
16023
15625
  INPUT_HISTORY_DEFAULT_MAX,
16024
15626
  InputHistoryStore,
@@ -16049,6 +15651,7 @@ export {
16049
15651
  applyRewindToConversation,
16050
15652
  attachPlanCheckpoint,
16051
15653
  attachTodosCheckpoint,
15654
+ boardStore,
16052
15655
  buildNamespacePayloads,
16053
15656
  cleanOrphanLocks,
16054
15657
  clearPlan,
@@ -16110,6 +15713,7 @@ export {
16110
15713
  scrubPersistedSessionEvent,
16111
15714
  scrubPersistedSessionSummary,
16112
15715
  sessionIdResolutionError,
15716
+ setBoardStorePort,
16113
15717
  setPlanItemStatus,
16114
15718
  setProgress,
16115
15719
  stripGoalDeliverableMarker,