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,706 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, lstatSync, readdirSync, statSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { basename, dirname, join, resolve } from "node:path";
5
+ import type { Diagnostic } from "@letta-ai/trajectory";
6
+ import type { DiscoveredSession } from "../types.js";
7
+ import {
8
+ MAX_NATIVE_BYTES, MAX_NATIVE_RECORDS, NativeInputError, nativeByteLimit, object, readBoundedText,
9
+ reportNativeError, safeNativeId, type NativeDiscoveryOptions, type NativeReadOptions, type NativeTranscript,
10
+ } from "./common.js";
11
+
12
+ /**
13
+ * Kimi CLI keeps one directory per session under
14
+ * `~/.kimi/sessions/<workspace-md5>/<session-uuid>/`. `wire.jsonl` is the
15
+ * transcript source of truth: an append-only event stream
16
+ * (`{timestamp, message:{type, payload}}`, protocol stamp on line 1) whose user
17
+ * turns are `TurnBegin`, whose prose is `ContentPart`, whose tool calls and
18
+ * results are `ToolCall` / `ToolResult` rows keyed by call id, and whose
19
+ * lifecycle rows (`StepBegin`, `StatusUpdate`, …) are state, not conversation.
20
+ * A session that spawned children holds them under `subagents/<agent-id>/`,
21
+ * each with its own `wire.jsonl` and a `meta.json`; the parent's own
22
+ * `SubagentEvent` rows mirror those children's streams. `state.json` is a
23
+ * metadata sidecar (title) and is absent on interrupted sessions;
24
+ * `context.jsonl` is compacted working memory, never a source.
25
+ * Format facts: docs/research/2026-09-16-c10-kimi.md.
26
+ */
27
+ const KIMI_SESSION_FILE = "wire.jsonl";
28
+
29
+ /**
30
+ * The pinned library has no Kimi adapter, so this reader projects the replayed
31
+ * wire into the Copilot CLI event contract — the documented event-per-line
32
+ * contract that keeps what a canonical record needs: a native per-row id, a
33
+ * per-event timestamp, tool calls linked to their results by call id, and a
34
+ * terminal outcome flag. `source_type` is restored to this source in the
35
+ * normalizer, so the projection stays a detail of a source the library cannot
36
+ * read.
37
+ */
38
+ export const KIMI_PROJECTION_SOURCE = "copilot-cli";
39
+
40
+ const STATE_FILE = "state.json";
41
+ const META_FILE = "meta.json";
42
+ const REGISTRY_DIRECTORY = "subagents";
43
+ /** `~/.kimi/kimi.json` registers the workspace paths whose MD5 names a sessions subdirectory. */
44
+ const WORKSPACES_FILE = "kimi.json";
45
+ // <root>/<workspace>/<session>/wire.jsonl is the documented layout, and a child
46
+ // adds <session>/subagents/<agent>/wire.jsonl; the spare level holds a listing
47
+ // rooted deeper, without turning discovery into an unbounded walk.
48
+ const MAX_DEPTH = 4;
49
+ const MAX_SIDECAR_BYTES = 4 * 1024 * 1024;
50
+
51
+ /**
52
+ * Wire events Kimi writes beside the conversation: step, stream, turn boundary,
53
+ * and context state.
54
+ */
55
+ const LIFECYCLE_KINDS: Record<string, true> = {
56
+ StepBegin: true,
57
+ StepInterrupted: true,
58
+ ToolCallPart: true,
59
+ StatusUpdate: true,
60
+ TurnEnd: true,
61
+ ApprovalRequest: true,
62
+ ApprovalResponse: true,
63
+ CompactionBegin: true,
64
+ CompactionEnd: true,
65
+ };
66
+
67
+ export function kimiRoots(): string[] {
68
+ return [join(homedir(), ".kimi", "sessions")];
69
+ }
70
+
71
+ interface KimiState {
72
+ title?: string;
73
+ }
74
+
75
+ function nonblank(value: unknown): string | undefined {
76
+ return typeof value === "string" && value.trim() ? value : undefined;
77
+ }
78
+
79
+ /**
80
+ * The sidecar is metadata, not transcript: a missing, oversized, or malformed
81
+ * state costs the title and never the session, so every failure here degrades
82
+ * instead of throwing.
83
+ */
84
+ function readState(path: string): { state: KimiState; unreadable: boolean } {
85
+ if (!existsSync(path)) return { state: {}, unreadable: false };
86
+ try {
87
+ const value: unknown = JSON.parse(readBoundedText(path, MAX_SIDECAR_BYTES));
88
+ if (!object(value)) return { state: {}, unreadable: true };
89
+ return { state: { title: nonblank(value.custom_title) }, unreadable: false };
90
+ } catch {
91
+ return { state: {}, unreadable: true };
92
+ }
93
+ }
94
+
95
+ /**
96
+ * A child agent's own directory records what the parent asked it to be:
97
+ * `subagent_type` and the launch `description`. Unreadable or absent, it costs
98
+ * the child's title and nickname, never the child.
99
+ */
100
+ export function readKimiSubagentMeta(directory: string): { agentType?: string; description?: string } {
101
+ const path = join(directory, META_FILE);
102
+ if (!existsSync(path)) return {};
103
+ try {
104
+ const value: unknown = JSON.parse(readBoundedText(path, MAX_SIDECAR_BYTES));
105
+ if (!object(value)) return {};
106
+ return { agentType: nonblank(value.subagent_type), description: nonblank(value.description) };
107
+ } catch {
108
+ return {};
109
+ }
110
+ }
111
+
112
+ /**
113
+ * The session's workspace path is not written in the session: the directory
114
+ * that holds it is the MD5 of the path Kimi was launched in, and
115
+ * `kimi.json` (beside the sessions root) registers those paths. Hashing each
116
+ * registered path finds the one this session was made from; a workspace missing
117
+ * from the registry (deleted, renamed, or never registered) leaves the cwd
118
+ * unknown instead of guessing, and the shared registry is never fatal.
119
+ */
120
+ function readWorkspaceCwd(sessionPath: string): string | undefined {
121
+ const sessionDirectory = dirname(sessionPath);
122
+ // A child's directory is <session>/subagents/<agent>/; its workspace is the session's.
123
+ const owner = basename(dirname(sessionDirectory)) === REGISTRY_DIRECTORY
124
+ ? dirname(dirname(dirname(sessionDirectory)))
125
+ : dirname(sessionDirectory);
126
+ const workspace = basename(owner);
127
+ if (!/^[0-9a-f]{32}$/.test(workspace)) return undefined;
128
+ // The registry sits beside the root that holds the workspace directories.
129
+ const registry = join(dirname(dirname(owner)), WORKSPACES_FILE);
130
+ if (!existsSync(registry)) return undefined;
131
+ try {
132
+ const value: unknown = JSON.parse(readBoundedText(registry, MAX_SIDECAR_BYTES));
133
+ if (!object(value) || !Array.isArray(value.work_dirs)) return undefined;
134
+ for (const entry of value.work_dirs) {
135
+ if (!object(entry)) continue;
136
+ const path = nonblank(entry.path);
137
+ if (path && createHash("md5").update(path).digest("hex") === workspace) return path;
138
+ }
139
+ } catch {
140
+ // A registry that cannot be read costs the cwd, not the transcript.
141
+ }
142
+ return undefined;
143
+ }
144
+
145
+ export function discoverKimiSessions(roots: string[], options: NativeDiscoveryOptions): DiscoveredSession[] {
146
+ if (!Number.isSafeInteger(options.limit) || options.limit < 1) throw new RangeError("Kimi discovery limit must be positive.");
147
+ const items: DiscoveredSession[] = [];
148
+ const seen = new Set<string>();
149
+
150
+ function walk(path: string, depth: number): void {
151
+ if (seen.has(path)) return;
152
+ seen.add(path);
153
+ try {
154
+ const stat = lstatSync(path);
155
+ if (stat.isSymbolicLink() || stat.isFile()) return;
156
+ if (depth > MAX_DEPTH) {
157
+ options.onDiagnostic?.({ code: "incomplete_evidence", sourcePath: path, message: "Kimi discovery reached its directory depth limit." });
158
+ return;
159
+ }
160
+ const entries = readdirSync(path, { withFileTypes: true });
161
+ const names = new Set(entries.map((entry) => entry.name));
162
+ if (names.has(KIMI_SESSION_FILE)) {
163
+ // A directory reached through a `subagents/` registry is a child: the
164
+ // walk only gets there when its parent has no wire to list it (a partial
165
+ // rsync, a deleted transcript) or when the scan is rooted inside the
166
+ // registry. It is listed with the parent its path names, exactly as
167
+ // `collectChildren` lists it, so one transcript keeps one ledger id while
168
+ // the parent's wire comes and goes.
169
+ const parent = basename(dirname(path)) === REGISTRY_DIRECTORY ? nonblank(basename(dirname(dirname(path)))) : undefined;
170
+ const listing = sessionListing(path, options, parent ? { id: parent } : undefined);
171
+ if (!listing) return;
172
+ items.push(listing);
173
+ // A child's transcript is a session of its own, registered under the
174
+ // parent that spawned it. Discovery does not descend into a session
175
+ // otherwise: its other subdirectories hold that session's own files.
176
+ if (names.has(REGISTRY_DIRECTORY)) collectChildren(path, listing.id, options, items);
177
+ return;
178
+ }
179
+ for (const entry of entries) {
180
+ if (entry.isDirectory()) walk(join(path, entry.name), depth + 1);
181
+ }
182
+ } catch (error) {
183
+ if (object(error) && error.code === "ENOENT" && depth === 0) return;
184
+ reportNativeError(options, path);
185
+ }
186
+ }
187
+
188
+ for (const root of roots) walk(resolve(root), 0);
189
+
190
+ const ordered = items
191
+ .sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "") || a.path.localeCompare(b.path));
192
+ const nativeIds = new Set<string>();
193
+ const deduplicated = ordered.filter((item) => {
194
+ if (nativeIds.has(item.id)) return false;
195
+ nativeIds.add(item.id);
196
+ return true;
197
+ });
198
+ return options.select ? options.select(deduplicated) : deduplicated.slice(0, options.limit);
199
+ }
200
+
201
+ function sidecarSize(directory: string, name: string): number {
202
+ try {
203
+ return statSync(join(directory, name)).size;
204
+ } catch {
205
+ return 0;
206
+ }
207
+ }
208
+
209
+ /**
210
+ * One wire-bearing directory as a listing. A session's own directory keys it by
211
+ * its name; a child is keyed by the parent its path names
212
+ * (`<parent-uuid>:<agent-id>`) because an agent id is unique only within its
213
+ * parent. Every file a listing's snapshot covers is counted here — the wire and
214
+ * the sidecars beside it, present or not — so two callers cannot disagree about
215
+ * a session's size, and the title comes from `state.json` when it exists and the
216
+ * caller's own metadata (a child's `meta.json` description) otherwise.
217
+ */
218
+ function sessionListing(
219
+ directory: string,
220
+ options: NativeDiscoveryOptions,
221
+ parent?: { id: string; toolCallId?: string; title?: string },
222
+ ): DiscoveredSession | null {
223
+ const id = parent ? `${parent.id}:${basename(directory)}` : basename(directory);
224
+ if (!safeNativeId(id)) {
225
+ options.onDiagnostic?.({ code: "invalid_metadata", sourcePath: directory, message: "Kimi session has an invalid native session ID." });
226
+ return null;
227
+ }
228
+ try {
229
+ const wire = statSync(join(directory, KIMI_SESSION_FILE));
230
+ const title = readState(join(directory, STATE_FILE)).state.title ?? parent?.title;
231
+ return {
232
+ id,
233
+ source: "kimi",
234
+ path: join(directory, KIMI_SESSION_FILE),
235
+ updatedAt: wire.mtime.toISOString(),
236
+ sizeBytes: wire.size + sidecarSize(directory, STATE_FILE) + sidecarSize(directory, META_FILE),
237
+ ...(title ? { title } : {}),
238
+ native: {
239
+ kind: "kimi-wire",
240
+ ...(parent ? { parentSessionId: parent.id } : {}),
241
+ ...(parent?.toolCallId ? { parentToolUseId: parent.toolCallId } : {}),
242
+ },
243
+ };
244
+ } catch (error) {
245
+ if (object(error) && error.code === "ENOENT") return null;
246
+ reportNativeError(options, directory);
247
+ return null;
248
+ }
249
+ }
250
+
251
+ /**
252
+ * A session's children are sessions: each `<parent>/subagents/<agent>/wire.jsonl`
253
+ * is listed with its own transcript, its parent from the path it is nested in
254
+ * (`kimi:<parent-uuid>:<agent-id>` — an agent id is short and only unique
255
+ * within its parent), and the tool call that spawned it when the parent's wire
256
+ * binds the two. The parent's `SubagentEvent` rows carry
257
+ * `parent_tool_call_id` → `agent_id`; that binding is read here, once per
258
+ * parent, so ingest never re-walks the store.
259
+ */
260
+ function collectChildren(
261
+ parentDirectory: string,
262
+ parentId: string,
263
+ options: NativeDiscoveryOptions,
264
+ items: DiscoveredSession[],
265
+ ): void {
266
+ const registry = join(parentDirectory, REGISTRY_DIRECTORY);
267
+ let entries: string[];
268
+ try {
269
+ entries = readdirSync(registry);
270
+ } catch {
271
+ reportNativeError(options, registry);
272
+ return;
273
+ }
274
+ const bindings = readSpawnBindings(join(parentDirectory, KIMI_SESSION_FILE), new Set(entries), options);
275
+ for (const entry of entries) {
276
+ if (!safeNativeId(entry)) continue;
277
+ const directory = join(registry, entry);
278
+ try {
279
+ // A registry entry is read only where it really is a directory: the store
280
+ // walk does not follow symlinks, and neither does this.
281
+ const stat = lstatSync(directory);
282
+ if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
283
+ const listing = sessionListing(directory, options, {
284
+ id: parentId,
285
+ toolCallId: bindings.get(entry),
286
+ title: readKimiSubagentMeta(directory).description,
287
+ });
288
+ if (listing) items.push(listing);
289
+ } catch (error) {
290
+ if (object(error) && error.code === "ENOENT") continue;
291
+ reportNativeError(options, directory);
292
+ }
293
+ }
294
+ }
295
+
296
+ /**
297
+ * The parent-side binding of a spawned child to the tool call that spawned it:
298
+ * `SubagentEvent` rows name both `agent_id` and `parent_tool_call_id`, and the
299
+ * first binding for a registered child wins. Nothing else in the store carries
300
+ * this edge, so it is read while the store is walked; a wire too large to read
301
+ * or a child bound later costs the binding, not the child's parent.
302
+ */
303
+ function readSpawnBindings(
304
+ wirePath: string,
305
+ agents: Set<string>,
306
+ options: NativeDiscoveryOptions,
307
+ ): Map<string, string> {
308
+ const bindings = new Map<string, string>();
309
+ if (agents.size === 0) return bindings;
310
+ let text: string;
311
+ try {
312
+ text = readBoundedText(wirePath, MAX_NATIVE_BYTES);
313
+ } catch {
314
+ options.onDiagnostic?.({
315
+ code: "incomplete_evidence", sourcePath: wirePath,
316
+ message: "Kimi subagent spawn bindings were not read: the parent's wire is unreadable or over the byte limit.",
317
+ });
318
+ return bindings;
319
+ }
320
+ for (const line of text.split("\n")) {
321
+ if (bindings.size === agents.size) break;
322
+ if (!line.includes("SubagentEvent")) continue;
323
+ let parsed: unknown;
324
+ try {
325
+ parsed = JSON.parse(line);
326
+ } catch {
327
+ continue;
328
+ }
329
+ const message = object(parsed) && object(parsed.message) ? parsed.message : null;
330
+ const payload = message && message.type === "SubagentEvent" && object(message.payload) ? message.payload : null;
331
+ if (!payload) continue;
332
+ const agentId = nonblank(payload.agent_id);
333
+ const toolCallId = nonblank(payload.parent_tool_call_id);
334
+ if (!agentId || !toolCallId || !agents.has(agentId) || !safeNativeId(toolCallId)) continue;
335
+ if (!bindings.has(agentId)) bindings.set(agentId, toolCallId);
336
+ }
337
+ return bindings;
338
+ }
339
+
340
+ type Row = Record<string, unknown>;
341
+
342
+ interface WireEvent {
343
+ /** 1-based line number: an append-only file only ever grows at its end. */
344
+ line: number;
345
+ timestamp: string | null;
346
+ kind: string;
347
+ payload: Record<string, unknown>;
348
+ }
349
+
350
+ interface Skips {
351
+ lifecycle: Map<string, number>;
352
+ mirror: number;
353
+ content: number;
354
+ unknown: Map<string, number>;
355
+ /** Result rows for a call whose result was already projected. */
356
+ repeats: number;
357
+ invalidLines: number;
358
+ malformedLines: number;
359
+ }
360
+
361
+ function isoSeconds(value: unknown): string | null {
362
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return null;
363
+ return new Date(value * 1000).toISOString();
364
+ }
365
+
366
+ /**
367
+ * Decode the wire's own rows. The protocol stamp on line 1
368
+ * (`{"type":"metadata","protocol_version":"1.10"}`) is the file's header, not an
369
+ * event: it is neither projected nor counted as a skip. Kimi appends while it
370
+ * runs, so a torn last line or a line from a newer protocol is counted, never
371
+ * fatal: the transcript is what the prefix holds.
372
+ */
373
+ function parseWire(text: string, skips: Skips): WireEvent[] {
374
+ const events: WireEvent[] = [];
375
+ const lines = text.split("\n");
376
+ for (let index = 0; index < lines.length; index += 1) {
377
+ const line = lines[index];
378
+ if (!line || !line.trim()) continue;
379
+ let parsed: unknown;
380
+ try {
381
+ parsed = JSON.parse(line);
382
+ } catch {
383
+ skips.invalidLines += 1;
384
+ continue;
385
+ }
386
+ if (!object(parsed)) {
387
+ skips.malformedLines += 1;
388
+ continue;
389
+ }
390
+ if (parsed.type === "metadata") continue;
391
+ const message = object(parsed.message) ? parsed.message : null;
392
+ const kind = message ? nonblank(message.type) : undefined;
393
+ if (!message || !kind) {
394
+ skips.malformedLines += 1;
395
+ continue;
396
+ }
397
+ events.push({
398
+ line: index + 1,
399
+ timestamp: isoSeconds(parsed.timestamp),
400
+ kind,
401
+ payload: object(message.payload) ? message.payload : {},
402
+ });
403
+ }
404
+ return events;
405
+ }
406
+
407
+ function asJsonObject(text: string): Record<string, unknown> | null {
408
+ if (!text.trim()) return null;
409
+ try {
410
+ const parsed: unknown = JSON.parse(text);
411
+ return object(parsed) ? parsed : null;
412
+ } catch {
413
+ return null;
414
+ }
415
+ }
416
+
417
+ /**
418
+ * A tool call's arguments as one JSON object. Kimi announces a call with the
419
+ * arguments known at that moment, so the row usually holds only a *prefix* of
420
+ * them (220 of 309 calls on this host open an object they do not close): the
421
+ * remainder streams into `ToolCallPart` rows that follow, interleaved with other
422
+ * events. The call's arguments are therefore the *shortest prefix* of its own
423
+ * argument bytes — the row's text plus the fragments up to the next call — that
424
+ * parses as an object.
425
+ *
426
+ * Fragments carry no call id, so they are attributed *positionally*: a fragment
427
+ * that follows another call's row belongs to that later call's window. The rule
428
+ * is exact while the protocol keeps a call's fragments ahead of the next
429
+ * announcement — 309/309 real calls here, none starved — but a straggler that
430
+ * completed an earlier call after a later one was announced would be attributed
431
+ * to the later call, and if the join parsed, that call would carry a
432
+ * wrong-but-valid object. Nothing in the wire can tell the two apart; the
433
+ * contract is the position. A call whose window never completes keeps the prefix
434
+ * the row holds, and its `tool_arguments_json` stays verbatim rather than
435
+ * invented.
436
+ */
437
+ function resolveArguments(events: WireEvent[], index: number, raw: unknown): unknown {
438
+ const text = typeof raw === "string" ? raw : "";
439
+ const announced = asJsonObject(text);
440
+ if (announced) return announced;
441
+ let joined = text;
442
+ for (let next = index + 1; next < events.length && events[next].kind !== "ToolCall"; next += 1) {
443
+ if (events[next].kind !== "ToolCallPart") continue;
444
+ const part = events[next].payload.arguments_part;
445
+ if (typeof part !== "string" || !part) continue;
446
+ joined += part;
447
+ const parsed = asJsonObject(joined);
448
+ if (parsed) return parsed;
449
+ }
450
+ return text || {};
451
+ }
452
+
453
+ /** A tool result's text: its output, or the message beside it when the output is empty. */
454
+ function resultText(returned: Record<string, unknown>): string {
455
+ const output = typeof returned.output === "string" ? returned.output : null;
456
+ if (output && output.trim()) return output;
457
+ const message = typeof returned.message === "string" ? returned.message : null;
458
+ if (message && message.trim()) return message;
459
+ if (output) return output;
460
+ if (returned.output === undefined || returned.output === null) return message ?? "";
461
+ return typeof returned.output === "string" ? returned.output : JSON.stringify(returned.output);
462
+ }
463
+
464
+ /**
465
+ * Replay the wire into the projection lane, in file order.
466
+ *
467
+ * `TurnBegin` is a user turn, `ContentPart` is one complete part (a `think` part
468
+ * is reasoning, a `text` part is assistant prose) — unlike Grok's token
469
+ * fragments, these are whole blocks, so nothing is concatenated across rows —
470
+ * `ToolCall` is the call, and `ToolResult` is its outcome keyed by call id with
471
+ * `ok = !return_value.is_error`. `SubagentEvent` rows mirror a child's stream
472
+ * that the child's own `wire.jsonl` holds, so they are state here; lifecycle
473
+ * rows are state too. Every skip is typed and counted.
474
+ */
475
+ function replayWire(text: string, sessionId: string, cwd: string | undefined, limit: number): {
476
+ rows: Row[]; skips: Skips; first: string | null;
477
+ } {
478
+ const skips: Skips = {
479
+ lifecycle: new Map(), mirror: 0, content: 0, unknown: new Map(), repeats: 0, invalidLines: 0, malformedLines: 0,
480
+ };
481
+ const events = parseWire(text, skips);
482
+ const rows: Row[] = [];
483
+ // One result per call id: a re-emitted result row (a resumed or retried call)
484
+ // must not put two records behind one call.
485
+ const results = new Set<string>();
486
+ let written = 0;
487
+ let first: string | null = null;
488
+
489
+ function emit(row: Row): void {
490
+ written += Buffer.byteLength(JSON.stringify(row)) + 1;
491
+ if (written > limit) throw new NativeInputError("Kimi transcript exceeds the native byte limit.");
492
+ rows.push(row);
493
+ }
494
+
495
+ for (let index = 0; index < events.length; index += 1) {
496
+ const event = events[index];
497
+ // A row without a wire id keeps a stable identity from its own line: the
498
+ // file is append-only, so a line number never moves.
499
+ const id = `kimi-line-${event.line}`;
500
+ const at = event.timestamp ? { timestamp: event.timestamp } : {};
501
+ first ??= event.timestamp;
502
+ if (event.kind === "TurnBegin") {
503
+ // A top-level turn carries content parts; a subagent's turn carries its
504
+ // whole prompt as one string (5/5 children on this host), git context and all.
505
+ const input = event.payload.user_input;
506
+ const parts = typeof input === "string"
507
+ ? [input]
508
+ : Array.isArray(input)
509
+ ? input
510
+ .map((part) => (object(part) && part.type === "text" && typeof part.text === "string" ? part.text : null))
511
+ .filter((part): part is string => part !== null && part.length > 0)
512
+ : [];
513
+ if (!parts.length || !parts.some((part) => part.trim().length > 0)) {
514
+ skips.content += 1;
515
+ continue;
516
+ }
517
+ emit({
518
+ type: "hook.start", id, ...at,
519
+ data: {
520
+ hookType: "userPromptSubmitted",
521
+ input: { prompt: parts.join("\n"), sessionId, ...(cwd ? { cwd } : {}) },
522
+ },
523
+ });
524
+ continue;
525
+ }
526
+ if (event.kind === "ContentPart") {
527
+ const partType = event.payload.type;
528
+ const raw = partType === "think" ? event.payload.think : partType === "text" ? event.payload.text : null;
529
+ const part = typeof raw === "string" ? raw : null;
530
+ if (!part || !part.trim()) {
531
+ skips.content += 1;
532
+ continue;
533
+ }
534
+ emit({
535
+ type: "assistant.message", id, ...at,
536
+ data: partType === "think" ? { reasoningText: part } : { content: part },
537
+ });
538
+ continue;
539
+ }
540
+ if (event.kind === "ToolCall") {
541
+ const callId = event.payload.id;
542
+ if (!safeNativeId(callId)) {
543
+ skips.unknown.set("ToolCall_without_id", (skips.unknown.get("ToolCall_without_id") ?? 0) + 1);
544
+ continue;
545
+ }
546
+ const fn = object(event.payload.function) ? event.payload.function : {};
547
+ const name = nonblank(fn.name);
548
+ emit({
549
+ type: "assistant.message", id, ...at,
550
+ data: {
551
+ toolRequests: [{
552
+ toolCallId: callId,
553
+ ...(name ? { name } : {}),
554
+ arguments: resolveArguments(events, index, fn.arguments),
555
+ }],
556
+ },
557
+ });
558
+ continue;
559
+ }
560
+ if (event.kind === "ToolResult") {
561
+ const callId = event.payload.tool_call_id;
562
+ if (!safeNativeId(callId)) {
563
+ skips.unknown.set("ToolResult_without_id", (skips.unknown.get("ToolResult_without_id") ?? 0) + 1);
564
+ continue;
565
+ }
566
+ if (results.has(callId)) {
567
+ skips.repeats += 1;
568
+ continue;
569
+ }
570
+ results.add(callId);
571
+ const returned = object(event.payload.return_value) ? event.payload.return_value : {};
572
+ const ok = returned.is_error !== true;
573
+ const output = resultText(returned);
574
+ emit({
575
+ type: "tool.execution_complete", id, ...at,
576
+ data: {
577
+ toolCallId: callId,
578
+ success: ok,
579
+ ...(ok ? { result: { content: output } } : { error: { message: output } }),
580
+ },
581
+ });
582
+ continue;
583
+ }
584
+ if (event.kind === "SubagentEvent") {
585
+ skips.mirror += 1;
586
+ continue;
587
+ }
588
+ if (LIFECYCLE_KINDS[event.kind] === true) {
589
+ skips.lifecycle.set(event.kind, (skips.lifecycle.get(event.kind) ?? 0) + 1);
590
+ continue;
591
+ }
592
+ skips.unknown.set(event.kind, (skips.unknown.get(event.kind) ?? 0) + 1);
593
+ }
594
+ return { rows, skips, first };
595
+ }
596
+
597
+ function tally(values: Map<string, number>): number {
598
+ let total = 0;
599
+ for (const count of values.values()) total += count;
600
+ return total;
601
+ }
602
+
603
+ function named(values: Map<string, number>, limit = 4): string {
604
+ return [...values.keys()].sort().slice(0, limit).join(", ");
605
+ }
606
+
607
+ /** Every skip is typed and counted, so a dropped wire event never vanishes silently. */
608
+ function skipDiagnostics(skips: Skips): Diagnostic[] {
609
+ const diagnostics: Diagnostic[] = [];
610
+ if (skips.lifecycle.size) {
611
+ diagnostics.push({
612
+ code: "noise_record_dropped",
613
+ message: `Skipped ${tally(skips.lifecycle)} Kimi lifecycle wire event(s) (${named(skips.lifecycle)}).`,
614
+ count: tally(skips.lifecycle),
615
+ });
616
+ }
617
+ if (skips.mirror) {
618
+ diagnostics.push({
619
+ code: "noise_record_dropped",
620
+ message: `Skipped ${skips.mirror} Kimi subagent event mirror(s); the child session's own wire holds that transcript.`,
621
+ count: skips.mirror,
622
+ });
623
+ }
624
+ if (skips.unknown.size) {
625
+ diagnostics.push({
626
+ code: "noise_record_dropped",
627
+ message: `Skipped ${tally(skips.unknown)} Kimi wire event(s) with no supported transcript shape (${named(skips.unknown)}).`,
628
+ count: tally(skips.unknown),
629
+ });
630
+ }
631
+ if (skips.repeats) {
632
+ diagnostics.push({
633
+ code: "noise_record_dropped",
634
+ message: `Skipped ${skips.repeats} repeated Kimi tool result row(s); the call's first result is its outcome.`,
635
+ count: skips.repeats,
636
+ });
637
+ }
638
+ if (skips.content) {
639
+ diagnostics.push({
640
+ code: "noise_record_dropped",
641
+ message: `Skipped ${skips.content} Kimi message event(s) with no text part.`,
642
+ count: skips.content,
643
+ });
644
+ }
645
+ if (skips.invalidLines) {
646
+ diagnostics.push({
647
+ code: "invalid_json_line",
648
+ message: `Skipped ${skips.invalidLines} invalid JSON line(s) in the Kimi wire protocol.`,
649
+ count: skips.invalidLines,
650
+ });
651
+ }
652
+ if (skips.malformedLines) {
653
+ diagnostics.push({
654
+ code: "invalid_json_line",
655
+ message: `Skipped ${skips.malformedLines} Kimi wire line(s) with no event object.`,
656
+ count: skips.malformedLines,
657
+ });
658
+ }
659
+ return diagnostics;
660
+ }
661
+
662
+ export function readKimiSession(item: DiscoveredSession, options: NativeReadOptions = {}): NativeTranscript {
663
+ if (!safeNativeId(item.id)) throw new NativeInputError("Unsupported Kimi native session ID.");
664
+ if (item.native && item.native.kind !== "kimi-wire") throw new NativeInputError("Unsupported Kimi native session.");
665
+ const limit = nativeByteLimit(options);
666
+ const text = readBoundedText(item.path, limit);
667
+ const directory = dirname(item.path);
668
+ const { state, unreadable } = readState(join(directory, STATE_FILE));
669
+ const cwd = readWorkspaceCwd(item.path);
670
+ const replay = replayWire(text, item.id, cwd, limit);
671
+ if (replay.rows.length > MAX_NATIVE_RECORDS) throw new NativeInputError("Kimi session exceeds the native record limit.");
672
+
673
+ // The listing's own id is stamped on the envelope, so the session keeps its
674
+ // group even when the wire holds nothing else this reader can project.
675
+ const rows: Row[] = [{
676
+ type: "session.start",
677
+ ...(replay.first ? { timestamp: replay.first } : {}),
678
+ data: {
679
+ sessionId: item.id,
680
+ ...(replay.first ? { startTime: replay.first } : {}),
681
+ context: { ...(cwd ? { cwd } : {}) },
682
+ },
683
+ }, ...replay.rows];
684
+
685
+ const diagnostics = skipDiagnostics(replay.skips);
686
+ if (unreadable) {
687
+ diagnostics.push({
688
+ code: "invalid_json_line",
689
+ message: "Ignored the Kimi session state sidecar: it is not a readable JSON object, so the title stays unset.",
690
+ });
691
+ }
692
+
693
+ const title = state.title ?? item.title;
694
+ return {
695
+ transcript: `${rows.map((row) => JSON.stringify(row)).join("\n")}\n`,
696
+ ...(title ? { title } : {}),
697
+ ...(cwd ? { cwd } : {}),
698
+ ...(diagnostics.length ? { diagnostics } : {}),
699
+ };
700
+ }
701
+
702
+ /** The files whose change invalidates a Kimi session's snapshot. */
703
+ export function kimiSessionFiles(item: DiscoveredSession): string[] {
704
+ const directory = dirname(item.path);
705
+ return [item.path, join(directory, STATE_FILE), join(directory, META_FILE)];
706
+ }