trajectories 0.0.0 → 0.1.1

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 (204) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +201 -0
  3. package/README.md +274 -2
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +8542 -0
  6. package/dist/convert.d.ts +31 -0
  7. package/dist/convert.js +431 -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 +1219 -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 +120 -0
  195. package/ops/install-cli.sh +63 -0
  196. package/ops/install-release.sh +49 -0
  197. package/ops/linux/install-systemd-user.sh +103 -0
  198. package/ops/macos/install-launchd.sh +72 -0
  199. package/ops/sync-settings.sh +37 -0
  200. package/package.json +19 -5
  201. package/skills/trajectories/SKILL.md +138 -0
  202. package/skills/trajectories/scripts/trajectories_recent.sh +26 -0
  203. package/skills/trajectories/scripts/trajectories_search.sh +26 -0
  204. package/index.js +0 -1
@@ -0,0 +1,457 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { readSnapshot as readPortableSnapshot } from "../../dist/index.js";
3
+ import {
4
+ NormalizationError,
5
+ normalizeToCanonical as upstreamNormalizeToCanonical,
6
+ normalizeTranscript as upstreamNormalizeTranscript,
7
+ } from "../../dist/normalization-runtime.js";
8
+ import { ledgerId } from "./identity.js";
9
+ import {
10
+ type AnyTrajectorySource,
11
+ type CanonicalRecord,
12
+ type Diagnostic,
13
+ type NormalizedRecord,
14
+ type TrajectorySource,
15
+ normalizeCheckpointToCanonical as upstreamNormalizeCheckpointToCanonical,
16
+ } from "@letta-ai/trajectory";
17
+ import { extractSessionParentage, type ParentageDiagnostic } from "./parentage.js";
18
+ import { readNativeSession } from "./native.js";
19
+ import { GROK_PROJECTION_SOURCE } from "./native/grok.js";
20
+ import { KIMI_PROJECTION_SOURCE } from "./native/kimi.js";
21
+ import type { NativeTranscript } from "./native/common.js";
22
+ import { withReadOnlyDb } from "./native/sqlite-ro.js";
23
+ import {
24
+ hashSessionRecords,
25
+ SESSION_HASH_VERSION,
26
+ } from "./session-hash.js";
27
+ import type {
28
+ CanonicalDbRecord,
29
+ DiscoveredSession,
30
+ NormalizedSessionResult,
31
+ SessionRecord,
32
+ } from "./types.js";
33
+
34
+ // Portable histories must never inherit the search normalizer's truncating defaults.
35
+ const fullHistory = {
36
+ bounds: { toolArguments: { maxCharacters: null }, toolResults: { maxCharacters: null } },
37
+ filters: { systemMessages: "include" as const, toolResults: "include" as const },
38
+ };
39
+ const normalizeToCanonical = (input: Parameters<typeof upstreamNormalizeToCanonical>[0]) =>
40
+ upstreamNormalizeToCanonical({ ...input, ...fullHistory });
41
+ const normalizeTranscript = (input: Parameters<typeof upstreamNormalizeTranscript>[0]) =>
42
+ upstreamNormalizeTranscript({ ...input, ...fullHistory });
43
+ const normalizeCheckpointToCanonical = (input: Parameters<typeof upstreamNormalizeCheckpointToCanonical>[0]) =>
44
+ upstreamNormalizeCheckpointToCanonical({ ...input, ...fullHistory });
45
+
46
+ /**
47
+ * The sources the pinned library cannot read, each with the lane its native
48
+ * reader projects into. A source's own reader emits the projection; the
49
+ * normalizer restores the records' provenance, and treats the listing's own id
50
+ * as authoritative so a projected stream that carries no group cannot take the
51
+ * library's `default` sentinel as its session id.
52
+ */
53
+ const PROJECTION_LANES: Record<string, TrajectorySource> = {
54
+ grok: GROK_PROJECTION_SOURCE,
55
+ kimi: KIMI_PROJECTION_SOURCE,
56
+ };
57
+
58
+ export async function normalizeDiscoveredSession(
59
+ item: DiscoveredSession,
60
+ options: { transcript?: string; groupId?: string; maxSourceBytes?: number; onReadError?: (error: unknown) => void; onParentageDiagnostic?: (diagnostic: ParentageDiagnostic) => void } = {},
61
+ ): Promise<NormalizedSessionResult | null> {
62
+ const source = item.source as AnyTrajectorySource;
63
+
64
+ let canonicalRecords: CanonicalRecord[] = [];
65
+ let rawNormalizedRecords: NormalizedRecord[] = [];
66
+ let diagnostics: Diagnostic[] = [];
67
+ let fileTranscript: string | null = null;
68
+ let nativeTranscript: NativeTranscript | undefined;
69
+
70
+ try {
71
+ if (source === "hermes") {
72
+ // Same gradient as the native adapters: a Hermes store held by its own
73
+ // writer is read from a private copy instead of failing the session.
74
+ const payload = withReadOnlyDb(item.path, (db) => JSON.stringify({
75
+ session: db
76
+ .query("SELECT * FROM sessions WHERE id = ? LIMIT 1")
77
+ .get(item.id) ?? undefined,
78
+ messages: db
79
+ .query("SELECT * FROM messages WHERE session_id = ? ORDER BY id ASC")
80
+ .all(item.id),
81
+ }));
82
+
83
+ const canonicalResult = normalizeWithFallback("hermes", payload, item.id);
84
+ if (!canonicalResult) return null;
85
+
86
+ canonicalRecords = canonicalResult.records;
87
+ diagnostics = canonicalResult.diagnostics;
88
+
89
+ try {
90
+ const normResult = normalizeTranscript({
91
+ source: "hermes",
92
+ transcript: payload,
93
+ });
94
+ rawNormalizedRecords = normResult.records;
95
+ } catch {
96
+ // ignore
97
+ }
98
+ } else if (source === "deepagents") {
99
+ const canonicalResult = await normalizeCheckpointToCanonical({
100
+ source: "deepagents",
101
+ checkpoint: {
102
+ path: item.path,
103
+ threadId: item.id,
104
+ },
105
+ });
106
+ canonicalRecords = canonicalResult.records;
107
+ diagnostics = canonicalResult.diagnostics;
108
+ } else if (item.source === "cline" || item.source === "copilot-cli") {
109
+ const transcript = options.transcript ?? readFileSync(item.path, "utf8");
110
+ fileTranscript = transcript;
111
+ const canonicalResult = (await readPortableSnapshot({
112
+ harness: item.source === "cline" ? "cline" : "github-copilot",
113
+ transcript,
114
+ nativeSessionId: item.id,
115
+ })).canonical;
116
+ const nativeId = canonicalResult.records[0]?.source_group_id;
117
+ if (nativeId !== item.id) throw new Error(`${item.source} packet and native directory identities disagree.`);
118
+ canonicalRecords = canonicalResult.records;
119
+ diagnostics = canonicalResult.diagnostics;
120
+ const seen = new Set<string>();
121
+ for (const record of canonicalRecords) {
122
+ if (item.source !== "copilot-cli" && seen.has(record.source_order_id)) continue;
123
+ seen.add(record.source_order_id);
124
+ rawNormalizedRecords.push(JSON.parse(record.record_json) as NormalizedRecord);
125
+ }
126
+ } else if (item.source === "fx") {
127
+ fileTranscript = options.transcript ?? readFileSync(item.path, "utf8");
128
+ // Schema 4 keeps authoritative history and result bodies beside session.json.
129
+ // Scanner-discovered FX sessions use the path so the SDK can capture them.
130
+ const canonicalResult = (await readPortableSnapshot({
131
+ harness: "fx",
132
+ ...(item.native?.kind === "fx-directory" ? { path: item.path } : { transcript: fileTranscript }),
133
+ nativeSessionId: item.id,
134
+ })).canonical;
135
+ const nativeId = canonicalResult.records[0]?.source_group_id;
136
+ if (nativeId !== item.id) throw new Error("fx packet and native directory identities disagree.");
137
+ canonicalRecords = canonicalResult.records;
138
+ diagnostics = canonicalResult.diagnostics;
139
+ const seen = new Set<string>();
140
+ for (const record of canonicalRecords) {
141
+ if (seen.has(record.source_order_id)) continue;
142
+ seen.add(record.source_order_id);
143
+ rawNormalizedRecords.push(JSON.parse(record.record_json) as NormalizedRecord);
144
+ }
145
+ } else {
146
+ // File-backed source
147
+ if (item.native && options.transcript === undefined) {
148
+ nativeTranscript = readNativeSession(item, { maxBytes: options.maxSourceBytes });
149
+ }
150
+ const transcript = options.transcript ?? nativeTranscript?.transcript ?? readFileSync(item.path, "utf8");
151
+ fileTranscript = transcript;
152
+ // The pinned library has no adapter for this source, so the native reader
153
+ // projects it into one it does know; what it read is still this source, so
154
+ // the records keep this source's provenance. That projection also makes the
155
+ // listing's own id authoritative: the reader stamps it on every group
156
+ // carrier it can emit, and a projected stream that carries none (a torn or
157
+ // missing sidecar on an agent-only session) would otherwise take the
158
+ // library's `default` sentinel as its group — every such session sharing one
159
+ // ledger id, each replacing the previous one's records.
160
+ const lane = PROJECTION_LANES[item.source];
161
+ const projected = lane !== undefined;
162
+ const normalizerSource = projected ? lane : source;
163
+ const canonicalResult = normalizeWithFallback(
164
+ normalizerSource, transcript, options.groupId ?? item.id, 0,
165
+ options.groupId !== undefined || projected,
166
+ );
167
+ if (!canonicalResult) return null;
168
+
169
+ canonicalRecords = projected
170
+ ? canonicalResult.records.map((record) => ({ ...record, source_type: item.source }))
171
+ : canonicalResult.records;
172
+ diagnostics = [...canonicalResult.diagnostics, ...(nativeTranscript?.diagnostics ?? [])];
173
+
174
+ try {
175
+ const normResult = normalizeTranscript({
176
+ source: normalizerSource,
177
+ transcript,
178
+ });
179
+ rawNormalizedRecords = normResult.records;
180
+ } catch {
181
+ // ignore
182
+ }
183
+ }
184
+ } catch (error) {
185
+ options.onReadError?.(error);
186
+ return null;
187
+ }
188
+
189
+ if (canonicalRecords.length === 0) {
190
+ return null;
191
+ }
192
+
193
+ const nativeSessionId = canonicalRecords[0].source_group_id || item.id;
194
+ // Upstream group/record identities are scoped to one source. Ledger stores
195
+ // many independent harnesses in one database, so make that scope explicit in
196
+ // primary keys rather than relying on native ids to be globally unique.
197
+ const ledgerSessionId = ledgerId(item.source, nativeSessionId);
198
+
199
+ // Extract session metadata
200
+ let cwd: string | null = null;
201
+ let model: string | null = null;
202
+ let startedAt: string | null = null;
203
+ let endedAt: string | null = null;
204
+ let firstUserText: string | null = null;
205
+
206
+ for (const r of canonicalRecords) {
207
+ const ts = r.source_timestamp ?? r.record_timestamp;
208
+ if (ts) {
209
+ if (!startedAt || ts < startedAt) startedAt = ts;
210
+ if (!endedAt || ts > endedAt) endedAt = ts;
211
+ }
212
+
213
+ if (r.record_type === "meta") {
214
+ try {
215
+ const parsed = JSON.parse(r.record_json);
216
+ if (parsed.cwd && typeof parsed.cwd === "string") cwd = parsed.cwd;
217
+ if (parsed.model && typeof parsed.model === "string") model = parsed.model;
218
+ } catch {
219
+ // ignore
220
+ }
221
+ } else if (r.record_type === "user" && !firstUserText && r.content) {
222
+ const t = r.content.trim();
223
+ // Skip harness-injected boilerplate when deriving a session title —
224
+ // these wrappers (plugin catalogs, env contexts) would otherwise
225
+ // become the display title of every affected session.
226
+ const isBoilerplate =
227
+ t.startsWith("<recommended_plugins>") ||
228
+ t.startsWith("<environment_context>") ||
229
+ t.startsWith("<user_info>") ||
230
+ t.startsWith("<system_warning>");
231
+ if (!isBoilerplate) {
232
+ firstUserText = t.slice(0, 120);
233
+ }
234
+ }
235
+ }
236
+
237
+ if (!startedAt && item.updatedAt) {
238
+ startedAt = item.updatedAt;
239
+ endedAt = item.updatedAt;
240
+ }
241
+ cwd = nativeTranscript?.cwd ?? cwd;
242
+ model = nativeTranscript?.model ?? model;
243
+ startedAt = nativeTranscript?.startedAt ?? startedAt;
244
+ endedAt = nativeTranscript?.endedAt ?? endedAt;
245
+
246
+ // Accumulate records into an appendable deterministic identity without
247
+ // rereading historical database rows.
248
+ const hashRecords = canonicalRecords.map((record) => ({
249
+ id: ledgerId(item.source, record.record_id),
250
+ record_type: record.record_type,
251
+ source_order_id: record.source_order_id,
252
+ component_index: record.component_index,
253
+ content_hash: record.content_hash,
254
+ }));
255
+ const contentHash = hashSessionRecords(item.source, ledgerSessionId, hashRecords);
256
+
257
+ const title = nativeTranscript?.title ?? item.title ?? firstUserText ?? `Session ${nativeSessionId.slice(0, 16)}`;
258
+ const now = new Date().toISOString();
259
+ const parentage = extractSessionParentage(item.source, item.path, {
260
+ sessionId: nativeSessionId,
261
+ transcript: fileTranscript ?? undefined,
262
+ // A source whose parentage lives in another session's directory records the
263
+ // edge at scan time, where the store is walked once; the extractor consumes
264
+ // it instead of re-walking per session.
265
+ registryParentSessionId: item.native?.kind === "grok-updates" ? item.native.parentSessionId : undefined,
266
+ registryParentToolUseId: item.native?.kind === "kimi-wire" ? item.native.parentToolUseId : undefined,
267
+ onDiagnostic: options.onParentageDiagnostic ?? ((diagnostic) => console.warn(JSON.stringify({ type: "parentage_warning", ...diagnostic }))),
268
+ });
269
+ if (parentage.parent_session_id) parentage.parent_session_id = ledgerId(item.source, parentage.parent_session_id);
270
+ if (parentage.parent_session_id === ledgerSessionId) parentage.parent_session_id = null;
271
+
272
+ const session: SessionRecord = {
273
+ id: ledgerSessionId,
274
+ source: item.source,
275
+ source_path: item.path,
276
+ content_hash: contentHash,
277
+ content_hash_version: SESSION_HASH_VERSION,
278
+ title,
279
+ cwd,
280
+ model,
281
+ started_at: startedAt,
282
+ ended_at: endedAt,
283
+ record_count: canonicalRecords.length,
284
+ created_at: now,
285
+ updated_at: now,
286
+ ...parentage,
287
+ };
288
+
289
+ const records: CanonicalDbRecord[] = canonicalRecords.map((r) => ({
290
+ id: ledgerId(item.source, r.record_id),
291
+ session_id: ledgerSessionId,
292
+ source_type: r.source_type,
293
+ record_type: r.record_type,
294
+ stable_source_record_id: r.stable_source_record_id ?? null,
295
+ source_identity_kind: r.source_identity_kind ?? null,
296
+ source_order_id: r.source_order_id,
297
+ component_index: r.component_index,
298
+ record_timestamp: r.record_timestamp ?? null,
299
+ source_timestamp: r.source_timestamp ?? null,
300
+ content: r.content ?? null,
301
+ tool_call_id: r.tool_call_id ?? null,
302
+ tool_name: r.tool_name ?? null,
303
+ tool_arguments_json: r.tool_arguments_json ?? null,
304
+ tool_result_json: r.tool_result_json ?? null,
305
+ tool_result_ok:
306
+ r.tool_result_ok === true ? 1 : r.tool_result_ok === false ? 0 : null,
307
+ record_json: r.record_json,
308
+ content_hash: r.content_hash,
309
+ record_hash: r.record_hash,
310
+ }));
311
+
312
+ return {
313
+ session,
314
+ records,
315
+ rawNormalizedRecords,
316
+ diagnostics,
317
+ };
318
+ }
319
+
320
+ export function canonicalRecordsFromTranscript(
321
+ source: TrajectorySource,
322
+ transcript: string,
323
+ groupId: string,
324
+ baseByteOffset = 0,
325
+ baseLineOffset = 0,
326
+ ): CanonicalDbRecord[] | null {
327
+ return canonicalRecordsWithDiagnostics(source, transcript, groupId, baseByteOffset, baseLineOffset)?.records ?? null;
328
+ }
329
+
330
+ /**
331
+ * The same projection as {@link canonicalRecordsFromTranscript}, keeping the
332
+ * normalization diagnostics of the chunk that produced the records so a chunked
333
+ * caller can accumulate coverage without re-normalizing the whole source.
334
+ */
335
+ export function canonicalRecordsWithDiagnostics(
336
+ source: TrajectorySource,
337
+ transcript: string,
338
+ groupId: string,
339
+ baseByteOffset = 0,
340
+ baseLineOffset = 0,
341
+ ): { records: CanonicalDbRecord[]; diagnostics: Diagnostic[] } | null {
342
+ // Native hook/user pairing needs the complete journal. Copilot ingestion uses
343
+ // the bounded whole-file SDK path; the old fragment parser loses direct prompts.
344
+ if (source === "copilot-cli") return null;
345
+ if (!Number.isSafeInteger(baseLineOffset) || baseLineOffset < 0) throw new Error("baseLineOffset must be a nonnegative safe integer");
346
+ const canonicalResult = normalizeWithFallback(
347
+ source,
348
+ transcript,
349
+ groupId,
350
+ baseByteOffset,
351
+ );
352
+ if (!canonicalResult) return null;
353
+ const sessionId = ledgerId(source, canonicalResult.records[0]?.source_group_id || groupId);
354
+ const records: CanonicalDbRecord[] = canonicalResult.records.map((r) => ({
355
+ id: ledgerId(source, r.record_id),
356
+ session_id: sessionId,
357
+ source_type: r.source_type,
358
+ record_type: r.record_type,
359
+ stable_source_record_id: r.stable_source_record_id ?? null,
360
+ source_identity_kind: r.source_identity_kind ?? null,
361
+ source_order_id: rebaseLineOrder(source, r, baseLineOffset),
362
+ component_index: r.component_index,
363
+ record_timestamp: r.record_timestamp ?? null,
364
+ source_timestamp: r.source_timestamp ?? null,
365
+ content: r.content ?? null,
366
+ tool_call_id: r.tool_call_id ?? null,
367
+ tool_name: r.tool_name ?? null,
368
+ tool_arguments_json: r.tool_arguments_json ?? null,
369
+ tool_result_json: r.tool_result_json ?? null,
370
+ tool_result_ok:
371
+ r.tool_result_ok === true ? 1 : r.tool_result_ok === false ? 0 : null,
372
+ record_json: r.record_json,
373
+ content_hash: r.content_hash,
374
+ record_hash: r.record_hash,
375
+ }));
376
+ return { records, diagnostics: canonicalResult.diagnostics };
377
+ }
378
+
379
+ function rebaseLineOrder(source: string, record: CanonicalRecord, baseLineOffset: number): string {
380
+ if (!baseLineOffset || record.record_type === "meta" || (source !== "cursor" && source !== "copilot-cli")) {
381
+ return record.source_order_id;
382
+ }
383
+ // Trajectory 0.3 uses chunk-local line sequences for these two adapters.
384
+ // Project that sequence into the verified source file; native identities stay unchanged.
385
+ const [rank, timestamp, sequence, ...identity] = record.source_order_id.split("|");
386
+ if (!sequence || !/^\d+$/.test(sequence) || identity.length === 0) throw new Error("Unsupported Trajectory source order format");
387
+ const absolute = (BigInt(sequence) + BigInt(baseLineOffset)).toString().padStart(sequence.length, "0");
388
+ return [rank, timestamp, absolute, ...identity].join("|");
389
+ }
390
+
391
+ function normalizeWithFallback(
392
+ source: TrajectorySource,
393
+ transcript: string,
394
+ fallbackGroupId: string,
395
+ baseByteOffset = 0,
396
+ groupIsAuthoritative = false,
397
+ ) {
398
+ const sourceContext = {
399
+ groupId: fallbackGroupId,
400
+ ...(baseByteOffset > 0 ? { baseByteOffset, partial: true as const } : {}),
401
+ };
402
+
403
+ // Step 1: try plain normalizeToCanonical
404
+ try {
405
+ return normalizeToCanonical({
406
+ source,
407
+ transcript,
408
+ ...(baseByteOffset > 0 || groupIsAuthoritative ? { sourceContext } : {}),
409
+ });
410
+ } catch (e1) {
411
+ if (e1 instanceof NormalizationError) {
412
+ if (e1.code === "source_group_required") {
413
+ // Retry with sourceContext.groupId
414
+ try {
415
+ return normalizeToCanonical({
416
+ source,
417
+ transcript,
418
+ sourceContext,
419
+ });
420
+ } catch (e2) {
421
+ if (e2 instanceof NormalizationError && (e2.code === "missing_assistant_records" || e2.code === "missing_user_records")) {
422
+ try {
423
+ return normalizeToCanonical({
424
+ source,
425
+ transcript,
426
+ sourceContext: { ...sourceContext, partial: true },
427
+ });
428
+ } catch {
429
+ return null;
430
+ }
431
+ }
432
+ return null;
433
+ }
434
+ }
435
+
436
+ if (e1.code === "missing_assistant_records" || e1.code === "missing_user_records") {
437
+ try {
438
+ return normalizeToCanonical({
439
+ source,
440
+ transcript,
441
+ sourceContext: baseByteOffset > 0 || groupIsAuthoritative ? { ...sourceContext, partial: true } : { partial: true },
442
+ });
443
+ } catch (e2) {
444
+ if (e2 instanceof NormalizationError && e2.code === "source_group_required") {
445
+ try {
446
+ return normalizeToCanonical({ source, transcript, sourceContext: { ...sourceContext, partial: true } });
447
+ } catch {
448
+ return null;
449
+ }
450
+ }
451
+ return null;
452
+ }
453
+ }
454
+ }
455
+ return null;
456
+ }
457
+ }