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