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,380 @@
1
+ # C9: Grok as a first-class source, 2026-09-16
2
+
3
+ ## Outcome
4
+
5
+ Grok Build is now a source this store discovers, reads, normalizes, and covers
6
+ like any other: `packages/core/src/native/grok.ts` (discovery of
7
+ `~/.grok/sessions/<url-encoded-cwd>/<session-id>/updates.jsonl`, the replay
8
+ that turns its append-only ACP update stream into canonical records, the
9
+ `summary.json` sidecar, and the parent-side subagent registry), wired through
10
+ `scanner.ts`, `native.ts`, `normalizer.ts`, `parentage.ts`, and `watch.ts`.
11
+ Sessions are keyed `grok:<uuidv7>`, the source gets its own `source_coverage`
12
+ row, and `packages/core/tests/native-grok.test.ts` (13 cases) covers the fixture plan.
13
+
14
+ The sentences this cycle exists to make impossible:
15
+
16
+ 1. **"Grok's streamed chunks are turns."** `user_message_chunk`,
17
+ `agent_thought_chunk`, and `agent_message_chunk` arrive as token fragments,
18
+ not messages: a record is a *run* of same-role fragments, concatenated with
19
+ no separator, flushed on a role change or when tool activity interrupts it.
20
+ 2. **"A tool result is whatever the last `tool_call_update` said."** Only a
21
+ terminal update (`completed` / `failed`) is a result, at most one per call
22
+ id: `in_progress` and status-less progress rows are superseded, and a
23
+ terminal row for a call that already has a result is counted rather than
24
+ projected — so a retry or a re-emitted update cannot put two results behind
25
+ one call.
26
+ 3. **"the exit is the evidence."** `ok = status !== "failed"` survives into
27
+ `records.tool_result_ok` (0/1), so a failed tool is a stored fact.
28
+ 4. **"What the adapter drops, nobody knows."** Every drop — lifecycle updates,
29
+ superseded progress, repeated terminal rows, unrecognized kinds, non-text
30
+ content, invalid or torn lines — becomes a typed, counted diagnostic in
31
+ `ingest_diagnostics`, with the kind names in the bounded sample.
32
+ 5. **"A subagent's parent is written on the child."** Grok records the edge
33
+ *under the parent* (`<parent>/subagents/<child>/meta.json`); the scan reads it
34
+ while it is already walking the store, and the child carries it into ingest.
35
+
36
+ ### Evidence, and what kind it is
37
+
38
+ **OBSERVED** (this tree, this cycle):
39
+
40
+ - `bun test` — 639 pass / 0 fail, 4,845 assertions, 44 files (the new file
41
+ contributes 13 cases / 97 assertions); `bun run typecheck` and
42
+ `bun run build` clean.
43
+ - The real-corpus pass over the read-only `~/.grok` store into a scratch store
44
+ under `/tmp` (below): 5 sessions, 103 records, 0 parent edges, 2 failed tool
45
+ results preserved, FTS hits present, second pass fully deduped.
46
+ - The primary fixture shapes, read (never written) from `~/.grok/sessions`:
47
+ layout, row grammar, `_meta` keys, tool statuses, and `summary.json` keys.
48
+
49
+ **INFERRED** (not run here):
50
+
51
+ - Compaction never truncates the transcript: `auto_compact_*` /
52
+ `compaction_checkpoint` rows are lifecycle, and the stream is append-only, so
53
+ replaying it preserves history (the scout's risk list).
54
+ - Grok's GC can prune ephemeral worktree directories while session directories
55
+ survive; the reader never resolves a cwd on disk, so a pruned worktree cannot
56
+ break a read.
57
+ - The registry edge is proven for a child whose session directory lives in a
58
+ *different* cwd folder than its parent: exercised synthetically (no real
59
+ subagent exists on this host), following Wake's contract.
60
+
61
+ **Not done at all:** no upstream `@letta-ai/trajectory` adapter was written or
62
+ proposed; `~/.grok` was never modified; the production Ledger store was never
63
+ opened by this cycle; no embedding or semantic measurement was taken; no cwd
64
+ flattening was implemented (each session keeps the cwd it ran in).
65
+
66
+ ## The format, as observed
67
+
68
+ Layout (`~/.grok/sessions`): `<url-encoded-cwd>/<session-uuidv7>/` holds
69
+ `updates.jsonl` (the transcript), `summary.json` (metadata sidecar),
70
+ `events.jsonl`, `chat_history.jsonl` (a compacted snapshot — not a source),
71
+ `signals.json`, `prompt_context.json`, and `*.lock` files;
72
+ `session_search.sqlite` sits at the sessions root (Grok's own index, not a
73
+ source). `subagents/<child>/meta.json` appears under a *parent's* session
74
+ directory when a subagent was spawned.
75
+
76
+ `updates.jsonl` rows are ACP notifications:
77
+ `{"timestamp": <unix seconds>, "method": "session/update" | "_x.ai/session/update",
78
+ "params": {"sessionId": "<uuid>", "update": {"sessionUpdate": "<kind>", …},
79
+ "_meta": {"eventId": "<uuid>-<n>", "agentTimestampMs": <ms>, …}}}`.
80
+
81
+ Aggregate counts over the 5 sessions / 226 rows on this host (content never
82
+ printed; only kinds, statuses, key names, and counts):
83
+
84
+ | fact | observed |
85
+ | --- | --- |
86
+ | kinds | `tool_call_update` 119, `tool_call` 27, `hook_execution` 22, `agent_thought_chunk` 19, `agent_message_chunk` 14, `user_message_chunk` 11, `turn_completed` 11, `retry_state` 2, `session_recap` 1 |
87
+ | tool statuses | `completed` 25, `in_progress` 66, absent (progress) 26, `failed` 2 |
88
+ | `params._meta` keys | `eventId`, `agentTimestampMs` on all 226 rows; `totalTokens`, `promptId`, `streamStartMs`, `turnStartMs`, `updateType` on 113; `chunkId` 33, `updateParams` 80, `cancelTrigger` 1 |
89
+ | ids | `eventId` unique on 226/226 rows, prefixed by the session id; every row's `params.sessionId` and every directory name agree (5/5 sessions) |
90
+ | text | every chunk's `content` is `{type: "text", text}`; tool results are `content: [{type: "content", content: {type: "text", text}}]` |
91
+ | `summary.json` keys | `info{id,cwd}`, `session_summary`, `generated_title`, `created_at`, `updated_at`, `last_active_at`, `current_model_id`, `num_messages`, `next_trace_turn`, `chat_format_version`, `request_id`, `grok_home`, `agent_name`, `reasoning_effort`, `head_branch` (2/5 sessions), `git_root_dir`/`head_commit`/`git_remotes` (3/5) |
92
+ | registry | 0 `subagents/` directories across all 5 sessions (no real parent edge exists on this host) |
93
+
94
+ Row grammar → canonical record:
95
+
96
+ | `sessionUpdate` | record |
97
+ | --- | --- |
98
+ | `user_message_chunk` run | `user` |
99
+ | `agent_thought_chunk` run | `reasoning` |
100
+ | `agent_message_chunk` run | `assistant` |
101
+ | `tool_call` (`toolCallId`, `title`, `rawInput`) | `assistant-tool-call` (`tool_name` = `title`, falling back to `_meta["x.ai/tool"].name`; `tool_arguments_json` = `rawInput`) |
102
+ | `tool_call_update` terminal | `tool` (`tool_call_id` = the call, `tool_result_ok` = `status !== "failed"`, `tool_result_json` = the result text) |
103
+ | `hook_execution`, `turn_completed`, `session_recap`, `retry_state`, `task_*`, `auto_compact_*`, `compaction_checkpoint`, `plan`, `current_mode_update` | dropped, counted |
104
+ | anything else | dropped, counted |
105
+
106
+ ## Assembly: the rules, and why each one
107
+
108
+ **Runs.** Fragments are concatenated with no separator and no trimming: Grok
109
+ splits tokens *including the spaces between them* across rows, so
110
+ `"Read the"` + `" config"` is one record `"Read the config"`. Wake's replay
111
+ states the same rule and the same hazard — its comment on `append_message_chunk`
112
+ says the incremental fragments must not go through the generic content join or
113
+ "the space the model wrote at a token boundary gets eaten"
114
+ (`crates/wake-core/src/adapters/grok.rs#L198-L218`, `#L159-L196`). A run flushes
115
+ on a role change or when a tool call, a terminal tool result, or the end of the
116
+ stream interrupts it, which is Wake's `ensure`/`flush` pair (`#L159-L196`)
117
+ narrowed to *role* boundaries: Ledger keeps a tool call as its own record
118
+ (`assistant-tool-call`) and its result as a `tool` record, so the run cannot
119
+ straddle them. Lifecycle rows deliberately do *not* split a run, and that is
120
+ enough for the observed stream: every one of the three conversational local
121
+ sessions opens with a user chunk, and its user-chunk runs are exactly its turns
122
+ (1, 1, and 9), so a real turn boundary is a role change.
123
+
124
+ **Tool results.** `tool_call` projects the call (id, name, arguments) and
125
+ terminal `tool_call_update` projects the result, keyed by `toolCallId`;
126
+ `completed` → `ok: true`, `failed` → `ok: false`, and no other status produces a
127
+ record — an unknown status is counted instead of being guessed at. The
128
+ `in_progress` rows (66 of the 119 updates here) and the 26 status-less progress
129
+ rows carry partial or empty content and are superseded by the terminal row that
130
+ follows (Wake back-fills the call's output the same way, overwriting with the
131
+ final state: `#L226-L250`). The replay also remembers which call ids have
132
+ produced a result, because nothing proves ACP never re-emits a terminal row (a
133
+ retry after a failure is the obvious way it could): the *first* terminal row is
134
+ the call's result and every later one is counted, so one call never carries two
135
+ `tool` records and a later row cannot rewrite an outcome already stored. Ada's
136
+ round-1 finding — the replay originally emitted every terminal row — was fixed
137
+ here, with the regression below confirmed failing before the dedupe went in
138
+ (4 projected results for two calls instead of 2). A result's text is the joined
139
+ `content` blocks; when a row has no content block, its structured `rawOutput` is
140
+ serialized rather than lost.
141
+
142
+ **Identity.** Each projected event carries its update's own `eventId` as the
143
+ row id, so the canonical record's identity is `native`
144
+ (`stable_source_record_id` = the id), which is stable across appends and
145
+ re-reads. A row without an id falls back to a synthetic
146
+ `grok-line-<n>` — a line number in an append-only file never moves, whereas a
147
+ byte offset would shift when an earlier run grows. Sequence (`source_order_id`)
148
+ and per-record timestamps come from the row's position and
149
+ `_meta.agentTimestampMs`.
150
+
151
+ **Sidecar.** `summary.json` supplies the title (`generated_title`, then
152
+ `session_summary` — Wake's precedence, `grok.rs#L73-L110`), `cwd`
153
+ (`info.cwd`), model (`current_model_id`), branch (`head_branch`, absent in 3/5
154
+ local summaries), and the session's span (`created_at` … `updated_at`, which
155
+ contains the update stream's own bounds in all 5 local sessions: e.g.
156
+ 10:35:20.032Z…10:36:02.858Z around a stream of 10:35:31Z…10:36:02Z). The
157
+ sidecar is metadata only: a missing, oversized, or malformed one costs title,
158
+ cwd, model, and the envelope — never the transcript.
159
+
160
+ ## The projection lane, and the decision behind it
161
+
162
+ The pinned normalizer (`@letta-ai/trajectory` 0.3.0) has **no** Grok adapter:
163
+ `normalizeToCanonical({source: "grok", …})` fails with
164
+ `NormalizationError(unknown_source)`, and the adapter registry is a frozen
165
+ object, not an extension point. Ledger's established answer is the one it
166
+ already uses twice — project the native store into a contract the library
167
+ *documents*, and let the library do identity, hashing, bounds, and validation
168
+ (Cursor IDE stores → capture JSONL; OpenCode SQLite → export JSON).
169
+
170
+ | candidate lane | what it keeps | what it loses |
171
+ | --- | --- | --- |
172
+ | `atif` (the library's generic interchange format) | steps, prose, reasoning, tool calls, per-step timestamps, session + trajectory ids | **the terminal outcome**: ATIF observations carry `source_call_id` and content only, so `ok` is always `null` and a failed tool is indistinguishable from a successful one |
173
+ | `letta-code` (kind-tagged client rows) | per-row ids, `captured_at`, `resultOk` | calls and results share one row: a result that arrives in a later update than its call cannot be projected without duplicating the call id |
174
+ | `cursor` (role/message content blocks) | blocks, `is_error` → `ok`, native row ids | **timestamps**: the adapter reads none of them, so every record's time would be synthesized |
175
+ | **`copilot-cli` (event-per-line JSONL)** — taken | per-event `id` and `timestamp`, `assistant.message` with `reasoningText`/`content`/`toolRequests[]`, `tool.execution_complete` with `toolCallId` + `success`, `session.start` context (`sessionId`, `cwd`, `branch`), and unknown event types dropped with diagnostics | nothing this source carries |
176
+
177
+ The chosen contract is not a coincidence of taste: an ACP update stream and
178
+ Copilot CLI's event stream are the same shape — one notification per line, an
179
+ id per event, calls and results as separate events, and a boolean outcome on the
180
+ result — which is why the projection is nearly a rename rather than a rewrite.
181
+ `GROK_PROJECTION_SOURCE` in `native/grok.ts` is the single place the lane is
182
+ named, and `normalizer.ts` restores the records' provenance
183
+ (`record.source_type = "grok"`) right after normalization.
184
+
185
+ Recorded consequences, so nobody has to rediscover them:
186
+
187
+ - `records.source_type` is `grok` and `sessions.source` is `grok`; both match
188
+ the store's provenance axis. `ledgerId` already prefixes record ids with the
189
+ ledger source, so identity was never the lane's.
190
+ - The single `meta` record's `record_json` names the *projection lane*
191
+ (`"source":"copilot-cli"`), because that field is the library's record of the
192
+ transcript it was handed, and both `record_hash` (`sha256(record_json)`) and
193
+ the meta `content_hash` are computed over it. Ledger does not rewrite
194
+ library-owned hashes; the note is the disclosure.
195
+ - The session-level `model` comes from the sidecar (the `meta` record carries
196
+ cwd/branch only, since the projection's `session.start` has no model field).
197
+ - Tool results are subject to the library's default bounds (2,500 characters,
198
+ head-tail); the real-corpus pass recorded `tool_result_truncated` on 15
199
+ records, the same policy every other source gets.
200
+ - **A projected source's group is authoritative.** The reader stamps the
201
+ listing's own id on every group carrier the projection can emit
202
+ (`session.start`, `hook.start`), but an agent-only session with a missing or
203
+ torn sidecar emits neither, and the library would then fall back to its
204
+ `default` sentinel: every such session would take the ledger id
205
+ `grok:default`, and the next one would replace the previous one's records
206
+ through the store's same-id path — permanently, because that session's cursor
207
+ still matches its unchanged fingerprint. `normalizeDiscoveredSession` therefore
208
+ passes the group as authoritative for projected sources
209
+ (`options.groupId !== undefined || projected`), which is where the
210
+ projected-source set lives. The reader's own rows always agree with it, so
211
+ `source_group_conflict` cannot fire; non-projected sources are byte-identical
212
+ to before. This was found by the cycle's adversarial review, which reproduced
213
+ the loss with two sidecar-less agent-only sessions, and the regression below
214
+ was confirmed failing with the flag removed (`grok:default` present) before
215
+ being kept.
216
+
217
+ ## Parentage: the edge lives under the parent
218
+
219
+ `<parent-session-dir>/subagents/<child-id>/meta.json`:
220
+ `{"parent_session_id", "child_session_id", "subagent_id", "created_at"}`.
221
+ Wake's loader is the pinned contract (`grok_group.rs#L179-L242`): the parent is
222
+ `parent_session_id` when non-empty and the enclosing session directory otherwise
223
+ (`#L218-L222`), *both* `child_session_id` and `subagent_id` name the child
224
+ (`#L227-L237`), and the first registration wins (`#L235`). Ledger adds a
225
+ self-edge drop (a session is never its own parent) and keeps Wake's chain
226
+ walking out of scope on purpose: the edge recorded is the **direct** edge, not
227
+ the root parent — flattening is a reporting decision, and flattening the *cwd*
228
+ (Wake's `canonical_project`) is not implemented at all: each session keeps the
229
+ cwd it ran in.
230
+
231
+ The lookup is a *reverse* one — the child's own directory says nothing about its
232
+ parent — so someone has to walk the store. Ledger reads the registries **in the
233
+ scanner**, which is already walking `<root>/<cwd>/<session>/`:
234
+ `discoverGrokSessions` collects `child → parent` while it lists, and attaches the
235
+ edge to the child's listing as `native.parentSessionId`. That buys two things a
236
+ per-session read cannot: the edge is fresh on every pass (a registry written
237
+ between two syncs is visible to the next scan), and ingest pays no extra I/O —
238
+ no per-session walk, no process-lifetime cache that could go stale. The
239
+ extractor (`parentage.ts`, `case "grok"`) consumes the edge, and keeps the
240
+ `…/subagents/<child>/…` path shape as its second piece of evidence for a
241
+ listing that arrives without one. The edge participates in the session's native
242
+ fingerprint, so learning (or losing) a parent re-ingests exactly that session.
243
+
244
+ ## Verification
245
+
246
+ **Suite.** `packages/core/tests/native-grok.test.ts` — 13 cases:
247
+
248
+ | case | what it pins |
249
+ | --- | --- |
250
+ | discovery | the documented root, newest-first order, sidecar title/`updated_at`, `native.kind`, the `listable` outcome and the `missing_root` outcome for an absent root |
251
+ | dedup/symlink/page | a symlinked tree is not listed, an id is listed once, `--limit` bounds the page |
252
+ | single turn | the exact projected transcript, the run concatenation (no separators), reasoning vs assistant records, `source_type = grok`, the sidecar envelope, and the lifecycle diagnostic with its count |
253
+ | tools | call/result pairing by id, `tool_result_ok` 1 and 0, `tool_arguments_json`, and the superseded-progress count |
254
+ | repeated terminals | `[completed, completed]` projects one result and counts the repeat; `[failed, completed]` keeps the first terminal (`success = false`) even when the later row contradicts it |
255
+ | resilience | `retry_state`/`task_backgrounded` counted, an unknown kind counted, a torn line counted, the transcript still complete |
256
+ | sidecar | no sidecar → stream-bounded span + first-user title; malformed sidecar → diagnostic, session still readable |
257
+ | lifecycle-only | a session whose updates are all lifecycle rows projects no conversation rows |
258
+ | parentage | explicit parent, `subagent_id`-only fallback to the enclosing directory, self-edge dropped, edges landing on both sessions' rows |
259
+ | registry-rooted listing | a child laid out under its parent's registry is not listed by the whole-store walk, and is listed — with the parent taken from the path shape — when the scan is rooted at the registry |
260
+ | identity/stability | unchanged re-read is `unchanged`; an appended turn adds records without moving or rewriting stored ones; sidecar-only change is `deduped` with stable record ids |
261
+ | group identity | two agent-only sidecar-less sessions (plus one with a torn sidecar) keep distinct `grok:<uuid>` ids with both record sets intact, and no `grok:default` session exists |
262
+ | durable skips | the reader's diagnostics are stored in `ingest_diagnostics` |
263
+
264
+ The default-scan tests in `cli-canonical.test.ts` were extended with `grok`
265
+ (source list, seeded root, and the "no root at all" set): the source is part of
266
+ the bare `scan`/`sync` contract, so its absence is reported where the other
267
+ fourteen are.
268
+
269
+ **Real corpus.** Read-only `~/.grok`, scratch store `/tmp/c9-grok-scratch`
270
+ (never the production store; `~/.grok` untouched):
271
+
272
+ | measurement | value |
273
+ | --- | --- |
274
+ | discovery | 5 sessions from `~/.grok/sessions` (3 cwd folders) |
275
+ | first `build --sources grok --embedding none` | 5 scanned / 5 imported / 0 failed / 0 tombstoned, 103 records, wall ≈ 0.12 s |
276
+ | second pass (same store) | 5 scanned / 0 imported / 5 deduped / 0 new records — idempotent |
277
+ | records | `assistant-tool-call` 27, `tool` 27, `reasoning` 19, `assistant` 14, `user` 11, `meta` 5 |
278
+ | `source_type` | 103/103 `grok` |
279
+ | failed tools preserved | 2 (`tool_result_ok = 0`) in the largest session |
280
+ | parent edges | 0 (no `subagents/` registry exists on this host — the path is covered synthetically) |
281
+ | diagnostics | `noise_record_dropped` 128 occurrences over 5 sessions (lifecycle + superseded progress + unknown kinds); `tool_result_truncated` 15 (library default bounds) |
282
+ | coverage | `grok` / `/Users/mac/.grok/sessions` / `listable` / discovered 5 |
283
+ | search | 103 FTS rows; `read_file` 3 hits, `cache` 4 hits |
284
+ | sessions with no conversation | 2 sessions hold a single `hook_execution` row (378 bytes) and normalize to a metadata-only record set, with the session's own span from the sidecar |
285
+
286
+ ## Decisions
287
+
288
+ 1. **A borrowed lane, named in one place.** No wrapped or forked copy of the
289
+ library, no private canonical-record builder: Grok's records go through the
290
+ same identity, hashing, bounds, and validation path as every other source,
291
+ and the divergence (which lane) is one exported constant plus one restore
292
+ line. Building canonical records in Ledger was rejected as a second
293
+ convention: it would duplicate `buildCanonicalRecords`' identity rules and
294
+ drift from them silently.
295
+ 2. **`copilot-cli` rather than `atif`.** The library's generic interchange
296
+ format would have been the tidy choice, and it cannot express a failed tool
297
+ result. A stored `tool_result_ok = 0` is worth more than a tidy lane name,
298
+ and the lane is documented rather than implied.
299
+ 3. **Terminal-only results.** Projecting an `in_progress` row would invent a
300
+ result the run had not produced, and would leave a stale record behind when
301
+ the terminal row replaced it (`ok` would flip in place), so progress rows are
302
+ counted, not projected.
303
+ 4. **Native identity from `eventId`.** The update id is the only per-record
304
+ native identity Grok writes; using it makes appends idempotent without a
305
+ content digest, and the line-number fallback is append-stable by
306
+ construction.
307
+ 5. **Registry reads at scan time.** One walk, already paid for, beats a
308
+ per-session walk (`O(sessions²)` on a large store) or a process-lifetime
309
+ cache (a registry written mid-process would be invisible until restart).
310
+ The edge rides in `native_json`, so `ingest_queue`'s existing "native
311
+ metadata changed → reset retry state" rule re-ingests just the child.
312
+ 6. **Sidecar as envelope, stream as records.** `started_at`/`ended_at` come from
313
+ the sidecar when it exists: it is Grok's own record of the session's span, it
314
+ is never narrower than the stream, and it is the only span a hook-only
315
+ session has. Per-record times stay the stream's.
316
+ 7. **Skips are diagnostics, not silence.** The reader's counts land in the same
317
+ `ingest_diagnostics` rows as the library's diagnostics, with the dropped kind
318
+ names in the bounded sample, so "what did we not read?" is answerable from
319
+ the store.
320
+ 8. **The scan probes `~/.grok/sessions` as a directory root**, like the other
321
+ native sources: an absent root is `missing_root` (counted, not listed), an
322
+ unreadable one is `unreadable_root`, and a healthy one makes the coverage row
323
+ real.
324
+
325
+ ## Ported mechanics (Wake MIT, `iAmCorey/Wake@d9f702b6df0e85763f8ab597c09bf4ca6a168db4` v0.6.5 — mechanics with attribution, no code copied)
326
+
327
+ | Ledger | Wake pin | What was kept |
328
+ | --- | --- | --- |
329
+ | chunk runs concatenated with no separator, flushed on role change / tool activity | `crates/wake-core/src/adapters/grok.rs#L159-L196`, `#L198-L218` | the rule and its reason (token fragments split at spaces), narrowed to role boundaries because Ledger keeps calls and results as records |
330
+ | tool updates back-fill the call, terminal state wins, `status: "failed"` is the error | `grok.rs#L226-L250` | "the terminal update supersedes the partial ones"; Ledger stores the outcome as `tool_result_ok` rather than folding the output into the call |
331
+ | known lifecycle kinds are named, everything else is counted, not guessed | `grok.rs#L323-L335` | the closed kind list and the counter; Ledger reports the count and the names as a diagnostic |
332
+ | summary sidecar: `generated_title` then `session_summary`, `info.cwd`, `current_model_id`, `head_branch` | `grok.rs#L73-L110` | the precedence and the field choices |
333
+ | parent from `parent_session_id`, else the enclosing session directory; both `child_session_id` and `subagent_id` name the child; first registration wins | `grok_group.rs#L179-L242` (`#L218-L222`, `#L227-L237`) | the whole fallback rule, plus a self-edge drop |
334
+ | the child's registry entry is read from the parent's side, once, for the whole store | `grok_group.rs#L179-L186` | the direction of the read and its one-walk cost; Ledger moves the walk into the scan it already runs |
335
+
336
+ **Adapted:** Wake's transcript message keeps thinking and tool calls *inside*
337
+ one assistant message; Ledger's canonical contract has separate `reasoning`,
338
+ `assistant-tool-call`, and `tool` record types, so the replay is the same state
339
+ machine with different grains. Wake's group context also resolves a canonical
340
+ *project* (percent-decoding the cwd folder, `grok_group.rs#L151-L178`,
341
+ `#L265-L283`) and flattens a nested subagent to its root parent
342
+ (`#L244-L263`); Ledger records the direct edge and never rewrites the session's
343
+ cwd.
344
+
345
+ **Not ported:** worktree registry/`worktrees.db` (`grok_worktree_slug`,
346
+ `is_ephemeral_worktree`) and `relocations` handling, image attachment decoding
347
+ (`apply_tool_update`'s `decode_images` path — Ledger's projection carries text
348
+ only), and `chat_history.jsonl` reading (a compacted snapshot; `updates.jsonl`
349
+ is the source of truth).
350
+
351
+ ## Boundaries
352
+
353
+ - **The upstream adapter is still missing.** `@letta-ai/trajectory` 0.3.0 lists
354
+ fourteen sources and not `grok`; a future adapter would make this projection
355
+ unnecessary, and its records would not be hash-compatible with these. Cycle
356
+ 9's "first-class" is Ledger-side: discovery, assembly, coverage, parentage,
357
+ identity.
358
+ - **No cwd flattening** — a session's `cwd` is the directory it ran in, and a
359
+ worktree session keeps its worktree path.
360
+ - **Discovery does not descend into a session directory.** Its other
361
+ subdirectories hold a session's own artifacts (`terminal/*.log`, `recap_requests/`),
362
+ and the child of a subagent is expected in its own cwd folder, which is the
363
+ layout the format spec and Wake's loader assume.
364
+ - **Two local sessions are metadata-only** (a single `hook_execution` row each,
365
+ 378 bytes): they are listed, with a sidecar-derived span, and the library's
366
+ partial-transcript path admits their meta record. Not special-cased.
367
+ - **Unknown kinds are counted, not mapped.** A future Grok kind is a diagnostic
368
+ row until someone maps it here.
369
+ - **A result whose call never appeared** is dropped by the library itself
370
+ (`orphan_tool_result`), and a call whose updates never reached a terminal
371
+ state stays a call with no result: Ledger adds no second mechanism for either.
372
+ - **`auto_compact_*`, `retry_state`, and `turn_completed` are dropped**
373
+ including their token/usage payloads: usage accounting is a different feature
374
+ with its own consumer, not a transcript record.
375
+ - **Embeddings were not exercised for this source** (`--embedding none` in the
376
+ measured pass); records are ingest-complete without them.
377
+ - **The append path is not used.** Grok's `updates.jsonl` is incrementally
378
+ appendable, but the run accumulator needs the whole stream (a run can span any
379
+ chunk boundary), so a changed session is re-read whole under its snapshot
380
+ fingerprint — the same gradient OpenCode and Cursor native sources use.
@@ -0,0 +1,38 @@
1
+ # Wake port program (2026-09-16)
2
+
3
+ Ten bounded cycles porting patterns from `iAmCorey/Wake@d9f702b6` (v0.6.5,
4
+ MIT; evidence in [confer/2026-09-14-wake/wake.md](../../confer/2026-09-14-wake/wake.md))
5
+ plus two new harness banks. Each cycle carries: the observed Ledger failure it
6
+ fixes (or the measured trade it buys), the pinned upstream mechanism, and the
7
+ open-source positioning case. Order encodes dependencies: the derive-rule
8
+ counter (C2) precedes the index-shape changes that need a one-shot re-derivation
9
+ (C3, C4, C5).
10
+
11
+ Protocol per cycle (owner-approved 2026-09-16, rewound from the 2026-09-12
12
+ loop command via our own ledger): subagents handle explore → implement →
13
+ adversarial review → improve → independent verify; the parent owns architecture,
14
+ design, final review and git. Each increment opens a PR immediately, monitors
15
+ cloud review within 10–15 min, and triages **two rounds** — fix the valid,
16
+ explicitly defer the rest. Merge history-preserving at LGTM or only-lows,
17
+ fast-forward main (that IS the CLI rebuild), file the cycle's follow-up issue
18
+ for remaining nits, then continue. One consolidated follow-up issue closes the
19
+ program if nits remain at the end. `codegraph` before grep; Wake's MIT source
20
+ at `/tmp/ledger-confer/wake` is the authority for ported mechanisms; Gemini
21
+ embeddings (`--embedding gemini`) only where a cycle genuinely needs vectors.
22
+
23
+ | # | Cycle | Fixes / buys (concrete case) | Open-source case |
24
+ | --- | --- | --- | --- |
25
+ | C1 | Copy-fallback read-only SQLite (probe → copy `db`+`-wal`+`-shm` → read copy; never `immutable=1`) + `db_cache_stamp = max(mtime(db), mtime(-wal))` invalidation | A store held by its own CLI costs a coverage row today (`read_error`, `native/common.ts`). Wake's gradient converts locked stores to working reads | "Works on stores while the agent is using them" — first-run experience on any live machine; the #1 operational complaint class for tools like ours |
26
+ | C2 | Derive-rule counter (index-format version in `schema_meta`, distinct from `SCHEMA_VERSION`; mismatch → one full re-derivation) | Index-shape changes currently must ride schema migrations; re-derivation has no first-class switch | Lets contributors evolve the index without migration ceremonies — the difference between "rebuild your DB" and "it re-derives once on next scan" |
27
+ | C3 | Self-referential echo filter (exclude Ledger/MCP-lookup transcripts from the index; C2 bumps force re-derivation) | We index the transcripts of the agents that call `ledger search` — your own queries come back as hits. Unnamed bug, observed daily | Self-hosting agents are our primary users; a store that echoes itself reads as broken. Wake ships the same filter (`wake_*` tools, `FTS_FORMAT` counter) |
28
+ | C4 | Title index (`titles_fts` analog, trigram-free first pass), title hits rank above body, `seq=0`, no fabricated seq | Titles render in results but are unsearchable (`records_fts` has no title column) | Session-find-by-name is the human's first query; every session browser ships it |
29
+ | C5 | Trigram tokenizer decision + swap (with whole-query `LIKE` degradation, surfaced in the envelope) | #34 common-prefix floor: mid-token substrings and CJK are unmatchable under `porter unicode61` | Code-identifier search is the agent's query shape (`useEffect(`); Wake proves it works in SQLite FTS5 alone — **CLOSED 2026-09-16: measured, refused** (all binding bars failed — +52.5% store, 54 s re-derivation, 190–200 ms common substrings; and trigram does not fix #34, whose floor is match-set-bound: see the [C5 note](2026-09-16-c5-trigram.md) for measurements, the kept design contract, and reopen conditions) |
30
+ | C6 | Recency prior before RRF (bm25 × time-decay, arithmetic-only) | All lanes order by relevance with `started_at DESC` tiebreak only; "the session from today" loses to an old strong match | Cheap relevance win every user feels; no extension dependency, single ORDER BY term |
31
+ | C7 | Tombstone double-track (path + logical key, same transaction as removal) | Deletion today is undefined; source truncation is not authorization to erase, and user deletion must survive re-scans | Privacy posture: explicit delete semantics before anyone trusts us with their history; Wake's tombstones + system Trash is the reference UX |
32
+ | C8 | Remote truthfulness primitives (unreachable ≠ empty, partial ≠ complete, per-path probe, exit-24 handling, disposable cache lifecycle) | #14/ADR-0005 blocked on the decision, but the failure-semantics vocabulary has no owner; port it as tested primitives without shipping a transport — **SHIPPED 2026-09-16: `packages/core/src/sync-truth.ts` + 25 test cases, no transport, no schema version spent** (see the [C8 note](2026-09-16-c8-remote-truth.md)) | Every multi-machine user hits this; honest sync failure is a moat vs "it silently showed nothing" — Wake's `remote.rs` is the pinned authority |
33
+ | C9 | Grok first-class (discovery, assembly, canonical records via the Trajectory shape, registry-backed parentage, coverage row) | #51: owner-approved first-class bank; `~/.grok` carries real data on this host; its subagent registry is source-provable parentage (rare) — **SHIPPED 2026-09-16: `packages/core/src/native/grok.ts` + scan-time registry edges + 13 test cases; 5 real sessions / 103 records ingested read-only, 0 real edges, 2 failed results preserved (see the [C9 note](2026-09-16-c9-grok.md))** | Breadth is the obvious open-source pitch; parentage from a second registry source proves the model generalizes |
34
+ | C10 | Kimi (#51; Claude-shaped transcripts, `parent_tool_use_id`) | Owner-approved bank; needs fixture sourcing before implementation — **SHIPPED 2026-09-16: `packages/core/src/native/kimi.ts` + streamed-argument assembly + path/tool-call-bound child edges + 15 test cases; 8 real sessions (3 top-level + 5 subagents) / 806 records ingested read-only into a scratch store, 5 real parent edges all tool-call-bound, 1 failed result preserved (see the [C10 note](2026-09-16-c10-kimi.md))** | Same; plus Trajectory-format conformance for another harness — production first ingest still gated on #69 |
35
+
36
+ Out of scope per owner: Qoder, Antigravity (encrypted — structurally impossible), WorkBuddy; DSH deferred behind #49. Never port: whole-session replace without retained generations, stats-only dedup, in-process scan lock, dropped diagnostics counters.
37
+
38
+ Order note: C1 is independent and mechanical — first. C2 must precede C3/C4/C5 (they change index shape). C5 is the largest and touches the same DDL as C4 — sequence C4 then C5, with C5's decision round explicit (index size vs substring coverage, measured). C6 after C5 (ranking experiments on the final tokenizer). C7/C8 are boundary work, independent. C9/C10 last so new banks land on the finished index.
@@ -0,0 +1,157 @@
1
+ # Cursor Blob-Store Chats
2
+
3
+ Audited **2026-09-17** against a local corpus of **589 `store.db` files, 492 MB**
4
+ under `~/.cursor/chats/`, written by Cursor between 2026-05-25 and 2026-09-17.
5
+ Every claim below was measured across all 589 files, not sampled. Nothing in the
6
+ corpus was modified; the reader opens these databases read-only.
7
+
8
+ ## Why this was unsupported
9
+
10
+ `discoverCursorSessions` previously walked to `store.db`, emitted
11
+ `incomplete_evidence` with "Cursor store.db schema is unverified and unsupported",
12
+ and moved on. Cursor's older `state.vscdb` composer store was supported, so the
13
+ ledger held 609 Cursor sessions while 589 newer agent chats were skipped
14
+ entirely — roughly a quarter of the local Cursor corpus.
15
+
16
+ ## Layout
17
+
18
+ ```
19
+ ~/.cursor/chats/<workspaceHash>/<agentId>/store.db
20
+ meta (key TEXT PRIMARY KEY, value TEXT) -- one row, key '0'
21
+ blobs (id TEXT PRIMARY KEY, data BLOB) -- id is the content's SHA-256, hex
22
+ ```
23
+
24
+ `meta.value` is **hex-encoded ASCII JSON**, not raw bytes — the only field in the
25
+ format carrying that extra layer. Decoded:
26
+
27
+ ```json
28
+ { "agentId": "<uuid>", "latestRootBlobId": "<64 hex>", "name": "New Agent",
29
+ "mode": "default", "isRunEverything": true, "createdAt": 1779717061154 }
30
+ ```
31
+
32
+ `blobs.data` is raw: message blobs are UTF-8 JSON, the root blob is binary.
33
+
34
+ ## Root blob: protobuf
35
+
36
+ `latestRootBlobId` names a protobuf-encoded blob. Observed fields:
37
+
38
+ | field | wire | meaning |
39
+ | --- | --- | --- |
40
+ | 1 | 2 (32 bytes) | repeated, **ordered** message blob IDs |
41
+ | 5 | 2 | nested record containing `system_prompt` |
42
+ | 8 | 2 (32 bytes) | a further blob reference |
43
+ | 9 | 2 | workspace URI (`file:///…`) — the session's `cwd` |
44
+ | 18 | 2 | repeated filesystem paths (open/context files) |
45
+ | 10, 21, 22, 26, 27 | 0/2 | not interpreted |
46
+
47
+ The reader takes fields 1 and 9 and skips everything else **by wire type**, so a
48
+ field Cursor adds later cannot break the walk.
49
+
50
+ A naive scan for the `0A 20` byte pair instead of real varint parsing looks
51
+ plausible and is wrong: it matched inside other fields, inflating 15,860 real
52
+ message IDs to 16,202 and inventing 16 "missing" blobs and 176 "non-JSON" ones.
53
+ Parsed correctly the corpus is clean — **15,860 messages, zero missing, zero
54
+ non-JSON, zero missing roles** — and no root repeats an ID, so the content hash
55
+ is a stable per-record identity.
56
+
57
+ ## Messages
58
+
59
+ Each message blob is `{ "role", "content", … }` where `content` is a string or a
60
+ block list. Roles across the corpus: `tool` 9,388 · `assistant` 4,690 ·
61
+ `user` 1,193 · `system` 589 (one per session).
62
+
63
+ Block names differ from the capture format `@letta-ai/trajectory`'s cursor
64
+ adapter decodes, which is the substance of the translation:
65
+
66
+ | Cursor block | canonical block |
67
+ | --- | --- |
68
+ | `text {text}` | `text {text}` (unchanged) |
69
+ | `reasoning {text, signature}` | `thinking {thinking}` |
70
+ | `tool-call {toolCallId, toolName, args}` | `tool_use {id, name, input}` |
71
+ | `tool-result {toolCallId, result, experimental_content}` | `tool_result {tool_use_id, content}` |
72
+ | `redacted-reasoning {data}` | none — see below |
73
+
74
+ `providerOptions.cursor.modelName` carries the model (`composer-2.5-fast`,
75
+ `cursor-grok-4.5-high-fast`), which the adapter reads from `message.model`.
76
+
77
+ A tool-call ID is **not** a path and is not validated with `safeNativeId`: 292
78
+ IDs in the corpus contain a separator that check rejects.
79
+
80
+ ## Deliberate losses
81
+
82
+ Two kinds of record cannot be carried, and both are handed to the adapter
83
+ unchanged so its own `noise_record_dropped` diagnostic records the drop rather
84
+ than this reader hiding it:
85
+
86
+ - **`system` rows** (589, one per session). The upstream cursor adapter accepts
87
+ only `user` and `assistant` rows. Carrying the system prompt would require a
88
+ change in `@letta-ai/trajectory`, not here.
89
+ - **`redacted-reasoning` blocks.** The payload is provider-encrypted; there is no
90
+ plaintext to hand on, and inventing one would fabricate trajectory content.
91
+
92
+ `tool` rows *are* carried, remapped to the `user` role, because the canonical
93
+ shape keeps a tool result on the turn that carries it. Without that remap the
94
+ adapter would drop 9,388 records — 59% of the corpus.
95
+
96
+ ## Measured result
97
+
98
+ Against the full local corpus, after the change:
99
+
100
+ - 589/589 discovered, 589/589 read
101
+ - **22,274 canonical records** from 588 sessions
102
+ - `cwd` on 589, title on 589, model on 577
103
+ - one session yields no records: it has no assistant turn, an empty draft
104
+ - normalization diagnostics: `noise_record_dropped` 5,225 (the deliberate losses
105
+ above), `tool_result_truncated` 3,629, `timestamps_synthesized` 588
106
+
107
+ Timestamps are synthesized for every session because message blobs carry no
108
+ per-message time; only `meta.createdAt` exists, which the reader maps to
109
+ `startedAt`.
110
+
111
+ ## ACP sessions (issue #70)
112
+
113
+ Cursor's ACP layout is a different path with the same file name:
114
+ `~/.cursor/acp-sessions/<id>/store.db`, beside a `meta.json`. Measured
115
+ **2026-09-17 on host `charles`** (Cursor CLI ACP, Linux), read-only over SSH:
116
+ **171 session directories, 30 with a `store.db`, 1.4 GB total**. The same host
117
+ also holds 80 `chats/` stores.
118
+
119
+ The format is the same as `chats/` in every respect that matters:
120
+
121
+ - schema is `blobs(id TEXT PRIMARY KEY, data BLOB)` + `meta(key TEXT PRIMARY KEY, value TEXT)`
122
+ - `meta.value` is hex-encoded JSON in 28 of 28 readable rows
123
+ - the root blob is the same protobuf: field 1 the ordered message IDs, field 9 the
124
+ workspace URI, present on all 30
125
+ - the same block vocabulary: `tool-call`, `tool-result`, `reasoning`, `text`,
126
+ `redacted-reasoning`
127
+
128
+ Across all 30 stores — 2,451 messages — there are **no missing blobs, no non-JSON
129
+ message blobs, no roots with trailing bytes, no field-1 value that is not 32
130
+ bytes, and no tool call or result missing its ID or name.** None of the conditions
131
+ that make the reader throw occur, so the `chats/` implementation reads this layout
132
+ unchanged, with no ACP-specific code.
133
+
134
+ Two differences worth recording:
135
+
136
+ - **ACP meta carries `blobEncryptionKey`**, which `chats/` meta does not. This is
137
+ the credential #70 warns about. The reader's allowlist — `agentId`,
138
+ `latestRootBlobId`, `name`, `createdAt` — already excludes it, and a fixture
139
+ with a planted key confirms nothing from meta reaches the transcript or title.
140
+ - **ACP also keeps `cwd` and `title` in the sibling `meta.json`** (141 dirs have
141
+ `{cwd, schemaVersion}`, the 30 with a store add `title`). The reader does not
142
+ need it: `cwd` comes from root field 9 and the title from `meta.name`, both
143
+ present on all 30.
144
+
145
+ ### A size worry that does not hold
146
+
147
+ #70 reports a live session at ~619 MB and infers a reader needs to cope with that
148
+ volume; it has since grown to 705 MB. Measured, that database contains **185
149
+ messages totalling 1.0 MB**. The remaining ~704 MB is blobs the current root does
150
+ not reference — superseded roots and out-of-line content the store keeps.
151
+
152
+ Because the reader walks the root's message list rather than the file, the
153
+ budget that matters is the referenced bytes, and **no session of the 30 comes near
154
+ `MAX_NATIVE_BYTES`**: the largest referenced total is 5.0 MB, against a 64 MB
155
+ limit. An earlier draft of this note claimed the 705 MB session would be refused
156
+ on the byte limit. That was wrong, and it was wrong because it reasoned from file
157
+ size instead of measuring the referenced set.