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,562 @@
|
|
|
1
|
+
import { closeSync, existsSync, fstatSync, openSync, readFileSync, readSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, isAbsolute, join } from "node:path";
|
|
3
|
+
import { withReadOnlyDb } from "./native/sqlite-ro.js";
|
|
4
|
+
import { readKimiSubagentMeta } from "./native/kimi.js";
|
|
5
|
+
|
|
6
|
+
export interface SessionParentage {
|
|
7
|
+
parent_session_id: string | null;
|
|
8
|
+
parent_tool_use_id: string | null;
|
|
9
|
+
agent_nickname: string | null;
|
|
10
|
+
thread_role: string | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const EMPTY_SESSION_PARENTAGE: SessionParentage = {
|
|
14
|
+
parent_session_id: null,
|
|
15
|
+
parent_tool_use_id: null,
|
|
16
|
+
agent_nickname: null,
|
|
17
|
+
thread_role: null,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export interface ParentageOptions {
|
|
21
|
+
sessionId?: string;
|
|
22
|
+
transcript?: string;
|
|
23
|
+
/**
|
|
24
|
+
* A parent edge a source's own registry recorded elsewhere, read at scan time
|
|
25
|
+
* (Grok registers a child under the parent's directory, so a per-session read
|
|
26
|
+
* cannot find it without walking the store).
|
|
27
|
+
*/
|
|
28
|
+
registryParentSessionId?: string;
|
|
29
|
+
/**
|
|
30
|
+
* The tool call that spawned a child session, read from the parent's own
|
|
31
|
+
* transcript at scan time (Kimi's parent wire binds `parent_tool_call_id` to
|
|
32
|
+
* the spawned agent, and nothing on the child names it).
|
|
33
|
+
*/
|
|
34
|
+
registryParentToolUseId?: string;
|
|
35
|
+
onDiagnostic?: (diagnostic: ParentageDiagnostic) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ParentageDiagnostic {
|
|
39
|
+
code: "invalid_metadata" | "read_error" | "unresolved_reference" | "conflicting_evidence" | "incomplete_evidence";
|
|
40
|
+
sourcePath: string;
|
|
41
|
+
message: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class ParentageError extends Error {
|
|
45
|
+
constructor(readonly code: ParentageDiagnostic["code"], message: string, readonly sourcePath = "") {
|
|
46
|
+
super(message);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function reportError(options: ParentageOptions, sourcePath: string, error: unknown): void {
|
|
51
|
+
options.onDiagnostic?.({
|
|
52
|
+
code: error instanceof ParentageError ? error.code : "read_error",
|
|
53
|
+
sourcePath: error instanceof ParentageError && error.sourcePath ? error.sourcePath : sourcePath,
|
|
54
|
+
message: error instanceof ParentageError ? error.message : "Could not read source parentage evidence.",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const FILE_PREFIX_BYTES = 64 * 1024;
|
|
59
|
+
const META_SCAN_LINES = 40;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Source-proved session parentage stored beside Trajectory records.
|
|
63
|
+
*
|
|
64
|
+
* `@letta-ai/trajectory` CanonicalRecord has no parent/subagent fields
|
|
65
|
+
* (`source_group_id` is this transcript's own id). Ledger keeps using
|
|
66
|
+
* Trajectory as the record SoT and folds the edge here.
|
|
67
|
+
*
|
|
68
|
+
* Proven shapes (independently implemented; do not copy AGPL Obelisk):
|
|
69
|
+
* - Codex `session_meta` (`parent_thread_id` / spawn / nickname)
|
|
70
|
+
* - Claude Code `<parent>/subagents/` path, sidechain `sessionId`, `.meta.json`
|
|
71
|
+
* - Pi / OpenClaw `branchedFrom` / `parentSession` session header
|
|
72
|
+
* - OMP nested `<parent>.jsonl` + `<parent>/<child>.jsonl` sibling header
|
|
73
|
+
* - Hermes `sessions.parent_session_id`
|
|
74
|
+
* - OpenCode session JSON `parentID` / SQLite `session.parent_id`
|
|
75
|
+
* - Cursor `.../<parent>/subagents/` path when that layout exists
|
|
76
|
+
* - Grok `<parent>/subagents/<child>/meta.json` registry, read at scan time
|
|
77
|
+
* - Kimi `<parent-session>/subagents/<agent>/` path, with the spawning tool
|
|
78
|
+
* call and `subagent_type` from the parent's wire and the child's `meta.json`
|
|
79
|
+
*/
|
|
80
|
+
export function extractSessionParentage(
|
|
81
|
+
source: string,
|
|
82
|
+
sourcePath: string,
|
|
83
|
+
options: ParentageOptions = {},
|
|
84
|
+
): SessionParentage {
|
|
85
|
+
try {
|
|
86
|
+
switch (source) {
|
|
87
|
+
case "codex":
|
|
88
|
+
return readCodexParentage(
|
|
89
|
+
options.transcript ?? readFilePrefix(sourcePath),
|
|
90
|
+
);
|
|
91
|
+
case "claude-code":
|
|
92
|
+
return extractClaudeCodeParentage(
|
|
93
|
+
sourcePath,
|
|
94
|
+
options.transcript ?? readFilePrefixIfPresent(sourcePath, !parentIdFromSubagentsPath(sourcePath)),
|
|
95
|
+
options,
|
|
96
|
+
);
|
|
97
|
+
case "cursor":
|
|
98
|
+
return extractPathSubagentParentage(sourcePath);
|
|
99
|
+
case "grok":
|
|
100
|
+
return extractGrokParentage(sourcePath, options);
|
|
101
|
+
case "kimi":
|
|
102
|
+
return extractKimiParentage(sourcePath, options);
|
|
103
|
+
case "pi":
|
|
104
|
+
case "openclaw":
|
|
105
|
+
return extractPiHeaderParentage(
|
|
106
|
+
sourcePath,
|
|
107
|
+
options.transcript ?? readFilePrefixIfPresent(sourcePath),
|
|
108
|
+
);
|
|
109
|
+
case "omp":
|
|
110
|
+
return extractOmpParentage(
|
|
111
|
+
sourcePath,
|
|
112
|
+
options.transcript ?? readFilePrefixIfPresent(sourcePath),
|
|
113
|
+
);
|
|
114
|
+
case "hermes":
|
|
115
|
+
return extractHermesParentage(sourcePath, options.sessionId);
|
|
116
|
+
case "opencode":
|
|
117
|
+
return extractOpenCodeParentage(sourcePath, options.sessionId, options.transcript);
|
|
118
|
+
default:
|
|
119
|
+
return { ...EMPTY_SESSION_PARENTAGE };
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
reportError(options, sourcePath, error);
|
|
123
|
+
return { ...EMPTY_SESSION_PARENTAGE };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function extractCodexParentageFromTranscript(
|
|
128
|
+
transcript: string,
|
|
129
|
+
options: ParentageOptions = {},
|
|
130
|
+
): SessionParentage {
|
|
131
|
+
return extractSessionParentage("codex", "", { ...options, transcript });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function readCodexParentage(transcript: string): SessionParentage {
|
|
135
|
+
const found: SessionParentage = { ...EMPTY_SESSION_PARENTAGE };
|
|
136
|
+
for (const obj of scanJsonlObjects(transcript, META_SCAN_LINES, "session_meta")) {
|
|
137
|
+
if (obj.type !== "session_meta") continue;
|
|
138
|
+
const payload = asRecord(obj.payload);
|
|
139
|
+
if (!payload) continue;
|
|
140
|
+
mergeParentage(found, parentageFromCodexMeta(payload));
|
|
141
|
+
if (found.parent_session_id && found.agent_nickname && found.thread_role) {
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return found;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function mergeParentage(
|
|
149
|
+
current: SessionParentage,
|
|
150
|
+
incoming: SessionParentage,
|
|
151
|
+
): SessionParentage {
|
|
152
|
+
current.parent_session_id = current.parent_session_id ?? incoming.parent_session_id;
|
|
153
|
+
current.parent_tool_use_id = current.parent_tool_use_id ?? incoming.parent_tool_use_id;
|
|
154
|
+
current.agent_nickname = current.agent_nickname ?? incoming.agent_nickname;
|
|
155
|
+
current.thread_role = current.thread_role ?? incoming.thread_role;
|
|
156
|
+
return current;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function parentageEquals(a: SessionParentage, b: SessionParentage): boolean {
|
|
160
|
+
return (
|
|
161
|
+
a.parent_session_id === b.parent_session_id &&
|
|
162
|
+
a.parent_tool_use_id === b.parent_tool_use_id &&
|
|
163
|
+
a.agent_nickname === b.agent_nickname &&
|
|
164
|
+
a.thread_role === b.thread_role
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function extractPathSubagentParentage(sourcePath: string): SessionParentage {
|
|
169
|
+
const parent = parentIdFromSubagentsPath(sourcePath);
|
|
170
|
+
if (!parent) return { ...EMPTY_SESSION_PARENTAGE };
|
|
171
|
+
return {
|
|
172
|
+
parent_session_id: parent,
|
|
173
|
+
parent_tool_use_id: null,
|
|
174
|
+
agent_nickname: null,
|
|
175
|
+
thread_role: sourcePath.replace(/\\/g, "/").includes("/subagents/workflows/")
|
|
176
|
+
? "workflow_agent"
|
|
177
|
+
: "subagent",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Grok's edge is recorded by the parent, not the child: the child's own
|
|
183
|
+
* directory holds no mention of its parent. The scanner reads the registry
|
|
184
|
+
* while it walks the store and passes the child's edge here; the path shape is
|
|
185
|
+
* the second piece of evidence, for a listing rooted inside a parent's
|
|
186
|
+
* registry directory (`<parent>/subagents/<child>/updates.jsonl`). A session is
|
|
187
|
+
* never its own parent.
|
|
188
|
+
*/
|
|
189
|
+
function extractGrokParentage(sourcePath: string, options: ParentageOptions): SessionParentage {
|
|
190
|
+
const parent = asNonEmptyString(options.registryParentSessionId) ?? parentIdFromSubagentsPath(sourcePath);
|
|
191
|
+
if (!parent) return { ...EMPTY_SESSION_PARENTAGE };
|
|
192
|
+
const child = asNonEmptyString(options.sessionId) ?? asNonEmptyString(basename(dirname(sourcePath)));
|
|
193
|
+
if (child === parent) return { ...EMPTY_SESSION_PARENTAGE };
|
|
194
|
+
return {
|
|
195
|
+
parent_session_id: parent,
|
|
196
|
+
parent_tool_use_id: null,
|
|
197
|
+
agent_nickname: null,
|
|
198
|
+
thread_role: "subagent",
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Kimi's edge is the filesystem shape plus the parent's own wire: a child
|
|
204
|
+
* transcript is `<parent-session>/subagents/<agent-id>/wire.jsonl`, so the
|
|
205
|
+
* enclosing session directory names the parent, and the child's `meta.json`
|
|
206
|
+
* carries the `subagent_type` the parent launched it as. The tool call that
|
|
207
|
+
* spawned the child is not written on the child at all — the parent's wire
|
|
208
|
+
* binds `parent_tool_call_id` → `agent_id` — so it arrives from the scan
|
|
209
|
+
* (`options.registryParentToolUseId`) when the binding was readable. A session
|
|
210
|
+
* is never its own parent.
|
|
211
|
+
*/
|
|
212
|
+
function extractKimiParentage(sourcePath: string, options: ParentageOptions): SessionParentage {
|
|
213
|
+
const parent = parentIdFromSubagentsPath(sourcePath);
|
|
214
|
+
if (!parent) return { ...EMPTY_SESSION_PARENTAGE };
|
|
215
|
+
const agentId = asNonEmptyString(basename(dirname(sourcePath)));
|
|
216
|
+
if (!agentId || agentId === parent) return { ...EMPTY_SESSION_PARENTAGE };
|
|
217
|
+
const child = asNonEmptyString(options.sessionId);
|
|
218
|
+
if (child === parent) return { ...EMPTY_SESSION_PARENTAGE };
|
|
219
|
+
return {
|
|
220
|
+
parent_session_id: parent,
|
|
221
|
+
parent_tool_use_id: asNonEmptyString(options.registryParentToolUseId),
|
|
222
|
+
agent_nickname: readKimiSubagentMeta(dirname(sourcePath)).agentType ?? null,
|
|
223
|
+
thread_role: "subagent",
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function extractClaudeCodeParentage(
|
|
228
|
+
sourcePath: string,
|
|
229
|
+
transcript: string | null,
|
|
230
|
+
options: ParentageOptions,
|
|
231
|
+
): SessionParentage {
|
|
232
|
+
const found = transcript ? parentageFromClaudeTranscript(transcript) : { ...EMPTY_SESSION_PARENTAGE };
|
|
233
|
+
if (!found) return { ...EMPTY_SESSION_PARENTAGE };
|
|
234
|
+
const fromPath = extractPathSubagentParentage(sourcePath);
|
|
235
|
+
if (found.parent_session_id && fromPath.parent_session_id && found.parent_session_id !== fromPath.parent_session_id) {
|
|
236
|
+
reportError(options, sourcePath, new ParentageError("conflicting_evidence", "Explicit Claude metadata overrides the parent directory."));
|
|
237
|
+
} else if (fromPath.thread_role === "workflow_agent") {
|
|
238
|
+
found.thread_role = fromPath.thread_role;
|
|
239
|
+
}
|
|
240
|
+
mergeParentage(found, fromPath);
|
|
241
|
+
|
|
242
|
+
mergeParentage(found, parentageFromClaudeMetaFile(sourcePath, options));
|
|
243
|
+
|
|
244
|
+
if (found.parent_session_id && !found.thread_role) {
|
|
245
|
+
found.thread_role = "subagent";
|
|
246
|
+
}
|
|
247
|
+
return found;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function parentIdFromSubagentsPath(sourcePath: string): string | null {
|
|
251
|
+
const parts = sourcePath.split(/[/\\]/).filter((part) => part.length > 0);
|
|
252
|
+
const idx = parts.lastIndexOf("subagents");
|
|
253
|
+
if (idx <= 0) return null;
|
|
254
|
+
const parent = asNonEmptyString(parts[idx - 1]);
|
|
255
|
+
if (!parent || parent === "." || parent === "..") return null;
|
|
256
|
+
return parent;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function parentageFromClaudeTranscript(transcript: string): SessionParentage | null {
|
|
260
|
+
const found: SessionParentage = { ...EMPTY_SESSION_PARENTAGE };
|
|
261
|
+
let childId: string | null = null;
|
|
262
|
+
// Match Trajectory's standalone-sidechain boundary, not an embedded child row.
|
|
263
|
+
for (const obj of scanJsonlObjects(transcript, Infinity)) {
|
|
264
|
+
if ((obj.type !== "user" && obj.type !== "assistant") || !asRecord(obj.message)) continue;
|
|
265
|
+
if (obj.isSidechain !== true) return null;
|
|
266
|
+
const agentId = asNonEmptyString(obj.agentId);
|
|
267
|
+
const sessionId = asNonEmptyString(obj.sessionId);
|
|
268
|
+
if (!agentId || !sessionId || agentId === sessionId) return { ...EMPTY_SESSION_PARENTAGE };
|
|
269
|
+
if ((childId && childId !== agentId) ||
|
|
270
|
+
(found.parent_session_id && found.parent_session_id !== sessionId)) {
|
|
271
|
+
throw new ParentageError("conflicting_evidence", "Claude sidechain rows disagree about the child or parent identity.");
|
|
272
|
+
}
|
|
273
|
+
childId = agentId;
|
|
274
|
+
found.parent_session_id = sessionId;
|
|
275
|
+
found.thread_role = "subagent";
|
|
276
|
+
}
|
|
277
|
+
return found;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function parentageFromClaudeMetaFile(sourcePath: string, options: ParentageOptions): SessionParentage {
|
|
281
|
+
if (!sourcePath.toLowerCase().endsWith(".jsonl")) {
|
|
282
|
+
return { ...EMPTY_SESSION_PARENTAGE };
|
|
283
|
+
}
|
|
284
|
+
const metaPath = `${sourcePath.slice(0, -".jsonl".length)}.meta.json`;
|
|
285
|
+
if (!existsSync(metaPath)) return { ...EMPTY_SESSION_PARENTAGE };
|
|
286
|
+
try {
|
|
287
|
+
const meta = asRecord(JSON.parse(readFileSync(metaPath, "utf8")));
|
|
288
|
+
if (!meta) throw new ParentageError("invalid_metadata", "Claude sidecar must be a JSON object.");
|
|
289
|
+
return {
|
|
290
|
+
parent_session_id: null,
|
|
291
|
+
parent_tool_use_id: asNonEmptyString(meta.toolUseId),
|
|
292
|
+
agent_nickname: asNonEmptyString(meta.agentType),
|
|
293
|
+
thread_role: null,
|
|
294
|
+
};
|
|
295
|
+
} catch (error) {
|
|
296
|
+
reportError(options, metaPath, error instanceof SyntaxError
|
|
297
|
+
? new ParentageError("invalid_metadata", "Claude sidecar is not valid JSON.") : error);
|
|
298
|
+
return { ...EMPTY_SESSION_PARENTAGE };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function extractPiHeaderParentage(
|
|
303
|
+
sourcePath: string,
|
|
304
|
+
transcript: string | null,
|
|
305
|
+
): SessionParentage {
|
|
306
|
+
const header = readPiSessionHeader(transcript ?? readFilePrefixIfPresent(sourcePath));
|
|
307
|
+
if (!header) return { ...EMPTY_SESSION_PARENTAGE };
|
|
308
|
+
const parent =
|
|
309
|
+
resolvePiParentRef(asNonEmptyString(header.branchedFrom), sourcePath);
|
|
310
|
+
const otherParent = resolvePiParentRef(asNonEmptyString(header.parentSession), sourcePath);
|
|
311
|
+
if (parent && otherParent && parent !== otherParent) {
|
|
312
|
+
throw new ParentageError("conflicting_evidence", "Pi header parent references disagree.");
|
|
313
|
+
}
|
|
314
|
+
const resolved = parent ?? otherParent;
|
|
315
|
+
if (resolved === header.id) throw new ParentageError("conflicting_evidence", "A session cannot be its own parent.");
|
|
316
|
+
if (!resolved) return { ...EMPTY_SESSION_PARENTAGE };
|
|
317
|
+
return {
|
|
318
|
+
parent_session_id: resolved,
|
|
319
|
+
parent_tool_use_id: null,
|
|
320
|
+
agent_nickname: null,
|
|
321
|
+
thread_role: "subagent",
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function extractOmpParentage(
|
|
326
|
+
sourcePath: string,
|
|
327
|
+
transcript: string | null,
|
|
328
|
+
): SessionParentage {
|
|
329
|
+
if (!readPiSessionHeader(transcript)) {
|
|
330
|
+
throw new ParentageError("invalid_metadata", "OMP child has no valid session header.");
|
|
331
|
+
}
|
|
332
|
+
const fromHeader = extractPiHeaderParentage(sourcePath, transcript);
|
|
333
|
+
if (fromHeader.parent_session_id) return fromHeader;
|
|
334
|
+
const siblingId = readPiSessionIdFromFile(`${dirname(sourcePath)}.jsonl`);
|
|
335
|
+
if (!siblingId) return { ...EMPTY_SESSION_PARENTAGE };
|
|
336
|
+
return {
|
|
337
|
+
parent_session_id: siblingId,
|
|
338
|
+
parent_tool_use_id: null,
|
|
339
|
+
agent_nickname: null,
|
|
340
|
+
thread_role: "subagent",
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function resolvePiParentRef(ref: string | null, childPath: string): string | null {
|
|
345
|
+
if (!ref) return null;
|
|
346
|
+
if (!ref.includes("/") && !ref.includes("\\") && !/\.jsonl$/i.test(ref)) return ref;
|
|
347
|
+
const target = isAbsolute(ref) ? ref : join(dirname(childPath), ref);
|
|
348
|
+
const id = readPiSessionIdFromFile(target);
|
|
349
|
+
if (!id) throw new ParentageError("unresolved_reference", "Parent reference has no readable session header.", target);
|
|
350
|
+
return id;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function readPiSessionIdFromFile(path: string, options: ParentageOptions = {}): string | null {
|
|
354
|
+
try {
|
|
355
|
+
const id = readPiSessionHeader(readFilePrefix(path))?.id;
|
|
356
|
+
if (!id) throw new ParentageError("invalid_metadata", "No valid session header in the bounded file prefix.");
|
|
357
|
+
return id;
|
|
358
|
+
} catch (error) {
|
|
359
|
+
reportError(options, path, error);
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function readPiSessionHeader(
|
|
365
|
+
transcript: string | null,
|
|
366
|
+
): { id: string; parentSession?: unknown; branchedFrom?: unknown } | null {
|
|
367
|
+
if (!transcript) return null;
|
|
368
|
+
for (const obj of scanJsonlObjects(transcript, META_SCAN_LINES, '"session"')) {
|
|
369
|
+
if (obj.type !== "session") continue;
|
|
370
|
+
const id = asNonEmptyString(obj.id);
|
|
371
|
+
if (!id) throw new ParentageError("invalid_metadata", "Session header has no nonblank id.");
|
|
372
|
+
return { id, parentSession: obj.parentSession, branchedFrom: obj.branchedFrom };
|
|
373
|
+
}
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function extractOpenCodeParentage(
|
|
378
|
+
sourcePath: string,
|
|
379
|
+
sessionId: string | undefined,
|
|
380
|
+
transcript?: string,
|
|
381
|
+
): SessionParentage {
|
|
382
|
+
const fromJson = parentageFromOpenCodeJson(sourcePath, transcript);
|
|
383
|
+
if (fromJson.parent_session_id || fromJson.parent_known) {
|
|
384
|
+
const { session_id: _id, parent_known: _known, ...parentage } = fromJson;
|
|
385
|
+
return parentage;
|
|
386
|
+
}
|
|
387
|
+
const id = fromJson.session_id ?? asNonEmptyString(sessionId);
|
|
388
|
+
if (!id) return { ...EMPTY_SESSION_PARENTAGE };
|
|
389
|
+
const dbPath = findOpenCodeDatabase(sourcePath);
|
|
390
|
+
if (!dbPath) return { ...EMPTY_SESSION_PARENTAGE };
|
|
391
|
+
return extractOpenCodeSqliteParentage(dbPath, id);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function parentageFromOpenCodeJson(sourcePath: string, transcript?: string): SessionParentage & {
|
|
395
|
+
session_id: string | null;
|
|
396
|
+
parent_known: boolean;
|
|
397
|
+
} {
|
|
398
|
+
const empty = { ...EMPTY_SESSION_PARENTAGE, session_id: null, parent_known: false };
|
|
399
|
+
if (transcript === undefined && (!/\.jsonl?$/i.test(sourcePath) || !existsSync(sourcePath))) {
|
|
400
|
+
return empty;
|
|
401
|
+
}
|
|
402
|
+
const text = transcript ?? readFileSync(sourcePath, "utf8");
|
|
403
|
+
let parsed: Record<string, unknown> | null;
|
|
404
|
+
try {
|
|
405
|
+
parsed = asRecord(JSON.parse(text));
|
|
406
|
+
} catch {
|
|
407
|
+
throw new ParentageError("invalid_metadata", "OpenCode session is not valid JSON.");
|
|
408
|
+
}
|
|
409
|
+
if (!parsed) throw new ParentageError("invalid_metadata", "OpenCode session must be a JSON object.");
|
|
410
|
+
const nestedInfo = asRecord(parsed.info);
|
|
411
|
+
if (("info" in parsed || "messages" in parsed) && !nestedInfo) {
|
|
412
|
+
throw new ParentageError("invalid_metadata", "OpenCode export has no valid info object.");
|
|
413
|
+
}
|
|
414
|
+
const info = nestedInfo ?? parsed;
|
|
415
|
+
const parent =
|
|
416
|
+
asNonEmptyString(info.parentID) ?? asNonEmptyString(info.parent_id);
|
|
417
|
+
const sessionId = asNonEmptyString(info.id);
|
|
418
|
+
const parentKnown = Object.hasOwn(info, "parentID") || Object.hasOwn(info, "parent_id");
|
|
419
|
+
if (!parent) return { ...empty, session_id: sessionId, parent_known: parentKnown };
|
|
420
|
+
return {
|
|
421
|
+
parent_session_id: parent,
|
|
422
|
+
parent_tool_use_id: null,
|
|
423
|
+
agent_nickname: null,
|
|
424
|
+
thread_role: "subagent",
|
|
425
|
+
session_id: sessionId,
|
|
426
|
+
parent_known: true,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function findOpenCodeDatabase(startPath: string): string | null {
|
|
431
|
+
if (startPath.toLowerCase().endsWith(".db") && existsSync(startPath)) {
|
|
432
|
+
return startPath;
|
|
433
|
+
}
|
|
434
|
+
let dir = dirname(startPath);
|
|
435
|
+
for (let i = 0; i < 8; i++) {
|
|
436
|
+
const candidate = join(dir, "opencode.db");
|
|
437
|
+
if (existsSync(candidate)) return candidate;
|
|
438
|
+
const parent = dirname(dir);
|
|
439
|
+
if (parent === dir) break;
|
|
440
|
+
dir = parent;
|
|
441
|
+
}
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function extractOpenCodeSqliteParentage(
|
|
446
|
+
dbPath: string,
|
|
447
|
+
sessionId: string,
|
|
448
|
+
): SessionParentage {
|
|
449
|
+
// Same gradient as the OpenCode adapter: a store held by its own writer is
|
|
450
|
+
// read from a private copy instead of costing the parent link.
|
|
451
|
+
const row = withReadOnlyDb(dbPath, (db) =>
|
|
452
|
+
db
|
|
453
|
+
.query<{ parent_id: string | null }, [string]>(
|
|
454
|
+
"SELECT parent_id FROM session WHERE id = ? LIMIT 1",
|
|
455
|
+
)
|
|
456
|
+
.get(sessionId));
|
|
457
|
+
const parent = asNonEmptyString(row?.parent_id);
|
|
458
|
+
if (!parent) return { ...EMPTY_SESSION_PARENTAGE };
|
|
459
|
+
return {
|
|
460
|
+
parent_session_id: parent,
|
|
461
|
+
parent_tool_use_id: null,
|
|
462
|
+
agent_nickname: null,
|
|
463
|
+
thread_role: "subagent",
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function extractHermesParentage(
|
|
468
|
+
sourcePath: string,
|
|
469
|
+
sessionId: string | undefined,
|
|
470
|
+
): SessionParentage {
|
|
471
|
+
if (!sessionId || !existsSync(sourcePath)) {
|
|
472
|
+
return { ...EMPTY_SESSION_PARENTAGE };
|
|
473
|
+
}
|
|
474
|
+
const row = withReadOnlyDb(sourcePath, (db) =>
|
|
475
|
+
db
|
|
476
|
+
.query<{ parent_session_id: string | null }, [string]>(
|
|
477
|
+
"SELECT parent_session_id FROM sessions WHERE id = ? LIMIT 1",
|
|
478
|
+
)
|
|
479
|
+
.get(sessionId));
|
|
480
|
+
const parent = asNonEmptyString(row?.parent_session_id);
|
|
481
|
+
if (!parent) return { ...EMPTY_SESSION_PARENTAGE };
|
|
482
|
+
return {
|
|
483
|
+
parent_session_id: parent,
|
|
484
|
+
parent_tool_use_id: null,
|
|
485
|
+
agent_nickname: null,
|
|
486
|
+
thread_role: "subagent",
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function parentageFromCodexMeta(payload: Record<string, unknown>): SessionParentage {
|
|
491
|
+
const source = asRecord(payload.source);
|
|
492
|
+
const subagent = asRecord(source?.subagent);
|
|
493
|
+
const spawn = asRecord(subagent?.thread_spawn);
|
|
494
|
+
const parent =
|
|
495
|
+
asNonEmptyString(payload.parent_thread_id) ??
|
|
496
|
+
asNonEmptyString(spawn?.parent_thread_id) ??
|
|
497
|
+
asNonEmptyString(subagent?.parent_thread_id) ??
|
|
498
|
+
asNonEmptyString(payload.forked_from_id);
|
|
499
|
+
return {
|
|
500
|
+
parent_session_id: parent,
|
|
501
|
+
parent_tool_use_id:
|
|
502
|
+
asNonEmptyString(payload.parent_tool_use_id) ??
|
|
503
|
+
asNonEmptyString(spawn?.parent_tool_use_id) ??
|
|
504
|
+
null,
|
|
505
|
+
agent_nickname:
|
|
506
|
+
asNonEmptyString(payload.agent_nickname) ??
|
|
507
|
+
asNonEmptyString(spawn?.agent_nickname) ??
|
|
508
|
+
null,
|
|
509
|
+
thread_role: asNonEmptyString(payload.thread_source),
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function* scanJsonlObjects(
|
|
514
|
+
transcript: string,
|
|
515
|
+
maxLines: number,
|
|
516
|
+
needle?: string,
|
|
517
|
+
): Generator<Record<string, unknown>> {
|
|
518
|
+
let offset = 0;
|
|
519
|
+
for (let i = 0; i < maxLines && offset < transcript.length; i++) {
|
|
520
|
+
const nl = transcript.indexOf("\n", offset);
|
|
521
|
+
const line = nl === -1 ? transcript.slice(offset) : transcript.slice(offset, nl);
|
|
522
|
+
offset = nl === -1 ? transcript.length : nl + 1;
|
|
523
|
+
if (!line.trim()) continue;
|
|
524
|
+
if (needle && !line.includes(needle)) continue;
|
|
525
|
+
try {
|
|
526
|
+
const obj = JSON.parse(line) as unknown;
|
|
527
|
+
const record = asRecord(obj);
|
|
528
|
+
if (record) yield record;
|
|
529
|
+
} catch {
|
|
530
|
+
throw new ParentageError("invalid_metadata", "Source metadata contains malformed JSONL.");
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
536
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
537
|
+
? (value as Record<string, unknown>)
|
|
538
|
+
: null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function asNonEmptyString(value: unknown): string | null {
|
|
542
|
+
return typeof value === "string" ? value.trim() || null : null;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function readFilePrefixIfPresent(path: string, requireComplete = false): string | null {
|
|
546
|
+
if (!existsSync(path)) return null;
|
|
547
|
+
return readFilePrefix(path, requireComplete);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function readFilePrefix(path: string, requireComplete = false): string {
|
|
551
|
+
const fd = openSync(path, "r");
|
|
552
|
+
try {
|
|
553
|
+
const buf = Buffer.alloc(FILE_PREFIX_BYTES);
|
|
554
|
+
const n = readSync(fd, buf, 0, buf.length, 0);
|
|
555
|
+
if (requireComplete && fstatSync(fd).size > n) {
|
|
556
|
+
throw new ParentageError("incomplete_evidence", "A bounded prefix cannot prove a standalone Claude sidechain.", path);
|
|
557
|
+
}
|
|
558
|
+
return buf.subarray(0, n).toString("utf8");
|
|
559
|
+
} finally {
|
|
560
|
+
closeSync(fd);
|
|
561
|
+
}
|
|
562
|
+
}
|