trajectories 0.0.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +201 -0
  3. package/README.md +288 -2
  4. package/dist/cli.js +30210 -0
  5. package/dist/convert.d.ts +31 -0
  6. package/dist/convert.js +431 -0
  7. package/dist/deepagents_checkpoint.py +362 -0
  8. package/dist/index.d.ts +7 -0
  9. package/dist/index.js +8402 -0
  10. package/dist/log-baseline.d.ts +26 -0
  11. package/dist/log-baseline.js +104 -0
  12. package/dist/log-content.d.ts +16 -0
  13. package/dist/log-content.js +80 -0
  14. package/dist/log-io.d.ts +10 -0
  15. package/dist/log-io.js +121 -0
  16. package/dist/log-read.d.ts +24 -0
  17. package/dist/log-read.js +117 -0
  18. package/dist/log-reconcile.d.ts +5 -0
  19. package/dist/log-reconcile.js +38 -0
  20. package/dist/log-records.d.ts +35 -0
  21. package/dist/log-records.js +85 -0
  22. package/dist/log-recovery.d.ts +18 -0
  23. package/dist/log-recovery.js +123 -0
  24. package/dist/log-stage.d.ts +14 -0
  25. package/dist/log-stage.js +138 -0
  26. package/dist/log-state.d.ts +89 -0
  27. package/dist/log-state.js +92 -0
  28. package/dist/log-switch.d.ts +3 -0
  29. package/dist/log-switch.js +98 -0
  30. package/dist/log-types.d.ts +129 -0
  31. package/dist/log-types.js +8 -0
  32. package/dist/log.d.ts +8 -0
  33. package/dist/log.js +62 -0
  34. package/dist/migrate.d.ts +29 -0
  35. package/dist/migrate.js +173 -0
  36. package/dist/normalization-runtime.d.ts +1 -0
  37. package/dist/normalization-runtime.js +3150 -0
  38. package/dist/portable.d.ts +9 -0
  39. package/dist/portable.js +26 -0
  40. package/dist/readers/canonical.d.ts +13 -0
  41. package/dist/readers/canonical.js +54 -0
  42. package/dist/readers/cline.d.ts +10 -0
  43. package/dist/readers/cline.js +107 -0
  44. package/dist/readers/cursor.d.ts +20 -0
  45. package/dist/readers/cursor.js +374 -0
  46. package/dist/readers/deepagents.d.ts +41 -0
  47. package/dist/readers/deepagents.js +163 -0
  48. package/dist/readers/fx.d.ts +11 -0
  49. package/dist/readers/fx.js +573 -0
  50. package/dist/readers/github-copilot.d.ts +8 -0
  51. package/dist/readers/github-copilot.js +216 -0
  52. package/dist/readers/grok-build.d.ts +12 -0
  53. package/dist/readers/grok-build.js +391 -0
  54. package/dist/readers/guards.d.ts +4 -0
  55. package/dist/readers/guards.js +121 -0
  56. package/dist/readers/pi-compaction.d.ts +7 -0
  57. package/dist/readers/pi-compaction.js +112 -0
  58. package/dist/readers/pi-context.d.ts +13 -0
  59. package/dist/readers/pi-context.js +64 -0
  60. package/dist/store.d.ts +25 -0
  61. package/dist/store.js +218 -0
  62. package/dist/types.d.ts +91 -0
  63. package/dist/types.js +1 -0
  64. package/dist/writers/claude-code.d.ts +5 -0
  65. package/dist/writers/claude-code.js +51 -0
  66. package/dist/writers/cline.d.ts +3 -0
  67. package/dist/writers/cline.js +77 -0
  68. package/dist/writers/codex.d.ts +3 -0
  69. package/dist/writers/codex.js +32 -0
  70. package/dist/writers/cursor.d.ts +3 -0
  71. package/dist/writers/cursor.js +197 -0
  72. package/dist/writers/deepagents.d.ts +3 -0
  73. package/dist/writers/deepagents.js +116 -0
  74. package/dist/writers/entropy.d.ts +5 -0
  75. package/dist/writers/entropy.js +33 -0
  76. package/dist/writers/fx.d.ts +3 -0
  77. package/dist/writers/fx.js +135 -0
  78. package/dist/writers/github-copilot.d.ts +3 -0
  79. package/dist/writers/github-copilot.js +71 -0
  80. package/dist/writers/grok-build.d.ts +3 -0
  81. package/dist/writers/grok-build.js +43 -0
  82. package/dist/writers/helpers.d.ts +17 -0
  83. package/dist/writers/helpers.js +75 -0
  84. package/dist/writers/omp.d.ts +3 -0
  85. package/dist/writers/omp.js +7 -0
  86. package/dist/writers/opencode.d.ts +3 -0
  87. package/dist/writers/opencode.js +65 -0
  88. package/dist/writers/pi.d.ts +3 -0
  89. package/dist/writers/pi.js +45 -0
  90. package/docs/cline-native-format.md +43 -0
  91. package/docs/copilot-native-format.md +78 -0
  92. package/docs/cursor-native-format.md +103 -0
  93. package/docs/deepagents-native-format.md +98 -0
  94. package/docs/evidence-api.md +170 -0
  95. package/docs/fx-native-format.md +87 -0
  96. package/docs/harness-parity-bootstrap.md +78 -0
  97. package/docs/harness-support-parity.md +107 -0
  98. package/docs/inspirations.md +103 -0
  99. package/docs/ledger-migration.md +141 -0
  100. package/docs/pi-compaction-context.md +75 -0
  101. package/docs/pi-provider-context.md +76 -0
  102. package/docs/research/2026-10-06-huihua-and-oar.md +176 -0
  103. package/docs/research/ledger/README.md +96 -0
  104. package/docs/research/ledger/confer/2026-09-13-round-10/README.md +61 -0
  105. package/docs/research/ledger/confer/2026-09-13-round-2/cass.md +72 -0
  106. package/docs/research/ledger/confer/2026-09-13-round-3/README.md +70 -0
  107. package/docs/research/ledger/confer/2026-09-13-round-5/README.md +61 -0
  108. package/docs/research/ledger/confer/2026-09-13-round-6/README.md +68 -0
  109. package/docs/research/ledger/confer/2026-09-13-round-7/README.md +64 -0
  110. package/docs/research/ledger/confer/2026-09-13-round-9/README.md +59 -0
  111. package/docs/research/ledger/confer/2026-09-14-wake/wake.md +146 -0
  112. package/docs/research/ledger/docs/inspirations.md +320 -0
  113. package/docs/research/ledger/docs/pr2-pr6-integration.md +53 -0
  114. package/docs/research/ledger/docs/research/2026-09-07-confer-inspirations.md +163 -0
  115. package/docs/research/ledger/docs/research/2026-09-07-letta-continuous-service.md +104 -0
  116. package/docs/research/ledger/docs/research/2026-09-08-claude-history-retrieval.md +117 -0
  117. package/docs/research/ledger/docs/research/2026-09-09-agentsview-native-discovery.md +87 -0
  118. package/docs/research/ledger/docs/research/2026-09-09-daemon-observation.md +231 -0
  119. package/docs/research/ledger/docs/research/2026-09-09-discovery-pagination.md +114 -0
  120. package/docs/research/ledger/docs/research/2026-09-09-harness-transcript-contract.md +145 -0
  121. package/docs/research/ledger/docs/research/2026-09-09-native-history-discovery.md +180 -0
  122. package/docs/research/ledger/docs/research/2026-09-09-native-history-implementation.md +137 -0
  123. package/docs/research/ledger/docs/research/2026-09-09-other-inspirations.md +279 -0
  124. package/docs/research/ledger/docs/research/2026-09-09-trajectory-and-letta-code.md +128 -0
  125. package/docs/research/ledger/docs/research/2026-09-13-diagnostics-coverage.md +116 -0
  126. package/docs/research/ledger/docs/research/2026-09-13-fts-rebuild.md +83 -0
  127. package/docs/research/ledger/docs/research/2026-09-13-read-path-split.md +74 -0
  128. package/docs/research/ledger/docs/research/2026-09-13-runtime-rollout.md +102 -0
  129. package/docs/research/ledger/docs/research/2026-09-13-scan-outcomes.md +80 -0
  130. package/docs/research/ledger/docs/research/2026-09-13-search-quality.md +105 -0
  131. package/docs/research/ledger/docs/research/2026-09-13-source-coverage.md +64 -0
  132. package/docs/research/ledger/docs/research/2026-09-13-sync-wall-budget.md +82 -0
  133. package/docs/research/ledger/docs/research/2026-09-13-typed-read-gate.md +81 -0
  134. package/docs/research/ledger/docs/research/2026-09-13-writer-lease.md +84 -0
  135. package/docs/research/ledger/docs/research/2026-09-16-c1-copy-fallback.md +227 -0
  136. package/docs/research/ledger/docs/research/2026-09-16-c10-kimi.md +414 -0
  137. package/docs/research/ledger/docs/research/2026-09-16-c2-derive-counter.md +216 -0
  138. package/docs/research/ledger/docs/research/2026-09-16-c3-echo-filter.md +305 -0
  139. package/docs/research/ledger/docs/research/2026-09-16-c4-title-index.md +349 -0
  140. package/docs/research/ledger/docs/research/2026-09-16-c5-trigram.md +268 -0
  141. package/docs/research/ledger/docs/research/2026-09-16-c6-recency-prior.md +438 -0
  142. package/docs/research/ledger/docs/research/2026-09-16-c7-tombstones.md +467 -0
  143. package/docs/research/ledger/docs/research/2026-09-16-c8-remote-truth.md +390 -0
  144. package/docs/research/ledger/docs/research/2026-09-16-c9-grok.md +380 -0
  145. package/docs/research/ledger/docs/research/2026-09-16-wake-port-program.md +38 -0
  146. package/docs/research/ledger/docs/research/2026-09-17-cursor-blobstore.md +157 -0
  147. package/docs/research/ledger/manifest.json +540 -0
  148. package/legacy/NOTICE.md +50 -0
  149. package/legacy/bin/ledger-core.ts +7 -0
  150. package/legacy/bin/ledger.ts +7 -0
  151. package/legacy/bin/traj-bridge.ts +7 -0
  152. package/legacy/scripts/import-claude-history.ts +130 -0
  153. package/legacy/scripts/native-history-smoke.ts +52 -0
  154. package/legacy/scripts/observe-daemon.ts +570 -0
  155. package/legacy/scripts/retitle-sessions.md +19 -0
  156. package/legacy/scripts/verify-runtime-rollout.ts +151 -0
  157. package/legacy/src/canonical.ts +108 -0
  158. package/legacy/src/cli.ts +1293 -0
  159. package/legacy/src/db.ts +2254 -0
  160. package/legacy/src/diagnostics.ts +87 -0
  161. package/legacy/src/discovery.ts +56 -0
  162. package/legacy/src/embeddings.ts +382 -0
  163. package/legacy/src/filter.ts +602 -0
  164. package/legacy/src/gemini_embeddings.ts +103 -0
  165. package/legacy/src/identity.ts +9 -0
  166. package/legacy/src/index.ts +10 -0
  167. package/legacy/src/ingest.ts +791 -0
  168. package/legacy/src/managed.ts +178 -0
  169. package/legacy/src/native/common.ts +68 -0
  170. package/legacy/src/native/cursor.ts +647 -0
  171. package/legacy/src/native/grok.ts +540 -0
  172. package/legacy/src/native/kimi.ts +706 -0
  173. package/legacy/src/native/opencode.ts +470 -0
  174. package/legacy/src/native/sqlite-ro.ts +281 -0
  175. package/legacy/src/native.ts +97 -0
  176. package/legacy/src/normalizer.ts +457 -0
  177. package/legacy/src/parentage.ts +562 -0
  178. package/legacy/src/pressure.ts +162 -0
  179. package/legacy/src/private-storage.ts +74 -0
  180. package/legacy/src/reader.ts +1255 -0
  181. package/legacy/src/scanner.ts +591 -0
  182. package/legacy/src/schema-gate.ts +263 -0
  183. package/legacy/src/schema.ts +829 -0
  184. package/legacy/src/semantic-top-k.ts +71 -0
  185. package/legacy/src/session-hash.ts +49 -0
  186. package/legacy/src/store-status.ts +218 -0
  187. package/legacy/src/sync-preparation.ts +45 -0
  188. package/legacy/src/sync-truth.ts +843 -0
  189. package/legacy/src/sync.ts +1233 -0
  190. package/legacy/src/types.ts +436 -0
  191. package/legacy/src/vocab.txt +30522 -0
  192. package/legacy/src/watch.ts +673 -0
  193. package/legacy/src/writer-lease.ts +310 -0
  194. package/ops/README.md +150 -0
  195. package/ops/install-cli.sh +63 -0
  196. package/ops/install-native.sh +35 -0
  197. package/ops/install-release.sh +49 -0
  198. package/ops/linux/install-systemd-user.sh +103 -0
  199. package/ops/macos/install-launchd.sh +72 -0
  200. package/ops/sync-settings.sh +37 -0
  201. package/package.json +21 -5
  202. package/skills/trajectories/SKILL.md +138 -0
  203. package/skills/trajectories/scripts/trajectories_recent.sh +26 -0
  204. package/skills/trajectories/scripts/trajectories_search.sh +26 -0
  205. package/index.js +0 -1
@@ -0,0 +1,540 @@
1
+ import { existsSync, lstatSync, readdirSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import type { Diagnostic } from "@letta-ai/trajectory";
5
+ import type { DiscoveredSession } from "../types.js";
6
+ import {
7
+ MAX_NATIVE_RECORDS, NativeInputError, nativeByteLimit, object, readBoundedText,
8
+ reportNativeError, safeNativeId, type NativeDiscoveryOptions, type NativeReadOptions, type NativeTranscript,
9
+ } from "./common.js";
10
+
11
+ /**
12
+ * Grok Build keeps one directory per session under
13
+ * `~/.grok/sessions/<url-encoded-cwd>/<session-id>/`. `updates.jsonl` is the
14
+ * transcript source of truth: an append-only ACP notification stream
15
+ * (`{timestamp, method, params:{sessionId, update}}`) whose prose arrives as
16
+ * role-tagged chunks and whose tool results arrive as `tool_call_update` rows
17
+ * keyed by `toolCallId`. `summary.json` is a metadata sidecar (cwd, title,
18
+ * timestamps, model, branch); `chat_history.jsonl` is a compacted snapshot and
19
+ * `session_search.sqlite` is Grok's own index — neither is a session source.
20
+ * Format facts: docs/research/2026-09-16-c9-grok.md.
21
+ */
22
+ const GROK_SESSION_FILE = "updates.jsonl";
23
+
24
+ /**
25
+ * The pinned library has no Grok adapter, so this reader projects each replayed
26
+ * update into the Copilot CLI event contract — the closest documented
27
+ * event-per-line contract to an ACP update stream, and one that keeps what a
28
+ * canonical record needs: a native per-update id, a per-event timestamp, tool
29
+ * calls linked to their results by call id, and a terminal outcome flag.
30
+ * `source_type` is restored to this source in the normalizer, so the projection
31
+ * stays a detail of a source the library cannot read.
32
+ */
33
+ export const GROK_PROJECTION_SOURCE = "copilot-cli";
34
+
35
+ const SUMMARY_FILE = "summary.json";
36
+ const REGISTRY_DIRECTORY = "subagents";
37
+ const REGISTRY_FILE = "meta.json";
38
+ // <root>/<cwd>/<session>/updates.jsonl is the documented layout; the spare
39
+ // levels hold a listing rooted deeper, without turning discovery into an
40
+ // unbounded walk. A directory that holds a session is not descended into: its
41
+ // other subdirectories hold a session's own logs, not sessions.
42
+ const MAX_DEPTH = 4;
43
+ const MAX_SIDECAR_BYTES = 4 * 1024 * 1024;
44
+
45
+ /** Lifecycle updates Grok writes beside the transcript: state, not conversation. */
46
+ const LIFECYCLE_KINDS = new Set([
47
+ "hook_execution",
48
+ "turn_completed",
49
+ "session_recap",
50
+ "retry_state",
51
+ "task_backgrounded",
52
+ "task_completed",
53
+ "auto_compact_started",
54
+ "auto_compact_completed",
55
+ "compaction_checkpoint",
56
+ "plan",
57
+ "current_mode_update",
58
+ ]);
59
+
60
+ export function grokRoots(): string[] {
61
+ return [join(homedir(), ".grok", "sessions")];
62
+ }
63
+
64
+ interface Summary {
65
+ cwd?: string;
66
+ branch?: string;
67
+ model?: string;
68
+ title?: string;
69
+ startedAt?: string;
70
+ updatedAt?: string;
71
+ }
72
+
73
+ function isoTime(value: unknown): string | undefined {
74
+ if (typeof value !== "string" || !value.trim()) return undefined;
75
+ const time = new Date(value);
76
+ return Number.isFinite(time.getTime()) ? time.toISOString() : undefined;
77
+ }
78
+
79
+ /**
80
+ * The sidecar is metadata, not transcript: a missing, oversized, or malformed
81
+ * summary costs title, cwd and model, and never the session, so every failure
82
+ * here degrades instead of throwing.
83
+ */
84
+ function readSummary(path: string): { summary: Summary; unreadable: boolean } {
85
+ if (!existsSync(path)) return { summary: {}, unreadable: false };
86
+ try {
87
+ const value: unknown = JSON.parse(readBoundedText(path, MAX_SIDECAR_BYTES));
88
+ if (!object(value)) return { summary: {}, unreadable: true };
89
+ const info = object(value.info) ? value.info : {};
90
+ const title = [value.generated_title, value.session_summary]
91
+ .find((candidate): candidate is string => typeof candidate === "string" && candidate.trim().length > 0);
92
+ return {
93
+ summary: {
94
+ cwd: typeof info.cwd === "string" && info.cwd.trim() ? info.cwd : undefined,
95
+ branch: typeof value.head_branch === "string" && value.head_branch.trim() ? value.head_branch : undefined,
96
+ model: typeof value.current_model_id === "string" && value.current_model_id.trim() ? value.current_model_id : undefined,
97
+ title,
98
+ startedAt: isoTime(value.created_at),
99
+ updatedAt: isoTime(value.updated_at) ?? isoTime(value.last_active_at),
100
+ },
101
+ unreadable: false,
102
+ };
103
+ } catch {
104
+ return { summary: {}, unreadable: true };
105
+ }
106
+ }
107
+
108
+ export function discoverGrokSessions(roots: string[], options: NativeDiscoveryOptions): DiscoveredSession[] {
109
+ if (!Number.isSafeInteger(options.limit) || options.limit < 1) throw new RangeError("Grok discovery limit must be positive.");
110
+ const items: DiscoveredSession[] = [];
111
+ const edges = new Map<string, string>();
112
+ const seen = new Set<string>();
113
+
114
+ function walk(path: string, depth: number): void {
115
+ if (seen.has(path)) return;
116
+ seen.add(path);
117
+ try {
118
+ const stat = lstatSync(path);
119
+ if (stat.isSymbolicLink() || stat.isFile()) return;
120
+ if (depth > MAX_DEPTH) {
121
+ options.onDiagnostic?.({ code: "incomplete_evidence", sourcePath: path, message: "Grok discovery reached its directory depth limit." });
122
+ return;
123
+ }
124
+ const entries = readdirSync(path, { withFileTypes: true });
125
+ const names = new Set(entries.map((entry) => entry.name));
126
+ if (names.has(GROK_SESSION_FILE)) {
127
+ const id = basename(path);
128
+ if (!safeNativeId(id)) {
129
+ options.onDiagnostic?.({ code: "invalid_metadata", sourcePath: path, message: "Grok session has an invalid native session ID." });
130
+ return;
131
+ }
132
+ const updates = statSync(join(path, GROK_SESSION_FILE));
133
+ const { summary } = readSummary(join(path, SUMMARY_FILE));
134
+ items.push({
135
+ id,
136
+ source: "grok",
137
+ path: join(path, GROK_SESSION_FILE),
138
+ updatedAt: summary.updatedAt ?? updates.mtime.toISOString(),
139
+ sizeBytes: updates.size + (names.has(SUMMARY_FILE) ? statSync(join(path, SUMMARY_FILE)).size : 0),
140
+ ...(summary.title ? { title: summary.title } : {}),
141
+ });
142
+ if (names.has(REGISTRY_DIRECTORY)) collectSubagentEdges(path, id, options, edges);
143
+ return;
144
+ }
145
+ for (const entry of entries) {
146
+ if (entry.isDirectory()) walk(join(path, entry.name), depth + 1);
147
+ }
148
+ } catch (error) {
149
+ if (object(error) && error.code === "ENOENT" && depth === 0) return;
150
+ reportNativeError(options, path);
151
+ }
152
+ }
153
+
154
+ for (const root of roots) walk(resolve(root), 0);
155
+
156
+ const ordered = items
157
+ .sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "") || a.path.localeCompare(b.path));
158
+ const nativeIds = new Set<string>();
159
+ const deduplicated = ordered.filter((item) => {
160
+ if (nativeIds.has(item.id)) return false;
161
+ nativeIds.add(item.id);
162
+ return true;
163
+ }).map((item) => {
164
+ const parent = edges.get(item.id);
165
+ return { ...item, native: { kind: "grok-updates" as const, ...(parent ? { parentSessionId: parent } : {}) } };
166
+ });
167
+ return options.select ? options.select(deduplicated) : deduplicated.slice(0, options.limit);
168
+ }
169
+
170
+ /**
171
+ * Parentage evidence read from the parent's side: every subagent Grok spawns is
172
+ * registered at `<parent>/subagents/<child>/meta.json`. The registration names
173
+ * the child by either of two fields and the parent by `parent_session_id`,
174
+ * falling back to the enclosing session directory; a session is never its own
175
+ * parent, and the first registration wins — the rules Wake's
176
+ * `load_subagent_parents` applies (`crates/wake-core/src/adapters/grok_group.rs`).
177
+ *
178
+ * The edge travels with the child's listing, so ingest never re-walks the store
179
+ * and a registry added between passes is visible to the next scan.
180
+ */
181
+ function collectSubagentEdges(
182
+ directory: string,
183
+ parent: string,
184
+ options: NativeDiscoveryOptions,
185
+ edges: Map<string, string>,
186
+ ): void {
187
+ const registry = join(directory, REGISTRY_DIRECTORY);
188
+ let entries: string[];
189
+ try {
190
+ entries = readdirSync(registry);
191
+ } catch {
192
+ reportNativeError(options, registry);
193
+ return;
194
+ }
195
+ for (const entry of entries) {
196
+ const metaPath = join(registry, entry, REGISTRY_FILE);
197
+ let meta: Record<string, unknown>;
198
+ try {
199
+ const parsed: unknown = JSON.parse(readBoundedText(metaPath, MAX_SIDECAR_BYTES));
200
+ if (!object(parsed)) throw new NativeInputError("Grok subagent registration must be a JSON object.");
201
+ meta = parsed;
202
+ } catch (error) {
203
+ if (object(error) && error.code === "ENOENT") continue;
204
+ options.onDiagnostic?.({ code: "invalid_metadata", sourcePath: metaPath, message: "Grok subagent registration could not be read." });
205
+ continue;
206
+ }
207
+ const parentId = typeof meta.parent_session_id === "string" && meta.parent_session_id.trim() ? meta.parent_session_id : parent;
208
+ for (const field of ["child_session_id", "subagent_id"]) {
209
+ const child = meta[field];
210
+ if (typeof child !== "string" || !safeNativeId(child) || child === parentId) continue;
211
+ if (!edges.has(child)) edges.set(child, parentId);
212
+ }
213
+ }
214
+ }
215
+
216
+ type RunRole = "user" | "thought" | "message";
217
+ type Row = Record<string, unknown>;
218
+
219
+ interface Run {
220
+ role: RunRole;
221
+ text: string;
222
+ /** The run's identity: the id of its first fragment. */
223
+ id: string;
224
+ timestamp: string | null;
225
+ model: string | undefined;
226
+ }
227
+
228
+ interface Skips {
229
+ lifecycle: Map<string, number>;
230
+ progress: number;
231
+ unknown: Map<string, number>;
232
+ content: number;
233
+ invalidLines: number;
234
+ malformedLines: number;
235
+ /** Terminal rows for a call whose result was already projected. */
236
+ repeatedTerminals: number;
237
+ }
238
+
239
+ function tally(values: Map<string, number>): number {
240
+ let total = 0;
241
+ for (const count of values.values()) total += count;
242
+ return total;
243
+ }
244
+
245
+ function named(values: Map<string, number>, limit = 4): string {
246
+ return [...values.keys()].sort().slice(0, limit).join(", ");
247
+ }
248
+
249
+ /**
250
+ * Replay the ACP stream.
251
+ *
252
+ * A run of same-role chunks becomes one record: chunk text is concatenated
253
+ * without a separator, because Grok splits tokens (including the spaces between
254
+ * them) across rows. A run flushes on a role change or when tool activity
255
+ * interrupts it, so one record's text never spans two speakers or straddles a
256
+ * tool call. A `tool_call_update` is a result only in its terminal state:
257
+ * in_progress and status-less progress rows are superseded by the terminal row
258
+ * that follows, and are counted rather than projected.
259
+ *
260
+ * Grok appends while it runs, so a torn last line or a line from a newer
261
+ * version is counted, never fatal: the transcript is what the prefix holds.
262
+ */
263
+ function replayUpdates(text: string, sessionId: string, summary: Summary, limit: number): {
264
+ rows: Row[]; skips: Skips; model: string | undefined;
265
+ } {
266
+ const rows: Row[] = [];
267
+ const skips: Skips = {
268
+ lifecycle: new Map(), progress: 0, unknown: new Map(), content: 0, invalidLines: 0, malformedLines: 0,
269
+ repeatedTerminals: 0,
270
+ };
271
+ // One result per call id: a repeated terminal row (a retry after a failure, a
272
+ // re-emitted update) must not project a second result for the same call.
273
+ const terminals = new Set<string>();
274
+ let run: Run | null = null;
275
+ let model = summary.model;
276
+ let written = 0;
277
+
278
+ function emit(row: Row): void {
279
+ written += Buffer.byteLength(JSON.stringify(row)) + 1;
280
+ if (written > limit) throw new NativeInputError("Grok transcript exceeds the native byte limit.");
281
+ rows.push(row);
282
+ }
283
+
284
+ function flush(): void {
285
+ const pending = run;
286
+ run = null;
287
+ if (!pending || !pending.text.length) return;
288
+ if (pending.role === "user") {
289
+ emit({
290
+ type: "hook.start",
291
+ id: pending.id,
292
+ ...(pending.timestamp ? { timestamp: pending.timestamp } : {}),
293
+ data: {
294
+ hookType: "userPromptSubmitted",
295
+ input: { prompt: pending.text, sessionId, ...(summary.cwd ? { cwd: summary.cwd } : {}) },
296
+ },
297
+ });
298
+ return;
299
+ }
300
+ const eventModel = pending.model ?? model;
301
+ emit({
302
+ type: "assistant.message",
303
+ id: pending.id,
304
+ ...(pending.timestamp ? { timestamp: pending.timestamp } : {}),
305
+ data: {
306
+ ...(pending.role === "thought" ? { reasoningText: pending.text } : { content: pending.text }),
307
+ ...(eventModel ? { model: eventModel } : {}),
308
+ },
309
+ });
310
+ }
311
+
312
+ const lines = text.split("\n");
313
+ for (let index = 0; index < lines.length; index += 1) {
314
+ const line = lines[index];
315
+ if (!line || !line.trim()) continue;
316
+ let parsed: unknown;
317
+ try {
318
+ parsed = JSON.parse(line);
319
+ } catch {
320
+ skips.invalidLines += 1;
321
+ continue;
322
+ }
323
+ const row = object(parsed) ? parsed : null;
324
+ const params = row && object(row.params) ? row.params : null;
325
+ const update = params && object(params.update) ? params.update : null;
326
+ if (!row || !params || !update) {
327
+ skips.malformedLines += 1;
328
+ continue;
329
+ }
330
+ const meta = object(params._meta) ? params._meta : {};
331
+ const agentTimestampMs = typeof meta.agentTimestampMs === "number" && Number.isFinite(meta.agentTimestampMs)
332
+ ? meta.agentTimestampMs
333
+ : typeof update._meta === "object" && object(update._meta) && typeof update._meta.agentTimestampMs === "number"
334
+ && Number.isFinite(update._meta.agentTimestampMs)
335
+ ? update._meta.agentTimestampMs
336
+ : null;
337
+ const timestamp = agentTimestampMs !== null
338
+ ? new Date(agentTimestampMs).toISOString()
339
+ : typeof row.timestamp === "number" && Number.isFinite(row.timestamp) && row.timestamp > 0
340
+ ? new Date(row.timestamp * 1000).toISOString()
341
+ : null;
342
+ const chunkModel = object(update._meta) && typeof update._meta.modelId === "string" ? update._meta.modelId : undefined;
343
+ if (chunkModel) model = chunkModel;
344
+ // A row without a usable update id keeps a stable identity from its own
345
+ // line: the file is append-only, so a line number never moves.
346
+ const id = typeof meta.eventId === "string" && safeNativeId(meta.eventId) ? meta.eventId : `grok-line-${index + 1}`;
347
+ const kind = update.sessionUpdate;
348
+ if (kind === "user_message_chunk" || kind === "agent_thought_chunk" || kind === "agent_message_chunk") {
349
+ const content = update.content;
350
+ const chunk = object(content) && content.type === "text" && typeof content.text === "string" ? content.text : null;
351
+ if (chunk === null) {
352
+ skips.content += 1;
353
+ continue;
354
+ }
355
+ const role: RunRole = kind === "user_message_chunk" ? "user" : kind === "agent_thought_chunk" ? "thought" : "message";
356
+ if (run && run.role !== role) flush();
357
+ run ??= { role, text: "", id, timestamp, model: chunkModel };
358
+ run.text += chunk;
359
+ continue;
360
+ }
361
+ if (kind === "tool_call") {
362
+ const callId = update.toolCallId;
363
+ if (!safeNativeId(callId)) {
364
+ skips.unknown.set("tool_call_without_id", (skips.unknown.get("tool_call_without_id") ?? 0) + 1);
365
+ continue;
366
+ }
367
+ flush();
368
+ const tool = object(update._meta) && object(update._meta["x.ai/tool"]) ? update._meta["x.ai/tool"] : null;
369
+ const name = typeof update.title === "string" && update.title.trim()
370
+ ? update.title
371
+ : tool && typeof tool.name === "string" && tool.name.trim() ? tool.name : undefined;
372
+ const eventModel = chunkModel ?? model;
373
+ emit({
374
+ type: "assistant.message",
375
+ id,
376
+ ...(timestamp ? { timestamp } : {}),
377
+ data: {
378
+ toolRequests: [{ toolCallId: callId, ...(name ? { name } : {}), arguments: update.rawInput ?? {} }],
379
+ ...(eventModel ? { model: eventModel } : {}),
380
+ },
381
+ });
382
+ continue;
383
+ }
384
+ if (kind === "tool_call_update") {
385
+ const callId = update.toolCallId;
386
+ if (!safeNativeId(callId)) {
387
+ skips.unknown.set("tool_call_update_without_id", (skips.unknown.get("tool_call_update_without_id") ?? 0) + 1);
388
+ continue;
389
+ }
390
+ const status = update.status;
391
+ if (status !== "completed" && status !== "failed") {
392
+ skips.progress += 1;
393
+ continue;
394
+ }
395
+ if (terminals.has(callId)) {
396
+ skips.repeatedTerminals += 1;
397
+ continue;
398
+ }
399
+ terminals.add(callId);
400
+ flush();
401
+ // ACP tool output is content blocks; a structured rawOutput is the only
402
+ // text a result can carry when no content block was written.
403
+ const blocks = update.content;
404
+ const parts = Array.isArray(blocks)
405
+ ? blocks
406
+ .map((block) => (object(block) && object(block.content) && block.content.type === "text" && typeof block.content.text === "string"
407
+ ? block.content.text
408
+ : null))
409
+ .filter((part): part is string => part !== null && part.length > 0)
410
+ : [];
411
+ const raw = update.rawOutput;
412
+ const output = parts.length
413
+ ? parts.join("\n")
414
+ : raw === undefined || raw === null ? "" : typeof raw === "string" ? raw : JSON.stringify(raw);
415
+ emit({
416
+ type: "tool.execution_complete",
417
+ id,
418
+ ...(timestamp ? { timestamp } : {}),
419
+ data: {
420
+ toolCallId: callId,
421
+ success: status !== "failed",
422
+ ...(status === "failed" ? { error: { message: output } } : { result: { content: output } }),
423
+ },
424
+ });
425
+ continue;
426
+ }
427
+ if (typeof kind === "string" && LIFECYCLE_KINDS.has(kind)) {
428
+ skips.lifecycle.set(kind, (skips.lifecycle.get(kind) ?? 0) + 1);
429
+ continue;
430
+ }
431
+ const label = typeof kind === "string" && kind ? kind : "missing_kind";
432
+ skips.unknown.set(label, (skips.unknown.get(label) ?? 0) + 1);
433
+ }
434
+ flush();
435
+ return { rows, skips, model };
436
+ }
437
+
438
+ /** Every skip is typed and counted, so a dropped update never vanishes silently. */
439
+ function skipDiagnostics(skips: Skips): Diagnostic[] {
440
+ const diagnostics: Diagnostic[] = [];
441
+ if (skips.lifecycle.size) {
442
+ diagnostics.push({
443
+ code: "noise_record_dropped",
444
+ message: `Skipped ${tally(skips.lifecycle)} Grok lifecycle update(s) (${named(skips.lifecycle)}).`,
445
+ count: tally(skips.lifecycle),
446
+ });
447
+ }
448
+ if (skips.progress) {
449
+ diagnostics.push({
450
+ code: "noise_record_dropped",
451
+ message: `Skipped ${skips.progress} non-terminal Grok tool progress update(s); the terminal update carries the result.`,
452
+ count: skips.progress,
453
+ });
454
+ }
455
+ if (skips.repeatedTerminals) {
456
+ diagnostics.push({
457
+ code: "noise_record_dropped",
458
+ message: `Skipped ${skips.repeatedTerminals} repeated Grok tool terminal update(s); the call's first terminal update is its result.`,
459
+ count: skips.repeatedTerminals,
460
+ });
461
+ }
462
+ if (skips.unknown.size) {
463
+ diagnostics.push({
464
+ code: "noise_record_dropped",
465
+ message: `Skipped ${tally(skips.unknown)} Grok update(s) with no supported transcript shape (${named(skips.unknown)}).`,
466
+ count: tally(skips.unknown),
467
+ });
468
+ }
469
+ if (skips.content) {
470
+ diagnostics.push({
471
+ code: "noise_record_dropped",
472
+ message: `Skipped ${skips.content} Grok message update(s) whose content is not a text block.`,
473
+ count: skips.content,
474
+ });
475
+ }
476
+ if (skips.invalidLines) {
477
+ diagnostics.push({
478
+ code: "invalid_json_line",
479
+ message: `Skipped ${skips.invalidLines} invalid JSON line(s) in the Grok update stream.`,
480
+ count: skips.invalidLines,
481
+ });
482
+ }
483
+ if (skips.malformedLines) {
484
+ diagnostics.push({
485
+ code: "invalid_json_line",
486
+ message: `Skipped ${skips.malformedLines} Grok line(s) with no session update object.`,
487
+ count: skips.malformedLines,
488
+ });
489
+ }
490
+ return diagnostics;
491
+ }
492
+
493
+ export function readGrokSession(item: DiscoveredSession, options: NativeReadOptions = {}): NativeTranscript {
494
+ if (!safeNativeId(item.id)) throw new NativeInputError("Unsupported Grok native session ID.");
495
+ if (item.native && item.native.kind !== "grok-updates") throw new NativeInputError("Unsupported Grok native session.");
496
+ const limit = nativeByteLimit(options);
497
+ const text = readBoundedText(item.path, limit);
498
+ const { summary, unreadable } = readSummary(join(dirname(item.path), SUMMARY_FILE));
499
+ const replay = replayUpdates(text, item.id, summary, limit);
500
+ if (replay.rows.length > MAX_NATIVE_RECORDS) throw new NativeInputError("Grok session exceeds the native record limit.");
501
+
502
+ const rows: Row[] = summary.startedAt
503
+ ? [{
504
+ type: "session.start",
505
+ timestamp: summary.startedAt,
506
+ data: {
507
+ sessionId: item.id,
508
+ startTime: summary.startedAt,
509
+ context: { ...(summary.cwd ? { cwd: summary.cwd } : {}), ...(summary.branch ? { branch: summary.branch } : {}) },
510
+ },
511
+ }, ...replay.rows]
512
+ : replay.rows;
513
+
514
+ const diagnostics = skipDiagnostics(replay.skips);
515
+ if (unreadable) {
516
+ diagnostics.push({
517
+ code: "invalid_json_line",
518
+ message: "Ignored the Grok session summary sidecar: it is not a readable JSON object, so title, cwd and model stay unset.",
519
+ });
520
+ }
521
+
522
+ const model = replay.model ?? summary.model;
523
+ return {
524
+ transcript: rows.length ? `${rows.map((row) => JSON.stringify(row)).join("\n")}\n` : "",
525
+ ...(summary.title ? { title: summary.title } : {}),
526
+ ...(summary.cwd ? { cwd: summary.cwd } : {}),
527
+ ...(model ? { model } : {}),
528
+ // The sidecar's own envelope bounds the session: it starts before the first
529
+ // update and ends at Grok's last recorded activity, which a hook-only
530
+ // session (no records at all) would otherwise have no span for.
531
+ ...(summary.startedAt ? { startedAt: summary.startedAt } : {}),
532
+ ...(summary.updatedAt ? { endedAt: summary.updatedAt } : {}),
533
+ ...(diagnostics.length ? { diagnostics } : {}),
534
+ };
535
+ }
536
+
537
+ /** The files whose change invalidates a Grok session's snapshot. */
538
+ export function grokSessionFiles(item: DiscoveredSession): string[] {
539
+ return [item.path, join(dirname(item.path), SUMMARY_FILE)];
540
+ }