@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.
- package/dist/chronicle/index.js +113 -41
- package/dist/chronicle/metrics-ingest.d.ts +7 -0
- package/dist/chronicle/metrics-schema.d.ts +4 -1
- package/dist/chronicle/project-server.js +94 -38
- package/dist/chronicle/query.d.ts +1 -0
- package/dist/chronicle/sqlite-journal-schema.d.ts +2 -1
- package/dist/chronicle/types.d.ts +2 -0
- package/dist/{agent-status-helpers.d.ts → coordination/agent-status-helpers.d.ts} +1 -1
- package/dist/{agent-status-tracker.d.ts → coordination/agent-status-tracker.d.ts} +2 -2
- package/dist/{middleware → coordination}/collab-pause.d.ts +1 -1
- package/dist/coordination/director-kanban-queue-helpers.d.ts +1 -1
- package/dist/coordination/fleet-status-tool.d.ts +1 -1
- package/dist/coordination/index.d.ts +5 -1
- package/dist/coordination/index.js +2061 -346
- package/dist/coordination/kanban-dispatch-port.d.ts +30 -0
- package/dist/coordination/kanban-ops-port.d.ts +21 -0
- package/dist/coordination/mailbox-hooks.d.ts +1 -1
- package/dist/coordination/mailbox-project-server.js +14 -9
- package/dist/core/context.d.ts +35 -44
- package/dist/core/conversation-state.d.ts +16 -52
- package/dist/core/index.js +53 -16
- package/dist/core/provider-runner.d.ts +2 -2
- package/dist/core/run-env.d.ts +7 -28
- package/dist/core/streaming-response-builder.d.ts +2 -2
- package/dist/execution/index.js +111 -153
- package/dist/goal/index.js +100 -22
- package/dist/hq/auth-store.d.ts +9 -0
- package/dist/hq/cost-bridge.d.ts +1 -1
- package/dist/hq/index.js +24 -2
- package/dist/index.d.ts +12 -5
- package/dist/index.js +27806 -33937
- package/dist/infrastructure/index.js +210 -132
- package/dist/infrastructure/provider-cache-ledger.d.ts +1 -1
- package/dist/infrastructure/token-counter.d.ts +5 -1
- package/dist/kernel/events/file-events.d.ts +6 -0
- package/dist/kernel/events/provider-events.d.ts +10 -7
- package/dist/kernel/events/session-events.d.ts +4 -4
- package/dist/kernel/events/tool-events.d.ts +12 -2
- package/dist/plugin/index.js +105 -35
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.js +47 -18
- package/dist/security/kanban-boundary.d.ts +1 -1
- package/dist/security/kanban-governance-port.d.ts +28 -0
- package/dist/session-catalog/index.js +2 -3
- package/dist/session-catalog/project-server.js +2 -3
- package/dist/session-catalog/protocol.d.ts +1 -1
- package/dist/session-catalog/registry.d.ts +1 -1
- package/dist/session-catalog/store-schema.d.ts +1 -1
- package/dist/session-catalog/store.d.ts +1 -1
- package/dist/storage/annotations-store.d.ts +1 -1
- package/dist/storage/board-store-port.d.ts +45 -0
- package/dist/storage/completed-work-checkpoint.d.ts +1 -1
- package/dist/storage/config-loader/types.d.ts +1 -1
- package/dist/storage/event-bus-port.d.ts +27 -0
- package/dist/storage/file-session-writer.d.ts +47 -5
- package/dist/storage/goal-coordination.d.ts +1 -1
- package/dist/storage/goal-store.d.ts +1 -1
- package/dist/storage/index.d.ts +3 -4
- package/dist/storage/index.js +1063 -1459
- package/dist/storage/plan-store.d.ts +1 -1
- package/dist/storage/queue-store.d.ts +1 -1
- package/dist/storage/replay-log-store.d.ts +1 -1
- package/dist/storage/session-recovery.d.ts +10 -0
- package/dist/storage/session-resume-validation.d.ts +13 -1
- package/dist/storage/session-store/events.d.ts +1 -1
- package/dist/storage/session-store/load-session-data.d.ts +1 -1
- package/dist/storage/session-store/rename-session.d.ts +1 -1
- package/dist/storage/session-store/resume-session.d.ts +3 -1
- package/dist/storage/session-store/session-store-index.d.ts +2 -1
- package/dist/storage/session-store/types.d.ts +1 -1
- package/dist/storage/session-store.d.ts +70 -0
- package/dist/storage/session-summary-tracker.d.ts +8 -0
- package/dist/storage/session-write-buffer.d.ts +31 -2
- package/dist/storage/task-store.d.ts +1 -1
- package/dist/storage/todos-checkpoint.d.ts +1 -1
- package/dist/storage/tool-audit-log.d.ts +1 -1
- package/dist/tasking/index.js +100 -22
- package/dist/tasking/task-tracker.d.ts +24 -1
- package/dist/types/compactor.d.ts +2 -2
- package/dist/types/context.d.ts +212 -0
- package/dist/types/conversation-state.d.ts +109 -0
- package/dist/types/error-handler.d.ts +2 -2
- package/dist/types/file-event-record.d.ts +6 -0
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.js +17 -1
- package/dist/types/permission.d.ts +3 -3
- package/dist/types/plugin.d.ts +3 -3
- package/dist/types/provider-runner.d.ts +2 -2
- package/dist/types/provider.d.ts +10 -0
- package/dist/types/run-env.d.ts +32 -0
- package/dist/types/session.d.ts +3 -0
- package/dist/types/slash-command.d.ts +2 -2
- package/dist/types/token-counter.d.ts +30 -1
- package/dist/types/tool-executor.d.ts +2 -2
- package/dist/types/tool.d.ts +5 -5
- package/dist/utils/context-breakdown.d.ts +2 -2
- package/dist/utils/context-evidence.d.ts +12 -12
- package/dist/utils/crash-shield.d.ts +9 -0
- package/dist/utils/heap-watchdog.js +11 -3
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +72 -156
- package/dist/utils/regex-guard.d.ts +7 -30
- package/dist/utils/todos-format.d.ts +1 -1
- package/dist/utils/tree-kill.d.ts +2 -0
- package/dist/utils/tree-kill.js +1 -0
- package/instructions/coordination/subagent-baseline.md +10 -0
- package/instructions/system-lite.md +2 -0
- package/instructions/system-pro.md +40 -0
- package/instructions/system.md +15 -0
- package/package.json +6 -9
- package/dist/defaults/index.d.ts +0 -69
- package/dist/defaults/index.js +0 -38133
- /package/dist/{fleet-notifier.d.ts → coordination/fleet-notifier.d.ts} +0 -0
- /package/dist/{session-registry-atomic-file.d.ts → session-catalog/session-registry-atomic-file.d.ts} +0 -0
- /package/dist/{session-registry-types.d.ts → session-catalog/session-registry-types.d.ts} +0 -0
- /package/dist/{session-registry.d.ts → session-catalog/session-registry.d.ts} +0 -0
package/dist/storage/index.js
CHANGED
|
@@ -1,867 +1,31 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
749
|
-
|
|
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
|
-
|
|
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
|
-
|
|
777
|
-
|
|
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
|
|
20
|
+
import * as fs from "node:fs";
|
|
857
21
|
import * as net from "node:net";
|
|
858
|
-
import * as
|
|
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
|
|
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 =
|
|
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
|
|
52
|
+
return path.join(os.tmpdir(), `wssc-v${SESSION_CATALOG_PROTOCOL_VERSION}`, `${key}.sock`);
|
|
889
53
|
}
|
|
890
54
|
function sessionCatalogProjectServerMetadataPath(projectDir) {
|
|
891
|
-
return
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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((
|
|
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 (
|
|
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((
|
|
1021
|
-
socket.once("close",
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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:
|
|
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
|
|
1239
|
-
import * as
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 =
|
|
608
|
+
const projectsDir = path3.join(this.globalRoot, "projects");
|
|
1445
609
|
let directories = [];
|
|
1446
610
|
try {
|
|
1447
|
-
directories = (await
|
|
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 =
|
|
617
|
+
const projectDir = path3.join(projectsDir, slug);
|
|
1454
618
|
let client;
|
|
1455
619
|
try {
|
|
1456
620
|
const metadata = JSON.parse(
|
|
1457
|
-
await
|
|
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 &&
|
|
640
|
+
if (current && path3.basename(current.binding.projectDir) === projectSlug2) {
|
|
1477
641
|
return current.binding.client.call("list_live", {}).catch(() => []);
|
|
1478
642
|
}
|
|
1479
|
-
const projectDir =
|
|
643
|
+
const projectDir = path3.join(this.globalRoot, "projects", projectSlug2);
|
|
1480
644
|
let client;
|
|
1481
645
|
try {
|
|
1482
646
|
const metadata = JSON.parse(
|
|
1483
|
-
await
|
|
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
|
|
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 ?
|
|
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(
|
|
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
|
|
740
|
+
import * as fs3 from "node:fs";
|
|
1577
741
|
import * as os2 from "node:os";
|
|
1578
|
-
import * as
|
|
742
|
+
import * as path4 from "node:path";
|
|
1579
743
|
function canonicalProjectRoot(absRoot) {
|
|
1580
|
-
const checkoutRoot =
|
|
1581
|
-
const dotGit =
|
|
744
|
+
const checkoutRoot = path4.resolve(absRoot);
|
|
745
|
+
const dotGit = path4.join(checkoutRoot, ".git");
|
|
1582
746
|
try {
|
|
1583
|
-
if (!
|
|
1584
|
-
const gitDirLine =
|
|
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 =
|
|
1588
|
-
const commonDirFile =
|
|
1589
|
-
if (!
|
|
1590
|
-
const commonDir =
|
|
1591
|
-
const worktreesDir =
|
|
1592
|
-
if (
|
|
1593
|
-
if (
|
|
1594
|
-
if (
|
|
1595
|
-
return
|
|
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(
|
|
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(
|
|
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
|
|
1629
|
-
return
|
|
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 ?
|
|
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 =
|
|
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 =
|
|
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:
|
|
1647
|
-
profilesDir:
|
|
810
|
+
globalConfig: path4.join(globalRoot, "config.json"),
|
|
811
|
+
profilesDir: path4.join(globalRoot, "profiles"),
|
|
1648
812
|
profileConfig: (name) => {
|
|
1649
|
-
return
|
|
813
|
+
return path4.join(globalRoot, "profiles", safeProfileName(name), "config.json");
|
|
1650
814
|
},
|
|
1651
815
|
profileStatuslineConfig: (name) => {
|
|
1652
|
-
return
|
|
816
|
+
return path4.join(globalRoot, "profiles", safeProfileName(name), "statusline.json");
|
|
1653
817
|
},
|
|
1654
818
|
profileModeConfig: (name) => {
|
|
1655
|
-
return
|
|
819
|
+
return path4.join(globalRoot, "profiles", safeProfileName(name), "mode.json");
|
|
1656
820
|
},
|
|
1657
821
|
profileProviderStatus: (name) => {
|
|
1658
|
-
return
|
|
822
|
+
return path4.join(globalRoot, "profiles", safeProfileName(name), "provider-status.json");
|
|
1659
823
|
},
|
|
1660
824
|
profileUpdateCache: (name) => {
|
|
1661
|
-
return
|
|
825
|
+
return path4.join(globalRoot, "profiles", safeProfileName(name), "update-cache.json");
|
|
1662
826
|
},
|
|
1663
|
-
secretsKey:
|
|
1664
|
-
globalMemory:
|
|
1665
|
-
globalSkills:
|
|
1666
|
-
globalClaudeSkills:
|
|
1667
|
-
globalDesignKits:
|
|
1668
|
-
globalPrompts:
|
|
1669
|
-
globalInstructions:
|
|
1670
|
-
promptUsage:
|
|
1671
|
-
cacheDir:
|
|
1672
|
-
modelsCache:
|
|
1673
|
-
modelsOverlayCache:
|
|
1674
|
-
historyFile:
|
|
1675
|
-
logFile:
|
|
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:
|
|
1678
|
-
projectMemory:
|
|
1679
|
-
projectSessions:
|
|
1680
|
-
projectTrust:
|
|
1681
|
-
projectMeta:
|
|
1682
|
-
projectLocalConfig:
|
|
1683
|
-
inProjectConfig:
|
|
1684
|
-
inProjectAgentsFile:
|
|
1685
|
-
inProjectSkills:
|
|
1686
|
-
inProjectClaudeSkills:
|
|
1687
|
-
inProjectPrompts:
|
|
1688
|
-
inProjectInstructions:
|
|
1689
|
-
inProjectDesignKits:
|
|
1690
|
-
inProjectWorktrees:
|
|
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:
|
|
1694
|
-
projectInputHistory:
|
|
1695
|
-
projectSpecs:
|
|
1696
|
-
projectTaskGraphs:
|
|
1697
|
-
projectSddSession:
|
|
1698
|
-
projectPlan:
|
|
1699
|
-
projectAutophase:
|
|
1700
|
-
projectSddBoards:
|
|
1701
|
-
projectRequirementIntakes:
|
|
1702
|
-
syncConfig:
|
|
1703
|
-
configHistoryDir:
|
|
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
|
|
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
|
|
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 =
|
|
2228
|
-
const rel =
|
|
2229
|
-
if (rel.startsWith("..") ||
|
|
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
|
|
2259
|
-
import * as
|
|
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
|
|
1441
|
+
import * as fs4 from "node:fs/promises";
|
|
2264
1442
|
import { hostname } from "node:os";
|
|
2265
|
-
import * as
|
|
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
|
|
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
|
|
1457
|
+
await fs4.unlink(lockPath);
|
|
2280
1458
|
return;
|
|
2281
1459
|
} catch {
|
|
2282
|
-
if (attempt < 2) await new Promise((
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1515
|
+
await fs4.rename(lockPath, tombstone);
|
|
2338
1516
|
} catch {
|
|
2339
1517
|
return false;
|
|
2340
1518
|
}
|
|
2341
|
-
void
|
|
1519
|
+
void fs4.unlink(tombstone).catch(() => void 0);
|
|
2342
1520
|
return true;
|
|
2343
1521
|
}
|
|
2344
1522
|
async function writeAtomicFile(filePath, registry) {
|
|
2345
|
-
const tmp =
|
|
2346
|
-
|
|
2347
|
-
`.${
|
|
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
|
|
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
|
|
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
|
|
1543
|
+
await fs4.copyFile(tmp, filePath);
|
|
2366
1544
|
try {
|
|
2367
|
-
const destHandle = await
|
|
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
|
|
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
|
|
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 =
|
|
2389
|
-
const base =
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
|
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 =
|
|
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 &&
|
|
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 && !
|
|
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 &&
|
|
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((
|
|
2549
|
-
this.pendingAgentsResolve =
|
|
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(
|
|
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
|
|
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 (!
|
|
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 && !
|
|
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
|
|
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
|
|
1966
|
+
let lockHandle = await fs5.open(lockPath, "wx").catch(() => null);
|
|
2789
1967
|
if (!lockHandle) {
|
|
2790
1968
|
if (await breakStaleLock(lockPath)) {
|
|
2791
|
-
lockHandle = await
|
|
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
|
-
(
|
|
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((
|
|
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
|
|
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
|
|
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((
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
3329
|
-
import * as
|
|
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
|
|
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
|
|
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
|
|
4085
|
-
import * as
|
|
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 =
|
|
4094
|
-
this.getSettingsConfigPath = getSettingsConfigPath ?? (() =>
|
|
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
|
|
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
|
|
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
|
|
4380
|
-
const rel =
|
|
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
|
|
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
|
|
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
|
|
4407
|
-
const rel =
|
|
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
|
|
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
|
|
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 =
|
|
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 :
|
|
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 =
|
|
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(
|
|
4464
|
-
cursor =
|
|
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
|
|
3650
|
+
await fs8.mkdir(path10.dirname(destPath), { recursive: true });
|
|
4473
3651
|
}
|
|
4474
3652
|
async function lstatIfExists(filePath) {
|
|
4475
3653
|
try {
|
|
4476
|
-
return await
|
|
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 =
|
|
4503
|
-
const traversesUp = normalizedRel === ".." || normalizedRel.startsWith(`..${
|
|
4504
|
-
if (
|
|
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 =
|
|
4513
|
-
const root =
|
|
4514
|
-
const relative8 =
|
|
4515
|
-
if (relative8.startsWith("..") ||
|
|
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
|
|
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
|
|
4737
|
-
import * as
|
|
3914
|
+
import * as fs9 from "node:fs/promises";
|
|
3915
|
+
import * as path11 from "node:path";
|
|
4738
3916
|
function configHistoryDir(globalRoot) {
|
|
4739
|
-
return
|
|
3917
|
+
return path11.join(globalRoot, "config-history");
|
|
4740
3918
|
}
|
|
4741
3919
|
function configSlug(absolutePath, globalRoot) {
|
|
4742
|
-
const rel =
|
|
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
|
|
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 =
|
|
3936
|
+
const backupFile = path11.join(backupDir, `${slug}-${ts}.json`);
|
|
4759
3937
|
try {
|
|
4760
|
-
await
|
|
4761
|
-
await
|
|
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
|
|
4263
|
+
const path35 = prefix ? `${prefix}.${key}` : key;
|
|
5086
4264
|
if (!Object.hasOwn(target, key)) {
|
|
5087
4265
|
target[key] = cloneJsonValue(defaultValue);
|
|
5088
|
-
changes.push({ path:
|
|
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,
|
|
4272
|
+
repair(current, defaultValue, path35, depth + 1);
|
|
5095
4273
|
} else {
|
|
5096
4274
|
target[key] = cloneJsonValue(defaultValue);
|
|
5097
|
-
changes.push({ path:
|
|
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:
|
|
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,
|
|
5391
|
-
const segments =
|
|
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:
|
|
5449
|
-
).map(({ path:
|
|
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:
|
|
5457
|
-
).map(({ path:
|
|
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:
|
|
5486
|
-
if (deleteNestedPath(out,
|
|
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
|
|
4853
|
+
import * as path12 from "node:path";
|
|
5676
4854
|
function samePath(a, b) {
|
|
5677
|
-
let ra =
|
|
5678
|
-
let rb =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
7265
|
-
import * as
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
7346
|
-
import * as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
7693
|
-
const base =
|
|
6870
|
+
const dir = path14.dirname(file);
|
|
6871
|
+
const base = path14.basename(file);
|
|
7694
6872
|
const prefix = `${base}.bak.`;
|
|
7695
|
-
const backups = (await
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
6923
|
+
const here = path15.dirname(fileURLToPath2(import.meta.url));
|
|
7746
6924
|
const candidates = [
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
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 =
|
|
7820
|
-
return relative8 && !relative8.startsWith("..") && !
|
|
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
|
|
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
|
|
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
|
|
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
|
|
7438
|
+
await fs13.mkdir(dir, { recursive: true });
|
|
8261
7439
|
const tmp = `${this.graphFile}.tmp`;
|
|
8262
|
-
await
|
|
8263
|
-
await
|
|
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
|
|
8722
|
-
import * as
|
|
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
|
|
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
|
|
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 =
|
|
8018
|
+
const file = path17.join(this.dir, `${id}.json`);
|
|
8841
8019
|
try {
|
|
8842
|
-
const raw = JSON.parse(await
|
|
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 =
|
|
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 =
|
|
8033
|
+
const file = path17.join(this.dir, `${id}.json`);
|
|
8856
8034
|
try {
|
|
8857
|
-
await
|
|
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
|
|
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
|
|
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((
|
|
8930
|
-
this.pendingRecords.push({ slug, at, resolve:
|
|
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
|
|
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
|
|
9014
|
-
import * as
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
8797
|
+
const pidAlive2 = options?.isPidAlive ?? isPidAlive;
|
|
9620
8798
|
const maxAgeMs = options?.maxLockAgeMs ?? 10 * 60 * 1e3;
|
|
9621
8799
|
const now = Date.now();
|
|
9622
|
-
const worktreesDir =
|
|
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 =
|
|
9628
|
-
const lockPath =
|
|
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 (!
|
|
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
|
|
9658
|
-
import * as
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
10390
|
-
return relative8 === "" || !relative8.startsWith("..") && !
|
|
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 ||
|
|
9571
|
+
if (!input || path23.isAbsolute(input)) return null;
|
|
10394
9572
|
const normalized = input.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
10395
|
-
const resolved =
|
|
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 =
|
|
10411
|
-
this.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 =
|
|
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 =
|
|
10446
|
-
if (
|
|
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 =
|
|
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 (
|
|
9726
|
+
if (path23.isAbsolute(entry.linkTarget)) {
|
|
10549
9727
|
throw new Error("absolute symlink target refused");
|
|
10550
9728
|
}
|
|
10551
|
-
const resolvedLink =
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
10613
|
-
|
|
10614
|
-
`.${
|
|
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 =
|
|
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 =
|
|
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((
|
|
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) =>
|
|
10737
|
-
child.on("close", (code) =>
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
10949
|
-
|
|
10950
|
-
|
|
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
|
|
11425
|
-
import * as
|
|
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
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
11595
|
-
} catch
|
|
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 -
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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.
|
|
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 (
|
|
11459
|
+
if (!this.opts.filePath) return;
|
|
12283
11460
|
this.cancelTimer();
|
|
12284
|
-
const
|
|
12285
|
-
|
|
12286
|
-
|
|
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,
|
|
11476
|
+
writeSync(fd, chunks.join(""), null, "utf8");
|
|
12291
11477
|
fsyncSync(fd);
|
|
12292
|
-
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
12710
|
-
*
|
|
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.
|
|
12736
|
-
*
|
|
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
|
-
|
|
12878
|
-
|
|
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
|
|
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
|
|
12360
|
+
import * as path29 from "node:path";
|
|
12980
12361
|
|
|
12981
12362
|
// src/storage/session-store/paths.ts
|
|
12982
|
-
import * as
|
|
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 ?
|
|
12368
|
+
return shardKey ? path28.join(storeDir, shardKey, "_manifest.json") : path28.join(storeDir, "_manifest.json");
|
|
12988
12369
|
}
|
|
12989
12370
|
function shardKeyForSessionId(id) {
|
|
12990
|
-
const dirName =
|
|
12371
|
+
const dirName = path28.dirname(id);
|
|
12991
12372
|
return dirName === "." ? "" : dirName;
|
|
12992
12373
|
}
|
|
12993
12374
|
async function ensureShardDir(storeDir, id) {
|
|
12994
|
-
const dirPath =
|
|
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 =
|
|
13006
|
-
const base =
|
|
13007
|
-
const sessDir =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
13672
|
-
return relative8 === "" || !relative8.startsWith("..") && !
|
|
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 =
|
|
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 =
|
|
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 =
|
|
13161
|
+
const root = path31.resolve(projectRoot);
|
|
13767
13162
|
const shown = validation.staleFiles.slice(0, NOTICE_PATH_LIMIT).map((entry) => {
|
|
13768
|
-
const relative8 =
|
|
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:
|
|
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 {
|
|
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 ?
|
|
14007
|
+
this.projectRoot = opts.projectRoot ? path33.resolve(opts.projectRoot) : void 0;
|
|
14530
14008
|
this.checkpointCas = this.projectRoot ? new SessionCheckpointCas({
|
|
14531
|
-
rootDir:
|
|
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:
|
|
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
|
|
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, "
|
|
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
|
|
14769
|
-
|
|
14770
|
-
|
|
14771
|
-
|
|
14772
|
-
|
|
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
|
|
14788
|
-
|
|
14789
|
-
|
|
14790
|
-
|
|
14791
|
-
|
|
14792
|
-
|
|
14793
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14882
|
-
|
|
14883
|
-
ids
|
|
14884
|
-
|
|
14885
|
-
|
|
14886
|
-
|
|
14887
|
-
|
|
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 ?
|
|
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
|
|
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 =
|
|
15289
|
-
const root =
|
|
15290
|
-
const rel =
|
|
15291
|
-
if (rel.startsWith("..") ||
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|