@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/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
+ });