trajectories 0.0.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +201 -0
  3. package/README.md +288 -2
  4. package/dist/cli.js +30210 -0
  5. package/dist/convert.d.ts +31 -0
  6. package/dist/convert.js +431 -0
  7. package/dist/deepagents_checkpoint.py +362 -0
  8. package/dist/index.d.ts +7 -0
  9. package/dist/index.js +8402 -0
  10. package/dist/log-baseline.d.ts +26 -0
  11. package/dist/log-baseline.js +104 -0
  12. package/dist/log-content.d.ts +16 -0
  13. package/dist/log-content.js +80 -0
  14. package/dist/log-io.d.ts +10 -0
  15. package/dist/log-io.js +121 -0
  16. package/dist/log-read.d.ts +24 -0
  17. package/dist/log-read.js +117 -0
  18. package/dist/log-reconcile.d.ts +5 -0
  19. package/dist/log-reconcile.js +38 -0
  20. package/dist/log-records.d.ts +35 -0
  21. package/dist/log-records.js +85 -0
  22. package/dist/log-recovery.d.ts +18 -0
  23. package/dist/log-recovery.js +123 -0
  24. package/dist/log-stage.d.ts +14 -0
  25. package/dist/log-stage.js +138 -0
  26. package/dist/log-state.d.ts +89 -0
  27. package/dist/log-state.js +92 -0
  28. package/dist/log-switch.d.ts +3 -0
  29. package/dist/log-switch.js +98 -0
  30. package/dist/log-types.d.ts +129 -0
  31. package/dist/log-types.js +8 -0
  32. package/dist/log.d.ts +8 -0
  33. package/dist/log.js +62 -0
  34. package/dist/migrate.d.ts +29 -0
  35. package/dist/migrate.js +173 -0
  36. package/dist/normalization-runtime.d.ts +1 -0
  37. package/dist/normalization-runtime.js +3150 -0
  38. package/dist/portable.d.ts +9 -0
  39. package/dist/portable.js +26 -0
  40. package/dist/readers/canonical.d.ts +13 -0
  41. package/dist/readers/canonical.js +54 -0
  42. package/dist/readers/cline.d.ts +10 -0
  43. package/dist/readers/cline.js +107 -0
  44. package/dist/readers/cursor.d.ts +20 -0
  45. package/dist/readers/cursor.js +374 -0
  46. package/dist/readers/deepagents.d.ts +41 -0
  47. package/dist/readers/deepagents.js +163 -0
  48. package/dist/readers/fx.d.ts +11 -0
  49. package/dist/readers/fx.js +573 -0
  50. package/dist/readers/github-copilot.d.ts +8 -0
  51. package/dist/readers/github-copilot.js +216 -0
  52. package/dist/readers/grok-build.d.ts +12 -0
  53. package/dist/readers/grok-build.js +391 -0
  54. package/dist/readers/guards.d.ts +4 -0
  55. package/dist/readers/guards.js +121 -0
  56. package/dist/readers/pi-compaction.d.ts +7 -0
  57. package/dist/readers/pi-compaction.js +112 -0
  58. package/dist/readers/pi-context.d.ts +13 -0
  59. package/dist/readers/pi-context.js +64 -0
  60. package/dist/store.d.ts +25 -0
  61. package/dist/store.js +218 -0
  62. package/dist/types.d.ts +91 -0
  63. package/dist/types.js +1 -0
  64. package/dist/writers/claude-code.d.ts +5 -0
  65. package/dist/writers/claude-code.js +51 -0
  66. package/dist/writers/cline.d.ts +3 -0
  67. package/dist/writers/cline.js +77 -0
  68. package/dist/writers/codex.d.ts +3 -0
  69. package/dist/writers/codex.js +32 -0
  70. package/dist/writers/cursor.d.ts +3 -0
  71. package/dist/writers/cursor.js +197 -0
  72. package/dist/writers/deepagents.d.ts +3 -0
  73. package/dist/writers/deepagents.js +116 -0
  74. package/dist/writers/entropy.d.ts +5 -0
  75. package/dist/writers/entropy.js +33 -0
  76. package/dist/writers/fx.d.ts +3 -0
  77. package/dist/writers/fx.js +135 -0
  78. package/dist/writers/github-copilot.d.ts +3 -0
  79. package/dist/writers/github-copilot.js +71 -0
  80. package/dist/writers/grok-build.d.ts +3 -0
  81. package/dist/writers/grok-build.js +43 -0
  82. package/dist/writers/helpers.d.ts +17 -0
  83. package/dist/writers/helpers.js +75 -0
  84. package/dist/writers/omp.d.ts +3 -0
  85. package/dist/writers/omp.js +7 -0
  86. package/dist/writers/opencode.d.ts +3 -0
  87. package/dist/writers/opencode.js +65 -0
  88. package/dist/writers/pi.d.ts +3 -0
  89. package/dist/writers/pi.js +45 -0
  90. package/docs/cline-native-format.md +43 -0
  91. package/docs/copilot-native-format.md +78 -0
  92. package/docs/cursor-native-format.md +103 -0
  93. package/docs/deepagents-native-format.md +98 -0
  94. package/docs/evidence-api.md +170 -0
  95. package/docs/fx-native-format.md +87 -0
  96. package/docs/harness-parity-bootstrap.md +78 -0
  97. package/docs/harness-support-parity.md +107 -0
  98. package/docs/inspirations.md +103 -0
  99. package/docs/ledger-migration.md +141 -0
  100. package/docs/pi-compaction-context.md +75 -0
  101. package/docs/pi-provider-context.md +76 -0
  102. package/docs/research/2026-10-06-huihua-and-oar.md +176 -0
  103. package/docs/research/ledger/README.md +96 -0
  104. package/docs/research/ledger/confer/2026-09-13-round-10/README.md +61 -0
  105. package/docs/research/ledger/confer/2026-09-13-round-2/cass.md +72 -0
  106. package/docs/research/ledger/confer/2026-09-13-round-3/README.md +70 -0
  107. package/docs/research/ledger/confer/2026-09-13-round-5/README.md +61 -0
  108. package/docs/research/ledger/confer/2026-09-13-round-6/README.md +68 -0
  109. package/docs/research/ledger/confer/2026-09-13-round-7/README.md +64 -0
  110. package/docs/research/ledger/confer/2026-09-13-round-9/README.md +59 -0
  111. package/docs/research/ledger/confer/2026-09-14-wake/wake.md +146 -0
  112. package/docs/research/ledger/docs/inspirations.md +320 -0
  113. package/docs/research/ledger/docs/pr2-pr6-integration.md +53 -0
  114. package/docs/research/ledger/docs/research/2026-09-07-confer-inspirations.md +163 -0
  115. package/docs/research/ledger/docs/research/2026-09-07-letta-continuous-service.md +104 -0
  116. package/docs/research/ledger/docs/research/2026-09-08-claude-history-retrieval.md +117 -0
  117. package/docs/research/ledger/docs/research/2026-09-09-agentsview-native-discovery.md +87 -0
  118. package/docs/research/ledger/docs/research/2026-09-09-daemon-observation.md +231 -0
  119. package/docs/research/ledger/docs/research/2026-09-09-discovery-pagination.md +114 -0
  120. package/docs/research/ledger/docs/research/2026-09-09-harness-transcript-contract.md +145 -0
  121. package/docs/research/ledger/docs/research/2026-09-09-native-history-discovery.md +180 -0
  122. package/docs/research/ledger/docs/research/2026-09-09-native-history-implementation.md +137 -0
  123. package/docs/research/ledger/docs/research/2026-09-09-other-inspirations.md +279 -0
  124. package/docs/research/ledger/docs/research/2026-09-09-trajectory-and-letta-code.md +128 -0
  125. package/docs/research/ledger/docs/research/2026-09-13-diagnostics-coverage.md +116 -0
  126. package/docs/research/ledger/docs/research/2026-09-13-fts-rebuild.md +83 -0
  127. package/docs/research/ledger/docs/research/2026-09-13-read-path-split.md +74 -0
  128. package/docs/research/ledger/docs/research/2026-09-13-runtime-rollout.md +102 -0
  129. package/docs/research/ledger/docs/research/2026-09-13-scan-outcomes.md +80 -0
  130. package/docs/research/ledger/docs/research/2026-09-13-search-quality.md +105 -0
  131. package/docs/research/ledger/docs/research/2026-09-13-source-coverage.md +64 -0
  132. package/docs/research/ledger/docs/research/2026-09-13-sync-wall-budget.md +82 -0
  133. package/docs/research/ledger/docs/research/2026-09-13-typed-read-gate.md +81 -0
  134. package/docs/research/ledger/docs/research/2026-09-13-writer-lease.md +84 -0
  135. package/docs/research/ledger/docs/research/2026-09-16-c1-copy-fallback.md +227 -0
  136. package/docs/research/ledger/docs/research/2026-09-16-c10-kimi.md +414 -0
  137. package/docs/research/ledger/docs/research/2026-09-16-c2-derive-counter.md +216 -0
  138. package/docs/research/ledger/docs/research/2026-09-16-c3-echo-filter.md +305 -0
  139. package/docs/research/ledger/docs/research/2026-09-16-c4-title-index.md +349 -0
  140. package/docs/research/ledger/docs/research/2026-09-16-c5-trigram.md +268 -0
  141. package/docs/research/ledger/docs/research/2026-09-16-c6-recency-prior.md +438 -0
  142. package/docs/research/ledger/docs/research/2026-09-16-c7-tombstones.md +467 -0
  143. package/docs/research/ledger/docs/research/2026-09-16-c8-remote-truth.md +390 -0
  144. package/docs/research/ledger/docs/research/2026-09-16-c9-grok.md +380 -0
  145. package/docs/research/ledger/docs/research/2026-09-16-wake-port-program.md +38 -0
  146. package/docs/research/ledger/docs/research/2026-09-17-cursor-blobstore.md +157 -0
  147. package/docs/research/ledger/manifest.json +540 -0
  148. package/legacy/NOTICE.md +50 -0
  149. package/legacy/bin/ledger-core.ts +7 -0
  150. package/legacy/bin/ledger.ts +7 -0
  151. package/legacy/bin/traj-bridge.ts +7 -0
  152. package/legacy/scripts/import-claude-history.ts +130 -0
  153. package/legacy/scripts/native-history-smoke.ts +52 -0
  154. package/legacy/scripts/observe-daemon.ts +570 -0
  155. package/legacy/scripts/retitle-sessions.md +19 -0
  156. package/legacy/scripts/verify-runtime-rollout.ts +151 -0
  157. package/legacy/src/canonical.ts +108 -0
  158. package/legacy/src/cli.ts +1293 -0
  159. package/legacy/src/db.ts +2254 -0
  160. package/legacy/src/diagnostics.ts +87 -0
  161. package/legacy/src/discovery.ts +56 -0
  162. package/legacy/src/embeddings.ts +382 -0
  163. package/legacy/src/filter.ts +602 -0
  164. package/legacy/src/gemini_embeddings.ts +103 -0
  165. package/legacy/src/identity.ts +9 -0
  166. package/legacy/src/index.ts +10 -0
  167. package/legacy/src/ingest.ts +791 -0
  168. package/legacy/src/managed.ts +178 -0
  169. package/legacy/src/native/common.ts +68 -0
  170. package/legacy/src/native/cursor.ts +647 -0
  171. package/legacy/src/native/grok.ts +540 -0
  172. package/legacy/src/native/kimi.ts +706 -0
  173. package/legacy/src/native/opencode.ts +470 -0
  174. package/legacy/src/native/sqlite-ro.ts +281 -0
  175. package/legacy/src/native.ts +97 -0
  176. package/legacy/src/normalizer.ts +457 -0
  177. package/legacy/src/parentage.ts +562 -0
  178. package/legacy/src/pressure.ts +162 -0
  179. package/legacy/src/private-storage.ts +74 -0
  180. package/legacy/src/reader.ts +1255 -0
  181. package/legacy/src/scanner.ts +591 -0
  182. package/legacy/src/schema-gate.ts +263 -0
  183. package/legacy/src/schema.ts +829 -0
  184. package/legacy/src/semantic-top-k.ts +71 -0
  185. package/legacy/src/session-hash.ts +49 -0
  186. package/legacy/src/store-status.ts +218 -0
  187. package/legacy/src/sync-preparation.ts +45 -0
  188. package/legacy/src/sync-truth.ts +843 -0
  189. package/legacy/src/sync.ts +1233 -0
  190. package/legacy/src/types.ts +436 -0
  191. package/legacy/src/vocab.txt +30522 -0
  192. package/legacy/src/watch.ts +673 -0
  193. package/legacy/src/writer-lease.ts +310 -0
  194. package/ops/README.md +150 -0
  195. package/ops/install-cli.sh +63 -0
  196. package/ops/install-native.sh +35 -0
  197. package/ops/install-release.sh +49 -0
  198. package/ops/linux/install-systemd-user.sh +103 -0
  199. package/ops/macos/install-launchd.sh +72 -0
  200. package/ops/sync-settings.sh +37 -0
  201. package/package.json +21 -5
  202. package/skills/trajectories/SKILL.md +138 -0
  203. package/skills/trajectories/scripts/trajectories_recent.sh +26 -0
  204. package/skills/trajectories/scripts/trajectories_search.sh +26 -0
  205. package/index.js +0 -1
@@ -0,0 +1,105 @@
1
+ # Search-quality: zero-hit widening and an evidence-bearing envelope, 2026-09-13
2
+
3
+ ## Outcome
4
+
5
+ Multi-word Ledger searches without boolean operators are exact-phrase FTS5
6
+ matches, so any query whose words are not adjacent — the normal case for
7
+ vague recall — returned zero. The escaped-term conjunction retry fired only
8
+ on FTS syntax errors, never on zero hits, and reported nothing.
9
+ `totalHits` was `hits.length` (bounded by `limit`) presented as an exact
10
+ count, and robot mode emitted bare hit lines with no envelope at all.
11
+
12
+ This cycle adds a gated prefix-OR widening for zero-hit lexical queries and
13
+ a `search_meta` envelope that makes counts, engines, fallbacks, and widening
14
+ refusals agent-readable. Live-store proof: four real vague queries derived
15
+ from actual record content went 0 → 10 hits (2–7 sessions each) while
16
+ adjacent-phrase and quoted queries stayed byte-identical.
17
+
18
+ ## Evidence chain
19
+
20
+ - Defects verified on merged main `2b08be4`:
21
+ `packages/core/src/db.ts` `buildFtsExpression` (phrase-wrapping),
22
+ `queryLexical` catch path (syntax-error-only retry), `totalHits`
23
+ accounting in all three engines, and `cli.ts` `handleQuery` robot payload.
24
+ - Round-2 Confer research pinned Cass's exact mechanics (patterns only,
25
+ rider license honored — no Cass code or engine reuse):
26
+ the widening gate composition, the `*term*` shape, strictly-more
27
+ acceptance, `ImplicitWildcard` tagging with quality factor 0.6, and the
28
+ envelope vocabulary
29
+ (`cass@c7cd1ad3ff7ee5d4323b9c840dcfcda683881e36`, see
30
+ [confer round 2](../../confer/2026-09-13-round-2/cass.md)).
31
+ - Divergences from Cass, deliberate: Ledger triggers only on 0 hits (its
32
+ default multi-word semantics IS the phrase, so sparse-but-nonzero results
33
+ are correct matches, not a failure); FTS5 rejects `*term*` (verified
34
+ locally — `unknown special query`), so the widened shape is a quoted
35
+ prefix OR-join `("tok"* OR "tok"*)`; refusal reasons ship in the payload
36
+ where Cass buries them in tracing logs.
37
+
38
+ ## Design
39
+
40
+ - Gates (all must hold, evaluated only after a 0-hit primary): no quote
41
+ anywhere in the query (phrase intent), no boolean operators, no user
42
+ wildcard; tokens ≤16 chars (over-long tokens are dropped, the remainder
43
+ still widens), ≥3 alphanumeric chars per widenable token (a quoted empty
44
+ phrase with `*` is not a no-op in FTS5 and can match unrelated records);
45
+ case-insensitive dedupe, ≤16 terms; corpus under
46
+ `LEDGER_WIDEN_MAX_RECORDS` (default 10,000, strict decimal parse, 0
47
+ disables, read per query). Adoption requires strictly more hits than
48
+ zero; every adopted hit is tagged `match_type: "implicit_wildcard"`.
49
+ - Envelope: `QueryResult.meta` carries `count_precision`
50
+ (`exact`/`all_matches_returned` when hits < limit,
51
+ `lower_bound`/`limit_bounded` at the boundary), engine `elapsed_ms`,
52
+ `search_mode`/`requested_search_mode` (equal today; the pair exists so a
53
+ future fallback cannot misreport), `fallback_reason` for the syntax-error
54
+ conjunction retry (now surfaced, including through hybrid), and the
55
+ `widened` report with `skip_reason` on refusal. Robot mode emits one
56
+ `search_meta` JSONL line before the hits; `--json` becomes
57
+ `{search_meta, hits}`; human output changes only when widening or
58
+ fallback fired (stderr note + `[widened]` header suffix).
59
+ - Hybrid widens its lexical leg only; adopted hits keep
60
+ `implicit_wildcard` through RRF fusion, and the hybrid envelope forwards
61
+ the lexical leg's `fallback_reason` and `widened`. Semantic never widens.
62
+
63
+ ## Verification (independent verifier; scripts were throwaway under /tmp)
64
+
65
+ - Stability: changed tree 405/407 three times; a full-repo copy on the fast
66
+ volume 406/407 three times; base `2b08be4` worktree 371/372 three times.
67
+ The only constant failure is the pre-existing `native-cursor`
68
+ classification failure (issue #18); the semantic CLI-spawn timeout is a
69
+ `/Volumes/dev` volume artifact (base code: 0.74 s from /tmp, 29.2 s from
70
+ `/Volumes/dev`). Typecheck and build clean.
71
+ - Live capability: four real vague queries ("worktree premortem fix",
72
+ "fair use throttle", "rebase token passthrough", …) went 0 → 10 hits on
73
+ the 901-session/156,801-record production store with correct envelopes;
74
+ an adjacent-phrase query and a quoted query were byte-identical before
75
+ and after; a syntax-error query reports `fallback_reason`.
76
+ - Overhead: 200 nonzero-hit queries — median 0.116 ms both sides (the
77
+ widening probe provably never runs; 0 `getRecordCount` calls
78
+ instrumented). Zero-hit queries pay the probe and, when under-ceiling
79
+ widening succeeds, the rerun: +0.53 ms median on a 2,400-record fixture.
80
+ On the 157k live store the default ceiling refuses (visible
81
+ `corpus_over_ceiling`, ~9 ms probe); raising it costs 0.14–1.5 s per
82
+ widened query — the documented price of opting a large store in.
83
+ - Adversarial round (before verify): 4,000-query FTS5 metacharacter fuzz —
84
+ 0 throws, 0 envelope malformations, 0 column-filter leaks; happy-path
85
+ invariance 15/16 byte-identical human outputs and 72/72 identical robot
86
+ payloads (the one difference: a legitimately widened zero-hit query);
87
+ ingest-staging's 13 content-absence assertions moved to quoted phrases —
88
+ reviewed as contract-preserving, not suppression.
89
+
90
+ ## Boundaries
91
+
92
+ - Widened results are OR-of-prefixes: recall-oriented, precision-low. The
93
+ `match_type` tag and the envelope are the contract for consumers that
94
+ care; ranking inside a widened set is bm25 over the OR expression.
95
+ - The default 10k ceiling means large stores refuse widening until an
96
+ operator opts in. A future FTS5 prefix index (`prefix=` on the virtual
97
+ table) could make widening cheap at scale; recorded as follow-up.
98
+ - `widened_query_failed` is defensive and not expected reachable (every
99
+ token is quoted+escaped; the 4,000-query fuzz never hit it).
100
+ - The ceiling counts the whole corpus, not the `--source`-filtered slice —
101
+ a filtered search cannot dodge the cost guard; documented.
102
+ - `--robot --json` changed shape (array → object). No in-repo consumer
103
+ parses it (wrapper scripts are passthrough); flagged as the one external
104
+ breaking vector in the PR description.
105
+ - No Cass code reused; no new dependencies; schema untouched.
@@ -0,0 +1,64 @@
1
+ # Durable source coverage, 2026-09-13
2
+
3
+ ## Outcome
4
+
5
+ Cycle 7's scan outcomes were dropped on every floor (`sync.ts` and
6
+ `watch.ts` destructured `sessions` only; `buildStore` logged them only
7
+ under `--verbose`), so the answer to "which sources are persistently
8
+ missing or failing?" evaporated with each pass. This cycle records them
9
+ durably: schema v12 adds `source_coverage` — one last-write-wins row per
10
+ source (`source` PK, root, status, discovered, detail, `first_seen_at`,
11
+ `updated_at`) — written by a single shared upsert from exactly the three
12
+ lease-holding passes (sync `runSync`, `buildStore`, watch drain), and
13
+ read by `ledger status`, which now carries a `coverage` block: the robot
14
+ array (sorted, `first_seen_at` internal) and a human count line
15
+ ("N/M sources listable, K missing") with one line per non-missing
16
+ anomaly. `scan` stays ephemeral by design (no lease, no handle). This
17
+ closes the "visible coverage reporting / expose unsupported-failed-
18
+ pending sources" gap named in round 1.
19
+
20
+ ## Evidence chain
21
+
22
+ [Confer round 9](../../confer/2026-09-13-round-9/README.md): AgentsView's
23
+ `raw_coverage` per-source rows (the transplantable shape) and its
24
+ version-in-key anti-pattern; Obelisk's proven failure mode — durable
25
+ markers with zero production readers — which is why the status reader
26
+ ships in the same change, and its write-last-in-transaction marker rule;
27
+ letta-code's last-write-wins outcome slot and its stale-success
28
+ qualifier (a post-pass transition must write); Cass's
29
+ persist-only-non-derivable-truth rule; the harness's single-choke-point
30
+ writer. Ops context this round: recovered a wedged sync that held the
31
+ lease 75× past its `--wall-ms` budget (#35); the live store migrated to
32
+ v11 with widening verified live.
33
+
34
+ ## Verification (combined adversarial + independent round)
35
+
36
+ - Lease safety: all three write points record under the writer lease
37
+ (probed — an external lease holder leaves rows and store mtime
38
+ byte-identical); no fourth write path exists; the scan path is
39
+ handle-free. Upsert: two processes × 60 rounds — zero errors, zero
40
+ duplicates, `first_seen_at` pinned, detail clearing verified,
41
+ spinning readers never observed partial sets.
42
+ - Contract: robot keys exactly `{source, root, status, discovered,
43
+ detail, updated_at}` sorted; human math (N/M, K missing; missing
44
+ counted, never listed); pre-v12 → `null` with the unknown line; empty
45
+ table → `[]` with the none line; reads never rewrite rows.
46
+ - Migration: a genuine v11 store (built with archived v11 code) lands at
47
+ v12 with an empty table, all data and FTS behavior preserved; the
48
+ v1…v11 fixture chain all migrates; `CREATE_TABLES_SQL` is the whole
49
+ v12 migration (equal-version re-runs restore a hand-dropped table).
50
+ - Mutations: 7/7 killed (each record-call removal, first_seen
51
+ re-stamping, detail retention, the text filter, and the
52
+ record-after-nothing-new ordering).
53
+ - Suite: 532 pass / 0 fail across 36 files (reproduced on a
54
+ byte-identical clone); typecheck/build clean. Live v11 store:
55
+ `status` returns `coverage: null` read-only, bytes unchanged.
56
+
57
+ ## Boundaries
58
+
59
+ - Coverage reflects the last pass that scanned each source; a source not
60
+ scanned this pass keeps its prior row (stale `updated_at` is the
61
+ signal). No TTL, no notification semantics, no counters — the row is
62
+ state, not history.
63
+ - `first_seen_at` is internal (not published).
64
+ - #35 (wall-budget enforcement) remains open as its own bug.
@@ -0,0 +1,82 @@
1
+ # Enforcing the sync wall budget across worker waits, 2026-09-13
2
+
3
+ ## Outcome
4
+
5
+ The sync pass's `--wall-ms` budget was consulted only at admission
6
+ gates; the worker-wave `Promise.all` had no timeout, and
7
+ `spawnJsonChild`'s kill timer never settled the awaited promise — a hung
8
+ child stdout drain hung the parent forever. Observed live: a wedged
9
+ holder at zero CPU kept the writer lease for 75× its 120 s budget and
10
+ blocked every write until manually killed (issue #35). This cycle makes
11
+ the wall real:
12
+
13
+ - `spawnJsonChild` settles on timeout — the SIGTERM → 250 ms grace →
14
+ SIGKILL sequence is raced against the reply drain, the stopped flag
15
+ (timer or abort) decides status, and the `finally` awaits the stop so
16
+ no child outlives the call.
17
+ - The wave await is raced against the remaining absolute wall; on
18
+ expiry the wave's children are killed, settled siblings keep their
19
+ real results (only still-pending invocations count as timed_out),
20
+ reservations are charged, and the pass proceeds through normal
21
+ control flow to a truthful summary — the deadline signal never lives
22
+ in the cleanup path, so an abort cannot inherit the wedge, and the
23
+ `finally` releases the lease.
24
+ - `SyncSummary.outcome` gains `wall_budget_exceeded`; precedence is
25
+ complete → `pending_pressure` → `wall_budget_exceeded` → … (pressure
26
+ outranks the wall: it is the stronger do-not-spin signal). Interrupted
27
+ rows stay durably queued without backoff — the next pass completes
28
+ them (pinned by test).
29
+
30
+ ## Evidence chain
31
+
32
+ [Confer round 10](../../confer/2026-09-13-round-10/README.md): Cass's
33
+ stall-vs-slow fault pair (PARK dies at the wall; PAGE_SLEEP — slow but
34
+ progressing within budget — is work, not a wedge) and typed compact
35
+ abort reporting; the harness's single-absolute-deadline rule clamped
36
+ into every wait; letta-code's outer group-kill layer that cannot be
37
+ defeated by a wedged child and its keep-the-deadline-out-of-the-abort
38
+ discipline; AgentsView's capture-finalization deadline shape (declared,
39
+ armed once, named outcome) and its bounded-drain fail-closed rule;
40
+ Obelisk's confirmed absence (budget-gates-admission is the shared blind
41
+ spot) and its progress-watermark insight (deferred — the worker budget
42
+ already bounds each child).
43
+
44
+ ## Verification (combined adversarial + independent round)
45
+
46
+ - **The wedge is fixed**: real parked child with a never-closing stdout
47
+ — spawn settles at deadline+grace with typed `timed_out`, child
48
+ reaped, descendant (if any) force-settled; the PARK pass ends at the
49
+ wall (~+500 ms reap) with `wall_budget_exceeded`, truthful counts, no
50
+ orphans, lease released, second pass completes the interrupted
51
+ session; PAGE_SLEEP (4×250 ms silent work under a 1.5 s wall)
52
+ completes normally.
53
+ - **Deadline math**: per-wave clamps from one absolute deadline; the
54
+ worker-vs-wall boundary holds at equality; no zero/negative timeouts;
55
+ maintenance still bounded. A wall spent before admission reports the
56
+ boundary without starting work.
57
+ - **Truthfulness**: settled-sibling partitioning verified (A replies,
58
+ B parked → completed 1, timed_out 1); outcome precedence including
59
+ the pressure-outranks-wall case; the adjacency fix keeps
60
+ `sessionBudgetLimited` honest when an earlier exhaustion zeroed the
61
+ selection.
62
+ - **Hygiene**: every race timer cleared on both outcomes; prompt
63
+ process exit in all probes and the real CLI; mutations 5/5 caught
64
+ (settle-on-timeout, wave race, outcome branch, adjacency, clamp
65
+ boundary).
66
+ - **Invariance**: full suite 541 pass / 0 fail across 36 files;
67
+ baseline-vs-current CLI summaries byte-identical (minus timing) on
68
+ normal passes; hermetic real-CLI syncs converge to `complete`.
69
+ - Documented truths: a stopped wave costs the kill grace plus reap, so
70
+ a pass can end up to ~500 ms past the wall; the kill is pid-scoped
71
+ (a pipe-holding descendant would live on as its own process — none
72
+ exists today); a reply inside the ~2 ms clock band counts as a
73
+ report, not a stop.
74
+
75
+ ## Boundaries
76
+
77
+ - Progress-watermark stall detection inside workers remains deferred
78
+ (per-worker budgets bound each child today).
79
+ - Watcher drain deadlines are a separate surface, unchanged.
80
+ - The launchd `ExitTimeOut` SIGTERM path still orphans worker children
81
+ (pre-existing); with the wall enforced, the daemon no longer wedges,
82
+ which removes the observed exposure.
@@ -0,0 +1,81 @@
1
+ # Typed read gate for foreign and incomplete stores, 2026-09-13
2
+
3
+ ## Outcome
4
+
5
+ Both read gates (`TrajectoryReader`'s `openReadOnlyStore` and
6
+ `readStoreStatus`) recognized an initialized store by `sessions`-table
7
+ presence alone. A foreign SQLite file with only that table passed the gate
8
+ and died per-verb with raw SQLite errors (`no such table: records`,
9
+ `no such column: updated_at`) — reproduced on main `3469076` for `status`,
10
+ `search`, `recent`, and `emit-pi`. This cycle replaces both gates with one
11
+ shared classifier (`schema-gate.ts`): absent/zero-byte →
12
+ `STORE_NOT_INITIALIZED`, not-a-database/corrupt-open → `STORE_UNREADABLE`,
13
+ too-new → `SCHEMA_MISMATCH`, sessions present but required pairs missing →
14
+ new `STORE_SCHEMA_INCOMPLETE` naming the first gap in sorted order with a
15
+ single-sourced remedy line. A plain table named `records_fts` no longer
16
+ passes (the FTS5 shadow table `records_fts_data` is the witness). The
17
+ three `applyEmbeddingProvider` calls the reader split made dead were
18
+ removed, and their live `--embedding` validation was restored as a shared
19
+ parse-time `assertKnownEmbedding` (a bogus value fails every verb).
20
+ Closes #27.
21
+
22
+ ## Evidence chain
23
+
24
+ [Confer round 6](../../confer/2026-09-13-round-6/README.md): AgentsView's
25
+ required-schema diff and error shape (MIT — the `:memory:` replay does not
26
+ port to Ledger's branch-dependent DDL; the creator/expectation invariant
27
+ was adapted as the migration-fixture derivation test), Obelisk's
28
+ (table, column) pair-list probe (AGPL — patterns only) and the four-outcome
29
+ ladder analysis, the harness's observed-vs-expected message wording,
30
+ letta-code's empty-vs-invalid distinction (zero-byte = not initialized,
31
+ never unreadable), and trajectory's finding that a valid SQLite file
32
+ lacking expected tables escapes as a raw driver error there too.
33
+
34
+ ## Design
35
+
36
+ - `REQUIRED_READ_SCHEMA`: 44 pairs — records (19 columns),
37
+ records_embeddings (5), sessions (16), plus presence-only pairs for
38
+ `records_fts` and its FTS5 shadow `records_fts_data` (a same-named plain
39
+ table is the #27 failure shape one level down; AgentsView's by-name
40
+ index check has exactly that hole and was noted, not copied).
41
+ - One classifier, single open, no TOCTOU (the `ok` state carries the very
42
+ handle the caller queries); every refusal path closes the handle.
43
+ - Determinism: the reported gap is the sorted-first missing pair — stable
44
+ across processes, locales, and physical column orders.
45
+ - One gate, documented: stores missing only row-contract columns refuse
46
+ all four read verbs even where some verbs could serve — the documented
47
+ trade for one consistent verdict.
48
+
49
+ ## Verification (combined adversarial + independent rounds)
50
+
51
+ - Classifier: 0 fd leaks across all states and both callers; boundary
52
+ ladder stable (missing/zero-byte/garbage/truncated/directory/symlink/
53
+ WAL-sidecar-missing/VIEW-named-sessions/version-vs-shape ordering);
54
+ double-close safety; determinism across 5 locales × 5 processes.
55
+ - Required list: every pair touched by read-path SQL (7 are row-contract
56
+ columns, documented); real historical DDL re-materialized per commit
57
+ confirms v1 → `records_embeddings` gap, v3/v4-identity →
58
+ `sessions.agent_nickname` gap, v4-parentage…v10 → ok — matching the
59
+ derived fixtures exactly; the live 901-session store classifies ok and
60
+ serves with store bytes unchanged.
61
+ - Mutation checks both directions: removing a load-bearing pair kills the
62
+ derivation test; adding a bogus column kills 24 tests.
63
+ - Suite: 466 pass / 1 fail of 467 — the pre-existing native-cursor #18;
64
+ the semantic spawn-timeout flake is environmental (baseline-identical;
65
+ ~13× slower from this checkout's volume). Typecheck/build clean.
66
+ - Fixes from the adversarial round before verification: `--embedding`
67
+ validation restored at parse time (it was live validation, not dead
68
+ code — the audit caught the removed calls' real function); the FTS
69
+ shadow-table witness; sidecar wording made precise (SQLite may recreate
70
+ `-shm` on a zero-frame WAL store — engine behavior, documented, gate
71
+ deliberately not tightened); fixture fidelity hardened (pre-v6 variants
72
+ drop `content_hash_version`) so the derivation test cannot drift from
73
+ real stores; shape-not-integrity scope documented.
74
+
75
+ ## Boundaries
76
+
77
+ - The gate checks shape, not integrity: damaged pages still surface
78
+ per-verb `RUNTIME_ERROR` (unchanged from baseline).
79
+ - `STORE_UNREADABLE` wording differs between reader and status (baseline
80
+ wording preserved; single-sourcing holds for the new code only).
81
+ - Index/column-shape validation inside FTS remains out of scope.
@@ -0,0 +1,84 @@
1
+ # Participating-writer lease between CLI verbs and the daemon, 2026-09-13
2
+
3
+ ## Outcome
4
+
5
+ Ledger's only write arbitration was `busy_timeout=15000` — a CLI `sync`
6
+ colliding with the 15-minute scheduled daemon (or any writer) blocked
7
+ silently for up to 15 seconds, and nothing prevented interleaved writers.
8
+ This cycle adds a hard writer lease: a sibling lock database
9
+ (`<db>-write.lock`) taken with a held `BEGIN IMMEDIATE` before any store
10
+ connection opens, with typed, actionable refusals replacing silent
11
+ blocking.
12
+
13
+ - Mutating verbs acquire before `new TrajectoryDb` (whose constructor
14
+ writes DDL): foreground `build`/`sync` wait 250 ms then fail typed
15
+ `WRITER_BUSY` naming the holder; the scheduled daemon
16
+ (`LEDGER_SCHEDULER=1`, set by both ops installers) soft-skips the pass
17
+ with a reason and exit 0; `watch` takes the lease per drain
18
+ (refcounted across overlapping drains) and skips the tick on busy,
19
+ opening the store lazily so an idle or skipped watcher touches nothing.
20
+ - Sync worker children are authorized by invocation, not environment: the
21
+ private verbs pass an explicit option, and the child refuses typed unless
22
+ the lease label's pid is its `process.ppid` — an ambient
23
+ `LEDGER_SYNC_WORKER=1` can never bypass a held lease.
24
+ - Release is fd teardown (process death frees it; no TTL, no reaper, no
25
+ pid-liveness checks — which letta-code round 3 shows are zombie-broken on
26
+ darwin). The label uses `INSERT OR REPLACE` and a self-conditional
27
+ (own-pid) delete so a releaser can never erase a successor's name. A
28
+ corrupt lock database is quarantine-renamed and retried once, never
29
+ unlinked in place (inode-split rule).
30
+
31
+ ## Evidence chain
32
+
33
+ All mechanisms are independently specified in
34
+ [confer round 3](../../confer/2026-09-13-round-3/README.md) with pinned
35
+ citations: Obelisk's TTL-free sibling-lock lease and read-policy ladder
36
+ (AGPL — patterns only), AgentsView's two-layer gate, worker env-marker
37
+ transport, and error-string discipline (MIT), Cass's never-unlink rule,
38
+ metadata-bearing lock, and scheduler-verb soft-skip (rider — patterns
39
+ only), the harness round-3 verdict that grace-then-proceed fences do not
40
+ translate to row-commit writers (claim-then-typed-busy is the correct
41
+ ladder), and the scout map of Ledger's write entry points on `b20664a`
42
+ (daemon = 15-min oneshot sync; constructor-write hazard).
43
+
44
+ ## Verification (adversarial + independent rounds)
45
+
46
+ - Adversarial round found two reproduced HIGHs (watch lease leak on drain
47
+ overlap silently disabling the daemon; ambient marker bypassing the
48
+ lease), a label-integrity bug (`UNIQUE` failure on stale labels — the
49
+ real cause of nameless refusals), a permanent-wedge on corrupt lock
50
+ dbs, and store-creation outside the lease by idle watchers — all fixed
51
+ with mutation-verified negative tests before verification.
52
+ - Independent verification (own probes, real CLI processes): foreign
53
+ holder → `WRITER_BUSY` naming pid+holder, store not created; scheduler
54
+ skip → exit 0 with `skipped:{reason,holder}` and a distinct human line,
55
+ store untouched; SIGKILL holder → next sync completes immediately;
56
+ ambient marker + foreign lease → refused both as worker verb and as
57
+ foreground sync; real parented worker completes (132 KB read, cursor
58
+ advanced); watch under a foreign lease skips and keeps running; live
59
+ launchd service untouched.
60
+ - Stability: full suite 424 pass / 0 fail across 34 files, three
61
+ consecutive runs (both known pre-existing failures — native-cursor #18
62
+ and the semantic volume flake — did not reproduce). Typecheck and build
63
+ clean.
64
+ - Overhead: 20 interleaved fresh-store sync passes per side — median 494
65
+ ms lease vs 489 ms baseline (+1.0%, within noise; lease max 510 <
66
+ baseline max 553); idle-store passes identical; steady-state
67
+ acquire+release 318 µs median (p99 755 µs); watch drain parity.
68
+
69
+ ## Boundaries
70
+
71
+ - The lease covers `build`, `sync` (and its authorized children), and
72
+ `watch` drains. Read verbs (`search`, `recent`, `emit-pi`) still open
73
+ `TrajectoryDb` — on an existing current-schema store that path takes no
74
+ write lock (measured 3.4 ms); a fresh-store open performs DDL outside
75
+ the lease (documented; read-path split is a later cycle).
76
+ - A refused foreground verb has bounded wait semantics only; there is no
77
+ queueing or fairness between contending foreground writers.
78
+ - The lease label is advisory metadata for error attribution; mutual
79
+ exclusion rests on SQLite's write lock, always.
80
+ - `commitIngest` already re-reads staged/live state inside its
81
+ transaction — no TOCTOU change was needed (harness round 3 confirms the
82
+ shape).
83
+ - No schema change to the store; the lock sidecar holds no user data
84
+ (deleting it while no writer runs is safe — documented escape hatch).
@@ -0,0 +1,227 @@
1
+ # C1: copy-fallback read-only SQLite for native stores, 2026-09-16
2
+
3
+ ## Outcome
4
+
5
+ A native store held by its own writer no longer costs a coverage row. Before
6
+ this cycle the same fixtures — a 60-composer Cursor `state.vscdb` and the host's
7
+ real 239.1 MiB `opencode.db`, each held by a second connection — produced **zero
8
+ sessions and a `read_error` diagnostic** after ~1.1 s of `busy_timeout`. Now the
9
+ store is read from a private copy: 60/60 and 5/5 sessions, real transcripts, no
10
+ diagnostics, and the ledger store's schema and index shape are untouched.
11
+
12
+ - `packages/core/src/native/sqlite-ro.ts` (new) — probe → private copy →
13
+ give-up gradient, plus `nativeDbStamp` / `cachedCopyPath` invalidation.
14
+ - `withReadOnlyDb` moved out of `native/common.ts`; `native/cursor.ts`,
15
+ `native/opencode.ts`, `parentage.ts` (Hermes and OpenCode parent links) and
16
+ `normalizer.ts` (Hermes assembly) read every native store through it. Error
17
+ translation (`NativeInputError` mapping) and `reportNativeError` /
18
+ `read_error` diagnostics are unchanged.
19
+ - Tests: `tests/native-sqlite-ro.test.ts` (9 cases),
20
+ `tests/native-sqlite-ro-window.test.ts` (the checkpoint-wrap regression), one
21
+ held-store case per migrated caller (`parentage.test.ts` ×2,
22
+ `normalizer.test.ts` ×1), plus two adapter tests re-pinned to the new
23
+ contract (`native-cursor.test.ts`, `native-opencode.test.ts`).
24
+ - Gates: `bun test` 549 pass / 0 fail · `bun run typecheck` · `bun run build`.
25
+
26
+ ### The gradient
27
+
28
+ 1. **Probe.** Read-only open, `PRAGMA busy_timeout = 1000`, and
29
+ `SELECT count(*) AS tables FROM sqlite_master`, all inside one deferred
30
+ transaction, so the caller's read runs on the same snapshot.
31
+ 2. **Copy fallback.** Only `SQLITE_BUSY` / `SQLITE_LOCKED` reach it. Copy the
32
+ database and its sidecars into a private 0700 directory (files 0600), open
33
+ the copy writable, and read it. The copy is accepted only when the database
34
+ file's `size:mtime` identity is unchanged from immediately before the
35
+ database copy to after the last sidecar: a checkpoint can backfill pages into
36
+ the database *and* restart the wal generation mid-copy, leaving frames and
37
+ index individually valid while the base is stale. A moved database is
38
+ discarded and copied once more; a second move gives up. A copy root that is
39
+ not this uid's at mode 0700 is refused before anything is written.
40
+ 3. **Give up.** Copy refused (byte budget, IO, unopenable, root not private) →
41
+ the direct error surfaces unchanged, i.e. exactly today's
42
+ retryable-vs-`read_error` classification.
43
+
44
+ A 512 MiB `MAX_SQLITE_COPY_BYTES` bounds the copy set (database plus every
45
+ sidecar present); the largest live store measured on this host is 239.1 MiB, so
46
+ real stores fit with ~2× headroom while a runaway `-wal` is refused instead of
47
+ copied. Copies are private agent history: they live in
48
+ `$TMPDIR/ledger-native-db/copy-<hash>-<pid>` — created 0700, files 0600, and
49
+ refused outright if that root already exists with weaker permissions — and are
50
+ removed by a process `exit` hook, by supersession when the stamp moves, and —
51
+ after a crash — by a dead-pid sweep before the next copy.
52
+
53
+ ## Ported mechanics (Wake MIT, `iAmCorey/Wake@d9f702b6df0e85` tag v0.6.5 — mechanics with attribution, no code copied)
54
+
55
+ Pin: `d9f702b6df0e85763f8ab597c09bf4ca6a168db4`, `crates/wake-core/src/adapters/sqlite_ro.rs` (77 lines), MIT License © 2026 Corey Chiu.
56
+
57
+ | Ledger | Wake pin | What was kept |
58
+ | --- | --- | --- |
59
+ | read-only open + `SELECT count(*) FROM sqlite_master` probe | `sqlite_ro.rs#L20-L28` | probe-first, so a lock is detected *before* the caller's read runs and diagnostics cannot double-fire |
60
+ | copy `db` + `-wal` + `-shm` into a temp dir, read the copy | `sqlite_ro.rs#L31-L45` | same shape: create the dir, copy the database, copy the sidecars that exist, reopen |
61
+ | never `immutable=1` | `sqlite_ro.rs#L16-L19` | an immutable open ignores the `-wal`, which is where a live store keeps its newest sessions |
62
+ | stamp = `max(mtime(db), mtime(-wal))` | `db_cache_stamp`, `sqlite_ro.rs#L68-L76` | `-shm` excluded: its read-marks change on every read and would invalidate per call |
63
+ | temp dir removed with the reader | `TempDirGuard`, `sqlite_ro.rs#L6-L15` | TypeScript has no `Drop`; the `exit` hook plus dead-pid sweep is the equivalent |
64
+
65
+ Deliberate deviations, each with its reason:
66
+
67
+ - **`-journal` is copied, and the copy is opened writable.** A rollback-journal
68
+ writer can leave uncommitted pages in the database file; with the journal
69
+ present, SQLite rolls the copy back to the committed state instead of
70
+ reporting in-flight rows as committed. Writable also lets the copy rebuild a
71
+ `-shm` the source never had.
72
+ - **Sidecar order is `-shm` before `-wal`.** The copied index can then never
73
+ claim frames the copied `-wal` does not contain. An older consistent snapshot
74
+ is the safe direction; a short frame read is not.
75
+ - **The copy trigger is lock contention only.** A copy answers concurrency, not
76
+ unreachable bytes: a store whose `-wal` was deleted out-of-band is not revived
77
+ by copying it, and reading it at its last checkpoint would publish partial
78
+ evidence as a whole session (Ledger's "missing evidence remains unknown").
79
+ `SQLITE_CANTOPEN`/`SQLITE_CORRUPT`/`SQLITE_NOTADB` keep the direct diagnosis.
80
+ - **The stamp invalidates a copy, not just a row cache.** Wake keys row caches
81
+ by `db_cache_stamp` per query; here the whole sync reuses one private copy, so
82
+ discovery and all per-session reads of a held store cost one copy (measured
83
+ below).
84
+ - **The database base is re-checked after the copy.** Frames and the wal index
85
+ stay individually valid across a checkpoint that backfills pages into the
86
+ database and restarts the wal generation mid-copy, so the copy would silently
87
+ lose the pages the checkpoint moved. The database file's `size:mtime`
88
+ identity, captured before its copy and re-read after the last sidecar, is the
89
+ narrow witness of exactly that transition; a moved base discards the copy and
90
+ retries once. (Wake has no equivalent because it never had a checkpoint wrap
91
+ in the window — it copies the same set with the same window.)
92
+ - **The copy root must be private.** A root that is not this uid's at mode 0700
93
+ is refused before anything is written; the gradient then gives up to the
94
+ direct diagnosis. Roots are created at 0700 explicitly rather than inheriting
95
+ the mode of their parent.
96
+ - **Bounded copy size and crash-safe cleanup** are Ledger additions; Wake's
97
+ `open_sqlite_ro` has neither bound nor sweep.
98
+
99
+ ### Adversarial review round (2026-09-16, three findings — all fixed)
100
+
101
+ 1. **WAL generation loss on checkpoint-wrap during copy (high).** Reproduced
102
+ against this module's own order and open semantics: with a `t1` row living
103
+ only in the `-wal` and a second table `t2`, a checkpoint plus one `t2` insert
104
+ between the database copy and the `-wal` copy made the copy answer
105
+ `t1=[]`, `t2=['B']` with no error. Fixed by the database-identity recheck plus
106
+ one retry above. Regression test:
107
+ `tests/native-sqlite-ro-window.test.ts` — it runs the module's own source
108
+ with only its `node:fs` binding redirected to a shim that fires the
109
+ checkpoint exactly before the `-wal` copy (Bun binds builtins at load time
110
+ and refuses both plugin and mock overrides), asserts the substitution
111
+ changed nothing else, and asserts the hook ran. Verified sensitive: with the
112
+ recheck disabled the test fails with `t1: ["A"]` → `t1: []`; with it, the
113
+ copy is discarded and retaken and the read is `t1: ["A"], t2: ["B"]`.
114
+ 2. **Copy root ownership/mode unverified (medium).** `mkdirSync` silently
115
+ accepted a pre-existing foreign 0755 root: world-readable transcript copies
116
+ until the per-file chmod, predictable copy-directory names, a
117
+ `rmSync`/`mkdirSync` re-creation race, and a sweep that could be steered by a
118
+ planted dead-pid entry. Fixed by `privateRoot()` (this uid, mode 0700,
119
+ refusing otherwise) and by creating the root explicitly at 0700 — the old
120
+ code created it implicitly, and the leaf's mode option does not apply to it.
121
+ Regression test: "refuses a copy root that is not private instead of writing
122
+ transcripts there" (root set to 0755 → the direct error surfaces, no copy
123
+ written, the root's entries are unchanged).
124
+ 3. **Citation range (low).** `sqlite_ro.rs#L68-L80` pointed past the 77-line
125
+ file; corrected to `#L68-L76` here and in the module.
126
+
127
+ ## Position in the program (C1 row, `docs/research/2026-09-16-wake-port-program.md`)
128
+
129
+ - Failure it fixes: *"A store held by its own CLI costs a coverage row today
130
+ (`read_error`, `native/common.ts`). Wake's gradient converts locked stores to
131
+ working reads."*
132
+ - Open-source case: *"'Works on stores while the agent is using them' — first-run
133
+ experience on any live machine; the #1 operational complaint class for tools
134
+ like ours."*
135
+ - Ordering: C1 is independent and mechanical, so it runs first; nothing here
136
+ touches the index shape that C2–C5 will change.
137
+
138
+ ## Measurements
139
+
140
+ Host: Apple M1, macOS, APFS `/tmp`. Fixtures: 60 composers × 6 bubbles (128 KiB)
141
+ for the adapter passes; a copy of the host's live `opencode.db` (239.1 MiB) for
142
+ the real-store pass. "Before" is the pre-cycle `withReadOnlyDb` body, executed
143
+ from a pristine `git archive HEAD` tree in `/tmp` so both versions ran in the
144
+ same process against the same fixtures.
145
+
146
+ **OBSERVED**
147
+
148
+ | measurement | before | after |
149
+ | --- | --- | --- |
150
+ | unlocked store, discovery + 60 session reads (median of 20 passes) | 12.04 ms | 11.94 ms (−0.1 ms, noise) |
151
+ | unlocked store, single read at the module boundary (mean of 400) | 88.6 µs | 91.8 µs (+3.2 µs) |
152
+ | locked store, rollback-journal `BEGIN EXCLUSIVE`, full pass | 0 sessions, `read_error`, 1120 ms | 60 sessions, 0 diagnostics, 1147 ms |
153
+ | locked store, WAL `locking_mode = EXCLUSIVE`, full pass | 0 sessions, `read_error`, 1101 ms | 60 sessions, 0 diagnostics, 1150 ms |
154
+ | real `opencode.db` copy held by an exclusive-locking writer | 0 sessions, `read_error`, 1115 ms | 5 sessions discovered, 3 read (1,078,667 transcript chars), 0 diagnostics, 1192 ms |
155
+ | locked store per-read cost after the first copy | n/a (unreadable) | first read 2.5 ms (copy + open), remaining 59 reads 32.6 ms (0.53 ms median) |
156
+ | transcripts on an unlocked store | 99,060 chars | 99,060 chars (identical) |
157
+ | ledger index size delta | — | N/A: no DDL, no schema/index change; `SCHEMA_VERSION` and every table DDL are untouched by this cycle |
158
+ | copy root / file modes | — | 0700 root (`.../ledger-native-db`), 0600 files; emptied at process exit in CLI runs — the dead-pid sweep covers a hard stop, and the suite removes the root itself because the test runner's teardown skips `exit` hooks |
159
+
160
+ Reproduction: the harness was throwaway. Build the Cursor-shaped fixture
161
+ (`state.vscdb`, `cursorDiskKV`, 60 composers), hold it with a second connection
162
+ (`BEGIN EXCLUSIVE`, or WAL + `locking_mode = EXCLUSIVE` + a write), and run the
163
+ same discovery + per-session read pass through a pristine pre-cycle tree
164
+ (`git archive HEAD packages/core/src | tar -x -C /tmp/baseline`) and the working
165
+ tree in one process. The real-store pass copies the live store to `/tmp` first
166
+ and locks only the *copy* — the user's store is never locked, and the copy is
167
+ deleted afterwards. Every property above is pinned by
168
+ `tests/native-sqlite-ro.test.ts` and the two re-pinned adapter tests.
169
+
170
+ **INFERRED** (not directly measured, listed so reviewers can attack them)
171
+
172
+ - The ~1.1 s still spent on a locked store is the probe's `busy_timeout`
173
+ grace, kept deliberately: a transient lock (< 1 s) resolves without a copy,
174
+ and the copy is taken once per stamp instead of once per read.
175
+ - The database-identity recheck closes the checkpoint/restart transition and
176
+ nothing else: ordinary WAL commits never touch the database file, so a
177
+ healthy copy is never rejected for writing. A `-wal` identity check would fire
178
+ on every commit and was rejected for that reason.
179
+ - Residual exposure: a rollback-journal copy taken while the writer commits can
180
+ still capture a half-written page (there is no wal to validate it), so a
181
+ copy-derived malformed-row verdict is possible and cannot be separated from
182
+ genuinely malformed content. The ingest pipeline re-reads a session whenever
183
+ the source fingerprint moves, so the window is bounded by the next write.
184
+ - Cross-process reuse is deliberately absent (copies are deleted at exit), so a
185
+ new sync of a still-held store copies again; the alternative leaks private
186
+ transcripts into `/tmp` across runs.
187
+ - A copy root left at mode 0755 by a pre-check revision of this branch disables
188
+ the fallback until it is removed or chmodded (`rm -rf $TMPDIR/ledger-native-db`
189
+ restores it); no released build ever created a non-private root.
190
+ - A recycled pid can keep one orphaned copy directory until that pid dies;
191
+ bounded by `MAX_SQLITE_COPY_BYTES` per store.
192
+
193
+ ## Cutover audit (2026-09-16, after cloud round 1)
194
+
195
+ Round 1's medium was that `parentage.ts` and `normalizer.ts` still opened
196
+ native stores directly, so a held Hermes/OpenCode store could keep its parent
197
+ link and its session assembly while the adapter reads succeeded. Both now call
198
+ `withReadOnlyDb` from `native/sqlite-ro.js`; the check below is exhaustive for
199
+ `packages/core/src`:
200
+
201
+ | opener | target | status |
202
+ | --- | --- | --- |
203
+ | `native/sqlite-ro.ts` (probe, copy) | agent stores | the gradient itself |
204
+ | `native/cursor.ts`, `native/opencode.ts` | Cursor, OpenCode stores | migrated (cycle 1) |
205
+ | `parentage.ts` `extractHermesParentage`, `extractOpenCodeSqliteParentage` | Hermes, OpenCode stores | migrated (round 1 fix) |
206
+ | `normalizer.ts` Hermes branch | Hermes store | migrated (round 1 fix) |
207
+ | `db.ts`, `sync.ts`, `writer-lease.ts`, `schema-gate.ts`, `reader.ts` | the Ledger store and its lease database | out of scope by definition: these are Ledger's own artifacts, not another application's, and the store's write path must keep its own locking |
208
+
209
+ `node:sqlite`/`DatabaseSync` appears nowhere in `packages/core` (the mentions in
210
+ `confer/` and `lab/` are third-party notes and run artifacts).
211
+
212
+ Tests, one held-store case per migrated caller: `parentage.test.ts` "reads
213
+ Hermes parent_session_id while the store is held by its writer" and "reads
214
+ OpenCode parent_id while the store is held by its writer" (each asserts
215
+ `SQLITE_BUSY` on the direct route first, then the parent link, then that no
216
+ diagnostic fired), and `normalizer.test.ts` "normalizes a Hermes store held by
217
+ its writer through the copy fallback" (direct route asserted blocked, both
218
+ turns normalized, and the same call reads the parent link, so the case covers
219
+ the parentage migration too).
220
+
221
+ ## Remaining out of scope
222
+
223
+ - Non-lock recovery failures (WAL store whose `-shm` was deleted out-of-band)
224
+ stay `read_error`; enabling the copy there needs a decision about publishing
225
+ checkpoint-only evidence, not a code change.
226
+ - Program doc pins the remaining cycles C2–C10; the consolidated follow-up
227
+ issue for this cycle's nits is opened by the parent at merge time.