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.
- package/CHANGELOG.md +36 -0
- package/LICENSE +201 -0
- package/README.md +288 -2
- package/dist/cli.js +30210 -0
- package/dist/convert.d.ts +31 -0
- package/dist/convert.js +431 -0
- package/dist/deepagents_checkpoint.py +362 -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 +1233 -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 +150 -0
- package/ops/install-cli.sh +63 -0
- package/ops/install-native.sh +35 -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 +21 -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,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.
|