trajectories 0.0.0 → 0.1.2

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 (205) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +201 -0
  3. package/README.md +288 -2
  4. package/dist/cli.js +30210 -0
  5. package/dist/convert.d.ts +31 -0
  6. package/dist/convert.js +431 -0
  7. package/dist/deepagents_checkpoint.py +362 -0
  8. package/dist/index.d.ts +7 -0
  9. package/dist/index.js +8402 -0
  10. package/dist/log-baseline.d.ts +26 -0
  11. package/dist/log-baseline.js +104 -0
  12. package/dist/log-content.d.ts +16 -0
  13. package/dist/log-content.js +80 -0
  14. package/dist/log-io.d.ts +10 -0
  15. package/dist/log-io.js +121 -0
  16. package/dist/log-read.d.ts +24 -0
  17. package/dist/log-read.js +117 -0
  18. package/dist/log-reconcile.d.ts +5 -0
  19. package/dist/log-reconcile.js +38 -0
  20. package/dist/log-records.d.ts +35 -0
  21. package/dist/log-records.js +85 -0
  22. package/dist/log-recovery.d.ts +18 -0
  23. package/dist/log-recovery.js +123 -0
  24. package/dist/log-stage.d.ts +14 -0
  25. package/dist/log-stage.js +138 -0
  26. package/dist/log-state.d.ts +89 -0
  27. package/dist/log-state.js +92 -0
  28. package/dist/log-switch.d.ts +3 -0
  29. package/dist/log-switch.js +98 -0
  30. package/dist/log-types.d.ts +129 -0
  31. package/dist/log-types.js +8 -0
  32. package/dist/log.d.ts +8 -0
  33. package/dist/log.js +62 -0
  34. package/dist/migrate.d.ts +29 -0
  35. package/dist/migrate.js +173 -0
  36. package/dist/normalization-runtime.d.ts +1 -0
  37. package/dist/normalization-runtime.js +3150 -0
  38. package/dist/portable.d.ts +9 -0
  39. package/dist/portable.js +26 -0
  40. package/dist/readers/canonical.d.ts +13 -0
  41. package/dist/readers/canonical.js +54 -0
  42. package/dist/readers/cline.d.ts +10 -0
  43. package/dist/readers/cline.js +107 -0
  44. package/dist/readers/cursor.d.ts +20 -0
  45. package/dist/readers/cursor.js +374 -0
  46. package/dist/readers/deepagents.d.ts +41 -0
  47. package/dist/readers/deepagents.js +163 -0
  48. package/dist/readers/fx.d.ts +11 -0
  49. package/dist/readers/fx.js +573 -0
  50. package/dist/readers/github-copilot.d.ts +8 -0
  51. package/dist/readers/github-copilot.js +216 -0
  52. package/dist/readers/grok-build.d.ts +12 -0
  53. package/dist/readers/grok-build.js +391 -0
  54. package/dist/readers/guards.d.ts +4 -0
  55. package/dist/readers/guards.js +121 -0
  56. package/dist/readers/pi-compaction.d.ts +7 -0
  57. package/dist/readers/pi-compaction.js +112 -0
  58. package/dist/readers/pi-context.d.ts +13 -0
  59. package/dist/readers/pi-context.js +64 -0
  60. package/dist/store.d.ts +25 -0
  61. package/dist/store.js +218 -0
  62. package/dist/types.d.ts +91 -0
  63. package/dist/types.js +1 -0
  64. package/dist/writers/claude-code.d.ts +5 -0
  65. package/dist/writers/claude-code.js +51 -0
  66. package/dist/writers/cline.d.ts +3 -0
  67. package/dist/writers/cline.js +77 -0
  68. package/dist/writers/codex.d.ts +3 -0
  69. package/dist/writers/codex.js +32 -0
  70. package/dist/writers/cursor.d.ts +3 -0
  71. package/dist/writers/cursor.js +197 -0
  72. package/dist/writers/deepagents.d.ts +3 -0
  73. package/dist/writers/deepagents.js +116 -0
  74. package/dist/writers/entropy.d.ts +5 -0
  75. package/dist/writers/entropy.js +33 -0
  76. package/dist/writers/fx.d.ts +3 -0
  77. package/dist/writers/fx.js +135 -0
  78. package/dist/writers/github-copilot.d.ts +3 -0
  79. package/dist/writers/github-copilot.js +71 -0
  80. package/dist/writers/grok-build.d.ts +3 -0
  81. package/dist/writers/grok-build.js +43 -0
  82. package/dist/writers/helpers.d.ts +17 -0
  83. package/dist/writers/helpers.js +75 -0
  84. package/dist/writers/omp.d.ts +3 -0
  85. package/dist/writers/omp.js +7 -0
  86. package/dist/writers/opencode.d.ts +3 -0
  87. package/dist/writers/opencode.js +65 -0
  88. package/dist/writers/pi.d.ts +3 -0
  89. package/dist/writers/pi.js +45 -0
  90. package/docs/cline-native-format.md +43 -0
  91. package/docs/copilot-native-format.md +78 -0
  92. package/docs/cursor-native-format.md +103 -0
  93. package/docs/deepagents-native-format.md +98 -0
  94. package/docs/evidence-api.md +170 -0
  95. package/docs/fx-native-format.md +87 -0
  96. package/docs/harness-parity-bootstrap.md +78 -0
  97. package/docs/harness-support-parity.md +107 -0
  98. package/docs/inspirations.md +103 -0
  99. package/docs/ledger-migration.md +141 -0
  100. package/docs/pi-compaction-context.md +75 -0
  101. package/docs/pi-provider-context.md +76 -0
  102. package/docs/research/2026-10-06-huihua-and-oar.md +176 -0
  103. package/docs/research/ledger/README.md +96 -0
  104. package/docs/research/ledger/confer/2026-09-13-round-10/README.md +61 -0
  105. package/docs/research/ledger/confer/2026-09-13-round-2/cass.md +72 -0
  106. package/docs/research/ledger/confer/2026-09-13-round-3/README.md +70 -0
  107. package/docs/research/ledger/confer/2026-09-13-round-5/README.md +61 -0
  108. package/docs/research/ledger/confer/2026-09-13-round-6/README.md +68 -0
  109. package/docs/research/ledger/confer/2026-09-13-round-7/README.md +64 -0
  110. package/docs/research/ledger/confer/2026-09-13-round-9/README.md +59 -0
  111. package/docs/research/ledger/confer/2026-09-14-wake/wake.md +146 -0
  112. package/docs/research/ledger/docs/inspirations.md +320 -0
  113. package/docs/research/ledger/docs/pr2-pr6-integration.md +53 -0
  114. package/docs/research/ledger/docs/research/2026-09-07-confer-inspirations.md +163 -0
  115. package/docs/research/ledger/docs/research/2026-09-07-letta-continuous-service.md +104 -0
  116. package/docs/research/ledger/docs/research/2026-09-08-claude-history-retrieval.md +117 -0
  117. package/docs/research/ledger/docs/research/2026-09-09-agentsview-native-discovery.md +87 -0
  118. package/docs/research/ledger/docs/research/2026-09-09-daemon-observation.md +231 -0
  119. package/docs/research/ledger/docs/research/2026-09-09-discovery-pagination.md +114 -0
  120. package/docs/research/ledger/docs/research/2026-09-09-harness-transcript-contract.md +145 -0
  121. package/docs/research/ledger/docs/research/2026-09-09-native-history-discovery.md +180 -0
  122. package/docs/research/ledger/docs/research/2026-09-09-native-history-implementation.md +137 -0
  123. package/docs/research/ledger/docs/research/2026-09-09-other-inspirations.md +279 -0
  124. package/docs/research/ledger/docs/research/2026-09-09-trajectory-and-letta-code.md +128 -0
  125. package/docs/research/ledger/docs/research/2026-09-13-diagnostics-coverage.md +116 -0
  126. package/docs/research/ledger/docs/research/2026-09-13-fts-rebuild.md +83 -0
  127. package/docs/research/ledger/docs/research/2026-09-13-read-path-split.md +74 -0
  128. package/docs/research/ledger/docs/research/2026-09-13-runtime-rollout.md +102 -0
  129. package/docs/research/ledger/docs/research/2026-09-13-scan-outcomes.md +80 -0
  130. package/docs/research/ledger/docs/research/2026-09-13-search-quality.md +105 -0
  131. package/docs/research/ledger/docs/research/2026-09-13-source-coverage.md +64 -0
  132. package/docs/research/ledger/docs/research/2026-09-13-sync-wall-budget.md +82 -0
  133. package/docs/research/ledger/docs/research/2026-09-13-typed-read-gate.md +81 -0
  134. package/docs/research/ledger/docs/research/2026-09-13-writer-lease.md +84 -0
  135. package/docs/research/ledger/docs/research/2026-09-16-c1-copy-fallback.md +227 -0
  136. package/docs/research/ledger/docs/research/2026-09-16-c10-kimi.md +414 -0
  137. package/docs/research/ledger/docs/research/2026-09-16-c2-derive-counter.md +216 -0
  138. package/docs/research/ledger/docs/research/2026-09-16-c3-echo-filter.md +305 -0
  139. package/docs/research/ledger/docs/research/2026-09-16-c4-title-index.md +349 -0
  140. package/docs/research/ledger/docs/research/2026-09-16-c5-trigram.md +268 -0
  141. package/docs/research/ledger/docs/research/2026-09-16-c6-recency-prior.md +438 -0
  142. package/docs/research/ledger/docs/research/2026-09-16-c7-tombstones.md +467 -0
  143. package/docs/research/ledger/docs/research/2026-09-16-c8-remote-truth.md +390 -0
  144. package/docs/research/ledger/docs/research/2026-09-16-c9-grok.md +380 -0
  145. package/docs/research/ledger/docs/research/2026-09-16-wake-port-program.md +38 -0
  146. package/docs/research/ledger/docs/research/2026-09-17-cursor-blobstore.md +157 -0
  147. package/docs/research/ledger/manifest.json +540 -0
  148. package/legacy/NOTICE.md +50 -0
  149. package/legacy/bin/ledger-core.ts +7 -0
  150. package/legacy/bin/ledger.ts +7 -0
  151. package/legacy/bin/traj-bridge.ts +7 -0
  152. package/legacy/scripts/import-claude-history.ts +130 -0
  153. package/legacy/scripts/native-history-smoke.ts +52 -0
  154. package/legacy/scripts/observe-daemon.ts +570 -0
  155. package/legacy/scripts/retitle-sessions.md +19 -0
  156. package/legacy/scripts/verify-runtime-rollout.ts +151 -0
  157. package/legacy/src/canonical.ts +108 -0
  158. package/legacy/src/cli.ts +1293 -0
  159. package/legacy/src/db.ts +2254 -0
  160. package/legacy/src/diagnostics.ts +87 -0
  161. package/legacy/src/discovery.ts +56 -0
  162. package/legacy/src/embeddings.ts +382 -0
  163. package/legacy/src/filter.ts +602 -0
  164. package/legacy/src/gemini_embeddings.ts +103 -0
  165. package/legacy/src/identity.ts +9 -0
  166. package/legacy/src/index.ts +10 -0
  167. package/legacy/src/ingest.ts +791 -0
  168. package/legacy/src/managed.ts +178 -0
  169. package/legacy/src/native/common.ts +68 -0
  170. package/legacy/src/native/cursor.ts +647 -0
  171. package/legacy/src/native/grok.ts +540 -0
  172. package/legacy/src/native/kimi.ts +706 -0
  173. package/legacy/src/native/opencode.ts +470 -0
  174. package/legacy/src/native/sqlite-ro.ts +281 -0
  175. package/legacy/src/native.ts +97 -0
  176. package/legacy/src/normalizer.ts +457 -0
  177. package/legacy/src/parentage.ts +562 -0
  178. package/legacy/src/pressure.ts +162 -0
  179. package/legacy/src/private-storage.ts +74 -0
  180. package/legacy/src/reader.ts +1255 -0
  181. package/legacy/src/scanner.ts +591 -0
  182. package/legacy/src/schema-gate.ts +263 -0
  183. package/legacy/src/schema.ts +829 -0
  184. package/legacy/src/semantic-top-k.ts +71 -0
  185. package/legacy/src/session-hash.ts +49 -0
  186. package/legacy/src/store-status.ts +218 -0
  187. package/legacy/src/sync-preparation.ts +45 -0
  188. package/legacy/src/sync-truth.ts +843 -0
  189. package/legacy/src/sync.ts +1233 -0
  190. package/legacy/src/types.ts +436 -0
  191. package/legacy/src/vocab.txt +30522 -0
  192. package/legacy/src/watch.ts +673 -0
  193. package/legacy/src/writer-lease.ts +310 -0
  194. package/ops/README.md +150 -0
  195. package/ops/install-cli.sh +63 -0
  196. package/ops/install-native.sh +35 -0
  197. package/ops/install-release.sh +49 -0
  198. package/ops/linux/install-systemd-user.sh +103 -0
  199. package/ops/macos/install-launchd.sh +72 -0
  200. package/ops/sync-settings.sh +37 -0
  201. package/package.json +21 -5
  202. package/skills/trajectories/SKILL.md +138 -0
  203. package/skills/trajectories/scripts/trajectories_recent.sh +26 -0
  204. package/skills/trajectories/scripts/trajectories_search.sh +26 -0
  205. package/index.js +0 -1
@@ -0,0 +1,281 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { createHash } from "node:crypto";
3
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join, resolve } from "node:path";
6
+
7
+ /**
8
+ * Read-only access to another application's SQLite store, with a private-copy
9
+ * fallback for stores a direct read cannot reach.
10
+ *
11
+ * Gradient ported from Wake@d9f702b6
12
+ * (`crates/wake-core/src/adapters/sqlite_ro.rs#L20-L46`): read-only open plus a
13
+ * probe query → copy the database and its sidecars into a private temp
14
+ * directory and read the copy → give up and surface the original error. A
15
+ * store held by its own writer (rollback-journal `BEGIN EXCLUSIVE`, WAL with
16
+ * `locking_mode = EXCLUSIVE`) answers `SQLITE_BUSY` to a direct reader but
17
+ * copies cleanly, because the copy is a different file with no lock holder.
18
+ *
19
+ * The source store is never written, and `immutable=1` is never used: an
20
+ * immutable open ignores the `-wal`, which is exactly where a live store keeps
21
+ * its newest sessions (`sqlite_ro.rs#L16-L19`).
22
+ */
23
+
24
+ /** Total bytes of database plus sidecars we will copy to reach a locked store. */
25
+ export const MAX_SQLITE_COPY_BYTES = 512 * 1024 * 1024;
26
+
27
+ /** Root of the private copies; one directory per (database, process). */
28
+ export const NATIVE_COPY_ROOT = join(tmpdir(), "ledger-native-db");
29
+
30
+ const COPY_FILE = "db.sqlite";
31
+ // Sidecars in the order they are copied. `-shm` precedes `-wal` so the copied
32
+ // index can never claim frames the copied `-wal` does not contain: an older
33
+ // consistent snapshot is the safe direction, a short frame read is not.
34
+ // `-journal` is a Ledger addition to the pinned copy set — a rollback-journal
35
+ // writer can leave uncommitted pages in the database file, and the copy is
36
+ // opened writable so SQLite rolls them back (see `openCopy`) instead of
37
+ // reporting them as committed.
38
+ const SIDECARS = ["-shm", "-wal", "-journal"] as const;
39
+ const PROBE = "SELECT count(*) AS tables FROM sqlite_master";
40
+
41
+ interface PrivateCopy {
42
+ /** `nativeDbStamp` of the source when the copy was taken. */
43
+ stamp: number;
44
+ dir: string;
45
+ path: string;
46
+ }
47
+
48
+ const copies = new Map<string, PrivateCopy>();
49
+ let cleanupRegistered = false;
50
+
51
+ /**
52
+ * Invalidation key for a copied store: the newest mtime of the database and
53
+ * its `-wal`, ported from Wake's `db_cache_stamp` (`sqlite_ro.rs#L68-L76`).
54
+ * WAL writers leave the database file untouched until a checkpoint, so a stamp
55
+ * that watched only the database would pin a stale copy while new sessions
56
+ * pile up in the `-wal`. `-shm` is deliberately excluded: it changes on every
57
+ * reader's read-mark and would force a copy per call.
58
+ */
59
+ export function nativeDbStamp(dbPath: string): number {
60
+ const mtime = (path: string): number => {
61
+ try { return statSync(path).mtimeMs; } catch { return 0; }
62
+ };
63
+ return Math.max(mtime(dbPath), mtime(`${dbPath}-wal`));
64
+ }
65
+
66
+ /**
67
+ * Path of the private copy currently serving `dbPath`, or null when the store
68
+ * is read directly. A copy whose stamp no longer matches the source is
69
+ * discarded: the caller must not read a snapshot the source has moved past.
70
+ */
71
+ export function cachedCopyPath(dbPath: string): string | null {
72
+ const target = resolve(dbPath);
73
+ const copy = copies.get(target);
74
+ if (!copy) return null;
75
+ if (copy.stamp === nativeDbStamp(target) && existsSync(copy.path)) return copy.path;
76
+ discard(target);
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * Runs `read` against a consistent snapshot of `dbPath`, in a deferred
82
+ * transaction so every query sees the same state. Never writes to the store.
83
+ */
84
+ export function withReadOnlyDb<T>(dbPath: string, read: (db: Database) => T): T {
85
+ const target = resolve(dbPath);
86
+ const cached = cachedCopyPath(target);
87
+ const warm = cached ? openCopy(cached) : null;
88
+ // A private copy that cannot be opened is stale furniture, not a verdict:
89
+ // drop it and let the source answer.
90
+ if (cached && !warm) discard(target);
91
+ if (warm) {
92
+ let probed = false;
93
+ try {
94
+ return runInSnapshot(warm, read, () => { probed = true; });
95
+ } catch (error) {
96
+ // A copy that cannot even be probed is not a verdict on the store; a
97
+ // read that ran on a readable copy fails for its own reasons and stands.
98
+ if (probed) throw error;
99
+ discard(target);
100
+ } finally { warm.close(); }
101
+ }
102
+ let probed = false;
103
+ try {
104
+ const db = new Database(target, { readonly: true });
105
+ try {
106
+ return runInSnapshot(db, read, () => { probed = true; });
107
+ } finally { db.close(); }
108
+ } catch (error) {
109
+ // The store answered the probe and `read` failed: that verdict stands.
110
+ if (probed) throw error;
111
+ // The copy answers *concurrency*, not unreachable bytes: a store whose
112
+ // `-wal` was deleted out-of-band is not revived by copying it, and reading
113
+ // it at its last checkpoint would report partial evidence as the whole
114
+ // session. Everything but lock contention keeps the direct diagnosis.
115
+ if (!isLockContention(error)) throw error;
116
+ const copy = acquireCopy(target, nativeDbStamp(target));
117
+ const db = copy ? openCopy(copy) : null;
118
+ if (!db) throw error;
119
+ let copyProbed = false;
120
+ try {
121
+ return runInSnapshot(db, read, () => { copyProbed = true; });
122
+ } catch (copyError) {
123
+ // Copy unavailable is a copy problem, not a store verdict: the caller
124
+ // keeps the direct diagnosis, the same one this store produced before
125
+ // the fallback existed.
126
+ throw copyProbed ? copyError : error;
127
+ } finally { db.close(); }
128
+ }
129
+ }
130
+
131
+ /** Another connection holding the store surfaces as `SQLITE_BUSY`/`SQLITE_LOCKED`. */
132
+ function isLockContention(error: unknown): boolean {
133
+ if (typeof error !== "object" || error === null || !("code" in error)) return false;
134
+ const { code } = error;
135
+ return typeof code === "string" && /^SQLITE_(?:BUSY|LOCKED)/.test(code);
136
+ }
137
+
138
+ /**
139
+ * The copy is ours: written only by this process, mode 0600, under a 0700
140
+ * directory, removed on exit. Opening it writable is what lets SQLite recover
141
+ * it — roll back a copied hot journal, rebuild a `-shm` the source never had —
142
+ * so a store in recovery is readable rather than a second `read_error`.
143
+ */
144
+ function openCopy(path: string): Database | null {
145
+ try { return new Database(path, { readwrite: true }); } catch { return null; }
146
+ }
147
+
148
+ function runInSnapshot<T>(db: Database, read: (db: Database) => T, probed?: () => void): T {
149
+ db.exec("PRAGMA busy_timeout = 1000");
150
+ return db.transaction(() => {
151
+ db.query(PROBE).get();
152
+ probed?.();
153
+ return read(db);
154
+ }).deferred();
155
+ }
156
+
157
+ /**
158
+ * Copies `dbPath` and its sidecars into this process's private directory and
159
+ * returns the copied database path, or null when the store cannot be copied
160
+ * within budget — the caller then surfaces the direct error unchanged.
161
+ */
162
+ function acquireCopy(target: string, stamp: number): string | null {
163
+ const dir = copyDir(target);
164
+ const path = join(dir, COPY_FILE);
165
+ try {
166
+ if (!privateRoot()) return null;
167
+ sweepOrphans();
168
+ if (setBytes(target) > MAX_SQLITE_COPY_BYTES) return null;
169
+ // Two attempts: a database that moves under the copy is rejected and read
170
+ // once more, because the move is usually a checkpoint that has finished by
171
+ // then.
172
+ for (let attempt = 0; attempt < 2; attempt++) {
173
+ // Recreate rather than overwrite: a sidecar the source no longer has (a
174
+ // checkpointed `-wal`) must not survive into the next snapshot.
175
+ rmSync(dir, { recursive: true, force: true });
176
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
177
+ const base = fileIdentity(target);
178
+ for (const source of [target, ...SIDECARS.map((suffix) => `${target}${suffix}`)]) {
179
+ if (!existsSync(source)) continue;
180
+ const destination = source === target ? path : `${path}${source.slice(target.length)}`;
181
+ // copyFileSync preserves the source mode; a read-only store would
182
+ // leave a copy SQLite cannot open for recovery.
183
+ copyFileSync(source, destination);
184
+ chmodSync(destination, 0o600);
185
+ }
186
+ // A checkpoint backfills pages into the database *and* restarts the wal
187
+ // generation. Frames and index then stay individually valid while the
188
+ // database base is stale, so the copy would silently lose every page the
189
+ // checkpoint moved — the reviewer's `t1=['A'] → t1=[]` case. The database
190
+ // file is the only witness: ordinary WAL commits never touch it, so its
191
+ // identity is stable for every copy that is actually consistent.
192
+ if (base !== null && base === fileIdentity(target)) {
193
+ // A store growing during the copy can outrun the pre-copy check; the
194
+ // overshoot is discarded rather than read.
195
+ if (setBytes(path) <= MAX_SQLITE_COPY_BYTES) {
196
+ copies.set(target, { stamp, dir, path });
197
+ registerCleanup();
198
+ return path;
199
+ }
200
+ break;
201
+ }
202
+ }
203
+ rmSync(dir, { recursive: true, force: true });
204
+ return null;
205
+ } catch {
206
+ rmSync(dir, { recursive: true, force: true });
207
+ return null;
208
+ }
209
+ }
210
+
211
+ /** `size:mtime` of a file, or null when it cannot be read — the copy base identity. */
212
+ function fileIdentity(path: string): string | null {
213
+ try {
214
+ const stat = statSync(path);
215
+ return `${stat.size}:${stat.mtimeMs}`;
216
+ } catch { return null; }
217
+ }
218
+
219
+ /**
220
+ * The copy root must be ours and private. A pre-existing world-readable root —
221
+ * another user on a shared TMPDIR, or a planted directory — would expose
222
+ * transcript copies until each file is chmodded and would let foreign entries
223
+ * steer the orphan sweep, so the fallback refuses to use it at all.
224
+ */
225
+ function privateRoot(): boolean {
226
+ mkdirSync(NATIVE_COPY_ROOT, { recursive: true, mode: 0o700 });
227
+ const root = statSync(NATIVE_COPY_ROOT);
228
+ const uid = typeof process.getuid === "function" ? process.getuid() : root.uid;
229
+ return root.isDirectory() && root.uid === uid && (root.mode & 0o777) === 0o700;
230
+ }
231
+
232
+ /** Bytes of a database plus whatever sidecars exist beside it. */
233
+ function setBytes(dbPath: string): number {
234
+ let bytes = 0;
235
+ for (const path of [dbPath, ...SIDECARS.map((suffix) => `${dbPath}${suffix}`)]) {
236
+ try {
237
+ const stat = statSync(path);
238
+ if (stat.isFile()) bytes += stat.size;
239
+ } catch { /* missing sidecar */ }
240
+ }
241
+ return bytes;
242
+ }
243
+
244
+ function copyDir(target: string): string {
245
+ const key = createHash("sha256").update(target).digest("hex").slice(0, 16);
246
+ return join(NATIVE_COPY_ROOT, `copy-${key}-${process.pid}`);
247
+ }
248
+
249
+ function discard(target: string): void {
250
+ const copy = copies.get(target);
251
+ if (!copy) return;
252
+ copies.delete(target);
253
+ rmSync(copy.dir, { recursive: true, force: true });
254
+ }
255
+
256
+ /**
257
+ * Copies are private agent history; they do not outlive the process that took
258
+ * them. A crash skips this hook, so `sweepOrphans` removes the directories of
259
+ * processes that no longer exist before the next copy.
260
+ */
261
+ function registerCleanup(): void {
262
+ if (cleanupRegistered) return;
263
+ cleanupRegistered = true;
264
+ process.once("exit", () => {
265
+ for (const copy of copies.values()) rmSync(copy.dir, { recursive: true, force: true });
266
+ });
267
+ }
268
+
269
+ function sweepOrphans(): void {
270
+ for (const entry of readdirSync(NATIVE_COPY_ROOT, { withFileTypes: true })) {
271
+ if (!entry.isDirectory()) continue;
272
+ const pid = /^copy-[0-9a-f]{16}-(\d+)$/.exec(entry.name)?.[1];
273
+ if (pid === undefined || Number(pid) === process.pid || isLive(Number(pid))) continue;
274
+ rmSync(join(NATIVE_COPY_ROOT, entry.name), { recursive: true, force: true });
275
+ }
276
+ }
277
+
278
+ function isLive(pid: number): boolean {
279
+ try { process.kill(pid, 0); return true; }
280
+ catch (error) { return (error as NodeJS.ErrnoException).code === "EPERM"; }
281
+ }
@@ -0,0 +1,97 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readdirSync, statSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import type { DiscoveredSession } from "./types.js";
5
+ import { readCursorSession } from "./native/cursor.js";
6
+ import { grokSessionFiles, readGrokSession } from "./native/grok.js";
7
+ import { kimiSessionFiles, readKimiSession } from "./native/kimi.js";
8
+ import { openCodeSessionFiles, readOpenCodeSession } from "./native/opencode.js";
9
+ import { MAX_NATIVE_RECORDS, type NativeReadOptions, type NativeTranscript } from "./native/common.js";
10
+
11
+ interface NativeIdentity {
12
+ nativeVersion: 2;
13
+ snapshot: string;
14
+ failureMaxBytes?: number;
15
+ }
16
+
17
+ export function nativeIdentity(value?: string | null): NativeIdentity | null {
18
+ if (!value) return null;
19
+ try {
20
+ const identity = JSON.parse(value);
21
+ return identity?.nativeVersion === 2 && typeof identity.snapshot === "string" ? identity : null;
22
+ } catch { return null; }
23
+ }
24
+
25
+ export function readNativeSession(item: DiscoveredSession, options: NativeReadOptions = {}): NativeTranscript {
26
+ if (item.native?.kind === "fx-directory") {
27
+ throw new Error("FX directory sessions require path-backed SDK normalization; readNativeSession cannot decode only session.json.");
28
+ }
29
+ return item.native?.kind === "cursor-sqlite" || item.native?.kind === "cursor-blobstore"
30
+ ? readCursorSession(item, options)
31
+ : item.native?.kind === "grok-updates"
32
+ ? readGrokSession(item, options)
33
+ : item.native?.kind === "kimi-wire"
34
+ ? readKimiSession(item, options)
35
+ : readOpenCodeSession(item, options);
36
+ }
37
+
38
+ /** Database sidecars and legacy part files can change while the primary file does not. */
39
+ export function nativeSourceFingerprint(item: DiscoveredSession): {
40
+ mtime: string; size: number; file_identity: string;
41
+ } | null {
42
+ if (!item.native) return null;
43
+ try {
44
+ if (!statSync(item.path).isFile()) return null;
45
+ const sqlite = item.native.kind === "cursor-sqlite" || item.native.kind === "cursor-blobstore"
46
+ || item.native.kind === "opencode-sqlite";
47
+ const paths = item.native.kind === "fx-directory"
48
+ ? fxSessionFiles(item.path)
49
+ : sqlite
50
+ ? [item.path, `${item.path}-wal`, `${item.path}-journal`]
51
+ : item.native.kind === "grok-updates"
52
+ ? grokSessionFiles(item)
53
+ : item.native.kind === "kimi-wire" ? kimiSessionFiles(item) : openCodeSessionFiles(item);
54
+ if (paths.length > MAX_NATIVE_RECORDS) return null;
55
+ const hash = createHash("sha256").update(JSON.stringify([item.native, item.id]));
56
+ let bytes = 0;
57
+ let modified = 0;
58
+ for (const path of [...new Set(paths)].sort()) {
59
+ hash.update(JSON.stringify(path));
60
+ try {
61
+ const stat = statSync(path, { bigint: true });
62
+ // SQLite read-only opens may chmod the WAL to match database permissions,
63
+ // changing ctime without changing its bytes. Keep content-bearing mtime,
64
+ // size and generation identity; ordinary native files still include ctime.
65
+ hash.update(JSON.stringify([stat.dev, stat.ino, stat.birthtimeNs, stat.mtimeNs,
66
+ ...(sqlite ? [] : [stat.ctimeNs]), stat.size].map(String)));
67
+ if (stat.isFile()) bytes += Number(stat.size);
68
+ modified = Math.max(modified, Number(stat.mtimeMs));
69
+ } catch (error) {
70
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
71
+ hash.update("missing");
72
+ }
73
+ }
74
+ return {
75
+ mtime: new Date(modified).toISOString(),
76
+ // A shared database's full byte size is not each session's backlog.
77
+ size: sqlite ? item.sizeBytes ?? -1 : bytes,
78
+ file_identity: JSON.stringify({ nativeVersion: 2, snapshot: hash.digest("hex") } satisfies NativeIdentity),
79
+ };
80
+ } catch (error) {
81
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
82
+ throw error;
83
+ }
84
+ }
85
+
86
+ function fxSessionFiles(sessionPath: string): string[] {
87
+ const directory = dirname(sessionPath);
88
+ const paths = [sessionPath, join(directory, "events.jsonl")];
89
+ try {
90
+ for (const entry of readdirSync(join(directory, "tool-results"), { withFileTypes: true })) {
91
+ if (entry.isFile() || entry.isSymbolicLink()) paths.push(join(directory, "tool-results", entry.name));
92
+ }
93
+ } catch (error) {
94
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
95
+ }
96
+ return paths;
97
+ }