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