@druumen/sessions-db 0.1.0 → 0.1.3
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/CHANGELOG.md +254 -1
- package/README.md +28 -0
- package/lib/index.cjs +1699 -0
- package/package.json +28 -7
- package/types/index.d.cts +62 -0
- package/types/index.d.ts +30 -96
package/lib/index.cjs
ADDED
|
@@ -0,0 +1,1699 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
|
|
19
|
+
// lib/index.mjs
|
|
20
|
+
var index_exports = {};
|
|
21
|
+
__export(index_exports, {
|
|
22
|
+
MAX_ASCEND_DEPTH: () => MAX_ASCEND_DEPTH,
|
|
23
|
+
MAX_EVENT_BYTES: () => MAX_EVENT_BYTES,
|
|
24
|
+
MAX_PARENT_CANDIDATES: () => MAX_PARENT_CANDIDATES,
|
|
25
|
+
PATHS: () => PATHS,
|
|
26
|
+
STORAGE_FILENAMES: () => STORAGE_FILENAMES,
|
|
27
|
+
STRONG_CORROBORATORS: () => STRONG_CORROBORATORS,
|
|
28
|
+
WEAK_CORROBORATORS: () => WEAK_CORROBORATORS,
|
|
29
|
+
appendEvent: () => appendEvent,
|
|
30
|
+
applyEvent: () => applyEvent,
|
|
31
|
+
capParentCandidates: () => capParentCandidates,
|
|
32
|
+
classifyCorroborators: () => classifyCorroborators,
|
|
33
|
+
closeSession: () => closeSession,
|
|
34
|
+
collectParentCandidates: () => collectParentCandidates,
|
|
35
|
+
computeEffectiveLastProgress: () => computeEffectiveLastProgress,
|
|
36
|
+
computeSweepTransitions: () => computeSweepTransitions,
|
|
37
|
+
emptyProjection: () => emptyProjection,
|
|
38
|
+
emptySession: () => emptySession,
|
|
39
|
+
extractTimestamp: () => extractTimestamp,
|
|
40
|
+
findByClaudeSessionId: () => findByClaudeSessionId,
|
|
41
|
+
findByTranscriptLineage: () => findByTranscriptLineage,
|
|
42
|
+
generateSessionId: () => generateSessionId,
|
|
43
|
+
initProjection: () => initProjection,
|
|
44
|
+
isSessionId: () => isSessionId,
|
|
45
|
+
linkTask: () => linkTask,
|
|
46
|
+
loadProjection: () => loadProjection,
|
|
47
|
+
meetsThreshold: () => meetsThreshold,
|
|
48
|
+
newEvent: () => newEvent,
|
|
49
|
+
pathsFromRoot: () => pathsFromRoot,
|
|
50
|
+
readAllEvents: () => readAllEvents,
|
|
51
|
+
rebuildFromEvents: () => rebuildFromEvents,
|
|
52
|
+
rebuildProjection: () => rebuildProjection,
|
|
53
|
+
recordSessionSeen: () => recordSessionSeen,
|
|
54
|
+
resolveIdentity: () => resolveIdentity,
|
|
55
|
+
resolveStoragePaths: () => resolveStoragePaths,
|
|
56
|
+
runSweep: () => runSweep,
|
|
57
|
+
sanitizeFirstPrompt: () => sanitizeFirstPrompt,
|
|
58
|
+
saveProjection: () => saveProjection,
|
|
59
|
+
scanFingerprintCandidates: () => scanFingerprintCandidates,
|
|
60
|
+
setAlias: () => setAlias,
|
|
61
|
+
setParent: () => setParent,
|
|
62
|
+
stripIdeWrappers: () => stripIdeWrappers,
|
|
63
|
+
stripSystemReminders: () => stripSystemReminders,
|
|
64
|
+
tryUpdateProjection: () => tryUpdateProjection,
|
|
65
|
+
unlinkTask: () => unlinkTask,
|
|
66
|
+
watchProjection: () => watchProjection
|
|
67
|
+
});
|
|
68
|
+
module.exports = __toCommonJS(index_exports);
|
|
69
|
+
|
|
70
|
+
// lib/storage.mjs
|
|
71
|
+
var import_node_fs3 = require("node:fs");
|
|
72
|
+
var import_node_path2 = require("node:path");
|
|
73
|
+
|
|
74
|
+
// lib/lock.mjs
|
|
75
|
+
var import_node_fs = require("node:fs");
|
|
76
|
+
var import_promises = require("node:timers/promises");
|
|
77
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
78
|
+
var DEFAULT_RETRY_MS = 50;
|
|
79
|
+
async function acquireLock(lockPath, opts = {}) {
|
|
80
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
81
|
+
const retryMs = opts.retryMs ?? DEFAULT_RETRY_MS;
|
|
82
|
+
const deadline = Date.now() + timeoutMs;
|
|
83
|
+
while (true) {
|
|
84
|
+
let fd;
|
|
85
|
+
try {
|
|
86
|
+
fd = (0, import_node_fs.openSync)(lockPath, "wx");
|
|
87
|
+
} catch (err) {
|
|
88
|
+
if (err && err.code === "EEXIST") {
|
|
89
|
+
if (Date.now() >= deadline) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`acquireLock: timeout after ${timeoutMs}ms (path=${lockPath})`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
await (0, import_promises.setTimeout)(retryMs);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const stamp = `${process.pid} ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
101
|
+
`;
|
|
102
|
+
(0, import_node_fs.writeSync)(fd, stamp);
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
let released = false;
|
|
106
|
+
const release = () => {
|
|
107
|
+
if (released) return;
|
|
108
|
+
released = true;
|
|
109
|
+
try {
|
|
110
|
+
(0, import_node_fs.closeSync)(fd);
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
(0, import_node_fs.unlinkSync)(lockPath);
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
return { release };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// lib/identity.mjs
|
|
123
|
+
var DEFAULT_TIME_WINDOW_HOURS = 72;
|
|
124
|
+
var DEFAULT_MIN_CORROBORATORS = 2;
|
|
125
|
+
var MAX_PARENT_CANDIDATES = 10;
|
|
126
|
+
var STRONG_CORROBORATORS = Object.freeze([
|
|
127
|
+
"same_cwd",
|
|
128
|
+
"same_worktree_realpath"
|
|
129
|
+
]);
|
|
130
|
+
var WEAK_CORROBORATORS = Object.freeze([
|
|
131
|
+
"same_branch_at_start",
|
|
132
|
+
"within_time_window"
|
|
133
|
+
]);
|
|
134
|
+
function classifyCorroborators(hits) {
|
|
135
|
+
let strong = 0;
|
|
136
|
+
let weak = 0;
|
|
137
|
+
for (const k of STRONG_CORROBORATORS) if (hits && hits[k] === true) strong += 1;
|
|
138
|
+
for (const k of WEAK_CORROBORATORS) if (hits && hits[k] === true) weak += 1;
|
|
139
|
+
return { strong, weak, total: strong + weak };
|
|
140
|
+
}
|
|
141
|
+
function meetsThreshold(counts, opts = {}) {
|
|
142
|
+
if (!counts || typeof counts !== "object") return false;
|
|
143
|
+
const min = typeof opts.minCorroborators === "number" ? opts.minCorroborators : DEFAULT_MIN_CORROBORATORS;
|
|
144
|
+
return counts.strong >= 1 && counts.total >= min;
|
|
145
|
+
}
|
|
146
|
+
function resolveIdentity(input) {
|
|
147
|
+
if (!input || typeof input !== "object") {
|
|
148
|
+
throw new TypeError("resolveIdentity: input required");
|
|
149
|
+
}
|
|
150
|
+
const {
|
|
151
|
+
projection,
|
|
152
|
+
claudeSessionId,
|
|
153
|
+
transcriptMeta = null,
|
|
154
|
+
gitContext = null,
|
|
155
|
+
cwd = null,
|
|
156
|
+
fingerprints = null,
|
|
157
|
+
now = Date.now(),
|
|
158
|
+
timeWindowHours = DEFAULT_TIME_WINDOW_HOURS,
|
|
159
|
+
minCorroborators = DEFAULT_MIN_CORROBORATORS,
|
|
160
|
+
mintStableId
|
|
161
|
+
} = input;
|
|
162
|
+
if (typeof mintStableId !== "function") {
|
|
163
|
+
throw new TypeError("resolveIdentity: mintStableId callback required");
|
|
164
|
+
}
|
|
165
|
+
if (typeof claudeSessionId !== "string" || claudeSessionId.length === 0) {
|
|
166
|
+
throw new TypeError("resolveIdentity: claudeSessionId required");
|
|
167
|
+
}
|
|
168
|
+
const p1 = findByClaudeSessionId(projection, claudeSessionId);
|
|
169
|
+
if (p1 !== null) {
|
|
170
|
+
return {
|
|
171
|
+
stableId: p1,
|
|
172
|
+
source: "claude_session_id_index",
|
|
173
|
+
confidence: "exact",
|
|
174
|
+
matched: { claude_session_id: claudeSessionId },
|
|
175
|
+
// P1 hit — do NOT compute parentCandidates. The session is identified;
|
|
176
|
+
// hub-spoke parent surfacing is only meaningful when we cannot resolve
|
|
177
|
+
// the exact identity from a stable cross-session signal.
|
|
178
|
+
parentCandidates: [],
|
|
179
|
+
parentCandidatesOmittedCount: 0
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const p2 = findByTranscriptLineage(projection, transcriptMeta);
|
|
183
|
+
if (p2 !== null) {
|
|
184
|
+
return {
|
|
185
|
+
stableId: p2.stableId,
|
|
186
|
+
source: "transcript_lineage",
|
|
187
|
+
confidence: "high",
|
|
188
|
+
matched: {
|
|
189
|
+
first_parent_uuid: transcriptMeta?.firstParentUuid ?? null,
|
|
190
|
+
matched_transcript_path: p2.matchedPath,
|
|
191
|
+
matched_last_uuid: p2.matchedLastUuid
|
|
192
|
+
},
|
|
193
|
+
parentCandidates: [],
|
|
194
|
+
parentCandidatesOmittedCount: 0
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const corrCtx = {
|
|
198
|
+
cwd: typeof cwd === "string" && cwd.length > 0 ? cwd : null,
|
|
199
|
+
worktreeRealpath: gitContext && typeof gitContext.worktreeRealpath === "string" && gitContext.worktreeRealpath.length > 0 ? gitContext.worktreeRealpath : null,
|
|
200
|
+
branch: gitContext && typeof gitContext.branch === "string" && gitContext.branch.length > 0 ? gitContext.branch : null,
|
|
201
|
+
now,
|
|
202
|
+
timeWindowHours
|
|
203
|
+
};
|
|
204
|
+
const fpScan = scanFingerprintCandidates(projection, fingerprints, corrCtx);
|
|
205
|
+
const above = [];
|
|
206
|
+
const below = [];
|
|
207
|
+
for (const c of fpScan) {
|
|
208
|
+
if (meetsThreshold(c.strengthCounts, { minCorroborators })) above.push(c);
|
|
209
|
+
else below.push(c);
|
|
210
|
+
}
|
|
211
|
+
if (above.length === 1) {
|
|
212
|
+
const accepted = above[0];
|
|
213
|
+
const { list: list2, omitted: omitted2 } = capParentCandidates(
|
|
214
|
+
// Other above-threshold (none in this branch) + all below-threshold.
|
|
215
|
+
below.filter((c) => c.stableId !== accepted.stableId)
|
|
216
|
+
);
|
|
217
|
+
return {
|
|
218
|
+
stableId: accepted.stableId,
|
|
219
|
+
source: "fingerprint_corroborator",
|
|
220
|
+
confidence: "low",
|
|
221
|
+
matched: {
|
|
222
|
+
fingerprints_matched: accepted.fingerprintsMatched,
|
|
223
|
+
corroborators: accepted.corroborators,
|
|
224
|
+
corroborator_count: accepted.corroboratorCount,
|
|
225
|
+
strong_corroborator_count: accepted.strengthCounts.strong
|
|
226
|
+
},
|
|
227
|
+
parentCandidates: list2,
|
|
228
|
+
parentCandidatesOmittedCount: omitted2
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const minted = mintStableId();
|
|
232
|
+
const matched = above.length >= 2 ? { ambiguous: true, ambiguous_count: above.length } : {};
|
|
233
|
+
const { list, omitted } = capParentCandidates([...above, ...below]);
|
|
234
|
+
return {
|
|
235
|
+
stableId: minted,
|
|
236
|
+
source: "minted",
|
|
237
|
+
confidence: "minted",
|
|
238
|
+
matched,
|
|
239
|
+
parentCandidates: list,
|
|
240
|
+
parentCandidatesOmittedCount: omitted
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function findByClaudeSessionId(projection, csid) {
|
|
244
|
+
if (!projection || !projection.sessions || typeof projection.sessions !== "object") {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
if (typeof csid !== "string" || csid.length === 0) return null;
|
|
248
|
+
for (const [stableId, session] of Object.entries(projection.sessions)) {
|
|
249
|
+
if (!session || !Array.isArray(session.claude_session_ids)) continue;
|
|
250
|
+
if (session.claude_session_ids.length === 0) continue;
|
|
251
|
+
if (session.claude_session_ids.includes(csid)) return stableId;
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
function findByTranscriptLineage(projection, transcriptMeta) {
|
|
256
|
+
if (!transcriptMeta || typeof transcriptMeta !== "object") return null;
|
|
257
|
+
const parent = typeof transcriptMeta.firstParentUuid === "string" ? transcriptMeta.firstParentUuid : null;
|
|
258
|
+
if (!parent || parent.length === 0) return null;
|
|
259
|
+
if (!projection || !projection.sessions || typeof projection.sessions !== "object") {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
for (const [stableId, session] of Object.entries(projection.sessions)) {
|
|
263
|
+
if (!session || !Array.isArray(session.transcript_files)) continue;
|
|
264
|
+
for (const tf of session.transcript_files) {
|
|
265
|
+
if (!tf || typeof tf !== "object") continue;
|
|
266
|
+
const lastUuid = typeof tf.last_uuid === "string" && tf.last_uuid.length > 0 ? tf.last_uuid : typeof tf.lastUuid === "string" && tf.lastUuid.length > 0 ? tf.lastUuid : null;
|
|
267
|
+
if (lastUuid && lastUuid === parent) {
|
|
268
|
+
return {
|
|
269
|
+
stableId,
|
|
270
|
+
matchedPath: typeof tf.path === "string" ? tf.path : null,
|
|
271
|
+
matchedLastUuid: lastUuid
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
function scanFingerprintCandidates(projection, fingerprints, corrCtx) {
|
|
279
|
+
const out = [];
|
|
280
|
+
if (!projection || !projection.sessions || typeof projection.sessions !== "object") {
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
if (!fingerprints || typeof fingerprints !== "object") return out;
|
|
284
|
+
const fpHuman = typeof fingerprints.first_human_prompt_v1 === "string" && fingerprints.first_human_prompt_v1.length > 0 ? fingerprints.first_human_prompt_v1 : null;
|
|
285
|
+
const fpLineage = typeof fingerprints.lineage_prefix_v1 === "string" && fingerprints.lineage_prefix_v1.length > 0 ? fingerprints.lineage_prefix_v1 : null;
|
|
286
|
+
if (fpHuman === null && fpLineage === null) return out;
|
|
287
|
+
const windowMs = (typeof corrCtx.timeWindowHours === "number" && corrCtx.timeWindowHours >= 0 ? corrCtx.timeWindowHours : DEFAULT_TIME_WINDOW_HOURS) * 3600 * 1e3;
|
|
288
|
+
for (const [stableId, session] of Object.entries(projection.sessions)) {
|
|
289
|
+
if (!session || !session.fingerprints || typeof session.fingerprints !== "object") continue;
|
|
290
|
+
const matched = [];
|
|
291
|
+
if (fpHuman !== null && typeof session.fingerprints.first_human_prompt_v1 === "string" && session.fingerprints.first_human_prompt_v1 === fpHuman) {
|
|
292
|
+
matched.push("first_human_prompt_v1");
|
|
293
|
+
}
|
|
294
|
+
if (fpLineage !== null && typeof session.fingerprints.lineage_prefix_v1 === "string" && session.fingerprints.lineage_prefix_v1 === fpLineage) {
|
|
295
|
+
matched.push("lineage_prefix_v1");
|
|
296
|
+
}
|
|
297
|
+
if (matched.length === 0) continue;
|
|
298
|
+
const corroborators = {
|
|
299
|
+
same_cwd: corrCtx.cwd !== null && typeof session.cwd === "string" && session.cwd.length > 0 && session.cwd === corrCtx.cwd,
|
|
300
|
+
same_worktree_realpath: corrCtx.worktreeRealpath !== null && typeof session.worktree_realpath === "string" && session.worktree_realpath.length > 0 && session.worktree_realpath === corrCtx.worktreeRealpath,
|
|
301
|
+
same_branch_at_start: corrCtx.branch !== null && typeof session.branch_at_start === "string" && session.branch_at_start.length > 0 && session.branch_at_start === corrCtx.branch,
|
|
302
|
+
within_time_window: false
|
|
303
|
+
};
|
|
304
|
+
if (typeof session.last_progress_at === "string" && session.last_progress_at.length > 0) {
|
|
305
|
+
const lastMs = Date.parse(session.last_progress_at);
|
|
306
|
+
if (Number.isFinite(lastMs)) {
|
|
307
|
+
const diffMs = corrCtx.now - lastMs;
|
|
308
|
+
corroborators.within_time_window = diffMs >= 0 && diffMs <= windowMs;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const corroboratorCount = Object.values(corroborators).filter(Boolean).length;
|
|
312
|
+
const strengthCounts = classifyCorroborators(corroborators);
|
|
313
|
+
out.push({
|
|
314
|
+
stableId,
|
|
315
|
+
fingerprintsMatched: matched,
|
|
316
|
+
corroborators,
|
|
317
|
+
corroboratorCount,
|
|
318
|
+
strengthCounts,
|
|
319
|
+
sessionLastProgressAt: typeof session.last_progress_at === "string" ? session.last_progress_at : null
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
function collectParentCandidates(rows) {
|
|
325
|
+
if (!Array.isArray(rows) || rows.length === 0) return [];
|
|
326
|
+
const seen = /* @__PURE__ */ new Map();
|
|
327
|
+
for (const r of rows) {
|
|
328
|
+
if (!r || typeof r.stableId !== "string") continue;
|
|
329
|
+
if (seen.has(r.stableId)) continue;
|
|
330
|
+
const strength = r.strengthCounts ?? classifyCorroborators(r.corroborators);
|
|
331
|
+
seen.set(r.stableId, {
|
|
332
|
+
stable_id: r.stableId,
|
|
333
|
+
source: "fingerprint",
|
|
334
|
+
confidence: "low",
|
|
335
|
+
reason: {
|
|
336
|
+
fingerprints_matched: [...r.fingerprintsMatched],
|
|
337
|
+
corroborator_count: r.corroboratorCount,
|
|
338
|
+
strong_corroborator_count: strength.strong,
|
|
339
|
+
weak_corroborator_count: strength.weak
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
return Array.from(seen.values());
|
|
344
|
+
}
|
|
345
|
+
function capParentCandidates(rows, opts = {}) {
|
|
346
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
347
|
+
return { list: [], omitted: 0 };
|
|
348
|
+
}
|
|
349
|
+
const cap = typeof opts.cap === "number" && opts.cap > 0 ? opts.cap : MAX_PARENT_CANDIDATES;
|
|
350
|
+
const dedup = /* @__PURE__ */ new Map();
|
|
351
|
+
for (const r of rows) {
|
|
352
|
+
if (!r || typeof r.stableId !== "string") continue;
|
|
353
|
+
if (!dedup.has(r.stableId)) dedup.set(r.stableId, r);
|
|
354
|
+
}
|
|
355
|
+
const sorted = Array.from(dedup.values()).sort((a, b) => {
|
|
356
|
+
const aStrong = (a.strengthCounts ?? classifyCorroborators(a.corroborators)).strong;
|
|
357
|
+
const bStrong = (b.strengthCounts ?? classifyCorroborators(b.corroborators)).strong;
|
|
358
|
+
if (bStrong !== aStrong) return bStrong - aStrong;
|
|
359
|
+
const aTs = a.sessionLastProgressAt ?? "";
|
|
360
|
+
const bTs = b.sessionLastProgressAt ?? "";
|
|
361
|
+
if (aTs !== bTs) return bTs.localeCompare(aTs);
|
|
362
|
+
return a.stableId.localeCompare(b.stableId);
|
|
363
|
+
});
|
|
364
|
+
const kept = sorted.slice(0, cap);
|
|
365
|
+
const omitted = Math.max(0, sorted.length - kept.length);
|
|
366
|
+
return { list: collectParentCandidates(kept), omitted };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// lib/paths.mjs
|
|
370
|
+
var import_node_fs2 = require("node:fs");
|
|
371
|
+
var import_node_path = require("node:path");
|
|
372
|
+
var MAX_ASCEND_DEPTH = 12;
|
|
373
|
+
var STORAGE_FILENAMES = Object.freeze({
|
|
374
|
+
eventsJsonl: "sessions-db-events.jsonl",
|
|
375
|
+
projectionJson: "sessions-db.json",
|
|
376
|
+
lockFile: "sessions-db.json.lock"
|
|
377
|
+
});
|
|
378
|
+
function resolveStoragePaths(opts = {}) {
|
|
379
|
+
if (typeof opts.rootPath === "string" && opts.rootPath.length > 0) {
|
|
380
|
+
const root = (0, import_node_path.resolve)(opts.rootPath);
|
|
381
|
+
return { root, ...buildFilePaths(root), source: "arg" };
|
|
382
|
+
}
|
|
383
|
+
const envRoot = process.env.DRUUMEN_SESSIONS_DB_ROOT;
|
|
384
|
+
if (typeof envRoot === "string" && envRoot.length > 0) {
|
|
385
|
+
const root = (0, import_node_path.resolve)(envRoot);
|
|
386
|
+
return { root, ...buildFilePaths(root), source: "env" };
|
|
387
|
+
}
|
|
388
|
+
const startCwd = (0, import_node_path.resolve)(
|
|
389
|
+
typeof opts.cwd === "string" && opts.cwd.length > 0 ? opts.cwd : process.cwd()
|
|
390
|
+
);
|
|
391
|
+
const found = ascendForExistingDb(startCwd);
|
|
392
|
+
if (found) {
|
|
393
|
+
return { root: found.root, ...buildFilePaths(found.root), source: found.source };
|
|
394
|
+
}
|
|
395
|
+
const defaultRoot = (0, import_node_path.join)(startCwd, ".dru-code");
|
|
396
|
+
return { root: defaultRoot, ...buildFilePaths(defaultRoot), source: "default" };
|
|
397
|
+
}
|
|
398
|
+
function buildFilePaths(root) {
|
|
399
|
+
return {
|
|
400
|
+
eventsJsonl: (0, import_node_path.join)(root, STORAGE_FILENAMES.eventsJsonl),
|
|
401
|
+
projectionJson: (0, import_node_path.join)(root, STORAGE_FILENAMES.projectionJson),
|
|
402
|
+
lockFile: (0, import_node_path.join)(root, STORAGE_FILENAMES.lockFile)
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function ascendForExistingDb(startCwd) {
|
|
406
|
+
let cwd = startCwd;
|
|
407
|
+
for (let depth = 0; depth < MAX_ASCEND_DEPTH; depth++) {
|
|
408
|
+
const ticketsLogsRoot = (0, import_node_path.join)(cwd, "tickets", "_logs");
|
|
409
|
+
if ((0, import_node_fs2.existsSync)((0, import_node_path.join)(ticketsLogsRoot, STORAGE_FILENAMES.projectionJson))) {
|
|
410
|
+
return { root: ticketsLogsRoot, source: "tickets-logs" };
|
|
411
|
+
}
|
|
412
|
+
const druCodeRoot = (0, import_node_path.join)(cwd, ".dru-code");
|
|
413
|
+
if ((0, import_node_fs2.existsSync)((0, import_node_path.join)(druCodeRoot, STORAGE_FILENAMES.projectionJson))) {
|
|
414
|
+
return { root: druCodeRoot, source: "dru-code" };
|
|
415
|
+
}
|
|
416
|
+
const parent = (0, import_node_path.dirname)(cwd);
|
|
417
|
+
if (parent === cwd) break;
|
|
418
|
+
cwd = parent;
|
|
419
|
+
}
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
function pathsFromRoot(root) {
|
|
423
|
+
if (typeof root !== "string" || root.length === 0) {
|
|
424
|
+
throw new TypeError("pathsFromRoot: root must be a non-empty string");
|
|
425
|
+
}
|
|
426
|
+
const abs = (0, import_node_path.isAbsolute)(root) ? root : (0, import_node_path.resolve)(root);
|
|
427
|
+
return { root: abs, ...buildFilePaths(abs) };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// lib/projection.mjs
|
|
431
|
+
var SCHEMA_VERSION = 2;
|
|
432
|
+
var FINGERPRINT_VERSIONS = ["first_human_prompt_v1", "lineage_prefix_v1"];
|
|
433
|
+
function emptyProjection() {
|
|
434
|
+
return {
|
|
435
|
+
_meta: {
|
|
436
|
+
schema_version: SCHEMA_VERSION,
|
|
437
|
+
fingerprint_versions: [...FINGERPRINT_VERSIONS],
|
|
438
|
+
updated: null,
|
|
439
|
+
event_count: 0,
|
|
440
|
+
last_event_id: null
|
|
441
|
+
},
|
|
442
|
+
sessions: {}
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
function emptySession(stableId, ts) {
|
|
446
|
+
return {
|
|
447
|
+
stable_id: stableId,
|
|
448
|
+
alias: null,
|
|
449
|
+
claude_session_ids: [],
|
|
450
|
+
transcript_files: [],
|
|
451
|
+
fingerprints: {
|
|
452
|
+
first_human_prompt_v1: null,
|
|
453
|
+
lineage_prefix_v1: null
|
|
454
|
+
},
|
|
455
|
+
parent_session_id: null,
|
|
456
|
+
parent_candidate_ids: [],
|
|
457
|
+
// Count of parent candidates that resolveIdentity omitted from the most
|
|
458
|
+
// recent session_seen due to the MAX_PARENT_CANDIDATES cap. 0 means the
|
|
459
|
+
// surfaced parent_candidate_ids are complete; >0 means CLI / audit
|
|
460
|
+
// should render "+ N more" or trigger a rebuild-from-events drill-down.
|
|
461
|
+
// Last-write-wins (mirrors identity_resolution semantics).
|
|
462
|
+
parent_candidates_omitted_count: 0,
|
|
463
|
+
// Audit trail of how the most recent session_seen resolved this stable_id
|
|
464
|
+
// — overwritten on every session_seen (always reflects the latest signal
|
|
465
|
+
// set). Null on first creation; populated by reduceSessionSeen when the
|
|
466
|
+
// event payload carries it. See identity.mjs / recordSessionSeen.
|
|
467
|
+
identity_resolution: null,
|
|
468
|
+
worktree_path_observed: null,
|
|
469
|
+
worktree_realpath: null,
|
|
470
|
+
worktree_registry_name: null,
|
|
471
|
+
git_common_dir: null,
|
|
472
|
+
branch_at_start: null,
|
|
473
|
+
branch_current: null,
|
|
474
|
+
head_at_start: null,
|
|
475
|
+
head_last_seen: null,
|
|
476
|
+
tasks: [],
|
|
477
|
+
projects: [],
|
|
478
|
+
activity_state: "active",
|
|
479
|
+
outcome: "open",
|
|
480
|
+
closed_at: null,
|
|
481
|
+
closed_reason: null,
|
|
482
|
+
created_at: ts,
|
|
483
|
+
last_progress_at: ts,
|
|
484
|
+
first_prompt_preview: null
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function applyEvent(projection, event) {
|
|
488
|
+
if (!projection || typeof projection !== "object" || !projection.sessions) {
|
|
489
|
+
throw new TypeError("applyEvent: projection missing or malformed");
|
|
490
|
+
}
|
|
491
|
+
if (!event || typeof event !== "object") {
|
|
492
|
+
throw new TypeError("applyEvent: event missing");
|
|
493
|
+
}
|
|
494
|
+
const { op, stable_id: stableId, ts } = event;
|
|
495
|
+
if (typeof stableId !== "string" || stableId.length === 0) {
|
|
496
|
+
throw new TypeError("applyEvent: event.stable_id required");
|
|
497
|
+
}
|
|
498
|
+
let session = projection.sessions[stableId];
|
|
499
|
+
if (!session) {
|
|
500
|
+
session = emptySession(stableId, ts);
|
|
501
|
+
projection.sessions[stableId] = session;
|
|
502
|
+
}
|
|
503
|
+
switch (op) {
|
|
504
|
+
case "session_seen":
|
|
505
|
+
reduceSessionSeen(session, event);
|
|
506
|
+
break;
|
|
507
|
+
case "session_link":
|
|
508
|
+
reduceSessionLink(session, event);
|
|
509
|
+
break;
|
|
510
|
+
case "alias_set":
|
|
511
|
+
reduceAliasSet(session, event);
|
|
512
|
+
break;
|
|
513
|
+
case "parent_set":
|
|
514
|
+
reduceParentSet(session, event);
|
|
515
|
+
break;
|
|
516
|
+
case "close":
|
|
517
|
+
reduceClose(session, event);
|
|
518
|
+
break;
|
|
519
|
+
case "sweep":
|
|
520
|
+
reduceSweep(session, event);
|
|
521
|
+
break;
|
|
522
|
+
case "session_unlink":
|
|
523
|
+
reduceSessionUnlink(session, event);
|
|
524
|
+
break;
|
|
525
|
+
case "manual_link":
|
|
526
|
+
reduceManualLink(session, event);
|
|
527
|
+
break;
|
|
528
|
+
default:
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
if (op !== "sweep" && ts && (!session.last_progress_at || ts > session.last_progress_at)) {
|
|
532
|
+
session.last_progress_at = ts;
|
|
533
|
+
}
|
|
534
|
+
projection._meta.event_count += 1;
|
|
535
|
+
projection._meta.last_event_id = event.event_id ?? projection._meta.last_event_id;
|
|
536
|
+
projection._meta.updated = ts ?? projection._meta.updated;
|
|
537
|
+
return projection;
|
|
538
|
+
}
|
|
539
|
+
function rebuildFromEvents(events) {
|
|
540
|
+
const projection = emptyProjection();
|
|
541
|
+
if (!Array.isArray(events)) return projection;
|
|
542
|
+
for (const event of events) {
|
|
543
|
+
applyEvent(projection, event);
|
|
544
|
+
}
|
|
545
|
+
return projection;
|
|
546
|
+
}
|
|
547
|
+
function reduceSessionSeen(session, event) {
|
|
548
|
+
const p = event.payload ?? {};
|
|
549
|
+
if (typeof p.claude_session_id === "string" && p.claude_session_id.length > 0) {
|
|
550
|
+
if (!session.claude_session_ids.includes(p.claude_session_id)) {
|
|
551
|
+
session.claude_session_ids.push(p.claude_session_id);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (p.transcript_file && typeof p.transcript_file === "object") {
|
|
555
|
+
const tf = p.transcript_file;
|
|
556
|
+
const idx = session.transcript_files.findIndex((t) => t && t.path === tf.path);
|
|
557
|
+
if (idx === -1) {
|
|
558
|
+
session.transcript_files.push({ ...tf });
|
|
559
|
+
} else {
|
|
560
|
+
session.transcript_files[idx] = { ...session.transcript_files[idx], ...tf };
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (p.fingerprints && typeof p.fingerprints === "object") {
|
|
564
|
+
if (session.fingerprints.first_human_prompt_v1 == null && typeof p.fingerprints.first_human_prompt_v1 === "string") {
|
|
565
|
+
session.fingerprints.first_human_prompt_v1 = p.fingerprints.first_human_prompt_v1;
|
|
566
|
+
}
|
|
567
|
+
if (session.fingerprints.lineage_prefix_v1 == null && typeof p.fingerprints.lineage_prefix_v1 === "string") {
|
|
568
|
+
session.fingerprints.lineage_prefix_v1 = p.fingerprints.lineage_prefix_v1;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
setIfPresent(session, p, "worktree_path_observed");
|
|
572
|
+
setIfPresent(session, p, "worktree_realpath");
|
|
573
|
+
setIfPresent(session, p, "worktree_registry_name");
|
|
574
|
+
setIfPresent(session, p, "git_common_dir");
|
|
575
|
+
setIfPresent(session, p, "branch_current");
|
|
576
|
+
setIfPresent(session, p, "head_last_seen");
|
|
577
|
+
setIfMissing(session, p, "branch_at_start");
|
|
578
|
+
setIfMissing(session, p, "head_at_start");
|
|
579
|
+
setIfMissing(session, p, "first_prompt_preview");
|
|
580
|
+
if (typeof p.cwd === "string" && session.cwd == null) {
|
|
581
|
+
session.cwd = p.cwd;
|
|
582
|
+
}
|
|
583
|
+
if (p.identity_resolution && typeof p.identity_resolution === "object") {
|
|
584
|
+
session.identity_resolution = p.identity_resolution;
|
|
585
|
+
}
|
|
586
|
+
if (typeof p.parent_candidates_omitted_count === "number" && p.parent_candidates_omitted_count >= 0 && Number.isFinite(p.parent_candidates_omitted_count)) {
|
|
587
|
+
session.parent_candidates_omitted_count = p.parent_candidates_omitted_count;
|
|
588
|
+
}
|
|
589
|
+
if (typeof session.parent_candidates_omitted_count !== "number") {
|
|
590
|
+
session.parent_candidates_omitted_count = 0;
|
|
591
|
+
}
|
|
592
|
+
if (Array.isArray(p.parent_candidate_ids)) {
|
|
593
|
+
for (const candidate of p.parent_candidate_ids) {
|
|
594
|
+
if (!candidate || typeof candidate !== "object") continue;
|
|
595
|
+
const candidateId = typeof candidate.stable_id === "string" && candidate.stable_id.length > 0 ? candidate.stable_id : typeof candidate.parent_id === "string" && candidate.parent_id.length > 0 ? candidate.parent_id : typeof candidate.id === "string" && candidate.id.length > 0 ? candidate.id : null;
|
|
596
|
+
if (candidateId === null) continue;
|
|
597
|
+
const dup = session.parent_candidate_ids.find((c) => {
|
|
598
|
+
const existingId = typeof c.stable_id === "string" ? c.stable_id : typeof c.parent_id === "string" ? c.parent_id : typeof c.id === "string" ? c.id : null;
|
|
599
|
+
return existingId !== null && existingId === candidateId;
|
|
600
|
+
});
|
|
601
|
+
if (!dup) session.parent_candidate_ids.push({ ...candidate });
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
function reduceSessionLink(session, event) {
|
|
606
|
+
const p = event.payload ?? {};
|
|
607
|
+
if (p.remove === true) return;
|
|
608
|
+
if (Array.isArray(p.tasks)) {
|
|
609
|
+
for (const t of p.tasks) {
|
|
610
|
+
if (typeof t === "string" && t.length > 0 && !session.tasks.includes(t)) {
|
|
611
|
+
session.tasks.push(t);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (Array.isArray(p.projects)) {
|
|
616
|
+
for (const proj of p.projects) {
|
|
617
|
+
if (typeof proj === "string" && proj.length > 0 && !session.projects.includes(proj)) {
|
|
618
|
+
session.projects.push(proj);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function reduceAliasSet(session, event) {
|
|
624
|
+
const p = event.payload ?? {};
|
|
625
|
+
if (p.alias === null) {
|
|
626
|
+
session.alias = null;
|
|
627
|
+
} else if (typeof p.alias === "string" && p.alias.length > 0) {
|
|
628
|
+
session.alias = p.alias;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
function reduceParentSet(session, event) {
|
|
632
|
+
const p = event.payload ?? {};
|
|
633
|
+
if (p.parent_session_id === null) {
|
|
634
|
+
session.parent_session_id = null;
|
|
635
|
+
} else if (typeof p.parent_session_id === "string" && p.parent_session_id.length > 0) {
|
|
636
|
+
session.parent_session_id = p.parent_session_id;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function reduceClose(session, event) {
|
|
640
|
+
const p = event.payload ?? {};
|
|
641
|
+
if (typeof p.outcome === "string" && p.outcome.length > 0) {
|
|
642
|
+
session.outcome = p.outcome;
|
|
643
|
+
}
|
|
644
|
+
session.closed_at = event.ts ?? session.closed_at;
|
|
645
|
+
if (typeof p.closed_reason === "string") {
|
|
646
|
+
session.closed_reason = p.closed_reason;
|
|
647
|
+
} else if (p.closed_reason === null) {
|
|
648
|
+
session.closed_reason = null;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function reduceSweep(session, event) {
|
|
652
|
+
const p = event.payload ?? {};
|
|
653
|
+
if (typeof p.activity_state === "string" && p.activity_state.length > 0) {
|
|
654
|
+
session.activity_state = p.activity_state;
|
|
655
|
+
}
|
|
656
|
+
if (typeof p.effective_last_progress === "string") {
|
|
657
|
+
if (!session.last_progress_at || p.effective_last_progress > session.last_progress_at) {
|
|
658
|
+
session.last_progress_at = p.effective_last_progress;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function reduceSessionUnlink(session, event) {
|
|
663
|
+
const p = event.payload ?? {};
|
|
664
|
+
if (Array.isArray(p.tasks) && p.tasks.length > 0) {
|
|
665
|
+
const removeSet = new Set(
|
|
666
|
+
p.tasks.filter((t) => typeof t === "string" && t.length > 0)
|
|
667
|
+
);
|
|
668
|
+
if (removeSet.size > 0 && Array.isArray(session.tasks)) {
|
|
669
|
+
session.tasks = session.tasks.filter((t) => !removeSet.has(t));
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (Array.isArray(p.projects) && p.projects.length > 0) {
|
|
673
|
+
const removeSet = new Set(
|
|
674
|
+
p.projects.filter((proj) => typeof proj === "string" && proj.length > 0)
|
|
675
|
+
);
|
|
676
|
+
if (removeSet.size > 0 && Array.isArray(session.projects)) {
|
|
677
|
+
session.projects = session.projects.filter((proj) => !removeSet.has(proj));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
function reduceManualLink(session, event) {
|
|
682
|
+
const p = event.payload ?? {};
|
|
683
|
+
if (Array.isArray(p.parent_candidate_ids)) {
|
|
684
|
+
for (const candidate of p.parent_candidate_ids) {
|
|
685
|
+
if (!candidate || typeof candidate !== "object") continue;
|
|
686
|
+
const candidateId = typeof candidate.parent_id === "string" ? candidate.parent_id : typeof candidate.id === "string" ? candidate.id : null;
|
|
687
|
+
const dup = session.parent_candidate_ids.find((c) => {
|
|
688
|
+
const existingId = typeof c.parent_id === "string" ? c.parent_id : typeof c.id === "string" ? c.id : null;
|
|
689
|
+
return existingId !== null && candidateId !== null && existingId === candidateId;
|
|
690
|
+
});
|
|
691
|
+
if (!dup) {
|
|
692
|
+
session.parent_candidate_ids.push({ ...candidate });
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
function setIfPresent(target, source, key) {
|
|
698
|
+
const v = source[key];
|
|
699
|
+
if (v !== void 0 && v !== null) {
|
|
700
|
+
target[key] = v;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function setIfMissing(target, source, key) {
|
|
704
|
+
const v = source[key];
|
|
705
|
+
if (target[key] == null && v !== void 0 && v !== null) {
|
|
706
|
+
target[key] = v;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// lib/uuid.mjs
|
|
711
|
+
var import_node_crypto = require("node:crypto");
|
|
712
|
+
var PREFIX = "sess_";
|
|
713
|
+
var UUIDV7_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
714
|
+
var SESSION_ID_RE = new RegExp(`^${PREFIX}${UUIDV7_RE.source.slice(1, -1)}$`);
|
|
715
|
+
var lastTimestampMs = -1;
|
|
716
|
+
var lastRandA = 0;
|
|
717
|
+
function generateSessionId() {
|
|
718
|
+
const bytes = Buffer.alloc(16);
|
|
719
|
+
(0, import_node_crypto.randomFillSync)(bytes);
|
|
720
|
+
const nowMs = Date.now();
|
|
721
|
+
let timestampMs = nowMs;
|
|
722
|
+
let randA;
|
|
723
|
+
if (nowMs <= lastTimestampMs) {
|
|
724
|
+
timestampMs = lastTimestampMs;
|
|
725
|
+
randA = lastRandA + 1 & 4095;
|
|
726
|
+
if (randA === 0) {
|
|
727
|
+
timestampMs += 1;
|
|
728
|
+
}
|
|
729
|
+
} else {
|
|
730
|
+
randA = (bytes[6] & 15) << 8 | bytes[7];
|
|
731
|
+
}
|
|
732
|
+
bytes.writeUIntBE(timestampMs, 0, 6);
|
|
733
|
+
bytes[6] = 112 | randA >>> 8 & 15;
|
|
734
|
+
bytes[7] = randA & 255;
|
|
735
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
736
|
+
lastTimestampMs = timestampMs;
|
|
737
|
+
lastRandA = randA;
|
|
738
|
+
const hex = bytes.toString("hex");
|
|
739
|
+
const uuid = hex.slice(0, 8) + "-" + hex.slice(8, 12) + "-" + hex.slice(12, 16) + "-" + hex.slice(16, 20) + "-" + hex.slice(20, 32);
|
|
740
|
+
return PREFIX + uuid;
|
|
741
|
+
}
|
|
742
|
+
function isSessionId(s) {
|
|
743
|
+
return typeof s === "string" && SESSION_ID_RE.test(s);
|
|
744
|
+
}
|
|
745
|
+
function extractTimestamp(sessionId) {
|
|
746
|
+
if (!isSessionId(sessionId)) {
|
|
747
|
+
throw new TypeError(`extractTimestamp: not a sessions-db id: ${sessionId}`);
|
|
748
|
+
}
|
|
749
|
+
const hex = sessionId.slice(PREFIX.length).replace(/-/g, "").slice(0, 12);
|
|
750
|
+
return Number.parseInt(hex, 16);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// lib/storage.mjs
|
|
754
|
+
var REPO_ROOT_DEFAULT = process.cwd();
|
|
755
|
+
var MAX_EVENT_BYTES = 4096;
|
|
756
|
+
var PATHS = Object.freeze({
|
|
757
|
+
eventsJsonl: "tickets/_logs/sessions-db-events.jsonl",
|
|
758
|
+
projectionJson: "tickets/_logs/sessions-db.json",
|
|
759
|
+
lockFile: "tickets/_logs/sessions-db.lock"
|
|
760
|
+
});
|
|
761
|
+
function newEvent({ op, stable_id, payload, ts, event_id }) {
|
|
762
|
+
if (typeof op !== "string" || op.length === 0) {
|
|
763
|
+
throw new TypeError("newEvent: op required");
|
|
764
|
+
}
|
|
765
|
+
if (typeof stable_id !== "string" || stable_id.length === 0) {
|
|
766
|
+
throw new TypeError("newEvent: stable_id required");
|
|
767
|
+
}
|
|
768
|
+
return {
|
|
769
|
+
ts: ts ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
770
|
+
// generateSessionId returns `sess_<uuidv7>` — re-prefix to `evt_` so
|
|
771
|
+
// event ids and stable ids are visually distinct in jsonl tails.
|
|
772
|
+
event_id: event_id ?? `evt_${generateSessionId().slice("sess_".length)}`,
|
|
773
|
+
op,
|
|
774
|
+
stable_id,
|
|
775
|
+
payload: payload ?? {}
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
async function appendEvent(event, opts = {}) {
|
|
779
|
+
const { eventsPath } = resolvePaths(opts);
|
|
780
|
+
ensureParentDir(eventsPath);
|
|
781
|
+
const line = JSON.stringify(event) + "\n";
|
|
782
|
+
const bytes = Buffer.byteLength(line, "utf8");
|
|
783
|
+
if (bytes > MAX_EVENT_BYTES) {
|
|
784
|
+
throw new Error(
|
|
785
|
+
`appendEvent: event payload too large (${bytes} bytes, max ${MAX_EVENT_BYTES}). Reduce payload size (sanitize transcript previews / fingerprints) or split into multiple events.`
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
(0, import_node_fs3.appendFileSync)(eventsPath, line, { flag: "a" });
|
|
789
|
+
}
|
|
790
|
+
function readAllEvents(opts = {}) {
|
|
791
|
+
const { eventsPath } = resolvePaths(opts);
|
|
792
|
+
if (!(0, import_node_fs3.existsSync)(eventsPath)) return { events: [], corruptions: [] };
|
|
793
|
+
const raw = (0, import_node_fs3.readFileSync)(eventsPath, "utf8");
|
|
794
|
+
const splitLines = raw.split("\n");
|
|
795
|
+
const nonEmpty = [];
|
|
796
|
+
for (let i = 0; i < splitLines.length; i++) {
|
|
797
|
+
if (splitLines[i].length > 0) {
|
|
798
|
+
nonEmpty.push({ lineNumber: i + 1, content: splitLines[i] });
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
const endsWithNewline = raw.length > 0 && raw.endsWith("\n");
|
|
802
|
+
const events = [];
|
|
803
|
+
const corruptions = [];
|
|
804
|
+
for (let idx = 0; idx < nonEmpty.length; idx++) {
|
|
805
|
+
const { lineNumber, content } = nonEmpty[idx];
|
|
806
|
+
try {
|
|
807
|
+
events.push(JSON.parse(content));
|
|
808
|
+
} catch (err) {
|
|
809
|
+
const isLastNonEmpty = idx === nonEmpty.length - 1;
|
|
810
|
+
const isTailPartial = isLastNonEmpty && !endsWithNewline;
|
|
811
|
+
corruptions.push({
|
|
812
|
+
lineNumber,
|
|
813
|
+
kind: isTailPartial ? "tail_partial" : "middle_corruption",
|
|
814
|
+
tolerated: isTailPartial,
|
|
815
|
+
excerpt: content.slice(0, 80),
|
|
816
|
+
error: String(err)
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return { events, corruptions };
|
|
821
|
+
}
|
|
822
|
+
async function loadProjection(opts = {}) {
|
|
823
|
+
const { projectionPath } = resolvePaths(opts);
|
|
824
|
+
if (!(0, import_node_fs3.existsSync)(projectionPath)) {
|
|
825
|
+
return rebuildProjectionInMemory(opts);
|
|
826
|
+
}
|
|
827
|
+
let raw;
|
|
828
|
+
try {
|
|
829
|
+
raw = (0, import_node_fs3.readFileSync)(projectionPath, "utf8");
|
|
830
|
+
} catch {
|
|
831
|
+
return rebuildProjectionInMemory(opts);
|
|
832
|
+
}
|
|
833
|
+
try {
|
|
834
|
+
const parsed = JSON.parse(raw);
|
|
835
|
+
if (!parsed || typeof parsed !== "object" || !parsed.sessions || typeof parsed.sessions !== "object" || !parsed._meta || typeof parsed._meta !== "object") {
|
|
836
|
+
return rebuildProjectionInMemory(opts);
|
|
837
|
+
}
|
|
838
|
+
return parsed;
|
|
839
|
+
} catch {
|
|
840
|
+
return rebuildProjectionInMemory(opts);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
async function saveProjection(projection, opts = {}) {
|
|
844
|
+
const { projectionPath, lockPath } = resolvePaths(opts);
|
|
845
|
+
ensureParentDir(projectionPath);
|
|
846
|
+
ensureParentDir(lockPath);
|
|
847
|
+
const withLock = opts.withLock !== false;
|
|
848
|
+
const lock = withLock ? await acquireLock(lockPath, {
|
|
849
|
+
timeoutMs: opts.lockTimeoutMs,
|
|
850
|
+
retryMs: opts.lockRetryMs
|
|
851
|
+
}) : null;
|
|
852
|
+
try {
|
|
853
|
+
saveProjectionUnlocked(projection, projectionPath);
|
|
854
|
+
} finally {
|
|
855
|
+
if (lock) lock.release();
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
function saveProjectionUnlocked(projection, projectionPath) {
|
|
859
|
+
const tmpPath = `${projectionPath}.tmp.${process.pid}`;
|
|
860
|
+
try {
|
|
861
|
+
if (projection && projection._meta) {
|
|
862
|
+
projection._meta.updated = (/* @__PURE__ */ new Date()).toISOString();
|
|
863
|
+
}
|
|
864
|
+
const body = JSON.stringify(projection, null, 2);
|
|
865
|
+
const fd = (0, import_node_fs3.openSync)(tmpPath, "w");
|
|
866
|
+
try {
|
|
867
|
+
(0, import_node_fs3.writeSync)(fd, body);
|
|
868
|
+
(0, import_node_fs3.fsyncSync)(fd);
|
|
869
|
+
} finally {
|
|
870
|
+
(0, import_node_fs3.closeSync)(fd);
|
|
871
|
+
}
|
|
872
|
+
(0, import_node_fs3.renameSync)(tmpPath, projectionPath);
|
|
873
|
+
} catch (err) {
|
|
874
|
+
try {
|
|
875
|
+
if ((0, import_node_fs3.existsSync)(tmpPath)) (0, import_node_fs3.unlinkSync)(tmpPath);
|
|
876
|
+
} catch {
|
|
877
|
+
}
|
|
878
|
+
throw err;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
async function rebuildProjection(opts = {}) {
|
|
882
|
+
const { projection, toleratedCorruptions } = rebuildProjectionInMemoryDetailed(opts);
|
|
883
|
+
await saveProjection(projection, opts);
|
|
884
|
+
return {
|
|
885
|
+
sessionCount: Object.keys(projection.sessions).length,
|
|
886
|
+
eventCount: projection._meta.event_count,
|
|
887
|
+
toleratedCorruptions
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
async function tryUpdateProjection(event, opts = {}) {
|
|
891
|
+
try {
|
|
892
|
+
await appendEvent(event, opts);
|
|
893
|
+
} catch (err) {
|
|
894
|
+
return { ok: false, error: `append: ${err && err.message ? err.message : String(err)}` };
|
|
895
|
+
}
|
|
896
|
+
const { lockPath } = resolvePaths(opts);
|
|
897
|
+
ensureParentDir(lockPath);
|
|
898
|
+
let lock;
|
|
899
|
+
try {
|
|
900
|
+
lock = await acquireLock(lockPath, {
|
|
901
|
+
timeoutMs: opts.lockTimeoutMs,
|
|
902
|
+
retryMs: opts.lockRetryMs
|
|
903
|
+
});
|
|
904
|
+
} catch (err) {
|
|
905
|
+
return { ok: false, error: err && err.message ? err.message : String(err) };
|
|
906
|
+
}
|
|
907
|
+
try {
|
|
908
|
+
const projection = await loadProjection(opts);
|
|
909
|
+
applyEvent(projection, event);
|
|
910
|
+
await saveProjection(projection, { ...opts, withLock: false });
|
|
911
|
+
return { ok: true };
|
|
912
|
+
} catch (err) {
|
|
913
|
+
return { ok: false, error: err && err.message ? err.message : String(err) };
|
|
914
|
+
} finally {
|
|
915
|
+
lock.release();
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
async function recordSessionSeen(opts) {
|
|
919
|
+
if (!opts || typeof opts !== "object") {
|
|
920
|
+
return { ok: false, error: "recordSessionSeen: opts required" };
|
|
921
|
+
}
|
|
922
|
+
const { claudeSessionId, payloadBuilder } = opts;
|
|
923
|
+
if (typeof claudeSessionId !== "string" || claudeSessionId.length === 0) {
|
|
924
|
+
return { ok: false, error: "recordSessionSeen: claudeSessionId required" };
|
|
925
|
+
}
|
|
926
|
+
if (typeof payloadBuilder !== "function") {
|
|
927
|
+
return { ok: false, error: "recordSessionSeen: payloadBuilder required" };
|
|
928
|
+
}
|
|
929
|
+
const { lockPath } = resolvePaths(opts);
|
|
930
|
+
ensureParentDir(lockPath);
|
|
931
|
+
let lock;
|
|
932
|
+
try {
|
|
933
|
+
lock = await acquireLock(lockPath, {
|
|
934
|
+
timeoutMs: opts.lockTimeoutMs,
|
|
935
|
+
retryMs: opts.lockRetryMs
|
|
936
|
+
});
|
|
937
|
+
} catch (err) {
|
|
938
|
+
return { ok: false, error: `lock: ${err && err.message ? err.message : String(err)}` };
|
|
939
|
+
}
|
|
940
|
+
try {
|
|
941
|
+
const projection = await loadProjection(opts);
|
|
942
|
+
const identityResolution = resolveIdentity({
|
|
943
|
+
projection,
|
|
944
|
+
claudeSessionId,
|
|
945
|
+
transcriptMeta: opts.transcriptMeta ?? null,
|
|
946
|
+
gitContext: opts.gitContext ?? null,
|
|
947
|
+
cwd: opts.cwd ?? null,
|
|
948
|
+
fingerprints: opts.fingerprints ?? null,
|
|
949
|
+
now: opts.now,
|
|
950
|
+
timeWindowHours: opts.timeWindowHours,
|
|
951
|
+
minCorroborators: opts.minCorroborators,
|
|
952
|
+
mintStableId: generateSessionId
|
|
953
|
+
});
|
|
954
|
+
const stableId = identityResolution.stableId;
|
|
955
|
+
const minted = identityResolution.source === "minted";
|
|
956
|
+
let payload;
|
|
957
|
+
try {
|
|
958
|
+
payload = payloadBuilder(stableId, identityResolution);
|
|
959
|
+
} catch (err) {
|
|
960
|
+
return {
|
|
961
|
+
ok: false,
|
|
962
|
+
error: `payloadBuilder: ${err && err.message ? err.message : String(err)}`
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
if (!payload || typeof payload !== "object") {
|
|
966
|
+
payload = {};
|
|
967
|
+
}
|
|
968
|
+
if (typeof payload.claude_session_id !== "string" || payload.claude_session_id.length === 0) {
|
|
969
|
+
payload = { ...payload, claude_session_id: claudeSessionId };
|
|
970
|
+
}
|
|
971
|
+
if (opts.storeFirstPrompt === false) {
|
|
972
|
+
payload.first_prompt_preview = null;
|
|
973
|
+
}
|
|
974
|
+
if (payload.identity_resolution === void 0) {
|
|
975
|
+
payload.identity_resolution = {
|
|
976
|
+
source: identityResolution.source,
|
|
977
|
+
confidence: identityResolution.confidence,
|
|
978
|
+
matched: identityResolution.matched
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
if (Array.isArray(identityResolution.parentCandidates) && identityResolution.parentCandidates.length > 0) {
|
|
982
|
+
const existing = Array.isArray(payload.parent_candidate_ids) ? payload.parent_candidate_ids : [];
|
|
983
|
+
payload.parent_candidate_ids = [
|
|
984
|
+
...existing,
|
|
985
|
+
...identityResolution.parentCandidates
|
|
986
|
+
];
|
|
987
|
+
}
|
|
988
|
+
if (typeof identityResolution.parentCandidatesOmittedCount === "number" && identityResolution.parentCandidatesOmittedCount > 0 && payload.parent_candidates_omitted_count === void 0) {
|
|
989
|
+
payload.parent_candidates_omitted_count = identityResolution.parentCandidatesOmittedCount;
|
|
990
|
+
}
|
|
991
|
+
const event = newEvent({
|
|
992
|
+
op: "session_seen",
|
|
993
|
+
stable_id: stableId,
|
|
994
|
+
payload
|
|
995
|
+
});
|
|
996
|
+
try {
|
|
997
|
+
await appendEvent(event, opts);
|
|
998
|
+
} catch (err) {
|
|
999
|
+
return {
|
|
1000
|
+
ok: false,
|
|
1001
|
+
error: `append: ${err && err.message ? err.message : String(err)}`
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
try {
|
|
1005
|
+
applyEvent(projection, event);
|
|
1006
|
+
await saveProjection(projection, { ...opts, withLock: false });
|
|
1007
|
+
} catch (err) {
|
|
1008
|
+
return {
|
|
1009
|
+
ok: false,
|
|
1010
|
+
error: `projection: ${err && err.message ? err.message : String(err)}`
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
return {
|
|
1014
|
+
ok: true,
|
|
1015
|
+
stableId,
|
|
1016
|
+
eventId: event.event_id,
|
|
1017
|
+
minted,
|
|
1018
|
+
identityResolution
|
|
1019
|
+
};
|
|
1020
|
+
} finally {
|
|
1021
|
+
lock.release();
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
function resolvePaths(opts) {
|
|
1025
|
+
if (opts && opts.paths) {
|
|
1026
|
+
const root = opts.root ?? REPO_ROOT_DEFAULT;
|
|
1027
|
+
const abs = (p) => (0, import_node_path2.isAbsolute)(p) ? p : (0, import_node_path2.resolve)(root, p);
|
|
1028
|
+
return {
|
|
1029
|
+
eventsPath: abs(opts.paths.eventsJsonl),
|
|
1030
|
+
projectionPath: abs(opts.paths.projectionJson),
|
|
1031
|
+
lockPath: abs(opts.paths.lockFile)
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
if (opts && typeof opts.rootPath === "string" && opts.rootPath.length > 0) {
|
|
1035
|
+
const r2 = resolveStoragePaths({ rootPath: opts.rootPath });
|
|
1036
|
+
return { eventsPath: r2.eventsJsonl, projectionPath: r2.projectionJson, lockPath: r2.lockFile };
|
|
1037
|
+
}
|
|
1038
|
+
if (opts && typeof opts.root === "string" && opts.root.length > 0) {
|
|
1039
|
+
const root = opts.root;
|
|
1040
|
+
const abs = (p) => (0, import_node_path2.isAbsolute)(p) ? p : (0, import_node_path2.resolve)(root, p);
|
|
1041
|
+
return {
|
|
1042
|
+
eventsPath: abs(PATHS.eventsJsonl),
|
|
1043
|
+
projectionPath: abs(PATHS.projectionJson),
|
|
1044
|
+
lockPath: abs(PATHS.lockFile)
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
const r = resolveStoragePaths({ cwd: opts && opts.cwd });
|
|
1048
|
+
return { eventsPath: r.eventsJsonl, projectionPath: r.projectionJson, lockPath: r.lockFile };
|
|
1049
|
+
}
|
|
1050
|
+
function ensureParentDir(filePath) {
|
|
1051
|
+
const dir = (0, import_node_path2.dirname)(filePath);
|
|
1052
|
+
(0, import_node_fs3.mkdirSync)(dir, { recursive: true });
|
|
1053
|
+
}
|
|
1054
|
+
function readAllEventsOrThrow(opts) {
|
|
1055
|
+
const { events, corruptions } = readAllEvents(opts);
|
|
1056
|
+
const fatal = corruptions.filter((c) => !c.tolerated);
|
|
1057
|
+
if (fatal.length > 0) {
|
|
1058
|
+
const summary = fatal.map((c) => `line ${c.lineNumber}: ${c.error}`).slice(0, 5).join("; ");
|
|
1059
|
+
const err = new Error(
|
|
1060
|
+
`events.jsonl middle-line corruption (${fatal.length} line${fatal.length === 1 ? "" : "s"}): ${summary}`
|
|
1061
|
+
);
|
|
1062
|
+
err.corruptions = fatal;
|
|
1063
|
+
throw err;
|
|
1064
|
+
}
|
|
1065
|
+
return { events, toleratedCorruptions: corruptions.length };
|
|
1066
|
+
}
|
|
1067
|
+
function rebuildProjectionInMemory(opts) {
|
|
1068
|
+
const { events } = readAllEventsOrThrow(opts);
|
|
1069
|
+
if (events.length === 0) return emptyProjection();
|
|
1070
|
+
return rebuildFromEvents(events);
|
|
1071
|
+
}
|
|
1072
|
+
function rebuildProjectionInMemoryDetailed(opts) {
|
|
1073
|
+
const { events, toleratedCorruptions } = readAllEventsOrThrow(opts);
|
|
1074
|
+
const projection = events.length === 0 ? emptyProjection() : rebuildFromEvents(events);
|
|
1075
|
+
return { projection, toleratedCorruptions };
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// lib/sweep.mjs
|
|
1079
|
+
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
1080
|
+
var DEFAULT_IDLE_THRESHOLD_DAYS = 14;
|
|
1081
|
+
var DEFAULT_ARCHIVE_THRESHOLD_DAYS = 30;
|
|
1082
|
+
function computeSweepTransitions(projection, opts = {}) {
|
|
1083
|
+
const now = typeof opts.now === "number" ? opts.now : Date.now();
|
|
1084
|
+
const idleThreshold = pickThreshold(
|
|
1085
|
+
opts.idleThresholdDays,
|
|
1086
|
+
projection && projection._meta && projection._meta.idle_threshold_days,
|
|
1087
|
+
DEFAULT_IDLE_THRESHOLD_DAYS
|
|
1088
|
+
);
|
|
1089
|
+
const archiveThreshold = pickThreshold(
|
|
1090
|
+
opts.archiveThresholdDays,
|
|
1091
|
+
projection && projection._meta && projection._meta.archive_threshold_days,
|
|
1092
|
+
DEFAULT_ARCHIVE_THRESHOLD_DAYS
|
|
1093
|
+
);
|
|
1094
|
+
const sessions = projection && projection.sessions ? projection.sessions : {};
|
|
1095
|
+
const transitions = [];
|
|
1096
|
+
for (const [stableId, session] of Object.entries(sessions)) {
|
|
1097
|
+
if (!session || typeof session !== "object") continue;
|
|
1098
|
+
if (session.activity_state === "archived") continue;
|
|
1099
|
+
const hasSignal = hasAnyParseableTimestamp(session);
|
|
1100
|
+
if (!hasSignal) {
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
const effective = computeEffectiveLastProgress(session);
|
|
1104
|
+
const effectiveMs = Date.parse(effective);
|
|
1105
|
+
if (!Number.isFinite(effectiveMs)) continue;
|
|
1106
|
+
const ageMs = now - effectiveMs;
|
|
1107
|
+
const ageDays = Math.floor(ageMs / MS_PER_DAY);
|
|
1108
|
+
let target;
|
|
1109
|
+
if (ageDays >= archiveThreshold) target = "archived";
|
|
1110
|
+
else if (ageDays >= idleThreshold) target = "idle";
|
|
1111
|
+
else target = "active";
|
|
1112
|
+
if (target === session.activity_state) continue;
|
|
1113
|
+
transitions.push({
|
|
1114
|
+
stable_id: stableId,
|
|
1115
|
+
from_state: session.activity_state,
|
|
1116
|
+
to_state: target,
|
|
1117
|
+
effective_last_progress: effective,
|
|
1118
|
+
age_days: ageDays
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
return transitions;
|
|
1122
|
+
}
|
|
1123
|
+
function computeEffectiveLastProgress(session) {
|
|
1124
|
+
if (!session || typeof session !== "object") {
|
|
1125
|
+
return (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1126
|
+
}
|
|
1127
|
+
let maxEpoch = -Infinity;
|
|
1128
|
+
const considerCandidate = (raw) => {
|
|
1129
|
+
if (typeof raw !== "string" || raw.length === 0) return;
|
|
1130
|
+
const epoch = Date.parse(raw);
|
|
1131
|
+
if (!Number.isFinite(epoch)) return;
|
|
1132
|
+
if (epoch > maxEpoch) maxEpoch = epoch;
|
|
1133
|
+
};
|
|
1134
|
+
considerCandidate(session.last_progress_at);
|
|
1135
|
+
if (Array.isArray(session.transcript_files)) {
|
|
1136
|
+
for (const tf of session.transcript_files) {
|
|
1137
|
+
if (tf && typeof tf === "object") considerCandidate(tf.mtime);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
considerCandidate(session.hive_watcher_last_seen);
|
|
1141
|
+
if (maxEpoch === -Infinity) {
|
|
1142
|
+
return (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1143
|
+
}
|
|
1144
|
+
return new Date(maxEpoch).toISOString();
|
|
1145
|
+
}
|
|
1146
|
+
function hasAnyParseableTimestamp(session) {
|
|
1147
|
+
if (typeof session.last_progress_at === "string" && Number.isFinite(Date.parse(session.last_progress_at))) {
|
|
1148
|
+
return true;
|
|
1149
|
+
}
|
|
1150
|
+
if (Array.isArray(session.transcript_files)) {
|
|
1151
|
+
for (const tf of session.transcript_files) {
|
|
1152
|
+
if (tf && typeof tf.mtime === "string" && Number.isFinite(Date.parse(tf.mtime))) {
|
|
1153
|
+
return true;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
if (typeof session.hive_watcher_last_seen === "string" && Number.isFinite(Date.parse(session.hive_watcher_last_seen))) {
|
|
1158
|
+
return true;
|
|
1159
|
+
}
|
|
1160
|
+
return false;
|
|
1161
|
+
}
|
|
1162
|
+
function pickThreshold(optsValue, metaValue, fallback) {
|
|
1163
|
+
if (typeof optsValue === "number" && Number.isFinite(optsValue) && optsValue > 0) {
|
|
1164
|
+
return optsValue;
|
|
1165
|
+
}
|
|
1166
|
+
if (typeof metaValue === "number" && Number.isFinite(metaValue) && metaValue > 0) {
|
|
1167
|
+
return metaValue;
|
|
1168
|
+
}
|
|
1169
|
+
return fallback;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// lib/operations.mjs
|
|
1173
|
+
var VALID_OUTCOMES = /* @__PURE__ */ new Set([
|
|
1174
|
+
"open",
|
|
1175
|
+
"done",
|
|
1176
|
+
"blocked",
|
|
1177
|
+
"abandoned",
|
|
1178
|
+
"merged",
|
|
1179
|
+
"superseded"
|
|
1180
|
+
]);
|
|
1181
|
+
var MAX_PARENT_CHAIN_DEPTH = 50;
|
|
1182
|
+
function storageOpts({ rootPath, root, paths } = {}) {
|
|
1183
|
+
const out = {};
|
|
1184
|
+
if (rootPath !== void 0) out.rootPath = rootPath;
|
|
1185
|
+
if (root !== void 0) out.root = root;
|
|
1186
|
+
if (paths !== void 0) out.paths = paths;
|
|
1187
|
+
return out;
|
|
1188
|
+
}
|
|
1189
|
+
async function ensureSessionExists(stableId, opts) {
|
|
1190
|
+
const projection = await loadProjection(storageOpts(opts));
|
|
1191
|
+
const session = projection.sessions && projection.sessions[stableId];
|
|
1192
|
+
if (!session) {
|
|
1193
|
+
return { ok: false, error: `stable_id not found: ${stableId}`, projection: null };
|
|
1194
|
+
}
|
|
1195
|
+
return { ok: true, projection, session };
|
|
1196
|
+
}
|
|
1197
|
+
async function commitOp({ op, stableId, payload, opts }) {
|
|
1198
|
+
const event = newEvent({ op, stable_id: stableId, payload });
|
|
1199
|
+
const result = await tryUpdateProjection(event, storageOpts(opts));
|
|
1200
|
+
if (!result.ok) {
|
|
1201
|
+
return { ok: false, error: result.error };
|
|
1202
|
+
}
|
|
1203
|
+
return { ok: true, event_id: event.event_id };
|
|
1204
|
+
}
|
|
1205
|
+
async function setAlias(opts) {
|
|
1206
|
+
if (!opts || typeof opts !== "object") {
|
|
1207
|
+
return { ok: false, error: "setAlias: opts required" };
|
|
1208
|
+
}
|
|
1209
|
+
const { stableId, alias, clear } = opts;
|
|
1210
|
+
if (typeof stableId !== "string" || stableId.length === 0) {
|
|
1211
|
+
return { ok: false, error: "setAlias: stableId required" };
|
|
1212
|
+
}
|
|
1213
|
+
const wantsClear = clear === true;
|
|
1214
|
+
const hasAlias = alias !== void 0 && alias !== null;
|
|
1215
|
+
if (wantsClear && hasAlias) {
|
|
1216
|
+
return { ok: false, error: "setAlias: alias and clear are mutually exclusive" };
|
|
1217
|
+
}
|
|
1218
|
+
if (!wantsClear && !hasAlias) {
|
|
1219
|
+
return { ok: false, error: "setAlias: provide alias or clear=true" };
|
|
1220
|
+
}
|
|
1221
|
+
if (hasAlias && (typeof alias !== "string" || alias.length === 0)) {
|
|
1222
|
+
return { ok: false, error: "setAlias: alias must be a non-empty string" };
|
|
1223
|
+
}
|
|
1224
|
+
const exists = await ensureSessionExists(stableId, opts);
|
|
1225
|
+
if (!exists.ok) return { ok: false, error: exists.error };
|
|
1226
|
+
const payload = wantsClear ? { alias: null } : { alias };
|
|
1227
|
+
return commitOp({ op: "alias_set", stableId, payload, opts });
|
|
1228
|
+
}
|
|
1229
|
+
async function linkTask(opts) {
|
|
1230
|
+
if (!opts || typeof opts !== "object") {
|
|
1231
|
+
return { ok: false, error: "linkTask: opts required" };
|
|
1232
|
+
}
|
|
1233
|
+
const { stableId } = opts;
|
|
1234
|
+
if (typeof stableId !== "string" || stableId.length === 0) {
|
|
1235
|
+
return { ok: false, error: "linkTask: stableId required" };
|
|
1236
|
+
}
|
|
1237
|
+
const tasks = normalizeIdList(opts.tasks);
|
|
1238
|
+
const projects = normalizeIdList(opts.projects);
|
|
1239
|
+
if (tasks.length === 0 && projects.length === 0) {
|
|
1240
|
+
return { ok: false, error: "linkTask: provide at least one task or project" };
|
|
1241
|
+
}
|
|
1242
|
+
const exists = await ensureSessionExists(stableId, opts);
|
|
1243
|
+
if (!exists.ok) return { ok: false, error: exists.error };
|
|
1244
|
+
const payload = {};
|
|
1245
|
+
if (tasks.length > 0) payload.tasks = tasks;
|
|
1246
|
+
if (projects.length > 0) payload.projects = projects;
|
|
1247
|
+
return commitOp({ op: "session_link", stableId, payload, opts });
|
|
1248
|
+
}
|
|
1249
|
+
async function unlinkTask(opts) {
|
|
1250
|
+
if (!opts || typeof opts !== "object") {
|
|
1251
|
+
return { ok: false, error: "unlinkTask: opts required" };
|
|
1252
|
+
}
|
|
1253
|
+
const { stableId } = opts;
|
|
1254
|
+
if (typeof stableId !== "string" || stableId.length === 0) {
|
|
1255
|
+
return { ok: false, error: "unlinkTask: stableId required" };
|
|
1256
|
+
}
|
|
1257
|
+
const tasks = normalizeIdList(opts.tasks);
|
|
1258
|
+
const projects = normalizeIdList(opts.projects);
|
|
1259
|
+
if (tasks.length === 0 && projects.length === 0) {
|
|
1260
|
+
return { ok: false, error: "unlinkTask: provide at least one task or project" };
|
|
1261
|
+
}
|
|
1262
|
+
const exists = await ensureSessionExists(stableId, opts);
|
|
1263
|
+
if (!exists.ok) return { ok: false, error: exists.error };
|
|
1264
|
+
const payload = {};
|
|
1265
|
+
if (tasks.length > 0) payload.tasks = tasks;
|
|
1266
|
+
if (projects.length > 0) payload.projects = projects;
|
|
1267
|
+
return commitOp({ op: "session_unlink", stableId, payload, opts });
|
|
1268
|
+
}
|
|
1269
|
+
async function setParent(opts) {
|
|
1270
|
+
if (!opts || typeof opts !== "object") {
|
|
1271
|
+
return { ok: false, error: "setParent: opts required" };
|
|
1272
|
+
}
|
|
1273
|
+
const { childId, parentId, clear } = opts;
|
|
1274
|
+
if (typeof childId !== "string" || childId.length === 0) {
|
|
1275
|
+
return { ok: false, error: "setParent: childId required" };
|
|
1276
|
+
}
|
|
1277
|
+
const wantsClear = clear === true;
|
|
1278
|
+
const hasParent = parentId !== void 0 && parentId !== null;
|
|
1279
|
+
if (wantsClear && hasParent) {
|
|
1280
|
+
return { ok: false, error: "setParent: parentId and clear are mutually exclusive" };
|
|
1281
|
+
}
|
|
1282
|
+
if (!wantsClear && !hasParent) {
|
|
1283
|
+
return { ok: false, error: "setParent: provide parentId or clear=true" };
|
|
1284
|
+
}
|
|
1285
|
+
if (hasParent && (typeof parentId !== "string" || parentId.length === 0)) {
|
|
1286
|
+
return { ok: false, error: "setParent: parentId must be a non-empty string" };
|
|
1287
|
+
}
|
|
1288
|
+
if (hasParent && parentId === childId) {
|
|
1289
|
+
return {
|
|
1290
|
+
ok: false,
|
|
1291
|
+
error: "setParent: parent and child cannot be the same stable_id"
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
const childCheck = await ensureSessionExists(childId, opts);
|
|
1295
|
+
if (!childCheck.ok) return { ok: false, error: childCheck.error };
|
|
1296
|
+
if (hasParent) {
|
|
1297
|
+
const projection = childCheck.projection;
|
|
1298
|
+
const parentSession = projection.sessions && projection.sessions[parentId];
|
|
1299
|
+
if (!parentSession) {
|
|
1300
|
+
return { ok: false, error: `stable_id not found: ${parentId}` };
|
|
1301
|
+
}
|
|
1302
|
+
let cursor = parentId;
|
|
1303
|
+
for (let depth = 0; depth < MAX_PARENT_CHAIN_DEPTH && cursor; depth++) {
|
|
1304
|
+
if (cursor === childId) {
|
|
1305
|
+
return {
|
|
1306
|
+
ok: false,
|
|
1307
|
+
error: `setParent: would create a cycle: proposed parent ${parentId} reaches child ${childId} after ${depth} hop(s)`
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
const ancestor = projection.sessions && projection.sessions[cursor];
|
|
1311
|
+
cursor = ancestor && ancestor.parent_session_id ? ancestor.parent_session_id : null;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
const payload = wantsClear ? { parent_session_id: null } : { parent_session_id: parentId };
|
|
1315
|
+
return commitOp({ op: "parent_set", stableId: childId, payload, opts });
|
|
1316
|
+
}
|
|
1317
|
+
async function closeSession(opts) {
|
|
1318
|
+
if (!opts || typeof opts !== "object") {
|
|
1319
|
+
return { ok: false, error: "closeSession: opts required" };
|
|
1320
|
+
}
|
|
1321
|
+
const { stableId, outcome, reason } = opts;
|
|
1322
|
+
if (typeof stableId !== "string" || stableId.length === 0) {
|
|
1323
|
+
return { ok: false, error: "closeSession: stableId required" };
|
|
1324
|
+
}
|
|
1325
|
+
if (typeof outcome !== "string" || outcome.length === 0) {
|
|
1326
|
+
return { ok: false, error: "closeSession: outcome required" };
|
|
1327
|
+
}
|
|
1328
|
+
if (!VALID_OUTCOMES.has(outcome)) {
|
|
1329
|
+
return {
|
|
1330
|
+
ok: false,
|
|
1331
|
+
error: `closeSession: outcome must be one of: ${[...VALID_OUTCOMES].join(", ")}`
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
if (reason !== void 0 && reason !== null && typeof reason !== "string") {
|
|
1335
|
+
return { ok: false, error: "closeSession: reason must be a string" };
|
|
1336
|
+
}
|
|
1337
|
+
const exists = await ensureSessionExists(stableId, opts);
|
|
1338
|
+
if (!exists.ok) return { ok: false, error: exists.error };
|
|
1339
|
+
const payload = { outcome };
|
|
1340
|
+
if (reason !== void 0) payload.closed_reason = reason;
|
|
1341
|
+
return commitOp({ op: "close", stableId, payload, opts });
|
|
1342
|
+
}
|
|
1343
|
+
async function runSweep(opts = {}) {
|
|
1344
|
+
const idleThresholdDays = opts.idleThresholdDays;
|
|
1345
|
+
const archiveThresholdDays = opts.archiveThresholdDays;
|
|
1346
|
+
if (idleThresholdDays !== void 0 && (!Number.isFinite(idleThresholdDays) || idleThresholdDays <= 0)) {
|
|
1347
|
+
return {
|
|
1348
|
+
ok: false,
|
|
1349
|
+
error: `runSweep: idleThresholdDays must be a positive number (got: ${idleThresholdDays})`
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
if (archiveThresholdDays !== void 0 && (!Number.isFinite(archiveThresholdDays) || archiveThresholdDays <= 0)) {
|
|
1353
|
+
return {
|
|
1354
|
+
ok: false,
|
|
1355
|
+
error: `runSweep: archiveThresholdDays must be a positive number (got: ${archiveThresholdDays})`
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
if (idleThresholdDays !== void 0 && archiveThresholdDays !== void 0 && archiveThresholdDays < idleThresholdDays) {
|
|
1359
|
+
return {
|
|
1360
|
+
ok: false,
|
|
1361
|
+
error: `runSweep: archiveThresholdDays (${archiveThresholdDays}) must be >= idleThresholdDays (${idleThresholdDays})`
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
const projection = await loadProjection(storageOpts(opts));
|
|
1365
|
+
const transitions = computeSweepTransitions(projection, {
|
|
1366
|
+
idleThresholdDays,
|
|
1367
|
+
archiveThresholdDays,
|
|
1368
|
+
now: opts.now
|
|
1369
|
+
});
|
|
1370
|
+
if (opts.dryRun === true) {
|
|
1371
|
+
return { ok: true, dryRun: true, transitions };
|
|
1372
|
+
}
|
|
1373
|
+
const applied = [];
|
|
1374
|
+
const failed = [];
|
|
1375
|
+
for (const t of transitions) {
|
|
1376
|
+
const event = newEvent({
|
|
1377
|
+
op: "sweep",
|
|
1378
|
+
stable_id: t.stable_id,
|
|
1379
|
+
payload: {
|
|
1380
|
+
activity_state: t.to_state,
|
|
1381
|
+
effective_last_progress: t.effective_last_progress
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
const result = await tryUpdateProjection(event, storageOpts(opts));
|
|
1385
|
+
if (result.ok) {
|
|
1386
|
+
applied.push({ ...t, event_id: event.event_id });
|
|
1387
|
+
} else {
|
|
1388
|
+
failed.push({ ...t, error: result.error });
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
const toIdle = applied.filter((a) => a.to_state === "idle").length;
|
|
1392
|
+
const toArchived = applied.filter((a) => a.to_state === "archived").length;
|
|
1393
|
+
return {
|
|
1394
|
+
ok: failed.length === 0,
|
|
1395
|
+
applied,
|
|
1396
|
+
failed,
|
|
1397
|
+
summary: {
|
|
1398
|
+
total: transitions.length,
|
|
1399
|
+
applied: applied.length,
|
|
1400
|
+
failed: failed.length,
|
|
1401
|
+
to_idle: toIdle,
|
|
1402
|
+
to_archived: toArchived
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
function normalizeIdList(input) {
|
|
1407
|
+
if (input === void 0 || input === null) return [];
|
|
1408
|
+
const arr = Array.isArray(input) ? input : [input];
|
|
1409
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1410
|
+
const out = [];
|
|
1411
|
+
for (const v of arr) {
|
|
1412
|
+
if (typeof v !== "string" || v.length === 0) continue;
|
|
1413
|
+
if (seen.has(v)) continue;
|
|
1414
|
+
seen.add(v);
|
|
1415
|
+
out.push(v);
|
|
1416
|
+
}
|
|
1417
|
+
return out;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// lib/init.mjs
|
|
1421
|
+
var import_node_fs4 = require("node:fs");
|
|
1422
|
+
var import_node_path3 = require("node:path");
|
|
1423
|
+
var SCHEMA_VERSION2 = 2;
|
|
1424
|
+
var FINGERPRINT_VERSIONS2 = ["first_human_prompt_v1", "lineage_prefix_v1"];
|
|
1425
|
+
async function initProjection(opts) {
|
|
1426
|
+
if (!opts || typeof opts !== "object") {
|
|
1427
|
+
return { ok: false, error: "initProjection: opts required" };
|
|
1428
|
+
}
|
|
1429
|
+
const { rootPath } = opts;
|
|
1430
|
+
let eventsPath;
|
|
1431
|
+
let projectionPath;
|
|
1432
|
+
let source = "arg";
|
|
1433
|
+
if (opts.paths) {
|
|
1434
|
+
if (typeof rootPath !== "string" || rootPath.length === 0) {
|
|
1435
|
+
return { ok: false, error: "initProjection: rootPath required when paths override is supplied" };
|
|
1436
|
+
}
|
|
1437
|
+
const eventsRel = opts.paths.eventsJsonl ?? PATHS.eventsJsonl;
|
|
1438
|
+
const projectionRel = opts.paths.projectionJson ?? PATHS.projectionJson;
|
|
1439
|
+
const abs = (p) => (0, import_node_path3.isAbsolute)(p) ? p : (0, import_node_path3.resolve)(rootPath, p);
|
|
1440
|
+
eventsPath = abs(eventsRel);
|
|
1441
|
+
projectionPath = abs(projectionRel);
|
|
1442
|
+
} else if (typeof rootPath === "string" && rootPath.length > 0) {
|
|
1443
|
+
const r = resolveStoragePaths({ rootPath });
|
|
1444
|
+
eventsPath = r.eventsJsonl;
|
|
1445
|
+
projectionPath = r.projectionJson;
|
|
1446
|
+
source = r.source;
|
|
1447
|
+
} else {
|
|
1448
|
+
const r = resolveStoragePaths();
|
|
1449
|
+
eventsPath = r.eventsJsonl;
|
|
1450
|
+
projectionPath = r.projectionJson;
|
|
1451
|
+
source = r.source;
|
|
1452
|
+
}
|
|
1453
|
+
const dirsToCreate = /* @__PURE__ */ new Set([(0, import_node_path3.dirname)(eventsPath), (0, import_node_path3.dirname)(projectionPath)]);
|
|
1454
|
+
const created = { dir: false, eventsJsonl: false, projectionJson: false };
|
|
1455
|
+
try {
|
|
1456
|
+
for (const dir of dirsToCreate) {
|
|
1457
|
+
if (!(0, import_node_fs4.existsSync)(dir)) {
|
|
1458
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
1459
|
+
created.dir = true;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
if (!(0, import_node_fs4.existsSync)(eventsPath)) {
|
|
1463
|
+
(0, import_node_fs4.writeFileSync)(eventsPath, "", { flag: "wx" });
|
|
1464
|
+
created.eventsJsonl = true;
|
|
1465
|
+
}
|
|
1466
|
+
if (!(0, import_node_fs4.existsSync)(projectionPath)) {
|
|
1467
|
+
const empty = emptyProjectionLiteral();
|
|
1468
|
+
try {
|
|
1469
|
+
(0, import_node_fs4.writeFileSync)(
|
|
1470
|
+
projectionPath,
|
|
1471
|
+
JSON.stringify(empty, null, 2),
|
|
1472
|
+
{ flag: "wx" }
|
|
1473
|
+
);
|
|
1474
|
+
created.projectionJson = true;
|
|
1475
|
+
} catch (err) {
|
|
1476
|
+
if (err && err.code === "EEXIST") {
|
|
1477
|
+
created.projectionJson = false;
|
|
1478
|
+
} else {
|
|
1479
|
+
throw err;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
} catch (err) {
|
|
1484
|
+
return {
|
|
1485
|
+
ok: false,
|
|
1486
|
+
error: `initProjection: ${err && err.message ? err.message : String(err)}`
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
return {
|
|
1490
|
+
ok: true,
|
|
1491
|
+
created,
|
|
1492
|
+
paths: { eventsJsonl: eventsPath, projectionJson: projectionPath },
|
|
1493
|
+
source
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
function emptyProjectionLiteral() {
|
|
1497
|
+
return {
|
|
1498
|
+
_meta: {
|
|
1499
|
+
schema_version: SCHEMA_VERSION2,
|
|
1500
|
+
fingerprint_versions: [...FINGERPRINT_VERSIONS2],
|
|
1501
|
+
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1502
|
+
event_count: 0,
|
|
1503
|
+
last_event_id: null
|
|
1504
|
+
},
|
|
1505
|
+
sessions: {}
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// lib/watch.mjs
|
|
1510
|
+
var import_node_fs5 = require("node:fs");
|
|
1511
|
+
var import_node_path4 = require("node:path");
|
|
1512
|
+
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
1513
|
+
var DEFAULT_DEBOUNCE_MS = 80;
|
|
1514
|
+
function watchProjection(rootPath, listener, opts = {}) {
|
|
1515
|
+
if (typeof rootPath !== "string" || rootPath.length === 0) {
|
|
1516
|
+
throw new TypeError("watchProjection: rootPath required");
|
|
1517
|
+
}
|
|
1518
|
+
if (typeof listener !== "function") {
|
|
1519
|
+
throw new TypeError("watchProjection: listener function required");
|
|
1520
|
+
}
|
|
1521
|
+
let projectionPath;
|
|
1522
|
+
if (opts.paths && opts.paths.projectionJson) {
|
|
1523
|
+
const projectionRel = opts.paths.projectionJson;
|
|
1524
|
+
projectionPath = (0, import_node_path4.isAbsolute)(projectionRel) ? projectionRel : (0, import_node_path4.resolve)(rootPath, projectionRel);
|
|
1525
|
+
} else {
|
|
1526
|
+
const r = resolveStoragePaths({ rootPath });
|
|
1527
|
+
projectionPath = r.projectionJson;
|
|
1528
|
+
}
|
|
1529
|
+
const pollIntervalMs = typeof opts.pollIntervalMs === "number" && opts.pollIntervalMs > 0 ? opts.pollIntervalMs : DEFAULT_POLL_INTERVAL_MS;
|
|
1530
|
+
const debounceMs = typeof opts.debounceMs === "number" && opts.debounceMs >= 0 ? opts.debounceMs : DEFAULT_DEBOUNCE_MS;
|
|
1531
|
+
let pendingTimer = null;
|
|
1532
|
+
let pendingType = null;
|
|
1533
|
+
const fireSoon = (type) => {
|
|
1534
|
+
pendingType = type;
|
|
1535
|
+
if (pendingTimer !== null) return;
|
|
1536
|
+
pendingTimer = setTimeout(() => {
|
|
1537
|
+
const t = pendingType;
|
|
1538
|
+
pendingTimer = null;
|
|
1539
|
+
pendingType = null;
|
|
1540
|
+
try {
|
|
1541
|
+
listener({ type: t, path: projectionPath });
|
|
1542
|
+
} catch {
|
|
1543
|
+
}
|
|
1544
|
+
}, debounceMs);
|
|
1545
|
+
};
|
|
1546
|
+
let fsWatcher = null;
|
|
1547
|
+
const tryAttachWatcher = () => {
|
|
1548
|
+
if (!(0, import_node_fs5.existsSync)(projectionPath)) return;
|
|
1549
|
+
if (fsWatcher) return;
|
|
1550
|
+
try {
|
|
1551
|
+
fsWatcher = (0, import_node_fs5.watch)(projectionPath, { persistent: false }, (eventType) => {
|
|
1552
|
+
if (eventType === "rename") {
|
|
1553
|
+
try {
|
|
1554
|
+
fsWatcher && fsWatcher.close();
|
|
1555
|
+
} catch {
|
|
1556
|
+
}
|
|
1557
|
+
fsWatcher = null;
|
|
1558
|
+
}
|
|
1559
|
+
fireSoon(eventType === "rename" ? "rename" : "change");
|
|
1560
|
+
});
|
|
1561
|
+
fsWatcher.on("error", () => {
|
|
1562
|
+
try {
|
|
1563
|
+
fsWatcher && fsWatcher.close();
|
|
1564
|
+
} catch {
|
|
1565
|
+
}
|
|
1566
|
+
fsWatcher = null;
|
|
1567
|
+
});
|
|
1568
|
+
} catch {
|
|
1569
|
+
fsWatcher = null;
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
tryAttachWatcher();
|
|
1573
|
+
let lastMtimeMs = readMtimeSafe(projectionPath);
|
|
1574
|
+
let pollTimer = setInterval(() => {
|
|
1575
|
+
if (!fsWatcher) tryAttachWatcher();
|
|
1576
|
+
const current = readMtimeSafe(projectionPath);
|
|
1577
|
+
if (current === null) {
|
|
1578
|
+
if (lastMtimeMs !== null) lastMtimeMs = null;
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
if (lastMtimeMs === null || current !== lastMtimeMs) {
|
|
1582
|
+
lastMtimeMs = current;
|
|
1583
|
+
fireSoon("poll");
|
|
1584
|
+
}
|
|
1585
|
+
}, pollIntervalMs);
|
|
1586
|
+
if (typeof pollTimer.unref === "function") pollTimer.unref();
|
|
1587
|
+
return {
|
|
1588
|
+
dispose() {
|
|
1589
|
+
if (pendingTimer !== null) {
|
|
1590
|
+
clearTimeout(pendingTimer);
|
|
1591
|
+
pendingTimer = null;
|
|
1592
|
+
pendingType = null;
|
|
1593
|
+
}
|
|
1594
|
+
if (pollTimer !== null) {
|
|
1595
|
+
clearInterval(pollTimer);
|
|
1596
|
+
pollTimer = null;
|
|
1597
|
+
}
|
|
1598
|
+
if (fsWatcher) {
|
|
1599
|
+
try {
|
|
1600
|
+
fsWatcher.close();
|
|
1601
|
+
} catch {
|
|
1602
|
+
}
|
|
1603
|
+
fsWatcher = null;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
function readMtimeSafe(path) {
|
|
1609
|
+
try {
|
|
1610
|
+
if (!(0, import_node_fs5.existsSync)(path)) return null;
|
|
1611
|
+
return (0, import_node_fs5.statSync)(path).mtimeMs;
|
|
1612
|
+
} catch {
|
|
1613
|
+
return null;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// lib/sanitize.mjs
|
|
1618
|
+
var SYSTEM_REMINDER_RE = /<system-reminder\b[^>]*>[\s\S]*?<\/system-reminder>/gi;
|
|
1619
|
+
var SYSTEM_RE = /<system\b[^>]*>[\s\S]*?<\/system>/gi;
|
|
1620
|
+
var THINKING_RE = /<thinking\b[^>]*>[\s\S]*?<\/thinking>/gi;
|
|
1621
|
+
var TOOL_USE_RE = /<tool_use\b[^>]*>[\s\S]*?<\/tool_use>/gi;
|
|
1622
|
+
var TOOL_RESULT_RE = /<tool_result\b[^>]*>[\s\S]*?<\/tool_result>/gi;
|
|
1623
|
+
var PARAMETER_RE = /<parameter\b[^>]*>[\s\S]*?<\/parameter>/gi;
|
|
1624
|
+
var IDE_OPENED_RE = /<ide_opened_file\b[^>]*>[\s\S]*?<\/ide_opened_file>/gi;
|
|
1625
|
+
var IDE_SELECTION_RE = /<ide_selection\b[^>]*>[\s\S]*?<\/ide_selection>/gi;
|
|
1626
|
+
var COMMAND_WRAPPER_RE = /<command-name\b[^>]*>[\s\S]*?<\/command-message>/gi;
|
|
1627
|
+
function stripSystemReminders(s) {
|
|
1628
|
+
if (typeof s !== "string") return "";
|
|
1629
|
+
return s.replace(SYSTEM_REMINDER_RE, "").replace(SYSTEM_RE, "").replace(THINKING_RE, "").replace(TOOL_USE_RE, "").replace(TOOL_RESULT_RE, "").replace(PARAMETER_RE, "");
|
|
1630
|
+
}
|
|
1631
|
+
function stripIdeWrappers(s) {
|
|
1632
|
+
if (typeof s !== "string") return "";
|
|
1633
|
+
return s.replace(IDE_OPENED_RE, "").replace(IDE_SELECTION_RE, "").replace(COMMAND_WRAPPER_RE, "");
|
|
1634
|
+
}
|
|
1635
|
+
function sanitizeFirstPrompt(raw, opts = {}) {
|
|
1636
|
+
if (typeof raw !== "string") return "";
|
|
1637
|
+
const maxLen = Number.isFinite(opts.maxLen) && opts.maxLen > 0 ? opts.maxLen : 200;
|
|
1638
|
+
let s = raw;
|
|
1639
|
+
s = s.normalize("NFKC");
|
|
1640
|
+
s = stripSystemReminders(s);
|
|
1641
|
+
s = stripIdeWrappers(s);
|
|
1642
|
+
s = stripSystemReminders(s);
|
|
1643
|
+
s = stripIdeWrappers(s);
|
|
1644
|
+
s = s.replace(/\r\n/g, "\n");
|
|
1645
|
+
s = s.replace(/\n{3,}/g, "\n\n");
|
|
1646
|
+
s = s.trim();
|
|
1647
|
+
if (s.length <= maxLen) return s;
|
|
1648
|
+
const cps = Array.from(s);
|
|
1649
|
+
if (cps.length <= maxLen) return s;
|
|
1650
|
+
return cps.slice(0, Math.max(0, maxLen - 1)).join("") + "\u2026";
|
|
1651
|
+
}
|
|
1652
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1653
|
+
0 && (module.exports = {
|
|
1654
|
+
MAX_ASCEND_DEPTH,
|
|
1655
|
+
MAX_EVENT_BYTES,
|
|
1656
|
+
MAX_PARENT_CANDIDATES,
|
|
1657
|
+
PATHS,
|
|
1658
|
+
STORAGE_FILENAMES,
|
|
1659
|
+
STRONG_CORROBORATORS,
|
|
1660
|
+
WEAK_CORROBORATORS,
|
|
1661
|
+
appendEvent,
|
|
1662
|
+
applyEvent,
|
|
1663
|
+
capParentCandidates,
|
|
1664
|
+
classifyCorroborators,
|
|
1665
|
+
closeSession,
|
|
1666
|
+
collectParentCandidates,
|
|
1667
|
+
computeEffectiveLastProgress,
|
|
1668
|
+
computeSweepTransitions,
|
|
1669
|
+
emptyProjection,
|
|
1670
|
+
emptySession,
|
|
1671
|
+
extractTimestamp,
|
|
1672
|
+
findByClaudeSessionId,
|
|
1673
|
+
findByTranscriptLineage,
|
|
1674
|
+
generateSessionId,
|
|
1675
|
+
initProjection,
|
|
1676
|
+
isSessionId,
|
|
1677
|
+
linkTask,
|
|
1678
|
+
loadProjection,
|
|
1679
|
+
meetsThreshold,
|
|
1680
|
+
newEvent,
|
|
1681
|
+
pathsFromRoot,
|
|
1682
|
+
readAllEvents,
|
|
1683
|
+
rebuildFromEvents,
|
|
1684
|
+
rebuildProjection,
|
|
1685
|
+
recordSessionSeen,
|
|
1686
|
+
resolveIdentity,
|
|
1687
|
+
resolveStoragePaths,
|
|
1688
|
+
runSweep,
|
|
1689
|
+
sanitizeFirstPrompt,
|
|
1690
|
+
saveProjection,
|
|
1691
|
+
scanFingerprintCandidates,
|
|
1692
|
+
setAlias,
|
|
1693
|
+
setParent,
|
|
1694
|
+
stripIdeWrappers,
|
|
1695
|
+
stripSystemReminders,
|
|
1696
|
+
tryUpdateProjection,
|
|
1697
|
+
unlinkTask,
|
|
1698
|
+
watchProjection
|
|
1699
|
+
});
|