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.
- package/CHANGELOG.md +23 -0
- package/LICENSE +201 -0
- package/README.md +274 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +8542 -0
- package/dist/convert.d.ts +31 -0
- package/dist/convert.js +431 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8402 -0
- package/dist/log-baseline.d.ts +26 -0
- package/dist/log-baseline.js +104 -0
- package/dist/log-content.d.ts +16 -0
- package/dist/log-content.js +80 -0
- package/dist/log-io.d.ts +10 -0
- package/dist/log-io.js +121 -0
- package/dist/log-read.d.ts +24 -0
- package/dist/log-read.js +117 -0
- package/dist/log-reconcile.d.ts +5 -0
- package/dist/log-reconcile.js +38 -0
- package/dist/log-records.d.ts +35 -0
- package/dist/log-records.js +85 -0
- package/dist/log-recovery.d.ts +18 -0
- package/dist/log-recovery.js +123 -0
- package/dist/log-stage.d.ts +14 -0
- package/dist/log-stage.js +138 -0
- package/dist/log-state.d.ts +89 -0
- package/dist/log-state.js +92 -0
- package/dist/log-switch.d.ts +3 -0
- package/dist/log-switch.js +98 -0
- package/dist/log-types.d.ts +129 -0
- package/dist/log-types.js +8 -0
- package/dist/log.d.ts +8 -0
- package/dist/log.js +62 -0
- package/dist/migrate.d.ts +29 -0
- package/dist/migrate.js +173 -0
- package/dist/normalization-runtime.d.ts +1 -0
- package/dist/normalization-runtime.js +3150 -0
- package/dist/portable.d.ts +9 -0
- package/dist/portable.js +26 -0
- package/dist/readers/canonical.d.ts +13 -0
- package/dist/readers/canonical.js +54 -0
- package/dist/readers/cline.d.ts +10 -0
- package/dist/readers/cline.js +107 -0
- package/dist/readers/cursor.d.ts +20 -0
- package/dist/readers/cursor.js +374 -0
- package/dist/readers/deepagents.d.ts +41 -0
- package/dist/readers/deepagents.js +163 -0
- package/dist/readers/fx.d.ts +11 -0
- package/dist/readers/fx.js +573 -0
- package/dist/readers/github-copilot.d.ts +8 -0
- package/dist/readers/github-copilot.js +216 -0
- package/dist/readers/grok-build.d.ts +12 -0
- package/dist/readers/grok-build.js +391 -0
- package/dist/readers/guards.d.ts +4 -0
- package/dist/readers/guards.js +121 -0
- package/dist/readers/pi-compaction.d.ts +7 -0
- package/dist/readers/pi-compaction.js +112 -0
- package/dist/readers/pi-context.d.ts +13 -0
- package/dist/readers/pi-context.js +64 -0
- package/dist/store.d.ts +25 -0
- package/dist/store.js +218 -0
- package/dist/types.d.ts +91 -0
- package/dist/types.js +1 -0
- package/dist/writers/claude-code.d.ts +5 -0
- package/dist/writers/claude-code.js +51 -0
- package/dist/writers/cline.d.ts +3 -0
- package/dist/writers/cline.js +77 -0
- package/dist/writers/codex.d.ts +3 -0
- package/dist/writers/codex.js +32 -0
- package/dist/writers/cursor.d.ts +3 -0
- package/dist/writers/cursor.js +197 -0
- package/dist/writers/deepagents.d.ts +3 -0
- package/dist/writers/deepagents.js +116 -0
- package/dist/writers/entropy.d.ts +5 -0
- package/dist/writers/entropy.js +33 -0
- package/dist/writers/fx.d.ts +3 -0
- package/dist/writers/fx.js +135 -0
- package/dist/writers/github-copilot.d.ts +3 -0
- package/dist/writers/github-copilot.js +71 -0
- package/dist/writers/grok-build.d.ts +3 -0
- package/dist/writers/grok-build.js +43 -0
- package/dist/writers/helpers.d.ts +17 -0
- package/dist/writers/helpers.js +75 -0
- package/dist/writers/omp.d.ts +3 -0
- package/dist/writers/omp.js +7 -0
- package/dist/writers/opencode.d.ts +3 -0
- package/dist/writers/opencode.js +65 -0
- package/dist/writers/pi.d.ts +3 -0
- package/dist/writers/pi.js +45 -0
- package/docs/cline-native-format.md +43 -0
- package/docs/copilot-native-format.md +78 -0
- package/docs/cursor-native-format.md +103 -0
- package/docs/deepagents-native-format.md +98 -0
- package/docs/evidence-api.md +170 -0
- package/docs/fx-native-format.md +87 -0
- package/docs/harness-parity-bootstrap.md +78 -0
- package/docs/harness-support-parity.md +107 -0
- package/docs/inspirations.md +103 -0
- package/docs/ledger-migration.md +141 -0
- package/docs/pi-compaction-context.md +75 -0
- package/docs/pi-provider-context.md +76 -0
- package/docs/research/2026-10-06-huihua-and-oar.md +176 -0
- package/docs/research/ledger/README.md +96 -0
- package/docs/research/ledger/confer/2026-09-13-round-10/README.md +61 -0
- package/docs/research/ledger/confer/2026-09-13-round-2/cass.md +72 -0
- package/docs/research/ledger/confer/2026-09-13-round-3/README.md +70 -0
- package/docs/research/ledger/confer/2026-09-13-round-5/README.md +61 -0
- package/docs/research/ledger/confer/2026-09-13-round-6/README.md +68 -0
- package/docs/research/ledger/confer/2026-09-13-round-7/README.md +64 -0
- package/docs/research/ledger/confer/2026-09-13-round-9/README.md +59 -0
- package/docs/research/ledger/confer/2026-09-14-wake/wake.md +146 -0
- package/docs/research/ledger/docs/inspirations.md +320 -0
- package/docs/research/ledger/docs/pr2-pr6-integration.md +53 -0
- package/docs/research/ledger/docs/research/2026-09-07-confer-inspirations.md +163 -0
- package/docs/research/ledger/docs/research/2026-09-07-letta-continuous-service.md +104 -0
- package/docs/research/ledger/docs/research/2026-09-08-claude-history-retrieval.md +117 -0
- package/docs/research/ledger/docs/research/2026-09-09-agentsview-native-discovery.md +87 -0
- package/docs/research/ledger/docs/research/2026-09-09-daemon-observation.md +231 -0
- package/docs/research/ledger/docs/research/2026-09-09-discovery-pagination.md +114 -0
- package/docs/research/ledger/docs/research/2026-09-09-harness-transcript-contract.md +145 -0
- package/docs/research/ledger/docs/research/2026-09-09-native-history-discovery.md +180 -0
- package/docs/research/ledger/docs/research/2026-09-09-native-history-implementation.md +137 -0
- package/docs/research/ledger/docs/research/2026-09-09-other-inspirations.md +279 -0
- package/docs/research/ledger/docs/research/2026-09-09-trajectory-and-letta-code.md +128 -0
- package/docs/research/ledger/docs/research/2026-09-13-diagnostics-coverage.md +116 -0
- package/docs/research/ledger/docs/research/2026-09-13-fts-rebuild.md +83 -0
- package/docs/research/ledger/docs/research/2026-09-13-read-path-split.md +74 -0
- package/docs/research/ledger/docs/research/2026-09-13-runtime-rollout.md +102 -0
- package/docs/research/ledger/docs/research/2026-09-13-scan-outcomes.md +80 -0
- package/docs/research/ledger/docs/research/2026-09-13-search-quality.md +105 -0
- package/docs/research/ledger/docs/research/2026-09-13-source-coverage.md +64 -0
- package/docs/research/ledger/docs/research/2026-09-13-sync-wall-budget.md +82 -0
- package/docs/research/ledger/docs/research/2026-09-13-typed-read-gate.md +81 -0
- package/docs/research/ledger/docs/research/2026-09-13-writer-lease.md +84 -0
- package/docs/research/ledger/docs/research/2026-09-16-c1-copy-fallback.md +227 -0
- package/docs/research/ledger/docs/research/2026-09-16-c10-kimi.md +414 -0
- package/docs/research/ledger/docs/research/2026-09-16-c2-derive-counter.md +216 -0
- package/docs/research/ledger/docs/research/2026-09-16-c3-echo-filter.md +305 -0
- package/docs/research/ledger/docs/research/2026-09-16-c4-title-index.md +349 -0
- package/docs/research/ledger/docs/research/2026-09-16-c5-trigram.md +268 -0
- package/docs/research/ledger/docs/research/2026-09-16-c6-recency-prior.md +438 -0
- package/docs/research/ledger/docs/research/2026-09-16-c7-tombstones.md +467 -0
- package/docs/research/ledger/docs/research/2026-09-16-c8-remote-truth.md +390 -0
- package/docs/research/ledger/docs/research/2026-09-16-c9-grok.md +380 -0
- package/docs/research/ledger/docs/research/2026-09-16-wake-port-program.md +38 -0
- package/docs/research/ledger/docs/research/2026-09-17-cursor-blobstore.md +157 -0
- package/docs/research/ledger/manifest.json +540 -0
- package/legacy/NOTICE.md +50 -0
- package/legacy/bin/ledger-core.ts +7 -0
- package/legacy/bin/ledger.ts +7 -0
- package/legacy/bin/traj-bridge.ts +7 -0
- package/legacy/scripts/import-claude-history.ts +130 -0
- package/legacy/scripts/native-history-smoke.ts +52 -0
- package/legacy/scripts/observe-daemon.ts +570 -0
- package/legacy/scripts/retitle-sessions.md +19 -0
- package/legacy/scripts/verify-runtime-rollout.ts +151 -0
- package/legacy/src/canonical.ts +108 -0
- package/legacy/src/cli.ts +1293 -0
- package/legacy/src/db.ts +2254 -0
- package/legacy/src/diagnostics.ts +87 -0
- package/legacy/src/discovery.ts +56 -0
- package/legacy/src/embeddings.ts +382 -0
- package/legacy/src/filter.ts +602 -0
- package/legacy/src/gemini_embeddings.ts +103 -0
- package/legacy/src/identity.ts +9 -0
- package/legacy/src/index.ts +10 -0
- package/legacy/src/ingest.ts +791 -0
- package/legacy/src/managed.ts +178 -0
- package/legacy/src/native/common.ts +68 -0
- package/legacy/src/native/cursor.ts +647 -0
- package/legacy/src/native/grok.ts +540 -0
- package/legacy/src/native/kimi.ts +706 -0
- package/legacy/src/native/opencode.ts +470 -0
- package/legacy/src/native/sqlite-ro.ts +281 -0
- package/legacy/src/native.ts +97 -0
- package/legacy/src/normalizer.ts +457 -0
- package/legacy/src/parentage.ts +562 -0
- package/legacy/src/pressure.ts +162 -0
- package/legacy/src/private-storage.ts +74 -0
- package/legacy/src/reader.ts +1255 -0
- package/legacy/src/scanner.ts +591 -0
- package/legacy/src/schema-gate.ts +263 -0
- package/legacy/src/schema.ts +829 -0
- package/legacy/src/semantic-top-k.ts +71 -0
- package/legacy/src/session-hash.ts +49 -0
- package/legacy/src/store-status.ts +218 -0
- package/legacy/src/sync-preparation.ts +45 -0
- package/legacy/src/sync-truth.ts +843 -0
- package/legacy/src/sync.ts +1219 -0
- package/legacy/src/types.ts +436 -0
- package/legacy/src/vocab.txt +30522 -0
- package/legacy/src/watch.ts +673 -0
- package/legacy/src/writer-lease.ts +310 -0
- package/ops/README.md +120 -0
- package/ops/install-cli.sh +63 -0
- package/ops/install-release.sh +49 -0
- package/ops/linux/install-systemd-user.sh +103 -0
- package/ops/macos/install-launchd.sh +72 -0
- package/ops/sync-settings.sh +37 -0
- package/package.json +19 -5
- package/skills/trajectories/SKILL.md +138 -0
- package/skills/trajectories/scripts/trajectories_recent.sh +26 -0
- package/skills/trajectories/scripts/trajectories_search.sh +26 -0
- 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
|
+
}
|