trajectories 0.0.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +201 -0
  3. package/README.md +288 -2
  4. package/dist/cli.js +30210 -0
  5. package/dist/convert.d.ts +31 -0
  6. package/dist/convert.js +431 -0
  7. package/dist/deepagents_checkpoint.py +362 -0
  8. package/dist/index.d.ts +7 -0
  9. package/dist/index.js +8402 -0
  10. package/dist/log-baseline.d.ts +26 -0
  11. package/dist/log-baseline.js +104 -0
  12. package/dist/log-content.d.ts +16 -0
  13. package/dist/log-content.js +80 -0
  14. package/dist/log-io.d.ts +10 -0
  15. package/dist/log-io.js +121 -0
  16. package/dist/log-read.d.ts +24 -0
  17. package/dist/log-read.js +117 -0
  18. package/dist/log-reconcile.d.ts +5 -0
  19. package/dist/log-reconcile.js +38 -0
  20. package/dist/log-records.d.ts +35 -0
  21. package/dist/log-records.js +85 -0
  22. package/dist/log-recovery.d.ts +18 -0
  23. package/dist/log-recovery.js +123 -0
  24. package/dist/log-stage.d.ts +14 -0
  25. package/dist/log-stage.js +138 -0
  26. package/dist/log-state.d.ts +89 -0
  27. package/dist/log-state.js +92 -0
  28. package/dist/log-switch.d.ts +3 -0
  29. package/dist/log-switch.js +98 -0
  30. package/dist/log-types.d.ts +129 -0
  31. package/dist/log-types.js +8 -0
  32. package/dist/log.d.ts +8 -0
  33. package/dist/log.js +62 -0
  34. package/dist/migrate.d.ts +29 -0
  35. package/dist/migrate.js +173 -0
  36. package/dist/normalization-runtime.d.ts +1 -0
  37. package/dist/normalization-runtime.js +3150 -0
  38. package/dist/portable.d.ts +9 -0
  39. package/dist/portable.js +26 -0
  40. package/dist/readers/canonical.d.ts +13 -0
  41. package/dist/readers/canonical.js +54 -0
  42. package/dist/readers/cline.d.ts +10 -0
  43. package/dist/readers/cline.js +107 -0
  44. package/dist/readers/cursor.d.ts +20 -0
  45. package/dist/readers/cursor.js +374 -0
  46. package/dist/readers/deepagents.d.ts +41 -0
  47. package/dist/readers/deepagents.js +163 -0
  48. package/dist/readers/fx.d.ts +11 -0
  49. package/dist/readers/fx.js +573 -0
  50. package/dist/readers/github-copilot.d.ts +8 -0
  51. package/dist/readers/github-copilot.js +216 -0
  52. package/dist/readers/grok-build.d.ts +12 -0
  53. package/dist/readers/grok-build.js +391 -0
  54. package/dist/readers/guards.d.ts +4 -0
  55. package/dist/readers/guards.js +121 -0
  56. package/dist/readers/pi-compaction.d.ts +7 -0
  57. package/dist/readers/pi-compaction.js +112 -0
  58. package/dist/readers/pi-context.d.ts +13 -0
  59. package/dist/readers/pi-context.js +64 -0
  60. package/dist/store.d.ts +25 -0
  61. package/dist/store.js +218 -0
  62. package/dist/types.d.ts +91 -0
  63. package/dist/types.js +1 -0
  64. package/dist/writers/claude-code.d.ts +5 -0
  65. package/dist/writers/claude-code.js +51 -0
  66. package/dist/writers/cline.d.ts +3 -0
  67. package/dist/writers/cline.js +77 -0
  68. package/dist/writers/codex.d.ts +3 -0
  69. package/dist/writers/codex.js +32 -0
  70. package/dist/writers/cursor.d.ts +3 -0
  71. package/dist/writers/cursor.js +197 -0
  72. package/dist/writers/deepagents.d.ts +3 -0
  73. package/dist/writers/deepagents.js +116 -0
  74. package/dist/writers/entropy.d.ts +5 -0
  75. package/dist/writers/entropy.js +33 -0
  76. package/dist/writers/fx.d.ts +3 -0
  77. package/dist/writers/fx.js +135 -0
  78. package/dist/writers/github-copilot.d.ts +3 -0
  79. package/dist/writers/github-copilot.js +71 -0
  80. package/dist/writers/grok-build.d.ts +3 -0
  81. package/dist/writers/grok-build.js +43 -0
  82. package/dist/writers/helpers.d.ts +17 -0
  83. package/dist/writers/helpers.js +75 -0
  84. package/dist/writers/omp.d.ts +3 -0
  85. package/dist/writers/omp.js +7 -0
  86. package/dist/writers/opencode.d.ts +3 -0
  87. package/dist/writers/opencode.js +65 -0
  88. package/dist/writers/pi.d.ts +3 -0
  89. package/dist/writers/pi.js +45 -0
  90. package/docs/cline-native-format.md +43 -0
  91. package/docs/copilot-native-format.md +78 -0
  92. package/docs/cursor-native-format.md +103 -0
  93. package/docs/deepagents-native-format.md +98 -0
  94. package/docs/evidence-api.md +170 -0
  95. package/docs/fx-native-format.md +87 -0
  96. package/docs/harness-parity-bootstrap.md +78 -0
  97. package/docs/harness-support-parity.md +107 -0
  98. package/docs/inspirations.md +103 -0
  99. package/docs/ledger-migration.md +141 -0
  100. package/docs/pi-compaction-context.md +75 -0
  101. package/docs/pi-provider-context.md +76 -0
  102. package/docs/research/2026-10-06-huihua-and-oar.md +176 -0
  103. package/docs/research/ledger/README.md +96 -0
  104. package/docs/research/ledger/confer/2026-09-13-round-10/README.md +61 -0
  105. package/docs/research/ledger/confer/2026-09-13-round-2/cass.md +72 -0
  106. package/docs/research/ledger/confer/2026-09-13-round-3/README.md +70 -0
  107. package/docs/research/ledger/confer/2026-09-13-round-5/README.md +61 -0
  108. package/docs/research/ledger/confer/2026-09-13-round-6/README.md +68 -0
  109. package/docs/research/ledger/confer/2026-09-13-round-7/README.md +64 -0
  110. package/docs/research/ledger/confer/2026-09-13-round-9/README.md +59 -0
  111. package/docs/research/ledger/confer/2026-09-14-wake/wake.md +146 -0
  112. package/docs/research/ledger/docs/inspirations.md +320 -0
  113. package/docs/research/ledger/docs/pr2-pr6-integration.md +53 -0
  114. package/docs/research/ledger/docs/research/2026-09-07-confer-inspirations.md +163 -0
  115. package/docs/research/ledger/docs/research/2026-09-07-letta-continuous-service.md +104 -0
  116. package/docs/research/ledger/docs/research/2026-09-08-claude-history-retrieval.md +117 -0
  117. package/docs/research/ledger/docs/research/2026-09-09-agentsview-native-discovery.md +87 -0
  118. package/docs/research/ledger/docs/research/2026-09-09-daemon-observation.md +231 -0
  119. package/docs/research/ledger/docs/research/2026-09-09-discovery-pagination.md +114 -0
  120. package/docs/research/ledger/docs/research/2026-09-09-harness-transcript-contract.md +145 -0
  121. package/docs/research/ledger/docs/research/2026-09-09-native-history-discovery.md +180 -0
  122. package/docs/research/ledger/docs/research/2026-09-09-native-history-implementation.md +137 -0
  123. package/docs/research/ledger/docs/research/2026-09-09-other-inspirations.md +279 -0
  124. package/docs/research/ledger/docs/research/2026-09-09-trajectory-and-letta-code.md +128 -0
  125. package/docs/research/ledger/docs/research/2026-09-13-diagnostics-coverage.md +116 -0
  126. package/docs/research/ledger/docs/research/2026-09-13-fts-rebuild.md +83 -0
  127. package/docs/research/ledger/docs/research/2026-09-13-read-path-split.md +74 -0
  128. package/docs/research/ledger/docs/research/2026-09-13-runtime-rollout.md +102 -0
  129. package/docs/research/ledger/docs/research/2026-09-13-scan-outcomes.md +80 -0
  130. package/docs/research/ledger/docs/research/2026-09-13-search-quality.md +105 -0
  131. package/docs/research/ledger/docs/research/2026-09-13-source-coverage.md +64 -0
  132. package/docs/research/ledger/docs/research/2026-09-13-sync-wall-budget.md +82 -0
  133. package/docs/research/ledger/docs/research/2026-09-13-typed-read-gate.md +81 -0
  134. package/docs/research/ledger/docs/research/2026-09-13-writer-lease.md +84 -0
  135. package/docs/research/ledger/docs/research/2026-09-16-c1-copy-fallback.md +227 -0
  136. package/docs/research/ledger/docs/research/2026-09-16-c10-kimi.md +414 -0
  137. package/docs/research/ledger/docs/research/2026-09-16-c2-derive-counter.md +216 -0
  138. package/docs/research/ledger/docs/research/2026-09-16-c3-echo-filter.md +305 -0
  139. package/docs/research/ledger/docs/research/2026-09-16-c4-title-index.md +349 -0
  140. package/docs/research/ledger/docs/research/2026-09-16-c5-trigram.md +268 -0
  141. package/docs/research/ledger/docs/research/2026-09-16-c6-recency-prior.md +438 -0
  142. package/docs/research/ledger/docs/research/2026-09-16-c7-tombstones.md +467 -0
  143. package/docs/research/ledger/docs/research/2026-09-16-c8-remote-truth.md +390 -0
  144. package/docs/research/ledger/docs/research/2026-09-16-c9-grok.md +380 -0
  145. package/docs/research/ledger/docs/research/2026-09-16-wake-port-program.md +38 -0
  146. package/docs/research/ledger/docs/research/2026-09-17-cursor-blobstore.md +157 -0
  147. package/docs/research/ledger/manifest.json +540 -0
  148. package/legacy/NOTICE.md +50 -0
  149. package/legacy/bin/ledger-core.ts +7 -0
  150. package/legacy/bin/ledger.ts +7 -0
  151. package/legacy/bin/traj-bridge.ts +7 -0
  152. package/legacy/scripts/import-claude-history.ts +130 -0
  153. package/legacy/scripts/native-history-smoke.ts +52 -0
  154. package/legacy/scripts/observe-daemon.ts +570 -0
  155. package/legacy/scripts/retitle-sessions.md +19 -0
  156. package/legacy/scripts/verify-runtime-rollout.ts +151 -0
  157. package/legacy/src/canonical.ts +108 -0
  158. package/legacy/src/cli.ts +1293 -0
  159. package/legacy/src/db.ts +2254 -0
  160. package/legacy/src/diagnostics.ts +87 -0
  161. package/legacy/src/discovery.ts +56 -0
  162. package/legacy/src/embeddings.ts +382 -0
  163. package/legacy/src/filter.ts +602 -0
  164. package/legacy/src/gemini_embeddings.ts +103 -0
  165. package/legacy/src/identity.ts +9 -0
  166. package/legacy/src/index.ts +10 -0
  167. package/legacy/src/ingest.ts +791 -0
  168. package/legacy/src/managed.ts +178 -0
  169. package/legacy/src/native/common.ts +68 -0
  170. package/legacy/src/native/cursor.ts +647 -0
  171. package/legacy/src/native/grok.ts +540 -0
  172. package/legacy/src/native/kimi.ts +706 -0
  173. package/legacy/src/native/opencode.ts +470 -0
  174. package/legacy/src/native/sqlite-ro.ts +281 -0
  175. package/legacy/src/native.ts +97 -0
  176. package/legacy/src/normalizer.ts +457 -0
  177. package/legacy/src/parentage.ts +562 -0
  178. package/legacy/src/pressure.ts +162 -0
  179. package/legacy/src/private-storage.ts +74 -0
  180. package/legacy/src/reader.ts +1255 -0
  181. package/legacy/src/scanner.ts +591 -0
  182. package/legacy/src/schema-gate.ts +263 -0
  183. package/legacy/src/schema.ts +829 -0
  184. package/legacy/src/semantic-top-k.ts +71 -0
  185. package/legacy/src/session-hash.ts +49 -0
  186. package/legacy/src/store-status.ts +218 -0
  187. package/legacy/src/sync-preparation.ts +45 -0
  188. package/legacy/src/sync-truth.ts +843 -0
  189. package/legacy/src/sync.ts +1233 -0
  190. package/legacy/src/types.ts +436 -0
  191. package/legacy/src/vocab.txt +30522 -0
  192. package/legacy/src/watch.ts +673 -0
  193. package/legacy/src/writer-lease.ts +310 -0
  194. package/ops/README.md +150 -0
  195. package/ops/install-cli.sh +63 -0
  196. package/ops/install-native.sh +35 -0
  197. package/ops/install-release.sh +49 -0
  198. package/ops/linux/install-systemd-user.sh +103 -0
  199. package/ops/macos/install-launchd.sh +72 -0
  200. package/ops/sync-settings.sh +37 -0
  201. package/package.json +21 -5
  202. package/skills/trajectories/SKILL.md +138 -0
  203. package/skills/trajectories/scripts/trajectories_recent.sh +26 -0
  204. package/skills/trajectories/scripts/trajectories_search.sh +26 -0
  205. package/index.js +0 -1
@@ -0,0 +1,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
+ `;