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,305 @@
1
+ # C3: the self-referential echo filter, 2026-09-16
2
+
3
+ ## Outcome
4
+
5
+ An agent's own lookups of Ledger no longer come back as search hits. The
6
+ transcripts of `ledger search` / `status` / `sync` / `scan` / `build` / `watch`
7
+ and of the Ledger MCP tools are now **canonical but unsearchable**: `records`
8
+ keeps every row exactly as the adapter wrote it, `records_fts` never receives
9
+ them, and the store reports how many it keeps out (`ledger status`:
10
+ `Echo filtered: 188 records (agent lookups of this store; never searchable)`;
11
+ robot: `echo_filtered: 188`). The exclusion is one pure rule over three canonical
12
+ fields — `record_type`, `tool_name`, `tool_arguments_json` — so ingest, the
13
+ index re-derivation, the parity gate, and the status verb classify a record
14
+ identically, and the rule change rides the C2 derive counter as `FTS_FORMAT`
15
+ `"1"` → `"2"`: every store written before the filter re-derives itself once, on
16
+ its next writer pass.
17
+
18
+ On a byte-consistent copy of the production store (1,121,468,416 bytes; 227,474
19
+ records; 2,604 sessions) the whole self-echo footprint is **188 records** — 94
20
+ `assistant-tool-call`s and their 94 paired `tool` results, across 14 sessions
21
+ (this morning's scout measured 40; the store grew fourfold while the cycles
22
+ ran). The re-derivation costs **10.0–10.4 s** (whole-table re-projection
23
+ 5.76 s · rule pass 0.37 s · derived-set insert 1 ms · index subtraction 0.16 s ·
24
+ parity + FTS5 integrity gates 2.82 s), parity is exact
25
+ (`227,474 − 188 = 227,286`), and a query whose rank-1 hit was the agent's own
26
+ previous identical search now returns only genuine records — with the genuine
27
+ hits intact. This repository's own 21 development sessions (9,176 records)
28
+ survive intact: every kept record is in the index (10/10 sampled by exact
29
+ `record_id`), every one of their 146 lookups is not (0/10 sampled), and
30
+ per-session `indexed = records − excluded` holds for all 21.
31
+
32
+ Measurements are timestamped by fixture: 40 at the morning scout, 188 on this
33
+ cycle's copy, 192 on the independent verifier's later copy — the store grows
34
+ while the cycles run; the rule's zero-false-positive property, not the count,
35
+ is the invariant.
36
+
37
+ The tree's own state proved the upgrade path once for real before this audit: the
38
+ production store already carried `fts_format = 2` and a `records_echo` of 188,
39
+ and re-running the rule against those records reproduced that set **exactly**
40
+ (188 stored, 188 recomputed, 0 on either side of the difference) — the shipped
41
+ rule is what derived the real store.
42
+
43
+ ## The rule
44
+
45
+ A record is a **lookup** (and so excluded from `records_fts`) when it is an
46
+ `assistant-tool-call` that either
47
+
48
+ - **names a Ledger MCP tool**: the name *ends* with a Ledger tool name at a word
49
+ boundary (`ledger_search`, `ledger_recent`, …; client prefixes vary:
50
+ `mcp__ledger__ledger_search`, `mcp_ledger_ledger_recent`, bare
51
+ `ledger_search`; `awake_search` and `xledger_search` do not match), or carries
52
+ the Claude Code server segment `mcp__ledger__` — the server's own namespace,
53
+ whatever the tool is called; or
54
+ - **runs a Ledger binary in command position** in a shell tool's arguments: the
55
+ basename `ledger`, `ledger-core`, or `traj-bridge` (extension `.ts`/`.js`/
56
+ `.exe`, any path form) as the first word of a command, or the word after a
57
+ separator (`&&`, `||`, `;`, `|`, `&`, newline), a command substitution
58
+ (`$(`, backtick), a prefix word (`sudo`, `env`, `time`, `nohup`, `nice`,
59
+ `exec`, `command`, `then`, `do`, …), or a runner (`bun run ./bin/ledger.ts`).
60
+
61
+ A `tool` record is excluded only through its **call**: `tool_call_id` paired to
62
+ an excluded call in the same session — never a guess from the result text.
63
+
64
+ Everything else is kept, deliberately: user and assistant dialogue (however much
65
+ it talks about Ledger), `Read`/`Edit`/`Grep`/`Write` calls, a `git commit -m
66
+ "…ledger…"`, a `cd` into this checkout, `bun test`, `cat ledger.ts`, `grep
67
+ ledger`, a quoted alternation (`rg -n "ard|ledger|review"`), a heredoc body that
68
+ begins `ledger search …` (a document, not a run), and any command whose
69
+ `ledger` is an argument. Reads and writes are treated alike (`status`, `sync`,
70
+ `scan`, `build`, `watch`, `--help`, `--version` all count): an agent dumping the
71
+ store's own counters and cursors into the corpus pollutes it exactly as a search
72
+ does. Quoting is lexical — a separator inside quotes is text — and heredoc
73
+ bodies are removed before segmentation.
74
+
75
+ Two shapes are knowingly not seen, and both err toward indexing: a Ledger
76
+ invocation from *inside* a script (a `subprocess.run(['ledger', …])` in a python
77
+ heredoc — measured: **3** of 4,728 declined candidates), and a runner this rule
78
+ does not model (`node`, `npx`). They cost a noisy hit, never a hidden record.
79
+ Wake makes the same trade with a preview fallback (`command_names_binary`) this
80
+ port declines: when a shell tool carries no structured arguments there is
81
+ nothing here to read, so the call stays indexed — measurably free, since **0**
82
+ shell-tool calls in the store carry null arguments.
83
+
84
+ ## Files
85
+
86
+ - `packages/core/src/filter.ts` (new) — the rule, and the three forms the store
87
+ uses it in: `isEchoCall` (pure), `echoExcludedIds` (one streaming pass over
88
+ `records`, used by the re-derivation, the parity gate, and the status count),
89
+ `echoExcludedBatchIds` (an ingest batch, resolving a result whose call sits in
90
+ an earlier batch through the same rule, and returning the call ids the writer
91
+ needs to repair a result committed before its call).
92
+ - `packages/core/src/schema.ts` — `FTS_FORMAT = "2"`; the `records_echo` DDL
93
+ (derived set, both its columns cascading on delete); `DERIVE_COUNTERS` grows an
94
+ `outputs` list so a counter that *matches* but whose output table is missing
95
+ still re-derives.
96
+ - `packages/core/src/db.ts` — open-time counter detection against the tables the
97
+ store already held (before the DDL creates the ones this build needs);
98
+ `insertRecordsInternal` writes canonical rows first, then consults
99
+ `echoExcludedBatchIds`, skipping the FTS row and writing the id into
100
+ `records_echo` — and taking back any already-committed result that pairs with
101
+ a call the batch just classified (index row deleted, id marked), so arrival
102
+ order cannot decide what a pair derives to; `rebuildRecordsFts` re-projects the whole index, recomputes the
103
+ rule set, rewrites `records_echo`, subtracts exactly those rows, and runs the
104
+ gates; `verifyRecordsFtsRebuild` checks `records − excluded == indexed` plus
105
+ “no excluded id without its record”.
106
+ - `packages/core/src/cli.ts` — `build` takes the pending re-derivation before its
107
+ import loop (and reports the wall time); `status` publishes `echo_filtered`
108
+ in both modes.
109
+ - `packages/core/src/watch.ts` — a drain takes the pending re-derivation under
110
+ the lease it already holds, before its batch, reporting the wall on stderr.
111
+ - `packages/core/src/store-status.ts` — the status field, published only when
112
+ the derivation under this build's rules has completed (see the gap below).
113
+ - `packages/core/tests/echo-filter.test.ts` (new) — the rule's positive and
114
+ negative table (15 lookups / 17 not-lookups), ingest
115
+ (canonical completeness + index exclusion + status count), the C2 bump
116
+ (flag → one-shot → parity → exactly once, including the status count's `null`
117
+ while the derivation is pending), and the counter-without-output state.
118
+ - Re-pinned to the new contract: `cli-canonical.test.ts` (`echo_filtered`),
119
+ `derive-counters.test.ts` (the refusal message now formats `FTS_FORMAT`),
120
+ `integration-migrations.test.ts` (the parity message names the excluded term).
121
+
122
+ ## Ported mechanics (Wake MIT, `iAmCorey/Wake@d9f702b6` tag v0.6.5 — mechanics with attribution, no code copied)
123
+
124
+ Pin: `d9f702b6df0e85` (`Release v0.6.5`), `crates/wake-core/src/{adapters/mod.rs,db.rs}`,
125
+ MIT License © 2026 Corey Chiu.
126
+
127
+ | Ledger | Wake pin | What was kept |
128
+ | --- | --- | --- |
129
+ | The lookup is dropped from the derived unit; dialogue is not | `adapters/mod.rs#L425-L447` (`units_from_messages`) | a tool call that *is* a lookup contributes nothing to the unit; the message text around it stays, and a message with nothing left is dropped whole — Ledger's analogue is record-level: the call and its paired result leave the index, every other record stays |
130
+ | MCP tools by word-boundary suffix, not substring | `#L469-L535` (`is_wake_lookup`, `ends_with_word`) | client prefixes vary, so the name must *end* with the tool name at a word boundary (`awake_search` is not `wake_search`); the server segment (`mcp__ledger__`) is kept as its own stronger signal |
131
+ | Shell binaries named in command position of the structured `command` field | `#L469-L535` (`WAKE_BINARIES`, `command_names_binary`, `names_binary`) | the structured field is read for *any* tool that carries one (string or argv array), never a substring of the preview; the command is segmented so a binary in argument position (`grep ledger`) is not a run, and a `wake-cli-old`-style near-miss is not a name |
132
+ | The narrowness is a test, not a hope | `#L599-L653` (`wake_lookup_detection_is_narrow`): `cargo test` not a lookup; `man wake-cli-old` not, even with its description mentioning the binary; `Grep`/`Read` not | `echo-filter.test.ts` ports the shape: our own `bun test`, a commit message, a checkout, a quoted alternation, a heredoc body, a `Read` of `ledger.md`, an `Edit` of `ledger.ts` — all kept |
133
+ | A derivation rule change bumps an index-format counter; an old store flags and re-derives once | `db.rs#L134-L196` (`FTS_FORMAT`, whose doc says “'2' = 过滤自指回声”; `open_conn` stamps and flags when `stored != FTS_FORMAT` and sessions existed) | exactly the C2 machinery, consumed: the value is the rule's name, the flag is transient and durable across a crash, a reader serves the stale index while a writer re-derives. C3 is the first bump (below) |
134
+
135
+ ## Deviations from the scout design (each with its reason)
136
+
137
+ - **The filter is a derived set (`records_echo`), not a SQL predicate inside
138
+ `FTS_REPOPULATE_SQL`.** The scout allowed “pure SQL or a derived marker”; this
139
+ is the marker, because the rule lives in TypeScript and a SQL restatement of it
140
+ would be a second implementation that can drift — the one thing a
141
+ self-referential filter must not do. The set is also what makes ingest and the
142
+ re-derivation *one decision*: the ingest path writes the id where it skips the
143
+ FTS row, the rebuild recomputes it from the same rule and subtracts it, the
144
+ parity gate and the status count read the same table back. The projection stays
145
+ whole-table on purpose — the rebuild is then a *repair*: re-project every
146
+ record, drop exactly what the rule names, and an index that drifted in either
147
+ direction comes back equal to `records` minus the filter.
148
+ - **The parity gate gained a second term, and an orphan check.** `records −
149
+ excluded == indexed` (scout) plus “every id in `records_echo` still backs a
150
+ record”. The foreign key already forbids an orphan, so the check exists to
151
+ catch the one thing the key cannot: a writer running with enforcement off.
152
+ - **A matched counter with a missing output re-derives.** `DERIVE_COUNTERS`
153
+ records the tables each rule derives, and a store whose counter names the
154
+ current rule but which cannot produce the rule's table is treated as an
155
+ unfinished derivation, not a derived store. This is beyond the scout sketch
156
+ but falls out of the second output: without it, a store that recorded `"2"`
157
+ and lost `records_echo` would report `0` filtered forever. The matching value
158
+ is left in place — it is not the counter that was wrong, the derivation is
159
+ incomplete — and the flag is raised for the next pass.
160
+ - **The rule is broader in shell shapes than the sketch.** Beyond first token /
161
+ separator / prefix word (`ledger/ledger-core/traj-bridge`, `bun run …`), the
162
+ port reads quoted strings correctly (`rg -n "a|ledger|b"` is one command
163
+ running `rg`), strips heredoc bodies, segments on newlines, `$(`, and
164
+ backticks, joins `bash -lc` argv arrays, and reads the Codex `cmd:"…"` strings
165
+ inside an `exec` input. Each extension is in the conservative direction —
166
+ structural command position, never a substring — and is pinned in the test
167
+ table.
168
+ - **`build` and `watch` take the pending re-derivation too.** C2's boundary note
169
+ left exactly this to C3 (`sync` was the only pass): a daemon-only machine
170
+ would otherwise keep serving an index derived under the old rules until some
171
+ sync ran. The watcher reports the wall time on stderr; `build` reports it in
172
+ its summary.
173
+ - **No per-query filtered count.** Per the contract and Wake: the envelope is
174
+ unchanged; the count lives where it is a property of the *store*, not of a
175
+ query.
176
+
177
+ ## Audit against the scout design
178
+
179
+ | Contract item | State |
180
+ | --- | --- |
181
+ | (a) MCP tool naming, suffix + server namespace | as designed |
182
+ | (b) shell binary in command position (`&& ; \|\| \| sudo time env`, `bun` runner) | as designed, extended (quotes, heredocs, substitutions, wrappers, argv, embedded `cmd:`) |
183
+ | (c) result paired by `tool_call_id`, same session | as designed; batch form resolves a call from an earlier batch through the store, store-wide form buffers results so source order cannot matter |
184
+ | never filter dialogue / read / edit / grep; reads *and* writes are lookups | as designed; 4,728 LIKE-superset candidates declined, spot-audited (checkout, commit message, `bun test`, tool reads/writes, quoted alternation, heredoc bodies) |
185
+ | record-level granularity; `records` canonical; `sessionRecords`/export untouched | as designed; fixture proves all 7 rows stay; 10/10 sampled dev records still indexed |
186
+ | derivation-time placement: ingest skip + re-derivation exclusion | as designed, via the marker table (deviation above) |
187
+ | `FTS_FORMAT` `1` → `2`, one re-derivation per store | as designed |
188
+ | parity `records − excluded == indexed` | as designed, plus orphan check |
189
+ | `echo_filtered` in `status` robot output | **gap → fixed**: the count was published from the derived set alone, so a store whose derivation was *pending* (the critical-pressure skip `sync.ts` documents, or a crash between open and the pass) reported `0` while the served index still held the lookups. `echo_filtered` is now published only when `fts_format` equals this build's **and** no `fts_reindex` is pending; otherwise `null` (“unknown … the next writer pass derives it”) |
190
+ | daemon paths wired (`build`, watch drain) | as designed (C2's boundary #1 taken) |
191
+ | envelope: no per-query filtered count | as designed (`reader.ts` untouched) |
192
+
193
+ ## Review round 2 (adversarial findings → fixes)
194
+
195
+ An adversarial review of this tree found four issues; all four are fixed here,
196
+ with regression tests for the first three and the API-surface document updated
197
+ for the fourth. None of them changes the design; three are ordering/coherence
198
+ holes in the rule's edges, one is documentation drift.
199
+
200
+ | # | Finding | Fix | Test |
201
+ | --- | --- | --- | --- |
202
+ | 1 (P2) | The batch form never repaired an **indexed result whose call arrives later**: a result committed in an earlier batch stayed in `records_fts` forever (the marker set cannot see it, and the parity gate trusts the set, so `records − excluded == indexed` still held). | On a batch that classifies a call, the same transaction now takes back the committed `tool` rows pairing those `tool_call_id`s — delete from `records_fts`, `INSERT OR REPLACE` into `records_echo` — so arrival order cannot decide what a pair derives to. Rows of the batch itself are touched idempotently (the delete matches nothing yet; the marker insert precedes the loop's own). | `a result committed before its call is repaired when the call arrives in a later batch` (both orders) |
203
+ | 2 (P2) | `bash -lc "ledger search x"` was a **false negative**: the wrapper's quoted script reaches `segmentWords` as one word, and the quote-stripping `basename` then rejected it for containing spaces — while the argv form and a script with an internal separator were detected. Incoherent for the most common wrapper shape. | The extracted script is quote-stripped and judged by `commandSegments` (the same segmentation as every other command string), which also makes quoting *inside* the script meaningful — a quoted alternation (`bash -lc "rg -n 'ard|ledger|review' docs"`) is no longer split raw and mis-read as a run. | `a shell wrapper's quoted -c script is one more command string` |
204
+ | 3 (P3) | `echoCallOf` hardcoded `'assistant-tool-call'`, so a `tool` row carrying a Ledger tool name was excluded at ingest and then **flipped back into the index** on the next re-derivation — the batch and store-wide forms disagreeing about one row. | The record's own `record_type` decides (`echoCallOf` is now the single place the rule meets the row, used by both forms), and the store lookup selects `record_type` it had previously only typed. A tool row is never a call: an orphan result with a Ledger tool name stays indexed, errs toward indexing, and is stable across a re-derivation. | `a tool result that carries a Ledger tool name is not a call, at ingest or after a re-derivation` |
205
+ | 4 (P3) | `docs/api-surface.md` was stale where robots read the contract: the `status` envelope lacked `echo_filtered` (and its `null` semantics), §3.1 still called `fts_format` `"1"`, and the re-derivation was described as `sync`'s alone. | Updated, plus the C3 surface the same file now has to describe: `build`'s summary `deriveMs` and its `Re-derived` prose line, the `records_echo` table (new §3.2 entry), the parity count's excluded term, and both counter sections naming `sync`/`build`/`watch` as the executors. | — (documentation; covered by the two code tests above) |
206
+
207
+ Gates after the round: **`bun test` 568 pass / 0 fail across 39 files**,
208
+ `bun run typecheck` clean, `bun run build` clean.
209
+
210
+ ## Measurements
211
+
212
+ Host: Apple M1, macOS 25.6, APFS. Fixture: a byte-consistent copy of the
213
+ production store (`sqlite3 …?mode=ro ".backup /tmp/ledger-c3/ledger.sqlite"`;
214
+ 1,121,468,416 bytes, 227,474 records, 2,604 sessions, 0 embeddings) — the
215
+ production store is never opened for writing, and the measurement harnesses
216
+ (throwaway scripts under `/tmp/ledger-c3`) were deleted afterwards.
217
+
218
+ The store was found **already derived** (`fts_format = 2`, `records_echo` = 188,
219
+ `records_fts` = 227,286) — the C3 build had run against it during development.
220
+ The “before” state below is therefore reconstructed from the store's own
221
+ projection: drop `records_echo` and `records_fts`, recreate the v11 DDL,
222
+ re-project every record whole (`5,783 ms`), set `fts_format = '1'` — a
223
+ bit-consistent stand-in for what a pre-C3 build leaves.
224
+
225
+ **OBSERVED**
226
+
227
+ | measurement | value |
228
+ | --- | --- |
229
+ | echo set, production store | **188 records** = 94 calls + 94 results, in **14 sessions** (omp 146, codex 40, claude-code 2); the calls are `bash` 73, `exec` 20, `Bash` 1 |
230
+ | scout's count this morning vs now | 40 → **188** (this repo's own sessions supplied the growth) |
231
+ | rule pass over the copy (`echoExcludedIds`) | **2,979 ms** first (cold) read, **372–500 ms** warm |
232
+ | stored set vs fresh rule computation | 188 vs 188, **0 differences** either way (the real store's derivation is exactly the shipped rule) |
233
+ | false positives | **0** across all 188: every call runs a Ledger binary in command position or names an MCP Ledger tool; every result pairs to a call in its own session. An independent separator-anchored pattern matched **92/94**; the two exceptions are path-prefixed runners (`/Users/mac/.bun/bin/bun …/ledger-core.ts`), which the rule normalizes by basename and the check's pattern did not |
234
+ | declined candidates (LIKE superset the rule keeps) | **4,728**; among them 3 script-embedded invocations (documented blind spot) and 7 direct `sqlite3` reads of the store file (kept by design, not lookups) |
235
+ | dev sessions of this checkout (`cwd` match) | **21 sessions, 9,176 records, 146 excluded**; per-session `indexed = records − excluded` for **all 21**, 0 mismatches; exact-membership sample: **10/10 kept records in `records_fts`, 0/10 excluded** |
236
+ | writer open of the pre-C3 copy | **3 ms**; `needsFtsReindex()` true (stored `"1"` ≠ `"2"`) |
237
+ | `reindexRecordsFts()`, four runs | **10,049 / 10,051 / 10,162 / 10,374 ms** |
238
+ | phase breakdown (same statements, timed individually) | repopulate (drop + DDL + `INSERT … SELECT`) **5,759 ms** · rule pass **372 ms** · derived-set insert **1 ms** · index subtraction (188 deletes) **159 ms** · parity + FTS5 `integrity-check` **2,818 ms** |
239
+ | state after | `records` 227,474 · `records_fts` 227,286 · `records_echo` 188 · parity true · `verifyRecordsFtsRebuild` passed · `schema_meta`: `fts_format=2`, `embeddings_format=1`, flag cleared |
240
+ | searches, before → after (totals bounded at 1000; marker = the rule's set) | `search_meta` **95 → 91** (4 echo results in the top 200 → 0) · `coverage` 1000 → 1000 (**16 → 0**) · `canonical trajectory` **30 → 28** (2 echo calls in the top 5 → 0; the previous identical search was rank 1) · `writer lease` **230 → 229** · `record_candidate` **45 → 43** (echo at rank 4 → gone) · `rebase token passthrough` **3 → 2** (echo at rank 1 → gone) · `ledger` 1000 → 1000 (**2 → 0**). Every non-echo hit stayed; the top-5 list of `canonical trajectory` went from `[ECHO, real, real, ECHO, real]` to five genuine tool records |
241
+ | CLI, copy | `status --robot` → `"echo_filtered":188`; human → `Echo filtered: 188 records (agent lookups of this store; never searchable)`; `search "canonical trajectory" --limit 3` → 3 genuine hits across 2 sessions, no echo |
242
+ | gap fix, verified live in the reconstructed pending state | raw set 0, served index still returning the rank-1 echo record, `status.echoFiltered` → **null** (was `0`); after the one-shot → **188** and parity |
243
+ | existing suite | **565 pass / 0 fail** across 39 files (`bun test`), `bun run typecheck` clean, `bun run build` clean — the round-1 tree; after the review round's three regression tests the same gates are **568 pass / 0 fail**, clean (see “Review round 2”) |
244
+
245
+ **INFERRED** (not directly measured, listed so reviewers can attack them)
246
+
247
+ - The 10.0–10.4 s band is **warm** and idle-host: the copy had been fully read
248
+ and rewritten several times before the timed runs. C2 measured the same
249
+ re-projection path at 19.4–33.3 s on the older store; the C3 addition is only
250
+ the rule pass + subtraction (~0.5 s of the 10 s), so the difference between the
251
+ two bands is page cache and host load, not a different mechanism. The first
252
+ cold post-merge pass on a user's machine should be expected in the C2 band.
253
+ - The status gate's *motivation* is derived from code, not from a failing
254
+ scenario observed in production: the pending state is reachable by design
255
+ (`sync` skips the one-shot under critical pressure, and the flag is durable)
256
+ and by crash between open and pass; only the reconstructed window was
257
+ measured. Its other reachable case — an older build's writer pass writing the
258
+ index back whole, leaving `fts_format` different from this build's — is the
259
+ version-crossing cost C2 already documents as “one more re-derivation”; the
260
+ gate now reports `null` instead of a stale count while it lasts.
261
+ - Any *further* change to the rule with `FTS_FORMAT` left at `"2"` would not
262
+ re-derive anything: the set is derived state keyed by the counter, not hashed
263
+ from the rule. The bump is the contract (as in Wake); the follow-up below
264
+ records it.
265
+ - The vectors lane is unaffected and cannot currently surface echo records: the
266
+ store holds 0 embeddings, so semantic/hybrid search has nothing to rank. If
267
+ the lane ships, a lookup's embedding would need the same exclusion — the set
268
+ is the natural input, and no decision is needed until then.
269
+
270
+ ## Position in the program (C3 row, `docs/research/2026-09-16-wake-port-program.md`)
271
+
272
+ - Failure it fixes: *“We index the transcripts of the agents that call `ledger
273
+ search` — your own queries come back as hits. Unnamed bug, observed daily.”*
274
+ - Open-source case: *“Self-hosting agents are our primary users; a store that
275
+ echoes itself reads as broken. Wake ships the same filter (`wake_*` tools,
276
+ `FTS_FORMAT` counter).”*
277
+ - Ordering: C3 follows C2 because it changes index shape and needs the one-shot;
278
+ it bumps `FTS_FORMAT` and is the first real consumer of the counter, plus the
279
+ first cycle to take C2's own boundary — the daemon passes now re-derive too.
280
+ C4 and C5 will bump the same counter and can rely on the marker-table pattern
281
+ introduced here (a derived set beside the index, maintained by the same
282
+ decision that writes the index).
283
+
284
+ ## Boundaries and follow-ups (not in this cycle)
285
+
286
+ 1. **In-script invocations stay unfiltered** (3 measured: python heredocs
287
+ running `subprocess.run(['ledger','sync',…])`), as do unmodelled runners
288
+ (`node`, `npx`) and — unlike Wake — an unstructured shell tool whose only
289
+ text is a preview. All err toward indexing; the test table pins the current
290
+ shape, so widening it is a deliberate change plus a bump.
291
+ 2. **Rule changes need the bump.** The rule's identity is `FTS_FORMAT`, not a
292
+ hash of `filter.ts`. A future edit to the rule without a bump leaves every
293
+ store's set as-is; the cycle that edits it must bump.
294
+ 3. **The vectors lane, if it ships, inherits the question**: a lookup's
295
+ embedding would be rankable while its text is not. The natural input is
296
+ `records_echo` (delete the embeddings with the record, or exclude at query
297
+ time); not decided here, because the store holds no vectors today.
298
+ 4. **`status` pays one extra presence check + one `schema_meta` row read** on a
299
+ store that has the table (both trivial, both read-only); the read path
300
+ (`search`, `recent`, …) is untouched, and the envelope carries no filtered
301
+ count by design.
302
+ 5. **The echo set is one row per excluded record.** At 188 rows for 227k records
303
+ it is nothing; a store where lookups dominate would grow it linearly with
304
+ their count (one row per pair, two small text columns), and the rebuild's
305
+ subtraction step is bounded by the set size, not the store.