@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,127 @@
1
+ /**
2
+ * @druumen/sessions-db — public TypeScript entry point.
3
+ *
4
+ * This file is HAND-CRAFTED and committed to the repo (not regenerated by
5
+ * `tsc`). The `tsc` build emits per-source-file declarations as `.d.mts`
6
+ * (because the sources are `.mjs`); this `.d.ts` curates the public type
7
+ * surface so cockpit-class consumers can write:
8
+ *
9
+ * import type {
10
+ * KnownSession,
11
+ * Projection,
12
+ * SessionEvent,
13
+ * ActivityState,
14
+ * Outcome,
15
+ * } from '@druumen/sessions-db';
16
+ *
17
+ * and resolve everything through one entry — without having to memorise
18
+ * which lib/* file each typedef lives in.
19
+ *
20
+ * The `lib/index.mjs` runtime entry is intentionally a Day-1 stub
21
+ * (re-exports happen on Day 3 — once the public *function* surface is
22
+ * settled). This `.d.ts` is safe to ship today because it is types-only
23
+ * and has zero runtime side effects.
24
+ *
25
+ * Convention for tsc-emitted neighbours:
26
+ * - Auto-generated: `types/<name>.d.mts` (mirrors `lib/<name>.mjs`)
27
+ * - Hand-crafted: `types/index.d.ts` (this file)
28
+ *
29
+ * `package.json` `"types"` points at `./types/index.d.ts` so this is the
30
+ * resolution entry. The neighbouring `.d.mts` files exist for direct
31
+ * sub-path imports (`@druumen/sessions-db/lib/storage.d.mts`) but cockpit
32
+ * should prefer this curated surface for forward compat.
33
+ */
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Type vocabulary — re-exported from `lib/types.d.mts` (which `tsc` lifted
37
+ // from the `@typedef` block in `lib/types.mjs`). This is the canonical
38
+ // surface for cockpit consumers.
39
+ // ---------------------------------------------------------------------------
40
+
41
+ export type {
42
+ // Branded scalars
43
+ SessionStableId,
44
+ ClaudeSessionId,
45
+ EventId,
46
+ Iso8601,
47
+ // Enums
48
+ ActivityState,
49
+ Outcome,
50
+ IdentitySource,
51
+ IdentityConfidence,
52
+ EventOp,
53
+ // Composite shapes
54
+ TranscriptFile,
55
+ IdentityResolution,
56
+ ParentCandidate,
57
+ KnownSession,
58
+ ProjectionMeta,
59
+ Projection,
60
+ SessionEvent,
61
+ } from './types.d.mts';
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Function signatures — re-exported from per-source `.d.mts` neighbours.
65
+ //
66
+ // We re-export TYPES of functions (typeof) rather than the runtime symbols
67
+ // because Day 2 keeps `lib/index.mjs` a runtime stub. Day 3 will flip
68
+ // `lib/index.mjs` into a real re-export hub and at that point `tsc` will
69
+ // regenerate `types/index.d.mts` with the same shape — but consumers who
70
+ // imported through this `.d.ts` see no breakage because the type names
71
+ // are identical.
72
+ //
73
+ // The `typeof import(...)` pattern is the standard TypeScript ambient
74
+ // re-export when the source is JS-with-JSDoc.
75
+ // ---------------------------------------------------------------------------
76
+
77
+ // uuid.mjs
78
+ export type GenerateSessionId = typeof import('./uuid.d.mts').generateSessionId;
79
+ export type IsSessionId = typeof import('./uuid.d.mts').isSessionId;
80
+ export type ExtractTimestamp = typeof import('./uuid.d.mts').extractTimestamp;
81
+
82
+ // projection.mjs
83
+ export type ApplyEvent = typeof import('./projection.d.mts').applyEvent;
84
+ export type EmptyProjection = typeof import('./projection.d.mts').emptyProjection;
85
+ export type EmptySession = typeof import('./projection.d.mts').emptySession;
86
+ export type RebuildFromEvents = typeof import('./projection.d.mts').rebuildFromEvents;
87
+
88
+ // storage.mjs
89
+ export type NewEvent = typeof import('./storage.d.mts').newEvent;
90
+ export type AppendEvent = typeof import('./storage.d.mts').appendEvent;
91
+ export type ReadAllEvents = typeof import('./storage.d.mts').readAllEvents;
92
+ export type LoadProjection = typeof import('./storage.d.mts').loadProjection;
93
+ export type SaveProjection = typeof import('./storage.d.mts').saveProjection;
94
+ export type RebuildProjection = typeof import('./storage.d.mts').rebuildProjection;
95
+ export type TryUpdateProjection = typeof import('./storage.d.mts').tryUpdateProjection;
96
+ export type RecordSessionSeen = typeof import('./storage.d.mts').recordSessionSeen;
97
+
98
+ // identity.mjs
99
+ export type ResolveIdentity = typeof import('./identity.d.mts').resolveIdentity;
100
+ export type FindByClaudeSessionId = typeof import('./identity.d.mts').findByClaudeSessionId;
101
+ export type FindByTranscriptLineage = typeof import('./identity.d.mts').findByTranscriptLineage;
102
+ export type ScanFingerprintCandidates = typeof import('./identity.d.mts').scanFingerprintCandidates;
103
+ export type CollectParentCandidates = typeof import('./identity.d.mts').collectParentCandidates;
104
+ export type CapParentCandidates = typeof import('./identity.d.mts').capParentCandidates;
105
+ export type ClassifyCorroborators = typeof import('./identity.d.mts').classifyCorroborators;
106
+ export type MeetsThreshold = typeof import('./identity.d.mts').meetsThreshold;
107
+
108
+ // sweep.mjs
109
+ export type ComputeSweepTransitions = typeof import('./sweep.d.mts').computeSweepTransitions;
110
+ export type ComputeEffectiveLastProgress = typeof import('./sweep.d.mts').computeEffectiveLastProgress;
111
+
112
+ // sanitize.mjs
113
+ export type SanitizeFirstPrompt = typeof import('./sanitize.d.mts').sanitizeFirstPrompt;
114
+ export type StripSystemReminders = typeof import('./sanitize.d.mts').stripSystemReminders;
115
+ export type StripIdeWrappers = typeof import('./sanitize.d.mts').stripIdeWrappers;
116
+
117
+ // transcript.mjs
118
+ export type ParseTranscriptFile = typeof import('./transcript.d.mts').parseTranscriptFile;
119
+ export type ListTranscriptFiles = typeof import('./transcript.d.mts').listTranscriptFiles;
120
+ export type WorkspaceHashFromCwd = typeof import('./transcript.d.mts').workspaceHashFromCwd;
121
+
122
+ // git-context.mjs
123
+ export type GitContextFn = typeof import('./git-context.d.mts').gitContext;
124
+
125
+ // paths.mjs (Day 4 — storage path resolution chain)
126
+ export type ResolveStoragePaths = typeof import('./paths.d.mts').resolveStoragePaths;
127
+ export type PathsFromRoot = typeof import('./paths.d.mts').pathsFromRoot;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Initialize sessions-db storage at the given root.
3
+ *
4
+ * Resolution semantics (Day 4):
5
+ *
6
+ * - `opts.paths` (legacy form) — fully-formed override: each `eventsJsonl`
7
+ * / `projectionJson` is anchored on `opts.rootPath` (or treated as
8
+ * absolute if it starts with `/`). Backward-compatible with Day 3
9
+ * callers that passed `paths: { eventsJsonl: 'custom/events.jsonl', ... }`.
10
+ *
11
+ * - `opts.rootPath` (Day 4 form, no `opts.paths`) — `rootPath` IS the
12
+ * storage directory; files live directly under it as
13
+ * `<rootPath>/sessions-db-events.jsonl` and `<rootPath>/sessions-db.json`.
14
+ * This is what cockpit's Setup Wizard passes (typically resolved to
15
+ * `<workspace>/.dru-code/`).
16
+ *
17
+ * - No opts (default) — delegates to `resolveStoragePaths()` which runs
18
+ * the env > existing-storage > cwd/.dru-code chain. Useful for ad-hoc
19
+ * "init wherever the resolver thinks it should go" scripts.
20
+ *
21
+ * @param {{
22
+ * rootPath?: string,
23
+ * paths?: { eventsJsonl?: string, projectionJson?: string, lockFile?: string },
24
+ * }} [opts]
25
+ * @returns {Promise<{
26
+ * ok: boolean,
27
+ * created?: { dir: boolean, eventsJsonl: boolean, projectionJson: boolean },
28
+ * paths?: { eventsJsonl: string, projectionJson: string },
29
+ * source?: string,
30
+ * error?: string,
31
+ * }>}
32
+ */
33
+ export function initProjection(opts?: {
34
+ rootPath?: string;
35
+ paths?: {
36
+ eventsJsonl?: string;
37
+ projectionJson?: string;
38
+ lockFile?: string;
39
+ };
40
+ }): Promise<{
41
+ ok: boolean;
42
+ created?: {
43
+ dir: boolean;
44
+ eventsJsonl: boolean;
45
+ projectionJson: boolean;
46
+ };
47
+ paths?: {
48
+ eventsJsonl: string;
49
+ projectionJson: string;
50
+ };
51
+ source?: string;
52
+ error?: string;
53
+ }>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Acquire an exclusive lock on `lockPath`.
3
+ *
4
+ * @param {string} lockPath - Absolute path to the lock file. Parent dir must
5
+ * exist; we do not mkdir-p (callers control layout).
6
+ * @param {{ timeoutMs?: number, retryMs?: number }} [opts]
7
+ * @returns {Promise<{ release: () => void }>} - Resolves with a release
8
+ * handle. `release()` is idempotent: calling it twice is a no-op.
9
+ *
10
+ * Throws on timeout: `Error("acquireLock: timeout after <ms>ms (path=...)").`
11
+ * Re-throws unexpected fs errors verbatim (anything other than EEXIST).
12
+ */
13
+ export function acquireLock(lockPath: string, opts?: {
14
+ timeoutMs?: number;
15
+ retryMs?: number;
16
+ }): Promise<{
17
+ release: () => void;
18
+ }>;
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Set or clear the human-readable alias on a session.
3
+ *
4
+ * Either `alias` (non-empty string) or `clear: true` must be provided —
5
+ * mutually exclusive. Validation matches the CLI's argparse behavior so the
6
+ * library consumer surface is symmetric with the CLI surface.
7
+ *
8
+ * @param {{
9
+ * stableId: string,
10
+ * alias?: string,
11
+ * clear?: boolean,
12
+ * rootPath?: string,
13
+ * root?: string,
14
+ * paths?: object,
15
+ * }} opts
16
+ * @returns {Promise<{ ok: boolean, event_id?: string, error?: string }>}
17
+ */
18
+ export function setAlias(opts: {
19
+ stableId: string;
20
+ alias?: string;
21
+ clear?: boolean;
22
+ rootPath?: string;
23
+ root?: string;
24
+ paths?: object;
25
+ }): Promise<{
26
+ ok: boolean;
27
+ event_id?: string;
28
+ error?: string;
29
+ }>;
30
+ /**
31
+ * Link a session to one or more tasks / projects (additive, idempotent).
32
+ *
33
+ * At least one of `tasks` / `projects` must be a non-empty array. The
34
+ * reducer already de-dupes against existing entries so re-running with the
35
+ * same payload is a no-op on projection state (but still writes an audit
36
+ * event).
37
+ *
38
+ * @param {{
39
+ * stableId: string,
40
+ * tasks?: string[],
41
+ * projects?: string[],
42
+ * rootPath?: string,
43
+ * root?: string,
44
+ * paths?: object,
45
+ * }} opts
46
+ * @returns {Promise<{ ok: boolean, event_id?: string, error?: string }>}
47
+ */
48
+ export function linkTask(opts: {
49
+ stableId: string;
50
+ tasks?: string[];
51
+ projects?: string[];
52
+ rootPath?: string;
53
+ root?: string;
54
+ paths?: object;
55
+ }): Promise<{
56
+ ok: boolean;
57
+ event_id?: string;
58
+ error?: string;
59
+ }>;
60
+ /**
61
+ * Unlink one or more tasks / projects from a session (set-based filter,
62
+ * idempotent). Removing an id that isn't present is a no-op on projection
63
+ * state but still produces an audit event — operator intent is recorded
64
+ * regardless of resulting state change.
65
+ *
66
+ * @param {{
67
+ * stableId: string,
68
+ * tasks?: string[],
69
+ * projects?: string[],
70
+ * rootPath?: string,
71
+ * root?: string,
72
+ * paths?: object,
73
+ * }} opts
74
+ * @returns {Promise<{ ok: boolean, event_id?: string, error?: string }>}
75
+ */
76
+ export function unlinkTask(opts: {
77
+ stableId: string;
78
+ tasks?: string[];
79
+ projects?: string[];
80
+ rootPath?: string;
81
+ root?: string;
82
+ paths?: object;
83
+ }): Promise<{
84
+ ok: boolean;
85
+ event_id?: string;
86
+ error?: string;
87
+ }>;
88
+ /**
89
+ * Set or clear the hub-spoke parent relationship for a session.
90
+ *
91
+ * Either `parentId` (non-empty string, distinct from `childId`) or `clear:
92
+ * true` must be provided. When setting a parent we:
93
+ * - reject self-cycle (parentId === childId, exit-1 in CLI)
94
+ * - verify parent exists
95
+ * - walk parent's ancestor chain up to MAX_PARENT_CHAIN_DEPTH and reject
96
+ * if `childId` appears anywhere — that would close a cycle of length
97
+ * ≥ 2 (e.g. existing A→B + proposed `setParent({childId: B, parentId: A})`
98
+ * would form A→B→A).
99
+ *
100
+ * The MAX_PARENT_CHAIN_DEPTH bound is a defense against a stale projection
101
+ * cycle (rare; would require an earlier guard bypass). 50 is generous —
102
+ * real hub-spoke chains are 1-3 hops.
103
+ *
104
+ * @param {{
105
+ * childId: string,
106
+ * parentId?: string,
107
+ * clear?: boolean,
108
+ * rootPath?: string,
109
+ * root?: string,
110
+ * paths?: object,
111
+ * }} opts
112
+ * @returns {Promise<{ ok: boolean, event_id?: string, error?: string }>}
113
+ */
114
+ export function setParent(opts: {
115
+ childId: string;
116
+ parentId?: string;
117
+ clear?: boolean;
118
+ rootPath?: string;
119
+ root?: string;
120
+ paths?: object;
121
+ }): Promise<{
122
+ ok: boolean;
123
+ event_id?: string;
124
+ error?: string;
125
+ }>;
126
+ /**
127
+ * Close (or reopen) a session with a terminal outcome.
128
+ *
129
+ * Outcome enum is enforced (matches projection schema): open | done |
130
+ * blocked | abandoned | merged | superseded. `open` is allowed — operators
131
+ * may reopen a previously-closed session by passing `outcome: 'open'`; the
132
+ * reducer's closed_at always tracks the latest close event so the reopen is
133
+ * visible in the audit trail.
134
+ *
135
+ * @param {{
136
+ * stableId: string,
137
+ * outcome: string,
138
+ * reason?: string,
139
+ * rootPath?: string,
140
+ * root?: string,
141
+ * paths?: object,
142
+ * }} opts
143
+ * @returns {Promise<{ ok: boolean, event_id?: string, error?: string }>}
144
+ */
145
+ export function closeSession(opts: {
146
+ stableId: string;
147
+ outcome: string;
148
+ reason?: string;
149
+ rootPath?: string;
150
+ root?: string;
151
+ paths?: object;
152
+ }): Promise<{
153
+ ok: boolean;
154
+ event_id?: string;
155
+ error?: string;
156
+ }>;
157
+ /**
158
+ * Compute and (optionally) apply activity_state transitions across all
159
+ * sessions in the projection.
160
+ *
161
+ * Returns:
162
+ * - dryRun: true → `{ ok: true, dryRun: true, transitions }` with the
163
+ * planned transitions list (no events written).
164
+ * - dryRun: false → `{ ok: boolean, applied, failed, summary }` after
165
+ * attempting each transition through `tryUpdateProjection`. `ok` is
166
+ * true when zero failures.
167
+ *
168
+ * Lock model: each transition acquires the projection lock independently
169
+ * via `tryUpdateProjection`. For typical sweep volumes (single digits per
170
+ * run) this is fine; if the workspace grows huge a future `--batch` mode
171
+ * can fold all transitions into a single under-lock pass.
172
+ *
173
+ * @param {{
174
+ * rootPath?: string,
175
+ * root?: string,
176
+ * paths?: object,
177
+ * idleThresholdDays?: number,
178
+ * archiveThresholdDays?: number,
179
+ * dryRun?: boolean,
180
+ * now?: number,
181
+ * }} [opts]
182
+ * @returns {Promise<
183
+ * | { ok: true, dryRun: true, transitions: Array<object> }
184
+ * | { ok: boolean, applied: Array<object>, failed: Array<object>, summary: object }
185
+ * >}
186
+ */
187
+ export function runSweep(opts?: {
188
+ rootPath?: string;
189
+ root?: string;
190
+ paths?: object;
191
+ idleThresholdDays?: number;
192
+ archiveThresholdDays?: number;
193
+ dryRun?: boolean;
194
+ now?: number;
195
+ }): Promise<{
196
+ ok: true;
197
+ dryRun: true;
198
+ transitions: Array<object>;
199
+ } | {
200
+ ok: boolean;
201
+ applied: Array<object>;
202
+ failed: Array<object>;
203
+ summary: object;
204
+ }>;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Resolve storage paths from caller opts + env + autodiscover.
3
+ *
4
+ * @param {{ rootPath?: string, cwd?: string }} [opts]
5
+ * @returns {{
6
+ * root: string,
7
+ * eventsJsonl: string,
8
+ * projectionJson: string,
9
+ * lockFile: string,
10
+ * source: 'arg' | 'env' | 'tickets-logs' | 'dru-code' | 'default',
11
+ * }}
12
+ */
13
+ export function resolveStoragePaths(opts?: {
14
+ rootPath?: string;
15
+ cwd?: string;
16
+ }): {
17
+ root: string;
18
+ eventsJsonl: string;
19
+ projectionJson: string;
20
+ lockFile: string;
21
+ source: "arg" | "env" | "tickets-logs" | "dru-code" | "default";
22
+ };
23
+ /**
24
+ * Helper for callers that already have a fully-resolved root and want to
25
+ * compute file paths (tests, custom integrations). Public so consumers can
26
+ * mirror the layout invariant without importing internal helpers.
27
+ *
28
+ * @param {string} root absolute or relative; resolved against cwd if relative
29
+ * @returns {{ root: string, eventsJsonl: string, projectionJson: string, lockFile: string }}
30
+ */
31
+ export function pathsFromRoot(root: string): {
32
+ root: string;
33
+ eventsJsonl: string;
34
+ projectionJson: string;
35
+ lockFile: string;
36
+ };
37
+ /**
38
+ * Hard cap on cwd-ascend depth. Twelve levels is generous — a typical
39
+ * worktree depth is 1-3, monorepos may go to 5-6. Pinning at 12 means the
40
+ * worst-case stat budget is 24 (two candidate paths × 12 levels) before
41
+ * we fall through to the default. Set deliberately conservative so the
42
+ * resolver never accidentally walks to `/` on a slow networked mount.
43
+ */
44
+ export const MAX_ASCEND_DEPTH: 12;
45
+ /**
46
+ * The three on-disk filenames (relative to whichever root the resolver
47
+ * picks). Frozen so callers can't accidentally mutate. Exported for tests
48
+ * + the rare library consumer that wants to know the canonical names.
49
+ */
50
+ export const STORAGE_FILENAMES: Readonly<{
51
+ eventsJsonl: "sessions-db-events.jsonl";
52
+ projectionJson: "sessions-db.json";
53
+ lockFile: "sessions-db.json.lock";
54
+ }>;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Build an empty projection skeleton. Sessions map starts empty; metadata
3
+ * has `event_count = 0` and `last_event_id = null`.
4
+ *
5
+ * @returns {{ _meta: object, sessions: Record<string, object> }}
6
+ */
7
+ export function emptyProjection(): {
8
+ _meta: object;
9
+ sessions: Record<string, object>;
10
+ };
11
+ /**
12
+ * Build a default session record. Caller passes the stable_id and the
13
+ * `created_at` timestamp (typically the first observing event's `ts`).
14
+ *
15
+ * @param {string} stableId
16
+ * @param {string} ts - ISO timestamp string used for both created_at and
17
+ * last_progress_at.
18
+ */
19
+ export function emptySession(stableId: string, ts: string): {
20
+ stable_id: string;
21
+ alias: any;
22
+ claude_session_ids: any[];
23
+ transcript_files: any[];
24
+ fingerprints: {
25
+ first_human_prompt_v1: any;
26
+ lineage_prefix_v1: any;
27
+ };
28
+ parent_session_id: any;
29
+ parent_candidate_ids: any[];
30
+ parent_candidates_omitted_count: number;
31
+ identity_resolution: any;
32
+ worktree_path_observed: any;
33
+ worktree_realpath: any;
34
+ worktree_registry_name: any;
35
+ git_common_dir: any;
36
+ branch_at_start: any;
37
+ branch_current: any;
38
+ head_at_start: any;
39
+ head_last_seen: any;
40
+ tasks: any[];
41
+ projects: any[];
42
+ activity_state: string;
43
+ outcome: string;
44
+ closed_at: any;
45
+ closed_reason: any;
46
+ created_at: string;
47
+ last_progress_at: string;
48
+ first_prompt_preview: any;
49
+ };
50
+ /**
51
+ * Apply a single event to a projection (mutating). Returns the same
52
+ * projection reference for fluent chaining.
53
+ *
54
+ * Unknown ops are tolerated — they update _meta but otherwise no-op so a
55
+ * future schema bump applied against an older binary degrades cleanly. We
56
+ * still bump `event_count` so the rebuild detector remains accurate.
57
+ *
58
+ * @param {object} projection
59
+ * @param {{ ts: string, event_id: string, op: string, stable_id: string,
60
+ * payload?: object }} event
61
+ * @returns {object} projection
62
+ */
63
+ export function applyEvent(projection: object, event: {
64
+ ts: string;
65
+ event_id: string;
66
+ op: string;
67
+ stable_id: string;
68
+ payload?: object;
69
+ }): object;
70
+ /**
71
+ * Fold an event array into a fresh projection. Used both for full rebuilds
72
+ * (storage.rebuildProjection) and for unit tests.
73
+ *
74
+ * @param {Array<object>} events
75
+ */
76
+ export function rebuildFromEvents(events: Array<object>): {
77
+ _meta: object;
78
+ sessions: Record<string, object>;
79
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Strip every `<system-reminder>...</system-reminder>` block from `s`, plus
3
+ * the related harness/system envelopes (`<system>`, `<thinking>`, `<tool_use>`,
4
+ * `<tool_result>`, `<parameter>`).
5
+ *
6
+ * @param {string} s
7
+ * @returns {string}
8
+ */
9
+ export function stripSystemReminders(s: string): string;
10
+ /**
11
+ * Strip IDE/harness wrappers (`<ide_opened_file>...`, `<ide_selection>...`,
12
+ * `<command-name>...</command-message>`).
13
+ * @param {string} s
14
+ * @returns {string}
15
+ */
16
+ export function stripIdeWrappers(s: string): string;
17
+ /**
18
+ * Sanitise a raw first-prompt string for safe persistence.
19
+ *
20
+ * Order matters and is the result of an adversarial review:
21
+ * 1. NFKC normalise FIRST. Fullwidth bracket variants (e.g.
22
+ * `<system-reminder>`) only fold into ASCII `<>` after NFKC; if we
23
+ * stripped before normalising the wrapper would survive the strip pass
24
+ * and then leak its body once normalisation happens.
25
+ * 2. Strip system-reminders + system envelopes.
26
+ * 3. Strip IDE/harness wrappers.
27
+ * 4. Defensive second pass: re-strip both families. Removing one wrapper
28
+ * can splice together text that now reads as a fresh wrapper (e.g.
29
+ * `<sys` + IDE block + `tem>...</system>`); the second pass closes that.
30
+ * 5. Trim and collapse runs of 3+ newlines to a paragraph break.
31
+ * 6. Truncate to `maxLen` (default 200) on a code-point boundary, append `…`.
32
+ *
33
+ * @param {string} raw
34
+ * @param {{ maxLen?: number }} [opts]
35
+ * @returns {string}
36
+ */
37
+ export function sanitizeFirstPrompt(raw: string, opts?: {
38
+ maxLen?: number;
39
+ }): string;