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,829 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The schema version this build writes: `PRAGMA user_version` on every writer
|
|
3
|
+
* open.
|
|
4
|
+
*
|
|
5
|
+
* 14 is reserved for `source_discovery` (PR #52): a store that ran that
|
|
6
|
+
* unmerged binary is at `user_version` 14 with discovery checkpoints and
|
|
7
|
+
* without `session_tombstones`. 15 is C7's `session_tombstones`. Both tables
|
|
8
|
+
* arrive from the same idempotent `CREATE_TABLES_SQL`; existing tombstones
|
|
9
|
+
* are never dropped.
|
|
10
|
+
*
|
|
11
|
+
* 16 adds nullable native-session and binding provenance to `records`.
|
|
12
|
+
* Writer opens migrate older layouts; read-only opens keep serving readable
|
|
13
|
+
* older stores without adding these optional columns. C8's `SYNC_STAMP_DDL`
|
|
14
|
+
* remains a contract only: this build does not install `sync_stamps`.
|
|
15
|
+
*/
|
|
16
|
+
export const SCHEMA_VERSION = 16;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The derive-rule counters: the version of the *rules* a derived index was
|
|
20
|
+
* produced by, recorded in `schema_meta` beside `SCHEMA_VERSION` rather than as
|
|
21
|
+
* part of it.
|
|
22
|
+
*
|
|
23
|
+
* The distinction is the whole point. A derived index is repairable from
|
|
24
|
+
* canonical, in-store data, so changing how it is derived must cost one
|
|
25
|
+
* re-derivation on the next writer pass — not a schema migration that every
|
|
26
|
+
* older build refuses outright. Wake splits the same way: `FTS_FORMAT` is
|
|
27
|
+
* compared at open and stamped next to a transient `fts_reindex` flag
|
|
28
|
+
* (`crates/wake-core/src/db.rs#L128-L136`, `#L176-L196`), while migrations move
|
|
29
|
+
* `NEWEST_COLUMN`/`NEWEST_TABLE` — the read-only gate, `#L231-L233`,
|
|
30
|
+
* `#L326-L334` — only when the store's *shape* changes.
|
|
31
|
+
*
|
|
32
|
+
* The value is a decimal integer: bump it by one whenever the derivation rules
|
|
33
|
+
* change, never re-use a value. Both the writer's detection and the read gate
|
|
34
|
+
* compare numerically, so the ordering stays monotonic in both directions (a
|
|
35
|
+
* `"10"` is newer than a `"2"`).
|
|
36
|
+
*
|
|
37
|
+
* `"1"` is the projection {@link FTS_REPOPULATE_SQL} implements — value for
|
|
38
|
+
* value what ingest writes (`insertRecordsInternal`, db.ts) and what the v11
|
|
39
|
+
* rebuild re-projected. `"2"` is that same projection with the self-referential
|
|
40
|
+
* echo filter applied (filter.ts): the records of an agent asking Ledger itself
|
|
41
|
+
* are canonical and complete, but no longer searchable material, so both ingest
|
|
42
|
+
* and the re-derivation leave them out of `records_fts`. `"3"` adds the title
|
|
43
|
+
* index ({@link TITLES_FTS_DDL}): a session whose title the store holds is
|
|
44
|
+
* searchable by that title, maintained by the triggers on `sessions` and
|
|
45
|
+
* re-derived by the same one-shot. The bump is what makes every store written
|
|
46
|
+
* before a change re-derive itself once on its next writer pass — exactly the
|
|
47
|
+
* shape change this counter exists to carry.
|
|
48
|
+
*/
|
|
49
|
+
export const FTS_FORMAT = "3";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The embeddings lane's counter: `"1"` is `getRecordEmbeddingText`
|
|
53
|
+
* (embeddings.ts), the text projection every stored vector was derived from.
|
|
54
|
+
* Provider identity and dimension are a separate axis, carried per row and
|
|
55
|
+
* already tracked by `compatibleEmbeddingCount`; this counter names only the
|
|
56
|
+
* rule that turns a record into the text that is embedded.
|
|
57
|
+
*/
|
|
58
|
+
export const EMBEDDINGS_FORMAT = "1";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* One derived index: where its rule version is recorded, the transient flag a
|
|
62
|
+
* mismatch raises, and what this build derives. Detection, the read gate and
|
|
63
|
+
* the tests all read this table, so a new derived index is one row here instead
|
|
64
|
+
* of a fourth special case.
|
|
65
|
+
*/
|
|
66
|
+
export interface DeriveCounter {
|
|
67
|
+
/** `schema_meta` key holding the rule version the store's data was derived under. */
|
|
68
|
+
readonly format: string;
|
|
69
|
+
/** `schema_meta` key set to `"1"` while a re-derivation is pending. */
|
|
70
|
+
readonly flag: string;
|
|
71
|
+
/** The rule version this build derives; bump on any rule change. */
|
|
72
|
+
readonly current: string;
|
|
73
|
+
/**
|
|
74
|
+
* Whether a store that records *no* value for this counter — every store
|
|
75
|
+
* written before the counter existed — counts as pending. True where this
|
|
76
|
+
* build can execute the re-derivation (the FTS projection: in-store, cheap,
|
|
77
|
+
* and it repairs drift on the way, which is Wake's stance for an unrecorded
|
|
78
|
+
* `fts_format`, `db.rs#L176-L196`); false where the re-derivation is deferred
|
|
79
|
+
* until its lane ships (the vectors lane). An unrecorded counter is not a
|
|
80
|
+
* *moved* one: the flag names a derivation that contradicts this build's
|
|
81
|
+
* rules, and a store that names nothing contradicts nothing. Raising it there
|
|
82
|
+
* would leave every upgraded store carrying a promise no pass in this build
|
|
83
|
+
* can keep.
|
|
84
|
+
*/
|
|
85
|
+
readonly flagsWhenUnrecorded: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* The tables a derivation under these rules produces. A matched counter
|
|
88
|
+
* claims its derivation happened, so a store that holds the value and not an
|
|
89
|
+
* output contradicts itself — the flag is raised for it exactly as a *moved*
|
|
90
|
+
* counter raises it. Empty where the derivation's output is not a table of
|
|
91
|
+
* its own (the vectors lane writes rows into `records_embeddings`, a table
|
|
92
|
+
* every store has carried since before the counter existed).
|
|
93
|
+
*/
|
|
94
|
+
readonly outputs: readonly string[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** In detection order, which is also the order a mismatch is reported in. */
|
|
98
|
+
export const DERIVE_COUNTERS: readonly DeriveCounter[] = [
|
|
99
|
+
{
|
|
100
|
+
format: "fts_format",
|
|
101
|
+
flag: "fts_reindex",
|
|
102
|
+
current: FTS_FORMAT,
|
|
103
|
+
flagsWhenUnrecorded: true,
|
|
104
|
+
// Rule "2" derives the index *and* the echo filter's set (C3), rule "3"
|
|
105
|
+
// the title index as well: a store whose counter names the current rule
|
|
106
|
+
// without one of those tables was derived by something that was not this
|
|
107
|
+
// rule — the indexes it serves and the tables that describe them would
|
|
108
|
+
// disagree, so the derivation runs again.
|
|
109
|
+
outputs: ["records_fts", "records_echo", "titles_fts"],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
format: "embeddings_format",
|
|
113
|
+
flag: "embeddings_rederive",
|
|
114
|
+
current: EMBEDDINGS_FORMAT,
|
|
115
|
+
flagsWhenUnrecorded: false,
|
|
116
|
+
outputs: [],
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The `records_fts` DDL, shared by the fresh-store DDL and the v11 rebuild so
|
|
122
|
+
* the two cannot drift. v11 adds no column, tokenizer, or option: the increment
|
|
123
|
+
* is re-deriving the index from `records` through the ingest projection, and
|
|
124
|
+
* what the cycle-8 measurements (docs/research/2026-09-13-fts-rebuild.md)
|
|
125
|
+
* support is the rebuild itself — hits identical, cost lower — not any
|
|
126
|
+
* particular internal layout.
|
|
127
|
+
*/
|
|
128
|
+
export const RECORDS_FTS_DDL = `CREATE VIRTUAL TABLE IF NOT EXISTS records_fts USING fts5(
|
|
129
|
+
record_id UNINDEXED,
|
|
130
|
+
session_id UNINDEXED,
|
|
131
|
+
source_type UNINDEXED,
|
|
132
|
+
record_type,
|
|
133
|
+
content,
|
|
134
|
+
tool_name,
|
|
135
|
+
tool_arguments,
|
|
136
|
+
tool_result,
|
|
137
|
+
tokenize="porter unicode61"
|
|
138
|
+
)`;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The `records` → `records_fts` projection, value for value what
|
|
142
|
+
* `insertRecordsInternal` writes at ingest time (filter.ts, "2" of the counter):
|
|
143
|
+
*
|
|
144
|
+
* - `record_id`/`session_id`/`source_type`/`record_type` come straight from the
|
|
145
|
+
* record row, which the ingest path wrote with the session's own id and
|
|
146
|
+
* source for native ingest; managed projections retain each record's native
|
|
147
|
+
* source. `records` remains the projection's source of truth in both cases.
|
|
148
|
+
* - `content`, `tool_name`, and `tool_arguments` index `NULL` as `''`, exactly
|
|
149
|
+
* as the ingest path's `?? ""` does.
|
|
150
|
+
* - `tool_result` prefers `tool_result_json` and falls back to `content` for
|
|
151
|
+
* tool records, then to `''` — the fallback that makes a tool record
|
|
152
|
+
* searchable by its result text when the adapter carried no result JSON.
|
|
153
|
+
*
|
|
154
|
+
* The projection itself is whole-table: the self-referential echo filter is a
|
|
155
|
+
* rule over canonical fields, so it is applied *to this projection* rather than
|
|
156
|
+
* inside it (the rebuild removes the filter's rows from the fresh index, db.ts;
|
|
157
|
+
* ingest simply never writes them). Keeping the SQL whole-table is what makes
|
|
158
|
+
* the rebuild a repair — it re-projects every record, then drops exactly the
|
|
159
|
+
* rows the rule names, so an index that drifted in either direction comes back
|
|
160
|
+
* equal to `records` minus the filter, not merely equal to a claim about it.
|
|
161
|
+
*/
|
|
162
|
+
export const FTS_REPOPULATE_SQL = `
|
|
163
|
+
INSERT INTO records_fts (
|
|
164
|
+
record_id, session_id, source_type, record_type, content,
|
|
165
|
+
tool_name, tool_arguments, tool_result
|
|
166
|
+
)
|
|
167
|
+
SELECT
|
|
168
|
+
id, session_id, source_type, record_type,
|
|
169
|
+
COALESCE(content, ''),
|
|
170
|
+
COALESCE(tool_name, ''),
|
|
171
|
+
COALESCE(tool_arguments_json, ''),
|
|
172
|
+
COALESCE(tool_result_json, CASE WHEN record_type = 'tool' THEN content END, '')
|
|
173
|
+
FROM records;`;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The v10 → v11 rebuild. The index is derived — `records` is the authoritative
|
|
177
|
+
* side — so the increment discards the old table and repopulates it, never
|
|
178
|
+
* editing it in place: a rebuild also repairs an index that had drifted from
|
|
179
|
+
* `records`, which merging segments in place cannot. The DDL is unchanged, so
|
|
180
|
+
* nothing but the marker and the re-derived index distinguishes a rebuilt store
|
|
181
|
+
* on paper: the migration transaction that runs this re-applies automerge and
|
|
182
|
+
* runs the post-copy gates (db.ts) before `user_version = 11`.
|
|
183
|
+
*/
|
|
184
|
+
export const FTS_REBUILD_MIGRATION_SQL = `
|
|
185
|
+
DROP TABLE IF EXISTS records_fts;
|
|
186
|
+
${RECORDS_FTS_DDL};
|
|
187
|
+
${FTS_REPOPULATE_SQL}
|
|
188
|
+
`;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The title index (format `"3"`): one row per session the store holds a title
|
|
192
|
+
* for, searched as a session-level lane of its own.
|
|
193
|
+
*
|
|
194
|
+
* Standalone rather than a column of `records_fts`, because a title is not a
|
|
195
|
+
* record: it belongs to the session, it has no record id to point at, and
|
|
196
|
+
* folding it into the record index would mean either a fake row (inventing a
|
|
197
|
+
* record that no adapter wrote) or a title indexed once per record of its
|
|
198
|
+
* session. Wake splits the same way for the same reason, `crates/wake-core/src/db.rs#L126-L130`:
|
|
199
|
+
* `titles_fts(key UNINDEXED, title)`, the key carried only to find and delete
|
|
200
|
+
* the row — a session's title is not a transcription message and has no `seq`
|
|
201
|
+
* to pair with, so it gets its own table instead of a fabricated message.
|
|
202
|
+
*
|
|
203
|
+
* `session_id` takes Wake's `key` role: UNINDEXED, present only so maintenance
|
|
204
|
+
* and joins can reach the row that belongs to one session. The tokenizer is the
|
|
205
|
+
* record index's (`porter unicode61`) and deliberately not Wake's trigram — C4
|
|
206
|
+
* is the trigram-free first pass, and the tokenizer decision is C5's
|
|
207
|
+
* (docs/research/ledger/docs/research/2026-09-16-wake-port-program.md).
|
|
208
|
+
*/
|
|
209
|
+
export const TITLES_FTS_DDL = `CREATE VIRTUAL TABLE IF NOT EXISTS titles_fts USING fts5(
|
|
210
|
+
session_id UNINDEXED,
|
|
211
|
+
title,
|
|
212
|
+
tokenize="porter unicode61"
|
|
213
|
+
)`;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* What makes a session's title indexable: a title the store actually holds.
|
|
217
|
+
* `NULL != ''` is `NULL`, and `NULL` is not true, so this one comparison covers
|
|
218
|
+
* an absent title as well as an empty one. It is a single constant because the
|
|
219
|
+
* repopulation, the maintenance triggers, and the rebuild's parity gate must
|
|
220
|
+
* agree on the set: a gate that counts something other than what the writer
|
|
221
|
+
* indexes is a gate that cannot hold.
|
|
222
|
+
*/
|
|
223
|
+
export const INDEXABLE_TITLE_SQL = "title != ''";
|
|
224
|
+
|
|
225
|
+
/** The `sessions` → `titles_fts` projection: value for value what the triggers maintain. */
|
|
226
|
+
export const TITLES_FTS_REPOPULATE_SQL = `
|
|
227
|
+
INSERT INTO titles_fts (session_id, title)
|
|
228
|
+
SELECT id, title FROM sessions WHERE ${INDEXABLE_TITLE_SQL};`;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The title index's whole re-derivation: discard and repopulate, exactly as
|
|
232
|
+
* `records_fts` is rebuilt — a rebuild is also the repair for an index that
|
|
233
|
+
* drifted from `sessions`, which deleting the one stale row would not be.
|
|
234
|
+
* The maintenance triggers live on `sessions`, so they survive this.
|
|
235
|
+
*/
|
|
236
|
+
export const TITLES_FTS_REBUILD_SQL = `
|
|
237
|
+
DROP TABLE IF EXISTS titles_fts;
|
|
238
|
+
${TITLES_FTS_DDL};
|
|
239
|
+
${TITLES_FTS_REPOPULATE_SQL}
|
|
240
|
+
`;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The title index's maintenance, as triggers on `sessions` so every write point
|
|
244
|
+
* has it and none has to remember it.
|
|
245
|
+
*
|
|
246
|
+
* The index is a projection of one canonical column, so the rule keeping it
|
|
247
|
+
* equal to that column is a predicate rather than a decision — the case where a
|
|
248
|
+
* trigger is right and a call site is not. `saveSession` writes a title on all
|
|
249
|
+
* three of its paths, `appendSessionRecords` writes the row without touching
|
|
250
|
+
* the title, `promoteStagedIngest` deletes the predecessor and upserts the
|
|
251
|
+
* replacement: a trigger covers all of them, covers whatever a later cycle
|
|
252
|
+
* adds, and covers a raw connection that bypasses the helpers (foreign keys off
|
|
253
|
+
* or not) — the reason `sessions` already carries
|
|
254
|
+
* `trg_sessions_diagnostics_delete`. A deleted session's title row goes with
|
|
255
|
+
* it, which no foreign key can enforce for a virtual table.
|
|
256
|
+
*
|
|
257
|
+
* The update trigger fires only when the title actually changed (`IS NOT`, so
|
|
258
|
+
* an absent title is compared too) and re-indexes that session's one row by
|
|
259
|
+
* delete-then-insert — Wake's shape, whose `upsert_session` is the single write
|
|
260
|
+
* point for the same pair (`db.rs#L1969-L1976`: `DELETE FROM titles_fts WHERE
|
|
261
|
+
* key = ?` then an insert when the title is not empty). `commitStagedIngest`'s
|
|
262
|
+
* `INSERT … ON CONFLICT DO UPDATE` takes the update path (and this trigger)
|
|
263
|
+
* when it changes a title and the insert path when the session is new, so
|
|
264
|
+
* either arrival lands in the index exactly once. The `DELETE` matches on an
|
|
265
|
+
* UNINDEXED column, which SQLite resolves with a scan of a table holding one
|
|
266
|
+
* row per titled session (Wake measures the same scan for the same reason).
|
|
267
|
+
*/
|
|
268
|
+
export const TITLES_FTS_TRIGGERS_SQL = `
|
|
269
|
+
CREATE TRIGGER IF NOT EXISTS trg_sessions_titles_insert
|
|
270
|
+
AFTER INSERT ON sessions
|
|
271
|
+
FOR EACH ROW
|
|
272
|
+
WHEN NEW.title != ''
|
|
273
|
+
BEGIN
|
|
274
|
+
INSERT INTO titles_fts (session_id, title) VALUES (NEW.id, NEW.title);
|
|
275
|
+
END;
|
|
276
|
+
|
|
277
|
+
CREATE TRIGGER IF NOT EXISTS trg_sessions_titles_update
|
|
278
|
+
AFTER UPDATE OF title ON sessions
|
|
279
|
+
FOR EACH ROW
|
|
280
|
+
WHEN NEW.title IS NOT OLD.title
|
|
281
|
+
BEGIN
|
|
282
|
+
DELETE FROM titles_fts WHERE session_id = NEW.id;
|
|
283
|
+
INSERT INTO titles_fts (session_id, title) SELECT NEW.id, NEW.title WHERE NEW.title != '';
|
|
284
|
+
END;
|
|
285
|
+
|
|
286
|
+
CREATE TRIGGER IF NOT EXISTS trg_sessions_titles_delete
|
|
287
|
+
AFTER DELETE ON sessions
|
|
288
|
+
FOR EACH ROW
|
|
289
|
+
BEGIN
|
|
290
|
+
DELETE FROM titles_fts WHERE session_id = OLD.id;
|
|
291
|
+
END;
|
|
292
|
+
`;
|
|
293
|
+
|
|
294
|
+
export const CREATE_TABLES_SQL = `
|
|
295
|
+
CREATE TABLE IF NOT EXISTS source_discovery (
|
|
296
|
+
source TEXT PRIMARY KEY,
|
|
297
|
+
root_key TEXT NOT NULL,
|
|
298
|
+
cursor TEXT,
|
|
299
|
+
status TEXT NOT NULL CHECK (status IN ('pending', 'exhausted', 'unknown')),
|
|
300
|
+
degraded INTEGER NOT NULL CHECK (degraded IN (0, 1)),
|
|
301
|
+
last_completed_at TEXT,
|
|
302
|
+
updated_at TEXT NOT NULL
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
306
|
+
id TEXT PRIMARY KEY,
|
|
307
|
+
source TEXT NOT NULL,
|
|
308
|
+
source_path TEXT NOT NULL,
|
|
309
|
+
content_hash TEXT NOT NULL,
|
|
310
|
+
content_hash_version INTEGER NOT NULL DEFAULT 1,
|
|
311
|
+
title TEXT,
|
|
312
|
+
cwd TEXT,
|
|
313
|
+
model TEXT,
|
|
314
|
+
started_at TEXT,
|
|
315
|
+
ended_at TEXT,
|
|
316
|
+
record_count INTEGER NOT NULL DEFAULT 0,
|
|
317
|
+
created_at TEXT NOT NULL,
|
|
318
|
+
updated_at TEXT NOT NULL,
|
|
319
|
+
parent_session_id TEXT,
|
|
320
|
+
parent_tool_use_id TEXT,
|
|
321
|
+
agent_nickname TEXT,
|
|
322
|
+
thread_role TEXT
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_source ON sessions(source);
|
|
326
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_content_hash ON sessions(content_hash);
|
|
327
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_started_at ON sessions(started_at);
|
|
328
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_parent_session_id ON sessions(parent_session_id);
|
|
329
|
+
|
|
330
|
+
CREATE TABLE IF NOT EXISTS records (
|
|
331
|
+
id TEXT PRIMARY KEY,
|
|
332
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
333
|
+
source_type TEXT NOT NULL,
|
|
334
|
+
native_session_id TEXT,
|
|
335
|
+
binding_id TEXT,
|
|
336
|
+
record_type TEXT NOT NULL,
|
|
337
|
+
stable_source_record_id TEXT,
|
|
338
|
+
source_identity_kind TEXT,
|
|
339
|
+
source_order_id TEXT NOT NULL,
|
|
340
|
+
component_index INTEGER NOT NULL DEFAULT 0,
|
|
341
|
+
record_timestamp TEXT,
|
|
342
|
+
source_timestamp TEXT,
|
|
343
|
+
content TEXT,
|
|
344
|
+
tool_call_id TEXT,
|
|
345
|
+
tool_name TEXT,
|
|
346
|
+
tool_arguments_json TEXT,
|
|
347
|
+
tool_result_json TEXT,
|
|
348
|
+
tool_result_ok INTEGER,
|
|
349
|
+
record_json TEXT NOT NULL,
|
|
350
|
+
content_hash TEXT NOT NULL,
|
|
351
|
+
record_hash TEXT NOT NULL
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
CREATE INDEX IF NOT EXISTS idx_records_session_id ON records(session_id);
|
|
355
|
+
CREATE INDEX IF NOT EXISTS idx_records_record_type ON records(record_type);
|
|
356
|
+
CREATE INDEX IF NOT EXISTS idx_records_source_order ON records(session_id, source_order_id, component_index);
|
|
357
|
+
CREATE INDEX IF NOT EXISTS idx_records_content_hash ON records(content_hash);
|
|
358
|
+
|
|
359
|
+
${RECORDS_FTS_DDL};
|
|
360
|
+
|
|
361
|
+
-- The title index (format "3"): a session-level lane beside the record index.
|
|
362
|
+
-- Created for every store on every writer open — an older store gets the empty
|
|
363
|
+
-- table and its maintenance triggers here, and the derive-rule counter's
|
|
364
|
+
-- one-shot fills it on the next pass.
|
|
365
|
+
${TITLES_FTS_DDL};
|
|
366
|
+
|
|
367
|
+
-- The store's own bookkeeping, not user data: the derive-rule counters
|
|
368
|
+
-- (DERIVE_COUNTERS) and the transient re-derivation flags a mismatch raises.
|
|
369
|
+
-- Kept out of SCHEMA_VERSION on purpose — migrations may add and drop keys here
|
|
370
|
+
-- freely without refusing an older build's read path (Wake keeps the same split
|
|
371
|
+
-- in its own schema_meta, db.rs#L25).
|
|
372
|
+
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
373
|
+
key TEXT PRIMARY KEY,
|
|
374
|
+
value TEXT NOT NULL
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
CREATE TABLE IF NOT EXISTS records_embeddings (
|
|
378
|
+
record_id TEXT PRIMARY KEY REFERENCES records(id) ON DELETE CASCADE,
|
|
379
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
380
|
+
embedding BLOB NOT NULL,
|
|
381
|
+
provider TEXT,
|
|
382
|
+
dim INTEGER,
|
|
383
|
+
created_at TEXT NOT NULL
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
CREATE INDEX IF NOT EXISTS idx_records_embeddings_session_id ON records_embeddings(session_id);
|
|
387
|
+
CREATE INDEX IF NOT EXISTS idx_records_embeddings_provider ON records_embeddings(provider, dim);
|
|
388
|
+
|
|
389
|
+
-- The self-referential echo filter's derived set (filter.ts): the record ids
|
|
390
|
+
-- excluded from records_fts, each with the session that owns it. Derived state
|
|
391
|
+
-- on the same footing as the index — written by the same ingest decision that
|
|
392
|
+
-- skips the FTS row, deleted with its record and its session through the
|
|
393
|
+
-- foreign keys below, and rebuilt from the rule by the index re-derivation —
|
|
394
|
+
-- never part of the records table, so canonical storage stays exactly what the
|
|
395
|
+
-- adapters produced. A store written before this table keeps reading: readers
|
|
396
|
+
-- never need it, and a writer creates it on open.
|
|
397
|
+
CREATE TABLE IF NOT EXISTS records_echo (
|
|
398
|
+
record_id TEXT PRIMARY KEY REFERENCES records(id) ON DELETE CASCADE,
|
|
399
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
-- User-initiated deletion (v15): one row per session a user deleted through the
|
|
403
|
+
-- explicit verb (ledger delete), and the only state the mechanism adds. These
|
|
404
|
+
-- rows are canonical, not derived: a tombstone is not a projection of anything,
|
|
405
|
+
-- so no rule can recompute it — it has no derive-rule counter, and the
|
|
406
|
+
-- re-derivation one-shot neither reads nor writes it.
|
|
407
|
+
--
|
|
408
|
+
-- The Ledger session id is the primary key because that is the axis the serving
|
|
409
|
+
-- path knows: every read verb resolves sessions through sessions.id, so one
|
|
410
|
+
-- probe per served row hides the session, while every canonical row it owns (and
|
|
411
|
+
-- every derived artifact it produced) is left exactly where it was. source and
|
|
412
|
+
-- native_id are the axis a re-scan can name *before* it reads anything, which is
|
|
413
|
+
-- what stops a copy of the source at another path from re-admitting the session.
|
|
414
|
+
-- source_path is provenance for the audit and deliberately not an admission key:
|
|
415
|
+
-- a path is not listing identity in this store — shared stores such as hermes
|
|
416
|
+
-- hold many distinct listings at one path (db.ts, getLiveIngestSession) — so
|
|
417
|
+
-- keying admission on it would block every other listing that shares the file.
|
|
418
|
+
--
|
|
419
|
+
-- Wake keeps the same pair for the same reason (crates/wake-core/src/db.rs,
|
|
420
|
+
-- #L76-L80: tombstones(file_path PK, key, deleted_at); #L537-L571: the row is
|
|
421
|
+
-- written in the same transaction as the removal; #L1002-L1018: both axes are
|
|
422
|
+
-- probed). Ledger's axes differ because its identity does: identity.ts keys a
|
|
423
|
+
-- session as source:native-id, stable across copies and content edits, while a
|
|
424
|
+
-- file path is not an identity at all.
|
|
425
|
+
--
|
|
426
|
+
-- mode is the one rewritable column: suppress (the default) closes serving and
|
|
427
|
+
-- admission and leaves every canonical row in place; hard removes the session's
|
|
428
|
+
-- records, vectors, echo markers, titles, diagnostics and staged replacements in
|
|
429
|
+
-- the same transaction, so the tombstone is all that is left of it. record_count
|
|
430
|
+
-- and deleted_at are the deletion event's own facts and are not restamped when a
|
|
431
|
+
-- suppress tombstone is escalated.
|
|
432
|
+
--
|
|
433
|
+
-- No derive-rule counter moves for this table — FTS_FORMAT stays "3": the
|
|
434
|
+
-- records-to-records_fts projection is untouched, and a tombstone is not a rule
|
|
435
|
+
-- over canonical record fields. Hiding by editing the index instead would have
|
|
436
|
+
-- to be re-derived by whoever next changes the rules, and the rebuild's parity
|
|
437
|
+
-- gate (records minus excluded equals indexed, db.ts) would be counting
|
|
438
|
+
-- suppression state as though it were derived data.
|
|
439
|
+
CREATE TABLE IF NOT EXISTS session_tombstones (
|
|
440
|
+
session_id TEXT PRIMARY KEY,
|
|
441
|
+
source TEXT NOT NULL,
|
|
442
|
+
native_id TEXT NOT NULL,
|
|
443
|
+
source_path TEXT,
|
|
444
|
+
mode TEXT NOT NULL CHECK (mode IN ('suppress', 'hard')),
|
|
445
|
+
reason TEXT NOT NULL,
|
|
446
|
+
record_count INTEGER NOT NULL,
|
|
447
|
+
deleted_at TEXT NOT NULL
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
-- The admission axis: every discovered listing probes (source, native id) once
|
|
451
|
+
-- per pass, and deleted sessions accumulate, so the pair carries an index even
|
|
452
|
+
-- though the table starts empty.
|
|
453
|
+
CREATE INDEX IF NOT EXISTS idx_session_tombstones_native
|
|
454
|
+
ON session_tombstones(source, native_id);
|
|
455
|
+
|
|
456
|
+
CREATE TABLE IF NOT EXISTS ingest_cursors (
|
|
457
|
+
source TEXT NOT NULL,
|
|
458
|
+
listing_id TEXT NOT NULL,
|
|
459
|
+
source_path TEXT NOT NULL,
|
|
460
|
+
session_id TEXT,
|
|
461
|
+
mtime TEXT NOT NULL,
|
|
462
|
+
size INTEGER NOT NULL,
|
|
463
|
+
bytes_ingested INTEGER NOT NULL,
|
|
464
|
+
record_count INTEGER NOT NULL DEFAULT 0,
|
|
465
|
+
status TEXT NOT NULL,
|
|
466
|
+
updated_at TEXT NOT NULL,
|
|
467
|
+
file_identity TEXT,
|
|
468
|
+
prefix_hash TEXT,
|
|
469
|
+
checkpoint_version INTEGER,
|
|
470
|
+
line_count INTEGER,
|
|
471
|
+
tail_offset INTEGER,
|
|
472
|
+
tail_hash TEXT,
|
|
473
|
+
PRIMARY KEY (source, listing_id, source_path)
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
CREATE INDEX IF NOT EXISTS idx_ingest_cursors_session_id ON ingest_cursors(session_id);
|
|
477
|
+
|
|
478
|
+
CREATE TABLE IF NOT EXISTS ingest_queue (
|
|
479
|
+
source TEXT NOT NULL,
|
|
480
|
+
listing_id TEXT NOT NULL,
|
|
481
|
+
source_path TEXT NOT NULL,
|
|
482
|
+
source_updated_at TEXT,
|
|
483
|
+
title TEXT,
|
|
484
|
+
size_bytes INTEGER,
|
|
485
|
+
native_json TEXT,
|
|
486
|
+
enqueued_at TEXT NOT NULL,
|
|
487
|
+
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
488
|
+
next_attempt_at TEXT,
|
|
489
|
+
last_attempt_at TEXT,
|
|
490
|
+
last_outcome TEXT,
|
|
491
|
+
PRIMARY KEY (source, listing_id, source_path)
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
CREATE INDEX IF NOT EXISTS idx_ingest_queue_ready
|
|
495
|
+
ON ingest_queue(next_attempt_at, source_updated_at, enqueued_at);
|
|
496
|
+
|
|
497
|
+
CREATE TABLE IF NOT EXISTS ingest_runtime_state (
|
|
498
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
499
|
+
scale_permille INTEGER NOT NULL CHECK (scale_permille BETWEEN 250 AND 1000),
|
|
500
|
+
calm_runs INTEGER NOT NULL CHECK (calm_runs >= 0),
|
|
501
|
+
last_severity TEXT NOT NULL CHECK (last_severity IN ('unavailable', 'calm', 'constrained', 'critical')),
|
|
502
|
+
updated_at TEXT NOT NULL
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
-- Rebuild generations retain canonical records until the replacement is complete.
|
|
506
|
+
-- They are not a raw source archive and are removed when promoted to the live index.
|
|
507
|
+
CREATE TABLE IF NOT EXISTS ingest_staged_sessions (
|
|
508
|
+
source TEXT NOT NULL,
|
|
509
|
+
listing_id TEXT NOT NULL,
|
|
510
|
+
source_path TEXT NOT NULL,
|
|
511
|
+
replaced_session_id TEXT NOT NULL,
|
|
512
|
+
session_json TEXT NOT NULL,
|
|
513
|
+
diagnostics_json TEXT,
|
|
514
|
+
PRIMARY KEY (source, listing_id, source_path)
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
CREATE TABLE IF NOT EXISTS ingest_staged_records (
|
|
518
|
+
source TEXT NOT NULL,
|
|
519
|
+
listing_id TEXT NOT NULL,
|
|
520
|
+
source_path TEXT NOT NULL,
|
|
521
|
+
id TEXT NOT NULL,
|
|
522
|
+
record_type TEXT NOT NULL,
|
|
523
|
+
source_order_id TEXT NOT NULL,
|
|
524
|
+
component_index INTEGER NOT NULL,
|
|
525
|
+
content_hash TEXT NOT NULL,
|
|
526
|
+
record_hash TEXT NOT NULL,
|
|
527
|
+
canonical_json TEXT NOT NULL,
|
|
528
|
+
PRIMARY KEY (source, listing_id, source_path, id),
|
|
529
|
+
FOREIGN KEY (source, listing_id, source_path)
|
|
530
|
+
REFERENCES ingest_staged_sessions(source, listing_id, source_path) ON DELETE CASCADE
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
CREATE INDEX IF NOT EXISTS idx_ingest_staged_records_order
|
|
534
|
+
ON ingest_staged_records(source, listing_id, source_path, source_order_id, component_index);
|
|
535
|
+
|
|
536
|
+
-- Normalization diagnostics of the records currently committed for a session,
|
|
537
|
+
-- aggregated to one row per code. Upstream emits one diagnostic per record, so
|
|
538
|
+
-- a single corpus can carry thousands of same-code findings; rows stay bounded
|
|
539
|
+
-- by (sessions x codes) instead. A publishing commit replaces its session's
|
|
540
|
+
-- rows, so re-importing identical content is idempotent and codes that stop
|
|
541
|
+
-- occurring disappear. Diagnostic text never carries raw transcript content.
|
|
542
|
+
CREATE TABLE IF NOT EXISTS ingest_diagnostics (
|
|
543
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
544
|
+
code TEXT NOT NULL,
|
|
545
|
+
source TEXT NOT NULL,
|
|
546
|
+
occurrence_count INTEGER NOT NULL,
|
|
547
|
+
sample TEXT,
|
|
548
|
+
first_seen_at TEXT NOT NULL,
|
|
549
|
+
last_seen_at TEXT NOT NULL,
|
|
550
|
+
PRIMARY KEY (session_id, code)
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
-- A session's findings describe records that are gone once the session is gone.
|
|
554
|
+
-- Enforce that for every writer, including raw connections that never enable
|
|
555
|
+
-- foreign keys, where the cascade above does not fire.
|
|
556
|
+
CREATE TRIGGER IF NOT EXISTS trg_sessions_diagnostics_delete
|
|
557
|
+
AFTER DELETE ON sessions
|
|
558
|
+
FOR EACH ROW
|
|
559
|
+
BEGIN
|
|
560
|
+
DELETE FROM ingest_diagnostics WHERE session_id = OLD.id;
|
|
561
|
+
END;
|
|
562
|
+
|
|
563
|
+
-- The title index's maintenance (format "3"): the same argument as the
|
|
564
|
+
-- diagnostics trigger above — a session's title describes a session, and the
|
|
565
|
+
-- index that holds it must follow every writer, not the call sites that
|
|
566
|
+
-- remember to.
|
|
567
|
+
${TITLES_FTS_TRIGGERS_SQL}
|
|
568
|
+
-- What the last lease-holding pass saw of each source. Scan outcomes are
|
|
569
|
+
-- recomputed per scan and were dropped on the floor; this table is the durable
|
|
570
|
+
-- half, so the status verb reports which sources are listable without running a
|
|
571
|
+
-- scan. It is state, not history: one row per source, rewritten in place by
|
|
572
|
+
-- every later pass (last verdict wins, including a source that stops being
|
|
573
|
+
-- listable), no TTL, no counters. first_seen_at is stamped once and survives
|
|
574
|
+
-- while the source keeps being seen, so the row also carries how long the
|
|
575
|
+
-- source has been covered; detail is a scan-outcome detail, capped and free
|
|
576
|
+
-- of store content. status carries the scanner's closed outcome set as a CHECK,
|
|
577
|
+
-- like the runtime state's severity: the set is closed upstream, so a value
|
|
578
|
+
-- outside it must fail at the write instead of surfacing as an unknown status
|
|
579
|
+
-- in a report. v12 adds only this table — no column, index, or migration
|
|
580
|
+
-- statement beyond the DDL, because an earlier store is migrated by running
|
|
581
|
+
-- this same CREATE_TABLES_SQL (db.ts, migrate).
|
|
582
|
+
CREATE TABLE IF NOT EXISTS source_coverage (
|
|
583
|
+
source TEXT PRIMARY KEY,
|
|
584
|
+
root TEXT NOT NULL,
|
|
585
|
+
status TEXT NOT NULL CHECK (status IN (
|
|
586
|
+
'listable', 'missing_root', 'root_wrong_shape', 'unreadable_root', 'listing_failed'
|
|
587
|
+
)),
|
|
588
|
+
discovered INTEGER NOT NULL,
|
|
589
|
+
detail TEXT,
|
|
590
|
+
first_seen_at TEXT NOT NULL,
|
|
591
|
+
updated_at TEXT NOT NULL
|
|
592
|
+
);
|
|
593
|
+
`;
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* The read contract, as (table, column) pairs. A `null` column means the pair
|
|
597
|
+
* is satisfied by the table's presence alone — these tables are queried by
|
|
598
|
+
* name, so an absent one is the gap to name.
|
|
599
|
+
*
|
|
600
|
+
* The list is derived from the read path, not from the DDL:
|
|
601
|
+
*
|
|
602
|
+
* - **What a read verb names.** `search`/`recent`/`status` join `sessions`
|
|
603
|
+
* (including the parentage columns in the hit projection), order by
|
|
604
|
+
* `updated_at`/`started_at`/`source_order_id`, MATCH `records_fts`, read
|
|
605
|
+
* whole `records` rows (including `record_json`), and score
|
|
606
|
+
* `records_embeddings` (`provider`, `dim`, `embedding`).
|
|
607
|
+
* - **What the row contracts project.** `SELECT *` into `SessionRecord` /
|
|
608
|
+
* `CanonicalDbRecord` types every other column below.
|
|
609
|
+
*
|
|
610
|
+
* Deliberately absent: `content_hash_version`, optional record provenance
|
|
611
|
+
* (`native_session_id`, `binding_id`), the ingest tables (cursors,
|
|
612
|
+
* queue, runtime state, staging, diagnostics — `status` treats diagnostics as
|
|
613
|
+
* optional coverage), and index presence. No read verb names them, so requiring
|
|
614
|
+
* them would refuse a store the read path can serve. `records_fts` is required
|
|
615
|
+
* as a real FTS5 index (its shadow table is the witness), which is a table
|
|
616
|
+
* check, not an index check.
|
|
617
|
+
*
|
|
618
|
+
* The boundary this draws is real, and the migration-fixture test pins it: a
|
|
619
|
+
* shape written before the parentage columns existed (schema v3 and older)
|
|
620
|
+
* cannot serve `search` at all — the FTS projection names `s.parent_session_id`
|
|
621
|
+
* — so it is refused *by name* here instead of dying on a raw `no such column`.
|
|
622
|
+
* A writer open (`ledger sync`/`ledger build`) migrates it and the reads follow.
|
|
623
|
+
* `user_version < SCHEMA_VERSION` with these pairs present stays readable and
|
|
624
|
+
* keeps serving reads (`status` semantics); this gate never migrates.
|
|
625
|
+
*/
|
|
626
|
+
export const REQUIRED_READ_SCHEMA: readonly (readonly [table: string, column: string | null])[] = [
|
|
627
|
+
// An absent table surfaces from the column walk itself: an empty
|
|
628
|
+
// `PRAGMA table_info` probe reports the bare table name, so `records` and
|
|
629
|
+
// `records_embeddings` need no presence pair. The two column-less tables
|
|
630
|
+
// (`records_fts`, `records_fts_data`) are the ones whose presence pair is
|
|
631
|
+
// itself the check.
|
|
632
|
+
["records", "id"],
|
|
633
|
+
["records", "session_id"],
|
|
634
|
+
["records", "source_type"],
|
|
635
|
+
["records", "record_type"],
|
|
636
|
+
["records", "stable_source_record_id"],
|
|
637
|
+
["records", "source_identity_kind"],
|
|
638
|
+
["records", "source_order_id"],
|
|
639
|
+
["records", "component_index"],
|
|
640
|
+
["records", "record_timestamp"],
|
|
641
|
+
["records", "source_timestamp"],
|
|
642
|
+
["records", "content"],
|
|
643
|
+
["records", "tool_call_id"],
|
|
644
|
+
["records", "tool_name"],
|
|
645
|
+
["records", "tool_arguments_json"],
|
|
646
|
+
["records", "tool_result_json"],
|
|
647
|
+
["records", "tool_result_ok"],
|
|
648
|
+
["records", "record_json"],
|
|
649
|
+
["records", "content_hash"],
|
|
650
|
+
["records", "record_hash"],
|
|
651
|
+
["records_embeddings", "record_id"],
|
|
652
|
+
["records_embeddings", "session_id"],
|
|
653
|
+
["records_embeddings", "embedding"],
|
|
654
|
+
["records_embeddings", "provider"],
|
|
655
|
+
["records_embeddings", "dim"],
|
|
656
|
+
["records_fts", null],
|
|
657
|
+
// The witness that `records_fts` is really the FTS5 index: only a genuine
|
|
658
|
+
// FTS5 virtual table has shadow tables, and a plain table with the same name
|
|
659
|
+
// and columns would pass every column check and then die on `MATCH`. Checked
|
|
660
|
+
// by presence rather than by matching `sqlite_master.sql` for "USING fts5".
|
|
661
|
+
["records_fts_data", null],
|
|
662
|
+
// No presence pair for `sessions`: a file without that table is refused as a
|
|
663
|
+
// foreign store before the pair walk runs (a gap could never name it).
|
|
664
|
+
["sessions", "id"],
|
|
665
|
+
["sessions", "source"],
|
|
666
|
+
["sessions", "source_path"],
|
|
667
|
+
["sessions", "content_hash"],
|
|
668
|
+
["sessions", "title"],
|
|
669
|
+
["sessions", "cwd"],
|
|
670
|
+
["sessions", "model"],
|
|
671
|
+
["sessions", "started_at"],
|
|
672
|
+
["sessions", "ended_at"],
|
|
673
|
+
["sessions", "record_count"],
|
|
674
|
+
["sessions", "created_at"],
|
|
675
|
+
["sessions", "updated_at"],
|
|
676
|
+
["sessions", "parent_session_id"],
|
|
677
|
+
["sessions", "parent_tool_use_id"],
|
|
678
|
+
["sessions", "agent_nickname"],
|
|
679
|
+
["sessions", "thread_role"],
|
|
680
|
+
];
|
|
681
|
+
|
|
682
|
+
export const INGEST_DIAGNOSTICS_SQL = `
|
|
683
|
+
CREATE TABLE IF NOT EXISTS ingest_diagnostics (
|
|
684
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
685
|
+
code TEXT NOT NULL,
|
|
686
|
+
source TEXT NOT NULL,
|
|
687
|
+
occurrence_count INTEGER NOT NULL,
|
|
688
|
+
sample TEXT,
|
|
689
|
+
first_seen_at TEXT NOT NULL,
|
|
690
|
+
last_seen_at TEXT NOT NULL,
|
|
691
|
+
PRIMARY KEY (session_id, code)
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
CREATE TRIGGER IF NOT EXISTS trg_sessions_diagnostics_delete
|
|
695
|
+
AFTER DELETE ON sessions
|
|
696
|
+
FOR EACH ROW
|
|
697
|
+
BEGIN
|
|
698
|
+
DELETE FROM ingest_diagnostics WHERE session_id = OLD.id;
|
|
699
|
+
END;
|
|
700
|
+
`;
|
|
701
|
+
|
|
702
|
+
export const V4_MIGRATION_SQL = `
|
|
703
|
+
-- Existing v3 rows used source-native ids as global primary keys. Prefix them
|
|
704
|
+
-- deterministically with the adapter name so independent harnesses cannot
|
|
705
|
+
-- overwrite one another when they reuse the same native session/record ids.
|
|
706
|
+
UPDATE records_embeddings
|
|
707
|
+
SET record_id = (
|
|
708
|
+
SELECT r.source_type || ':' || r.id FROM records r WHERE r.id = records_embeddings.record_id
|
|
709
|
+
),
|
|
710
|
+
session_id = (
|
|
711
|
+
SELECT s.source || ':' || s.id FROM sessions s WHERE s.id = records_embeddings.session_id
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
-- Session identity is always sessions.source, even when a historical FTS or
|
|
715
|
+
-- record row stored a different source_type. Record ids stay prefixed by the
|
|
716
|
+
-- record's own source_type so they remain stable relative to the record row.
|
|
717
|
+
UPDATE records_fts
|
|
718
|
+
SET record_id = source_type || ':' || record_id,
|
|
719
|
+
session_id = COALESCE(
|
|
720
|
+
(SELECT s.source || ':' || s.id FROM sessions s WHERE s.id = records_fts.session_id),
|
|
721
|
+
source_type || ':' || session_id
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
UPDATE records
|
|
725
|
+
SET id = source_type || ':' || id,
|
|
726
|
+
session_id = COALESCE(
|
|
727
|
+
(SELECT s.source || ':' || s.id FROM sessions s WHERE s.id = records.session_id),
|
|
728
|
+
source_type || ':' || session_id
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
UPDATE sessions
|
|
732
|
+
SET id = source || ':' || id;
|
|
733
|
+
|
|
734
|
+
-- Older Gemini backfill used the MiniLM/384 defaults. Repair that known
|
|
735
|
+
-- signature, but preserve other provider identities regardless of dimensions.
|
|
736
|
+
-- Float32 blob length is authoritative for dimensionality, not provider identity.
|
|
737
|
+
UPDATE records_embeddings
|
|
738
|
+
SET dim = length(embedding) / 4,
|
|
739
|
+
provider = CASE
|
|
740
|
+
WHEN provider = 'all-MiniLM-L6-v2' AND dim = 384
|
|
741
|
+
AND length(embedding) / 4 = 768 THEN 'gemini-embedding-2'
|
|
742
|
+
ELSE provider
|
|
743
|
+
END;
|
|
744
|
+
`;
|
|
745
|
+
|
|
746
|
+
export const REPAIR_EMBEDDING_METADATA_TRIGGERS_SQL = `
|
|
747
|
+
DROP TRIGGER IF EXISTS trg_records_embeddings_metadata_insert;
|
|
748
|
+
DROP TRIGGER IF EXISTS trg_records_embeddings_metadata_update;
|
|
749
|
+
|
|
750
|
+
CREATE TRIGGER trg_records_embeddings_metadata_insert
|
|
751
|
+
AFTER INSERT ON records_embeddings
|
|
752
|
+
FOR EACH ROW
|
|
753
|
+
BEGIN
|
|
754
|
+
UPDATE records_embeddings
|
|
755
|
+
SET dim = length(NEW.embedding) / 4,
|
|
756
|
+
provider = CASE
|
|
757
|
+
WHEN NEW.provider IS NOT NULL AND NEW.provider != '' THEN NEW.provider
|
|
758
|
+
WHEN length(NEW.embedding) / 4 = 768 THEN 'gemini-embedding-2'
|
|
759
|
+
WHEN length(NEW.embedding) / 4 = 384 THEN 'all-MiniLM-L6-v2'
|
|
760
|
+
ELSE NEW.provider
|
|
761
|
+
END
|
|
762
|
+
WHERE record_id = NEW.record_id;
|
|
763
|
+
END;
|
|
764
|
+
|
|
765
|
+
CREATE TRIGGER trg_records_embeddings_metadata_update
|
|
766
|
+
AFTER UPDATE OF embedding ON records_embeddings
|
|
767
|
+
FOR EACH ROW
|
|
768
|
+
BEGIN
|
|
769
|
+
UPDATE records_embeddings
|
|
770
|
+
SET dim = length(NEW.embedding) / 4,
|
|
771
|
+
provider = CASE
|
|
772
|
+
WHEN NEW.provider IS NOT NULL AND NEW.provider != '' THEN NEW.provider
|
|
773
|
+
WHEN length(NEW.embedding) / 4 = 768 THEN 'gemini-embedding-2'
|
|
774
|
+
WHEN length(NEW.embedding) / 4 = 384 THEN 'all-MiniLM-L6-v2'
|
|
775
|
+
ELSE NEW.provider
|
|
776
|
+
END
|
|
777
|
+
WHERE record_id = NEW.record_id;
|
|
778
|
+
END;
|
|
779
|
+
`;
|
|
780
|
+
|
|
781
|
+
export const V3_MIGRATION_SQL = `
|
|
782
|
+
ALTER TABLE records_embeddings ADD COLUMN provider TEXT;
|
|
783
|
+
ALTER TABLE records_embeddings ADD COLUMN dim INTEGER;
|
|
784
|
+
UPDATE records_embeddings SET provider = 'all-MiniLM-L6-v2', dim = 384 WHERE provider IS NULL;
|
|
785
|
+
CREATE INDEX IF NOT EXISTS idx_records_embeddings_provider ON records_embeddings(provider, dim);
|
|
786
|
+
`;
|
|
787
|
+
|
|
788
|
+
export const V2_MIGRATION_SQL = `
|
|
789
|
+
CREATE TABLE IF NOT EXISTS records_embeddings (
|
|
790
|
+
record_id TEXT PRIMARY KEY REFERENCES records(id) ON DELETE CASCADE,
|
|
791
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
792
|
+
embedding BLOB NOT NULL,
|
|
793
|
+
created_at TEXT NOT NULL
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
CREATE INDEX IF NOT EXISTS idx_records_embeddings_session_id ON records_embeddings(session_id);
|
|
797
|
+
`;
|
|
798
|
+
|
|
799
|
+
export const PARENTAGE_MIGRATION_SQL = `
|
|
800
|
+
ALTER TABLE sessions ADD COLUMN parent_session_id TEXT;
|
|
801
|
+
ALTER TABLE sessions ADD COLUMN parent_tool_use_id TEXT;
|
|
802
|
+
ALTER TABLE sessions ADD COLUMN agent_nickname TEXT;
|
|
803
|
+
ALTER TABLE sessions ADD COLUMN thread_role TEXT;
|
|
804
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_parent_session_id ON sessions(parent_session_id);
|
|
805
|
+
`;
|
|
806
|
+
|
|
807
|
+
export const INGEST_CURSORS_SQL = `
|
|
808
|
+
CREATE TABLE IF NOT EXISTS ingest_cursors (
|
|
809
|
+
source TEXT NOT NULL,
|
|
810
|
+
listing_id TEXT NOT NULL,
|
|
811
|
+
source_path TEXT NOT NULL,
|
|
812
|
+
session_id TEXT,
|
|
813
|
+
mtime TEXT NOT NULL,
|
|
814
|
+
size INTEGER NOT NULL,
|
|
815
|
+
bytes_ingested INTEGER NOT NULL,
|
|
816
|
+
record_count INTEGER NOT NULL DEFAULT 0,
|
|
817
|
+
status TEXT NOT NULL,
|
|
818
|
+
updated_at TEXT NOT NULL,
|
|
819
|
+
file_identity TEXT,
|
|
820
|
+
prefix_hash TEXT,
|
|
821
|
+
checkpoint_version INTEGER,
|
|
822
|
+
line_count INTEGER,
|
|
823
|
+
tail_offset INTEGER,
|
|
824
|
+
tail_hash TEXT,
|
|
825
|
+
PRIMARY KEY (source, listing_id, source_path)
|
|
826
|
+
);
|
|
827
|
+
|
|
828
|
+
CREATE INDEX IF NOT EXISTS idx_ingest_cursors_session_id ON ingest_cursors(session_id);
|
|
829
|
+
`;
|