@druumen/sessions-db 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +249 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +10 -0
  4. package/README.md +250 -0
  5. package/cli/_write-helpers.mjs +99 -0
  6. package/cli/alias.mjs +115 -0
  7. package/cli/argparse.mjs +296 -0
  8. package/cli/close.mjs +116 -0
  9. package/cli/find.mjs +185 -0
  10. package/cli/format.mjs +277 -0
  11. package/cli/link-parent.mjs +133 -0
  12. package/cli/link.mjs +132 -0
  13. package/cli/rebuild.mjs +98 -0
  14. package/cli/sessions-db-session-start-main.mjs +454 -0
  15. package/cli/sessions-db-session-start.mjs +56 -0
  16. package/cli/sessions-db.mjs +119 -0
  17. package/cli/sweep.mjs +171 -0
  18. package/cli/tree.mjs +127 -0
  19. package/lib/git-context.mjs +479 -0
  20. package/lib/identity.mjs +616 -0
  21. package/lib/index.mjs +145 -0
  22. package/lib/init.mjs +185 -0
  23. package/lib/lock.mjs +86 -0
  24. package/lib/operations.mjs +490 -0
  25. package/lib/paths.mjs +199 -0
  26. package/lib/projection.mjs +496 -0
  27. package/lib/sanitize.mjs +131 -0
  28. package/lib/storage.mjs +759 -0
  29. package/lib/sweep.mjs +209 -0
  30. package/lib/transcript.mjs +230 -0
  31. package/lib/types.mjs +276 -0
  32. package/lib/uuid.mjs +116 -0
  33. package/lib/watch.mjs +217 -0
  34. package/package.json +53 -0
  35. package/types/git-context.d.mts +98 -0
  36. package/types/identity.d.mts +658 -0
  37. package/types/index.d.mts +10 -0
  38. package/types/index.d.ts +127 -0
  39. package/types/init.d.mts +53 -0
  40. package/types/lock.d.mts +18 -0
  41. package/types/operations.d.mts +204 -0
  42. package/types/paths.d.mts +54 -0
  43. package/types/projection.d.mts +79 -0
  44. package/types/sanitize.d.mts +39 -0
  45. package/types/storage.d.mts +276 -0
  46. package/types/sweep.d.mts +58 -0
  47. package/types/transcript.d.mts +59 -0
  48. package/types/types.d.mts +255 -0
  49. package/types/uuid.d.mts +17 -0
  50. package/types/watch.d.mts +33 -0
@@ -0,0 +1,658 @@
1
+ /**
2
+ * Compute strong / weak / total counts from a corroborator hit map.
3
+ * @param {{ same_cwd: boolean, same_worktree_realpath: boolean,
4
+ * same_branch_at_start: boolean, within_time_window: boolean }} hits
5
+ * @returns {{ strong: number, weak: number, total: number }}
6
+ */
7
+ export function classifyCorroborators(hits: {
8
+ same_cwd: boolean;
9
+ same_worktree_realpath: boolean;
10
+ same_branch_at_start: boolean;
11
+ within_time_window: boolean;
12
+ }): {
13
+ strong: number;
14
+ weak: number;
15
+ total: number;
16
+ };
17
+ /**
18
+ * Acceptance gate: meets fingerprint+corroborator threshold for accept-as-
19
+ * identity (vs surface-as-parent_candidate).
20
+ *
21
+ * Requires BOTH:
22
+ * - strong >= 1 (at least one location-anchored signal); AND
23
+ * - total >= minCorroborators (default 2).
24
+ *
25
+ * @param {{ strong: number, weak: number, total: number }} counts
26
+ * @param {{ minCorroborators?: number }} opts
27
+ */
28
+ export function meetsThreshold(counts: {
29
+ strong: number;
30
+ weak: number;
31
+ total: number;
32
+ }, opts?: {
33
+ minCorroborators?: number;
34
+ }): boolean;
35
+ /**
36
+ * Public entry: resolve identity from a hook signal set, OR mint a fresh one.
37
+ *
38
+ * @param {{
39
+ * projection: Projection,
40
+ * claudeSessionId: string,
41
+ * transcriptMeta?: TranscriptMetaInput | null,
42
+ * gitContext?: GitContextInput | null,
43
+ * cwd?: string | null,
44
+ * fingerprints?: FingerprintInput | null,
45
+ * now?: number,
46
+ * timeWindowHours?: number,
47
+ * minCorroborators?: number,
48
+ * mintStableId: () => string,
49
+ * }} input
50
+ * @returns {IdentityResult}
51
+ */
52
+ export function resolveIdentity(input: {
53
+ projection: Projection;
54
+ claudeSessionId: string;
55
+ transcriptMeta?: TranscriptMetaInput | null;
56
+ gitContext?: GitContextInput | null;
57
+ cwd?: string | null;
58
+ fingerprints?: FingerprintInput | null;
59
+ now?: number;
60
+ timeWindowHours?: number;
61
+ minCorroborators?: number;
62
+ mintStableId: () => string;
63
+ }): IdentityResult;
64
+ /**
65
+ * Scan `projection.sessions` for the first record whose `claude_session_ids[]`
66
+ * contains `csid`. Returns the matching `stable_id` or `null`.
67
+ *
68
+ * Empty `claude_session_ids[]` are skipped so a skeleton record produced by
69
+ * a non-session_seen op (e.g. `manual_link`) never false-matches a fresh
70
+ * incoming claude_session_id.
71
+ *
72
+ * Exported for direct testing.
73
+ */
74
+ export function findByClaudeSessionId(projection: any, csid: any): string;
75
+ /**
76
+ * Look for a session whose `transcript_files[*].last_uuid` equals
77
+ * `transcriptMeta.firstParentUuid`. That equality means our incoming
78
+ * transcript starts off the tail of an existing session's transcript — i.e.
79
+ * fork or resume.
80
+ *
81
+ * Returns `{ stableId, matchedPath, matchedLastUuid }` on hit, `null`
82
+ * otherwise. `transcriptMeta == null` (or missing firstParentUuid) returns
83
+ * `null` cleanly.
84
+ *
85
+ * Exported for direct testing.
86
+ */
87
+ export function findByTranscriptLineage(projection: any, transcriptMeta: any): {
88
+ stableId: string;
89
+ matchedPath: any;
90
+ matchedLastUuid: any;
91
+ };
92
+ /**
93
+ * For every session whose fingerprints (any v1) match the incoming ones,
94
+ * compute corroborator hits and return one entry per match — the caller
95
+ * picks acceptance vs parent-candidate based on `minCorroborators`.
96
+ *
97
+ * Exported for direct testing.
98
+ *
99
+ * @param {Projection} projection
100
+ * @param {FingerprintInput|null} fingerprints
101
+ * @param {{
102
+ * cwd: string|null,
103
+ * worktreeRealpath: string|null,
104
+ * branch: string|null,
105
+ * now: number,
106
+ * timeWindowHours: number,
107
+ * }} corrCtx
108
+ * @returns {Array<{
109
+ * stableId: string,
110
+ * fingerprintsMatched: string[],
111
+ * corroborators: { same_cwd: boolean, same_worktree_realpath: boolean,
112
+ * same_branch_at_start: boolean, within_time_window: boolean },
113
+ * corroboratorCount: number,
114
+ * strengthCounts: { strong: number, weak: number, total: number },
115
+ * sessionLastProgressAt: string|null,
116
+ * }>}
117
+ */
118
+ export function scanFingerprintCandidates(projection: Projection, fingerprints: FingerprintInput | null, corrCtx: {
119
+ cwd: string | null;
120
+ worktreeRealpath: string | null;
121
+ branch: string | null;
122
+ now: number;
123
+ timeWindowHours: number;
124
+ }): Array<{
125
+ stableId: string;
126
+ fingerprintsMatched: string[];
127
+ corroborators: {
128
+ same_cwd: boolean;
129
+ same_worktree_realpath: boolean;
130
+ same_branch_at_start: boolean;
131
+ within_time_window: boolean;
132
+ };
133
+ corroboratorCount: number;
134
+ strengthCounts: {
135
+ strong: number;
136
+ weak: number;
137
+ total: number;
138
+ };
139
+ sessionLastProgressAt: string | null;
140
+ }>;
141
+ /**
142
+ * Convert raw fingerprint scan rows into stable parent candidate records,
143
+ * deduped by `stable_id`. The reason payload preserves which fingerprints
144
+ * matched + count summaries (strong + weak + total) so the caller can audit
145
+ * the surface later (e.g. CLI listing parent candidates with their evidence).
146
+ *
147
+ * Bytes-on-disk note: we deliberately do NOT include the per-corroborator
148
+ * boolean map (`{same_cwd, same_worktree_realpath, ...}`) here. That map
149
+ * costs ~120 bytes per candidate and pushes the cumulative payload past
150
+ * MAX_EVENT_BYTES (4 KiB POSIX PIPE_BUF) once a session accumulates ~10
151
+ * candidates. The summary counts are sufficient for "is this candidate
152
+ * strong evidence?" decisions; the exact corroborator vector is recoverable
153
+ * by re-scanning against the seed session when needed (CLI drill-down).
154
+ *
155
+ * @param {ReturnType<typeof scanFingerprintCandidates>} rows
156
+ * @returns {Array<{
157
+ * stable_id: string, source: 'fingerprint', confidence: 'low',
158
+ * reason: { fingerprints_matched: string[],
159
+ * corroborator_count: number,
160
+ * strong_corroborator_count: number,
161
+ * weak_corroborator_count: number },
162
+ * }>}
163
+ */
164
+ export function collectParentCandidates(rows: ReturnType<typeof scanFingerprintCandidates>): Array<{
165
+ stable_id: string;
166
+ source: "fingerprint";
167
+ confidence: "low";
168
+ reason: {
169
+ fingerprints_matched: string[];
170
+ corroborator_count: number;
171
+ strong_corroborator_count: number;
172
+ weak_corroborator_count: number;
173
+ };
174
+ }>;
175
+ /**
176
+ * Cap fingerprint scan rows to MAX_PARENT_CANDIDATES, sorted by
177
+ * (strong corroborator count desc, last_progress recency desc, stable_id
178
+ * asc-tie-break). Returns the surface-able candidate list plus the count of
179
+ * candidates omitted due to the cap so the caller can inject
180
+ * `parent_candidates_omitted_count` into the event payload.
181
+ *
182
+ * The cap exists because the SSoT events.jsonl uses MAX_EVENT_BYTES=4096
183
+ * (POSIX PIPE_BUF guarantee for atomic O_APPEND); an unbounded
184
+ * parent_candidates list can blow that budget and force appendEvent to
185
+ * reject the entire session_seen, losing the audit trail.
186
+ *
187
+ * @param {ReturnType<typeof scanFingerprintCandidates>} rows
188
+ * @param {{ cap?: number }} [opts]
189
+ * @returns {{ list: ReturnType<typeof collectParentCandidates>, omitted: number }}
190
+ */
191
+ export function capParentCandidates(rows: ReturnType<typeof scanFingerprintCandidates>, opts?: {
192
+ cap?: number;
193
+ }): {
194
+ list: ReturnType<typeof collectParentCandidates>;
195
+ omitted: number;
196
+ };
197
+ /**
198
+ * Hard cap on `parentCandidates[]` length. Keeps event payloads safely under
199
+ * `storage.MAX_EVENT_BYTES` (4 KiB POSIX PIPE_BUF guarantee).
200
+ *
201
+ * Empirical sizing (after collectParentCandidates payload trim — see that
202
+ * function's bytes-on-disk note): each candidate serializes to ~241 bytes
203
+ * (stable_id ≈ 50 + reason summary ≈ 130 + JSON wrapping ≈ 60). The rest
204
+ * of a typical session_seen payload (csid + transcript_file + fingerprints
205
+ * + git context + identity_resolution audit) is ~500–600 bytes. Budget:
206
+ * 4096 - 600 baseline = 3496 bytes for candidates
207
+ * 3496 / 241 ≈ 14.5 candidates worst-case
208
+ * We cap at 10 to retain safety margin — 10 × 241 + 600 ≈ 3010 bytes,
209
+ * comfortably under the cap with headroom for transcript_file edge cases.
210
+ *
211
+ * Codex round-1 review suggested 16; we reduced to 10 after measuring real
212
+ * payload bytes (their suggestion did not include the per-candidate sizing
213
+ * calculation; 16 candidates would intermittently exceed MAX_EVENT_BYTES
214
+ * and recreate the very rejection the cap exists to prevent).
215
+ *
216
+ * Exported so callers / tests can reason about it. Override is intentionally
217
+ * NOT exposed via opts — keeping it a constant prevents callers from passing
218
+ * a number large enough to re-trip the MAX_EVENT_BYTES rejection.
219
+ */
220
+ export const MAX_PARENT_CANDIDATES: 10;
221
+ /**
222
+ * Corroborator strength classification. Exported so storage.mjs / projection
223
+ * layers can reason about it consistently (e.g. CLI display, audit reports).
224
+ *
225
+ * STRONG: location-anchored — these uniquely identify a workspace slot.
226
+ * Two unrelated sessions cannot share `cwd` or `worktree_realpath`
227
+ * by accident.
228
+ * WEAK: signal-anchored — frequently shared by unrelated sessions.
229
+ * Many sessions live on `main` (same_branch_at_start) and inside
230
+ * any 72h window (within_time_window).
231
+ */
232
+ export const STRONG_CORROBORATORS: readonly string[];
233
+ export const WEAK_CORROBORATORS: readonly string[];
234
+ /**
235
+ * Pure identity reconciliation for sessions-db.
236
+ *
237
+ * Maps a SessionStart hook signal set
238
+ * (claude_session_id, transcript metadata, git context, fingerprints, cwd) to
239
+ * a `stable_id` using a 3-priority lookup chain. No IO, no time, no
240
+ * randomness beyond the optional `mintStableId` callback (which the caller
241
+ * must supply — typically `generateSessionId` from `uuid.mjs`).
242
+ *
243
+ * Strategy (try in order, first match wins):
244
+ *
245
+ * 1. claude_session_id_index (confidence='exact')
246
+ * Scan `projection.sessions[*].claude_session_ids[]` for an exact match.
247
+ * Empty arrays are skipped so non-session_seen-created skeleton records
248
+ * cannot false-match (regression guard from P2 storage round-1 review).
249
+ *
250
+ * 2. transcript_lineage (confidence='high')
251
+ * If `transcriptMeta.firstParentUuid` matches some
252
+ * session.transcript_files[*].lastUuid, this csid is a resume / fork of
253
+ * that session. Structurally derived ID — high confidence, no
254
+ * corroborator needed.
255
+ *
256
+ * 3. fingerprint_corroborator (confidence='low')
257
+ * A fingerprint (first_human_prompt_v1 OR lineage_prefix_v1) match
258
+ * ALONE is too weak. We classify corroborators into two strengths:
259
+ *
260
+ * STRONG (location-anchored — uniquely identify a workspace slot):
261
+ * - same_cwd
262
+ * - same_worktree_realpath
263
+ *
264
+ * WEAK (signal-anchored — frequently shared by unrelated sessions):
265
+ * - same_branch_at_start (e.g. dozens of sessions on `main`)
266
+ * - within_time_window (e.g. dozens of sessions inside any 72h)
267
+ *
268
+ * Acceptance requires BOTH:
269
+ * (a) at least 1 STRONG corroborator, AND
270
+ * (b) total (strong + weak) >= `minCorroborators` (default 2)
271
+ *
272
+ * This blocks the false-merge "same branch + same window alone is enough"
273
+ * pattern that codex round-1 review flagged: two unrelated sessions on
274
+ * `main` within 72h would have weak=2, but strong=0 — must be rejected.
275
+ *
276
+ * Ambiguity rule: if MULTIPLE candidates are above the threshold, we
277
+ * cannot pick one safely. We MINT a fresh stable_id and surface ALL
278
+ * above-threshold candidates as parent_candidates so a human / future
279
+ * manual_link can disambiguate. (Old behavior silently picked the first
280
+ * projection-iteration entry — ordering bug.)
281
+ *
282
+ * Fingerprint matches without enough corroborators are surfaced as
283
+ * `parentCandidates[]` (hub-spoke derivation hints — the caller decides
284
+ * whether to promote them later).
285
+ *
286
+ * To bound payload size (events.jsonl uses MAX_EVENT_BYTES=4096 cap),
287
+ * `parentCandidates[]` is hard-capped at MAX_PARENT_CANDIDATES (default
288
+ * 16). Sort key: (strong corroborator count desc, recency desc). When
289
+ * truncated, `parentCandidatesOmittedCount` carries the omitted count
290
+ * so callers can surface "+ N more" in CLI / audit.
291
+ *
292
+ * If all three miss, the caller mints a fresh stable_id via `mintStableId()`
293
+ * and we return `source: 'minted'`, `confidence: 'minted'`.
294
+ *
295
+ * Priority is strict — P1 hit short-circuits and never queries P2/P3, etc.
296
+ */
297
+ export type ProjectionSession = {
298
+ stable_id: string;
299
+ claude_session_ids: string[];
300
+ transcript_files: Array<{
301
+ path?: string;
302
+ last_uuid?: string | null;
303
+ }>;
304
+ fingerprints: {
305
+ first_human_prompt_v1: string | null;
306
+ lineage_prefix_v1: string | null;
307
+ };
308
+ cwd?: string | null;
309
+ worktree_realpath?: string | null;
310
+ branch_at_start?: string | null;
311
+ last_progress_at?: string | null;
312
+ };
313
+ /**
314
+ * Pure identity reconciliation for sessions-db.
315
+ *
316
+ * Maps a SessionStart hook signal set
317
+ * (claude_session_id, transcript metadata, git context, fingerprints, cwd) to
318
+ * a `stable_id` using a 3-priority lookup chain. No IO, no time, no
319
+ * randomness beyond the optional `mintStableId` callback (which the caller
320
+ * must supply — typically `generateSessionId` from `uuid.mjs`).
321
+ *
322
+ * Strategy (try in order, first match wins):
323
+ *
324
+ * 1. claude_session_id_index (confidence='exact')
325
+ * Scan `projection.sessions[*].claude_session_ids[]` for an exact match.
326
+ * Empty arrays are skipped so non-session_seen-created skeleton records
327
+ * cannot false-match (regression guard from P2 storage round-1 review).
328
+ *
329
+ * 2. transcript_lineage (confidence='high')
330
+ * If `transcriptMeta.firstParentUuid` matches some
331
+ * session.transcript_files[*].lastUuid, this csid is a resume / fork of
332
+ * that session. Structurally derived ID — high confidence, no
333
+ * corroborator needed.
334
+ *
335
+ * 3. fingerprint_corroborator (confidence='low')
336
+ * A fingerprint (first_human_prompt_v1 OR lineage_prefix_v1) match
337
+ * ALONE is too weak. We classify corroborators into two strengths:
338
+ *
339
+ * STRONG (location-anchored — uniquely identify a workspace slot):
340
+ * - same_cwd
341
+ * - same_worktree_realpath
342
+ *
343
+ * WEAK (signal-anchored — frequently shared by unrelated sessions):
344
+ * - same_branch_at_start (e.g. dozens of sessions on `main`)
345
+ * - within_time_window (e.g. dozens of sessions inside any 72h)
346
+ *
347
+ * Acceptance requires BOTH:
348
+ * (a) at least 1 STRONG corroborator, AND
349
+ * (b) total (strong + weak) >= `minCorroborators` (default 2)
350
+ *
351
+ * This blocks the false-merge "same branch + same window alone is enough"
352
+ * pattern that codex round-1 review flagged: two unrelated sessions on
353
+ * `main` within 72h would have weak=2, but strong=0 — must be rejected.
354
+ *
355
+ * Ambiguity rule: if MULTIPLE candidates are above the threshold, we
356
+ * cannot pick one safely. We MINT a fresh stable_id and surface ALL
357
+ * above-threshold candidates as parent_candidates so a human / future
358
+ * manual_link can disambiguate. (Old behavior silently picked the first
359
+ * projection-iteration entry — ordering bug.)
360
+ *
361
+ * Fingerprint matches without enough corroborators are surfaced as
362
+ * `parentCandidates[]` (hub-spoke derivation hints — the caller decides
363
+ * whether to promote them later).
364
+ *
365
+ * To bound payload size (events.jsonl uses MAX_EVENT_BYTES=4096 cap),
366
+ * `parentCandidates[]` is hard-capped at MAX_PARENT_CANDIDATES (default
367
+ * 16). Sort key: (strong corroborator count desc, recency desc). When
368
+ * truncated, `parentCandidatesOmittedCount` carries the omitted count
369
+ * so callers can surface "+ N more" in CLI / audit.
370
+ *
371
+ * If all three miss, the caller mints a fresh stable_id via `mintStableId()`
372
+ * and we return `source: 'minted'`, `confidence: 'minted'`.
373
+ *
374
+ * Priority is strict — P1 hit short-circuits and never queries P2/P3, etc.
375
+ */
376
+ export type Projection = {
377
+ _meta: object;
378
+ sessions: Record<string, ProjectionSession>;
379
+ };
380
+ /**
381
+ * Pure identity reconciliation for sessions-db.
382
+ *
383
+ * Maps a SessionStart hook signal set
384
+ * (claude_session_id, transcript metadata, git context, fingerprints, cwd) to
385
+ * a `stable_id` using a 3-priority lookup chain. No IO, no time, no
386
+ * randomness beyond the optional `mintStableId` callback (which the caller
387
+ * must supply — typically `generateSessionId` from `uuid.mjs`).
388
+ *
389
+ * Strategy (try in order, first match wins):
390
+ *
391
+ * 1. claude_session_id_index (confidence='exact')
392
+ * Scan `projection.sessions[*].claude_session_ids[]` for an exact match.
393
+ * Empty arrays are skipped so non-session_seen-created skeleton records
394
+ * cannot false-match (regression guard from P2 storage round-1 review).
395
+ *
396
+ * 2. transcript_lineage (confidence='high')
397
+ * If `transcriptMeta.firstParentUuid` matches some
398
+ * session.transcript_files[*].lastUuid, this csid is a resume / fork of
399
+ * that session. Structurally derived ID — high confidence, no
400
+ * corroborator needed.
401
+ *
402
+ * 3. fingerprint_corroborator (confidence='low')
403
+ * A fingerprint (first_human_prompt_v1 OR lineage_prefix_v1) match
404
+ * ALONE is too weak. We classify corroborators into two strengths:
405
+ *
406
+ * STRONG (location-anchored — uniquely identify a workspace slot):
407
+ * - same_cwd
408
+ * - same_worktree_realpath
409
+ *
410
+ * WEAK (signal-anchored — frequently shared by unrelated sessions):
411
+ * - same_branch_at_start (e.g. dozens of sessions on `main`)
412
+ * - within_time_window (e.g. dozens of sessions inside any 72h)
413
+ *
414
+ * Acceptance requires BOTH:
415
+ * (a) at least 1 STRONG corroborator, AND
416
+ * (b) total (strong + weak) >= `minCorroborators` (default 2)
417
+ *
418
+ * This blocks the false-merge "same branch + same window alone is enough"
419
+ * pattern that codex round-1 review flagged: two unrelated sessions on
420
+ * `main` within 72h would have weak=2, but strong=0 — must be rejected.
421
+ *
422
+ * Ambiguity rule: if MULTIPLE candidates are above the threshold, we
423
+ * cannot pick one safely. We MINT a fresh stable_id and surface ALL
424
+ * above-threshold candidates as parent_candidates so a human / future
425
+ * manual_link can disambiguate. (Old behavior silently picked the first
426
+ * projection-iteration entry — ordering bug.)
427
+ *
428
+ * Fingerprint matches without enough corroborators are surfaced as
429
+ * `parentCandidates[]` (hub-spoke derivation hints — the caller decides
430
+ * whether to promote them later).
431
+ *
432
+ * To bound payload size (events.jsonl uses MAX_EVENT_BYTES=4096 cap),
433
+ * `parentCandidates[]` is hard-capped at MAX_PARENT_CANDIDATES (default
434
+ * 16). Sort key: (strong corroborator count desc, recency desc). When
435
+ * truncated, `parentCandidatesOmittedCount` carries the omitted count
436
+ * so callers can surface "+ N more" in CLI / audit.
437
+ *
438
+ * If all three miss, the caller mints a fresh stable_id via `mintStableId()`
439
+ * and we return `source: 'minted'`, `confidence: 'minted'`.
440
+ *
441
+ * Priority is strict — P1 hit short-circuits and never queries P2/P3, etc.
442
+ */
443
+ export type TranscriptMetaInput = {
444
+ firstUuid?: string | null;
445
+ lastUuid?: string | null;
446
+ firstParentUuid?: string | null;
447
+ };
448
+ /**
449
+ * Pure identity reconciliation for sessions-db.
450
+ *
451
+ * Maps a SessionStart hook signal set
452
+ * (claude_session_id, transcript metadata, git context, fingerprints, cwd) to
453
+ * a `stable_id` using a 3-priority lookup chain. No IO, no time, no
454
+ * randomness beyond the optional `mintStableId` callback (which the caller
455
+ * must supply — typically `generateSessionId` from `uuid.mjs`).
456
+ *
457
+ * Strategy (try in order, first match wins):
458
+ *
459
+ * 1. claude_session_id_index (confidence='exact')
460
+ * Scan `projection.sessions[*].claude_session_ids[]` for an exact match.
461
+ * Empty arrays are skipped so non-session_seen-created skeleton records
462
+ * cannot false-match (regression guard from P2 storage round-1 review).
463
+ *
464
+ * 2. transcript_lineage (confidence='high')
465
+ * If `transcriptMeta.firstParentUuid` matches some
466
+ * session.transcript_files[*].lastUuid, this csid is a resume / fork of
467
+ * that session. Structurally derived ID — high confidence, no
468
+ * corroborator needed.
469
+ *
470
+ * 3. fingerprint_corroborator (confidence='low')
471
+ * A fingerprint (first_human_prompt_v1 OR lineage_prefix_v1) match
472
+ * ALONE is too weak. We classify corroborators into two strengths:
473
+ *
474
+ * STRONG (location-anchored — uniquely identify a workspace slot):
475
+ * - same_cwd
476
+ * - same_worktree_realpath
477
+ *
478
+ * WEAK (signal-anchored — frequently shared by unrelated sessions):
479
+ * - same_branch_at_start (e.g. dozens of sessions on `main`)
480
+ * - within_time_window (e.g. dozens of sessions inside any 72h)
481
+ *
482
+ * Acceptance requires BOTH:
483
+ * (a) at least 1 STRONG corroborator, AND
484
+ * (b) total (strong + weak) >= `minCorroborators` (default 2)
485
+ *
486
+ * This blocks the false-merge "same branch + same window alone is enough"
487
+ * pattern that codex round-1 review flagged: two unrelated sessions on
488
+ * `main` within 72h would have weak=2, but strong=0 — must be rejected.
489
+ *
490
+ * Ambiguity rule: if MULTIPLE candidates are above the threshold, we
491
+ * cannot pick one safely. We MINT a fresh stable_id and surface ALL
492
+ * above-threshold candidates as parent_candidates so a human / future
493
+ * manual_link can disambiguate. (Old behavior silently picked the first
494
+ * projection-iteration entry — ordering bug.)
495
+ *
496
+ * Fingerprint matches without enough corroborators are surfaced as
497
+ * `parentCandidates[]` (hub-spoke derivation hints — the caller decides
498
+ * whether to promote them later).
499
+ *
500
+ * To bound payload size (events.jsonl uses MAX_EVENT_BYTES=4096 cap),
501
+ * `parentCandidates[]` is hard-capped at MAX_PARENT_CANDIDATES (default
502
+ * 16). Sort key: (strong corroborator count desc, recency desc). When
503
+ * truncated, `parentCandidatesOmittedCount` carries the omitted count
504
+ * so callers can surface "+ N more" in CLI / audit.
505
+ *
506
+ * If all three miss, the caller mints a fresh stable_id via `mintStableId()`
507
+ * and we return `source: 'minted'`, `confidence: 'minted'`.
508
+ *
509
+ * Priority is strict — P1 hit short-circuits and never queries P2/P3, etc.
510
+ */
511
+ export type GitContextInput = {
512
+ worktreeRealpath?: string | null;
513
+ worktreePath?: string | null;
514
+ branch?: string | null;
515
+ };
516
+ /**
517
+ * Pure identity reconciliation for sessions-db.
518
+ *
519
+ * Maps a SessionStart hook signal set
520
+ * (claude_session_id, transcript metadata, git context, fingerprints, cwd) to
521
+ * a `stable_id` using a 3-priority lookup chain. No IO, no time, no
522
+ * randomness beyond the optional `mintStableId` callback (which the caller
523
+ * must supply — typically `generateSessionId` from `uuid.mjs`).
524
+ *
525
+ * Strategy (try in order, first match wins):
526
+ *
527
+ * 1. claude_session_id_index (confidence='exact')
528
+ * Scan `projection.sessions[*].claude_session_ids[]` for an exact match.
529
+ * Empty arrays are skipped so non-session_seen-created skeleton records
530
+ * cannot false-match (regression guard from P2 storage round-1 review).
531
+ *
532
+ * 2. transcript_lineage (confidence='high')
533
+ * If `transcriptMeta.firstParentUuid` matches some
534
+ * session.transcript_files[*].lastUuid, this csid is a resume / fork of
535
+ * that session. Structurally derived ID — high confidence, no
536
+ * corroborator needed.
537
+ *
538
+ * 3. fingerprint_corroborator (confidence='low')
539
+ * A fingerprint (first_human_prompt_v1 OR lineage_prefix_v1) match
540
+ * ALONE is too weak. We classify corroborators into two strengths:
541
+ *
542
+ * STRONG (location-anchored — uniquely identify a workspace slot):
543
+ * - same_cwd
544
+ * - same_worktree_realpath
545
+ *
546
+ * WEAK (signal-anchored — frequently shared by unrelated sessions):
547
+ * - same_branch_at_start (e.g. dozens of sessions on `main`)
548
+ * - within_time_window (e.g. dozens of sessions inside any 72h)
549
+ *
550
+ * Acceptance requires BOTH:
551
+ * (a) at least 1 STRONG corroborator, AND
552
+ * (b) total (strong + weak) >= `minCorroborators` (default 2)
553
+ *
554
+ * This blocks the false-merge "same branch + same window alone is enough"
555
+ * pattern that codex round-1 review flagged: two unrelated sessions on
556
+ * `main` within 72h would have weak=2, but strong=0 — must be rejected.
557
+ *
558
+ * Ambiguity rule: if MULTIPLE candidates are above the threshold, we
559
+ * cannot pick one safely. We MINT a fresh stable_id and surface ALL
560
+ * above-threshold candidates as parent_candidates so a human / future
561
+ * manual_link can disambiguate. (Old behavior silently picked the first
562
+ * projection-iteration entry — ordering bug.)
563
+ *
564
+ * Fingerprint matches without enough corroborators are surfaced as
565
+ * `parentCandidates[]` (hub-spoke derivation hints — the caller decides
566
+ * whether to promote them later).
567
+ *
568
+ * To bound payload size (events.jsonl uses MAX_EVENT_BYTES=4096 cap),
569
+ * `parentCandidates[]` is hard-capped at MAX_PARENT_CANDIDATES (default
570
+ * 16). Sort key: (strong corroborator count desc, recency desc). When
571
+ * truncated, `parentCandidatesOmittedCount` carries the omitted count
572
+ * so callers can surface "+ N more" in CLI / audit.
573
+ *
574
+ * If all three miss, the caller mints a fresh stable_id via `mintStableId()`
575
+ * and we return `source: 'minted'`, `confidence: 'minted'`.
576
+ *
577
+ * Priority is strict — P1 hit short-circuits and never queries P2/P3, etc.
578
+ */
579
+ export type FingerprintInput = {
580
+ first_human_prompt_v1?: string | null;
581
+ lineage_prefix_v1?: string | null;
582
+ };
583
+ /**
584
+ * Pure identity reconciliation for sessions-db.
585
+ *
586
+ * Maps a SessionStart hook signal set
587
+ * (claude_session_id, transcript metadata, git context, fingerprints, cwd) to
588
+ * a `stable_id` using a 3-priority lookup chain. No IO, no time, no
589
+ * randomness beyond the optional `mintStableId` callback (which the caller
590
+ * must supply — typically `generateSessionId` from `uuid.mjs`).
591
+ *
592
+ * Strategy (try in order, first match wins):
593
+ *
594
+ * 1. claude_session_id_index (confidence='exact')
595
+ * Scan `projection.sessions[*].claude_session_ids[]` for an exact match.
596
+ * Empty arrays are skipped so non-session_seen-created skeleton records
597
+ * cannot false-match (regression guard from P2 storage round-1 review).
598
+ *
599
+ * 2. transcript_lineage (confidence='high')
600
+ * If `transcriptMeta.firstParentUuid` matches some
601
+ * session.transcript_files[*].lastUuid, this csid is a resume / fork of
602
+ * that session. Structurally derived ID — high confidence, no
603
+ * corroborator needed.
604
+ *
605
+ * 3. fingerprint_corroborator (confidence='low')
606
+ * A fingerprint (first_human_prompt_v1 OR lineage_prefix_v1) match
607
+ * ALONE is too weak. We classify corroborators into two strengths:
608
+ *
609
+ * STRONG (location-anchored — uniquely identify a workspace slot):
610
+ * - same_cwd
611
+ * - same_worktree_realpath
612
+ *
613
+ * WEAK (signal-anchored — frequently shared by unrelated sessions):
614
+ * - same_branch_at_start (e.g. dozens of sessions on `main`)
615
+ * - within_time_window (e.g. dozens of sessions inside any 72h)
616
+ *
617
+ * Acceptance requires BOTH:
618
+ * (a) at least 1 STRONG corroborator, AND
619
+ * (b) total (strong + weak) >= `minCorroborators` (default 2)
620
+ *
621
+ * This blocks the false-merge "same branch + same window alone is enough"
622
+ * pattern that codex round-1 review flagged: two unrelated sessions on
623
+ * `main` within 72h would have weak=2, but strong=0 — must be rejected.
624
+ *
625
+ * Ambiguity rule: if MULTIPLE candidates are above the threshold, we
626
+ * cannot pick one safely. We MINT a fresh stable_id and surface ALL
627
+ * above-threshold candidates as parent_candidates so a human / future
628
+ * manual_link can disambiguate. (Old behavior silently picked the first
629
+ * projection-iteration entry — ordering bug.)
630
+ *
631
+ * Fingerprint matches without enough corroborators are surfaced as
632
+ * `parentCandidates[]` (hub-spoke derivation hints — the caller decides
633
+ * whether to promote them later).
634
+ *
635
+ * To bound payload size (events.jsonl uses MAX_EVENT_BYTES=4096 cap),
636
+ * `parentCandidates[]` is hard-capped at MAX_PARENT_CANDIDATES (default
637
+ * 16). Sort key: (strong corroborator count desc, recency desc). When
638
+ * truncated, `parentCandidatesOmittedCount` carries the omitted count
639
+ * so callers can surface "+ N more" in CLI / audit.
640
+ *
641
+ * If all three miss, the caller mints a fresh stable_id via `mintStableId()`
642
+ * and we return `source: 'minted'`, `confidence: 'minted'`.
643
+ *
644
+ * Priority is strict — P1 hit short-circuits and never queries P2/P3, etc.
645
+ */
646
+ export type IdentityResult = {
647
+ stableId: string;
648
+ source: "claude_session_id_index" | "transcript_lineage" | "fingerprint_corroborator" | "minted";
649
+ confidence: "exact" | "high" | "low" | "minted";
650
+ matched: object;
651
+ parentCandidates: Array<{
652
+ stable_id: string;
653
+ source: string;
654
+ confidence: string;
655
+ reason: object;
656
+ }>;
657
+ parentCandidatesOmittedCount?: number;
658
+ };
@@ -0,0 +1,10 @@
1
+ export { initProjection } from "./init.mjs";
2
+ export { watchProjection } from "./watch.mjs";
3
+ export { loadProjection, rebuildProjection, recordSessionSeen, tryUpdateProjection, newEvent, appendEvent, readAllEvents, saveProjection, PATHS, MAX_EVENT_BYTES } from "./storage.mjs";
4
+ export { setAlias, linkTask, unlinkTask, setParent, closeSession, runSweep } from "./operations.mjs";
5
+ export { resolveStoragePaths, pathsFromRoot, STORAGE_FILENAMES, MAX_ASCEND_DEPTH } from "./paths.mjs";
6
+ export { resolveIdentity, findByClaudeSessionId, findByTranscriptLineage, scanFingerprintCandidates, collectParentCandidates, capParentCandidates, classifyCorroborators, meetsThreshold, MAX_PARENT_CANDIDATES, STRONG_CORROBORATORS, WEAK_CORROBORATORS } from "./identity.mjs";
7
+ export { computeSweepTransitions, computeEffectiveLastProgress } from "./sweep.mjs";
8
+ export { sanitizeFirstPrompt, stripIdeWrappers, stripSystemReminders } from "./sanitize.mjs";
9
+ export { generateSessionId, isSessionId, extractTimestamp } from "./uuid.mjs";
10
+ export { applyEvent, emptyProjection, emptySession, rebuildFromEvents } from "./projection.mjs";