memhouse 0.18.12 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/AGENTS.md +22 -151
  2. package/CHANGELOG.md +263 -29
  3. package/LICENSE +202 -22
  4. package/LICENSES/agentlytics-MIT.txt +21 -0
  5. package/LICENSES/memhouse-MIT-until-0.18.11.txt +22 -0
  6. package/LICENSES/models.dev-MIT.txt +22 -0
  7. package/NOTICE +28 -0
  8. package/README.md +42 -20
  9. package/SECURITY.md +21 -8
  10. package/bin/memhouse.js +2223 -311
  11. package/docs/README.md +11 -9
  12. package/docs/guides/admin-password.md +37 -10
  13. package/docs/guides/configuration.md +13 -6
  14. package/docs/guides/daemons.md +64 -3
  15. package/docs/guides/import-claude-ai.md +204 -0
  16. package/docs/guides/install.md +11 -6
  17. package/docs/guides/invite.md +21 -7
  18. package/docs/guides/operate.md +114 -7
  19. package/docs/guides/search-and-resume.md +72 -3
  20. package/docs/guides/share.md +91 -52
  21. package/docs/guides/storage.md +44 -4
  22. package/docs/guides/troubleshooting.md +121 -14
  23. package/docs/guides/uninstall.md +4 -3
  24. package/docs/guides/update.md +16 -0
  25. package/docs/guides/upgrade-to-0.20.md +188 -0
  26. package/docs/releases/README.md +2 -1
  27. package/docs/releases/v0.18.10.md +5 -5
  28. package/docs/releases/v0.18.11.md +3 -3
  29. package/docs/releases/v0.20.0.md +627 -0
  30. package/docs/releases/v0.20.1.md +114 -0
  31. package/docs/tutorials/01-first-house.md +10 -7
  32. package/docs/tutorials/02-join-a-house.md +24 -15
  33. package/docs/tutorials/03-share-your-memory.md +40 -31
  34. package/editors/antigravity.js +1 -80
  35. package/editors/base.js +463 -9
  36. package/editors/bridge.js +119 -0
  37. package/editors/claude.js +273 -105
  38. package/editors/codex.js +278 -52
  39. package/editors/copilot.js +1 -70
  40. package/editors/cursor.js +1 -75
  41. package/editors/derive-fingerprints.json +18 -0
  42. package/editors/harness-paths.js +69 -0
  43. package/editors/index.js +1 -20
  44. package/editors/normalise/agy.js +910 -0
  45. package/editors/normalise/copilot-cli.js +321 -0
  46. package/editors/normalise/cursor-agent.js +852 -0
  47. package/editors/normalise/cursor.js +474 -0
  48. package/editors/normalise/devin.js +575 -0
  49. package/editors/normalise/gemini-cli.js +602 -0
  50. package/editors/normalise/goose.js +673 -0
  51. package/editors/normalise/opencode.js +633 -0
  52. package/editors/normalise/vscode.js +549 -0
  53. package/editors/normalise/zed.js +473 -0
  54. package/editors/opencode-v4.js +119 -0
  55. package/editors/scope.js +14 -3
  56. package/editors/sessions-log.js +349 -0
  57. package/editors/sqlite.js +28 -1
  58. package/editors/vscode.js +1 -72
  59. package/editors/windsurf.js +1 -143
  60. package/examples/01-local-one-machine/README.md +24 -19
  61. package/examples/01-local-one-machine/run.sh +1 -1
  62. package/examples/02-existing-clickhouse/README.md +30 -22
  63. package/examples/02-existing-clickhouse/run.sh +5 -4
  64. package/examples/03-team-house/README.md +44 -37
  65. package/examples/03-team-house/run.sh +35 -18
  66. package/examples/04-self-hosted-team/README.md +29 -14
  67. package/examples/04-self-hosted-team/compose.yml +5 -2
  68. package/examples/04-self-hosted-team/smoke.sh +26 -12
  69. package/examples/README.md +8 -4
  70. package/examples/fixtures/README.md +4 -1
  71. package/examples/fixtures/make-sessions.js +8 -5
  72. package/memhouse/admin-secret.js +89 -24
  73. package/memhouse/argv-secrets.js +64 -0
  74. package/memhouse/channel.js +49 -9
  75. package/memhouse/codex-plugin.js +194 -0
  76. package/memhouse/delivery/AGENT-INSTALL-PROMPT.md +2 -1
  77. package/memhouse/delivery/AGENT-INSTALL.md +22 -11
  78. package/memhouse/delivery/INVITATION.md.tpl +44 -45
  79. package/memhouse/delivery/PROMPT.md +53 -5
  80. package/memhouse/delivery/UPGRADE.md.tpl +1 -1
  81. package/memhouse/delivery/codex/AGENTS-BLOCK.md +11 -0
  82. package/memhouse/delivery/codex/SKILL.md +50 -0
  83. package/memhouse/delivery/kernel-install.md +17 -9
  84. package/memhouse/delivery/plugin/.claude-plugin/plugin.json +2 -2
  85. package/memhouse/delivery/plugin/reference/HOUSE.md +168 -41
  86. package/memhouse/delivery/plugin/skills/access/SKILL.md +13 -5
  87. package/memhouse/delivery/plugin/skills/admin/SKILL.md +14 -6
  88. package/memhouse/delivery/plugin/skills/house/SKILL.md +25 -8
  89. package/memhouse/delivery/plugin/skills/recall/SKILL.md +66 -8
  90. package/memhouse/delivery/plugin/skills/sql/SKILL.md +66 -7
  91. package/memhouse/deploy.js +1 -1
  92. package/memhouse/flags.js +35 -18
  93. package/memhouse/help.js +37 -0
  94. package/memhouse/host.js +82 -2
  95. package/memhouse/house/HOUSE.md +2 -2
  96. package/memhouse/house/convert-profile.js +638 -0
  97. package/memhouse/house/convert-v3.js +742 -0
  98. package/memhouse/house/doctor-v3.js +324 -0
  99. package/memhouse/house/host-alias.js +85 -0
  100. package/memhouse/house/house.js +3 -3
  101. package/memhouse/house/migrate.js +2 -2
  102. package/memhouse/house/profile-dedupe.js +183 -0
  103. package/memhouse/house/profile-edges.js +554 -0
  104. package/memhouse/house/profile-house.js +430 -0
  105. package/memhouse/house/profile-rollup.js +190 -0
  106. package/memhouse/house/q-cli.js +863 -0
  107. package/memhouse/house/queries-v3.js +1013 -0
  108. package/memhouse/house/reconcile-v3.js +192 -0
  109. package/memhouse/house/records-to-profile.js +174 -0
  110. package/memhouse/house/records-to-rows.js +169 -0
  111. package/memhouse/house/relocate.js +2 -1
  112. package/memhouse/house/schema-choice.js +121 -0
  113. package/memhouse/house/schema-profile.js +290 -0
  114. package/memhouse/house/schema-profile.sql.tpl +398 -0
  115. package/memhouse/house/schema-v3.js +267 -0
  116. package/memhouse/house/schema-v3.sql.tpl +213 -0
  117. package/memhouse/import/claude-ai.js +420 -0
  118. package/memhouse/import/import.js +372 -0
  119. package/memhouse/import/pyjson.js +206 -0
  120. package/memhouse/import/records.js +64 -0
  121. package/memhouse/import/writer-profile.js +192 -0
  122. package/memhouse/import/writer-v3.js +240 -0
  123. package/memhouse/import/zip.js +112 -0
  124. package/memhouse/invitation.js +102 -0
  125. package/memhouse/invitefile.js +5 -2
  126. package/memhouse/plugin-copies.js +176 -0
  127. package/memhouse/provision.js +7 -2
  128. package/memhouse/release-hint.js +24 -0
  129. package/memhouse/resume.js +4 -4
  130. package/memhouse/server/api-v3.js +632 -0
  131. package/memhouse/server/dashboard-auth.js +181 -0
  132. package/memhouse/server/dashboard-port.js +91 -0
  133. package/memhouse/server/queries.js +30 -0
  134. package/memhouse/server/server.js +118 -20
  135. package/memhouse/service.js +1 -1
  136. package/memhouse/share.js +687 -1
  137. package/memhouse/shipper/adapter-capture.js +756 -0
  138. package/memhouse/shipper/capture.js +2571 -0
  139. package/memhouse/shipper/dedupe-profile.js +237 -0
  140. package/memhouse/shipper/dedupe-v3.js +1027 -0
  141. package/memhouse/shipper/forks.js +233 -0
  142. package/memhouse/shipper/log-adapter.js +219 -0
  143. package/memhouse/shipper/maintenance.js +49 -0
  144. package/memhouse/shipper/normalise-run.js +148 -0
  145. package/memhouse/shipper/passlog.js +68 -5
  146. package/memhouse/shipper/profile-capture.js +903 -0
  147. package/memhouse/shipper/ship.js +91 -8
  148. package/memhouse/shipper/snapshots.js +193 -0
  149. package/memhouse/shipper/source-roots.js +111 -0
  150. package/memhouse/version-notice.js +66 -0
  151. package/package.json +14 -7
  152. package/public/THIRD_PARTY_NOTICES.txt +2 -2
  153. package/public/assets/index-CcNnhfM3.js +78 -0
  154. package/public/index.html +1 -1
  155. package/ui/package-lock.json +56 -37
  156. package/ui/package.json +1 -0
  157. package/ui/src/App.jsx +29 -14
  158. package/ui/src/components/ChatSidebar.jsx +69 -12
  159. package/ui/src/lib/api.js +76 -0
  160. package/ui/src/lib/dashboard-token.js +41 -0
  161. package/ui/src/main.jsx +22 -5
  162. package/ui/src/pages/Dashboard.jsx +22 -36
  163. package/ui/third-party-notices.js +119 -0
  164. package/ui/vite.config.js +5 -1
  165. package/docs/releases/v0.18.12.md +0 -59
  166. package/memhouse/COMPETITION.md +0 -221
  167. package/memhouse/DESIGN.md +0 -222
  168. package/memhouse/RESUME-AND-UPDATE.md +0 -182
  169. package/public/assets/index-CnWzrjF3.js +0 -78
@@ -0,0 +1,398 @@
1
+ -- memhouse's house tables as harness-sessions-sql's ClickHouse profile (step B1, tables and keys). The
2
+ -- tables, columns, keys and engines are harness-sessions-sql's schema/clickhouse.sql (v1.0.0);
3
+ -- memhouse adds, beside them:
4
+ -- * the member prefix {m}_ and the archive database {db}_archive (spec §3: physical names);
5
+ -- * on session_parts: first_seen (a first-seen time, never written into ts), folder, project,
6
+ -- harness_version, text_len, origin, extra, and the search columns over text and output;
7
+ -- * on sessions: the derived totals and rollups the readers use (counts, usage, tools, agents,
8
+ -- days, links), folder/project/git_branch, started/ended, first_prompt, origin;
9
+ -- * {m}_calls: a derived convenience (spec §3), one row per tool call;
10
+ -- * on session_links: r_copied_usage, r_copied_messages and r_unverified, memhouse's own
11
+ -- deployment columns (spec §4.8 leaves them out of content_sha). They are set on engine edges
12
+ -- only (owner 'engine', profile-edges.js): a fork's usage rows that copy its origin's, which
13
+ -- the rollup leaves out of the fork's totals (billed once, spec §4.5);
14
+ -- * on {m}_session_sources (informative in the spec): the capture engine's bookkeeping.
15
+ -- host: a machine (sessions-model/host/v0: 32 hex, or r- + 32 hex where no machine id can be read)
16
+ -- or a service that holds the sessions itself (s- + 32 hex, the sessions-model v1.x service-host
17
+ -- proposal: claude.ai's conversations); every host CHECK accepts all three forms.
18
+ -- ts is Nullable and in no key or partition: ReplacingMergeTree replaces only inside a partition
19
+ -- (harness-sessions-sql §3). No table is partitioned by time.
20
+ --
21
+ -- Rendered by schema-profile.js with {db} and {m}. Every statement is idempotent.
22
+
23
+ CREATE TABLE IF NOT EXISTS {db}.{m}_session_parts (
24
+ host LowCardinality(String),
25
+ CONSTRAINT host_form CHECK match(host, '^([rs]-)?[0-9a-f]{32}$') OR adapter = 'legacy',
26
+ harness LowCardinality(String),
27
+ harness_path LowCardinality(String),
28
+ session_id String,
29
+ incarnation String,
30
+ message_id String,
31
+ part_id String,
32
+ part_index UInt32,
33
+ parent_id String DEFAULT '',
34
+ order_key String,
35
+ agent_id String DEFAULT '',
36
+ role LowCardinality(String),
37
+ source LowCardinality(String) DEFAULT '',
38
+ to_user Nullable(Bool),
39
+ to_model Nullable(Bool),
40
+ status LowCardinality(String),
41
+ state LowCardinality(String),
42
+ on_active_branch Nullable(Bool),
43
+ ts Nullable(DateTime64(3, 'UTC')),
44
+ requested_model LowCardinality(String) DEFAULT '',
45
+ served_model LowCardinality(String) DEFAULT '',
46
+ effort LowCardinality(String) DEFAULT '',
47
+ response_id String DEFAULT '',
48
+ message_aliases Array(Tuple(id String, kind LowCardinality(String))),
49
+ cmp_sha FixedString(64),
50
+ message_content_sha FixedString(64),
51
+ kind LowCardinality(String),
52
+ text String DEFAULT '' CODEC(ZSTD(3)),
53
+ call_id String DEFAULT '',
54
+ tool_name LowCardinality(String) DEFAULT '',
55
+ args String DEFAULT '',
56
+ server LowCardinality(String) DEFAULT '',
57
+ is_error Nullable(Bool),
58
+ result_status LowCardinality(String) DEFAULT '',
59
+ output String DEFAULT '' CODEC(ZSTD(3)),
60
+ output_ref String DEFAULT '',
61
+ output_ref_sha256 String DEFAULT '',
62
+ output_ref_bytes Nullable(UInt64),
63
+ output_len Nullable(UInt64),
64
+ truncated Nullable(Bool),
65
+ exit_code Nullable(Int32),
66
+ media_type LowCardinality(String) DEFAULT '',
67
+ inline_b64 String DEFAULT '' CODEC(ZSTD(3)),
68
+ ref String DEFAULT '',
69
+ bytes_len Nullable(UInt64),
70
+ opaque Nullable(Bool),
71
+ signature_len Nullable(UInt32),
72
+ replaces_form LowCardinality(String) DEFAULT '',
73
+ replaces_value String DEFAULT '',
74
+ error_kind LowCardinality(String) DEFAULT '',
75
+ error_message String DEFAULT '',
76
+ http_status Nullable(UInt16),
77
+ level LowCardinality(String) DEFAULT '',
78
+ native_type LowCardinality(String) DEFAULT '',
79
+ part_source LowCardinality(String) DEFAULT '',
80
+ part_state LowCardinality(String) DEFAULT '',
81
+ content_sha FixedString(64),
82
+ adapter LowCardinality(String),
83
+ adapter_ver LowCardinality(String),
84
+ model_version LowCardinality(String),
85
+ rev UInt64,
86
+ is_deleted UInt8 DEFAULT 0,
87
+ removed UInt8 DEFAULT 0,
88
+ ingested_at DateTime64(3, 'UTC') DEFAULT now64(3),
89
+ writer String MATERIALIZED currentUser(),
90
+ -- memhouse's columns
91
+ first_seen DateTime64(3, 'UTC'), -- when this host first saw the message; ts stays the native time or null
92
+ harness_version LowCardinality(String) DEFAULT '',
93
+ folder String DEFAULT '',
94
+ project String DEFAULT '',
95
+ text_len UInt32 DEFAULT 0, -- characters of text before any cap
96
+ origin LowCardinality(String) DEFAULT 'ship', -- ship | import | legacy
97
+ extra String DEFAULT '{}',
98
+ search_ngram String MATERIALIZED lower(concat(text, '\n', output)), -- search reads text and output (B-Q2)
99
+ search_word String MATERIALIZED lower(concat(text, '\n', output)),
100
+ INDEX idx_search_ngram search_ngram TYPE text(tokenizer = ngrams(3)) GRANULARITY 1,
101
+ INDEX idx_search_word search_word TYPE text(tokenizer = splitByNonAlpha) GRANULARITY 1,
102
+ INDEX idx_ts ts TYPE minmax GRANULARITY 1,
103
+ INDEX idx_first_seen first_seen TYPE minmax GRANULARITY 1
104
+ ) ENGINE = ReplacingMergeTree(rev, is_deleted)
105
+ ORDER BY (host, harness, harness_path, session_id, incarnation, message_id, part_id)
106
+ SETTINGS index_granularity = 512, allow_experimental_replacing_merge_with_cleanup = 1;
107
+
108
+ CREATE TABLE IF NOT EXISTS {db}.{m}_sessions (
109
+ host LowCardinality(String),
110
+ CONSTRAINT host_form CHECK match(host, '^([rs]-)?[0-9a-f]{32}$') OR adapter = 'legacy',
111
+ host_label String DEFAULT '',
112
+ harness LowCardinality(String),
113
+ harness_path LowCardinality(String),
114
+ session_id String,
115
+ incarnation String,
116
+ engine LowCardinality(String) DEFAULT '',
117
+ client LowCardinality(String) DEFAULT '',
118
+ harness_version LowCardinality(String) DEFAULT '',
119
+ title String DEFAULT '',
120
+ title_set_by LowCardinality(String) DEFAULT 'unknown',
121
+ cwd String DEFAULT '',
122
+ last_cwd String DEFAULT '',
123
+ created_at Nullable(DateTime64(3, 'UTC')),
124
+ updated_at Nullable(DateTime64(3, 'UTC')),
125
+ state LowCardinality(String),
126
+ pinned Nullable(Bool),
127
+ mode LowCardinality(String) DEFAULT '',
128
+ kind LowCardinality(String) DEFAULT '',
129
+ reuse LowCardinality(String),
130
+ ts_source LowCardinality(String), -- a registry (spec §2, principle 7): any value is kept
131
+ aliases Array(Tuple(id String, kind LowCardinality(String))),
132
+ fingerprint Array(FixedString(64)),
133
+ setup Nullable(String),
134
+ content_sha FixedString(64),
135
+ adapter LowCardinality(String),
136
+ adapter_ver LowCardinality(String),
137
+ model_version LowCardinality(String),
138
+ rev UInt64,
139
+ is_deleted UInt8 DEFAULT 0,
140
+ removed UInt8 DEFAULT 0,
141
+ ingested_at DateTime64(3, 'UTC') DEFAULT now64(3),
142
+ writer String MATERIALIZED currentUser(),
143
+ -- memhouse's derived columns (from the session's parts, usage and links)
144
+ folder String DEFAULT '', -- the first cwd seen
145
+ project String DEFAULT '',
146
+ git_branch String DEFAULT '',
147
+ started Nullable(DateTime64(3, 'UTC')),
148
+ ended Nullable(DateTime64(3, 'UTC')),
149
+ messages UInt32 DEFAULT 0, -- kind = 'text' rows only
150
+ user_messages UInt32 DEFAULT 0,
151
+ assistant_messages UInt32 DEFAULT 0,
152
+ agent_messages UInt32 DEFAULT 0,
153
+ tool_calls UInt32 DEFAULT 0,
154
+ input_tokens UInt64 DEFAULT 0, -- the house split, applied by the reader rules (flags, per_record once per response)
155
+ output_tokens UInt64 DEFAULT 0,
156
+ cache_read_tokens UInt64 DEFAULT 0,
157
+ cache_write_tokens UInt64 DEFAULT 0,
158
+ reasoning_tokens UInt64 DEFAULT 0,
159
+ user_chars UInt64 DEFAULT 0,
160
+ assistant_chars UInt64 DEFAULT 0,
161
+ first_prompt String DEFAULT '',
162
+ models Array(LowCardinality(String)),
163
+ dominant_model LowCardinality(String) DEFAULT '',
164
+ days Array(Date),
165
+ usage Nested(
166
+ hour DateTime('UTC'),
167
+ model LowCardinality(String),
168
+ messages UInt32,
169
+ input UInt64,
170
+ output UInt64,
171
+ cache_read UInt64,
172
+ cache_write UInt64,
173
+ reasoning UInt64
174
+ ),
175
+ tools Nested(name LowCardinality(String), calls UInt32, errors UInt32),
176
+ agents Nested(
177
+ agent_id String,
178
+ type LowCardinality(String),
179
+ description String,
180
+ workflow String,
181
+ messages UInt32,
182
+ first_ts DateTime64(3, 'UTC')
183
+ ),
184
+ links Nested( -- derived from session_links, for the readers
185
+ kind LowCardinality(String),
186
+ origin String,
187
+ origin_store String,
188
+ evidence LowCardinality(String),
189
+ at String,
190
+ through String
191
+ ),
192
+ origin LowCardinality(String) DEFAULT 'ship'
193
+ ) ENGINE = ReplacingMergeTree(rev, is_deleted)
194
+ ORDER BY (host, harness, harness_path, session_id, incarnation);
195
+
196
+ CREATE TABLE IF NOT EXISTS {db}.{m}_session_usage (
197
+ host LowCardinality(String),
198
+ CONSTRAINT host_form CHECK match(host, '^([rs]-)?[0-9a-f]{32}$') OR adapter = 'legacy',
199
+ harness LowCardinality(String),
200
+ harness_path LowCardinality(String),
201
+ session_id String,
202
+ incarnation String,
203
+ usage_id String,
204
+ anchor_kind LowCardinality(String),
205
+ anchor_id String DEFAULT '',
206
+ input Nullable(UInt64),
207
+ output Nullable(UInt64),
208
+ cache_read Nullable(UInt64),
209
+ cache_write Nullable(UInt64),
210
+ reasoning Nullable(UInt64),
211
+ tool_prompt Nullable(UInt64),
212
+ cost Nullable(Decimal(18, 9)),
213
+ input_includes_cache Nullable(Bool),
214
+ output_includes_reasoning Nullable(Bool),
215
+ granularity LowCardinality(String),
216
+ model LowCardinality(String) DEFAULT '',
217
+ content_sha FixedString(64),
218
+ adapter LowCardinality(String),
219
+ adapter_ver LowCardinality(String),
220
+ model_version LowCardinality(String),
221
+ rev UInt64,
222
+ is_deleted UInt8 DEFAULT 0,
223
+ removed UInt8 DEFAULT 0,
224
+ ingested_at DateTime64(3, 'UTC') DEFAULT now64(3),
225
+ writer String MATERIALIZED currentUser(),
226
+ -- memhouse's columns
227
+ ts Nullable(DateTime64(3, 'UTC')), -- the anchor's time, for hourly rollups
228
+ origin LowCardinality(String) DEFAULT 'ship'
229
+ ) ENGINE = ReplacingMergeTree(rev, is_deleted)
230
+ ORDER BY (host, harness, harness_path, session_id, incarnation, usage_id);
231
+
232
+ CREATE TABLE IF NOT EXISTS {db}.{m}_session_links (
233
+ host LowCardinality(String),
234
+ CONSTRAINT host_form CHECK match(host, '^([rs]-)?[0-9a-f]{32}$') OR adapter = 'legacy',
235
+ harness LowCardinality(String),
236
+ harness_path LowCardinality(String),
237
+ session_id String,
238
+ incarnation String,
239
+ owner LowCardinality(String),
240
+ kind LowCardinality(String),
241
+ other_host LowCardinality(String) DEFAULT '',
242
+ CONSTRAINT other_host_form CHECK other_host = '' OR match(other_host, '^([rs]-)?[0-9a-f]{32}$') OR adapter = 'legacy',
243
+ other_harness_path LowCardinality(String) DEFAULT '',
244
+ other_harness LowCardinality(String),
245
+ other_session_id String,
246
+ other_incarnation String DEFAULT '',
247
+ at String DEFAULT '',
248
+ stated_on LowCardinality(String),
249
+ child_host LowCardinality(String) DEFAULT '',
250
+ CONSTRAINT child_host_form CHECK child_host = '' OR match(child_host, '^([rs]-)?[0-9a-f]{32}$') OR adapter = 'legacy',
251
+ child_harness_path LowCardinality(String) DEFAULT '',
252
+ child_harness LowCardinality(String),
253
+ child_session_id String,
254
+ child_incarnation Nullable(String),
255
+ origin_host LowCardinality(String) DEFAULT '',
256
+ CONSTRAINT origin_host_form CHECK origin_host = '' OR match(origin_host, '^([rs]-)?[0-9a-f]{32}$') OR adapter = 'legacy',
257
+ origin_harness_path LowCardinality(String) DEFAULT '',
258
+ origin_harness LowCardinality(String),
259
+ origin_session_id String,
260
+ origin_incarnation Nullable(String),
261
+ through_message_id String DEFAULT '',
262
+ ids_preserved Nullable(Bool),
263
+ marked Nullable(Bool),
264
+ usage_copied Nullable(Bool),
265
+ child_stored Nullable(Bool), -- null: unknown (a statement that does not say)
266
+ evidence LowCardinality(String) DEFAULT 'explicit',
267
+ verified Nullable(Bool),
268
+ statements Array(Tuple(host String, harness String, harness_path String, session_id String, incarnation String)),
269
+ content_sha FixedString(64),
270
+ adapter LowCardinality(String),
271
+ adapter_ver LowCardinality(String),
272
+ model_version LowCardinality(String),
273
+ rev UInt64,
274
+ is_deleted UInt8 DEFAULT 0,
275
+ removed UInt8 DEFAULT 0,
276
+ ingested_at DateTime64(3, 'UTC') DEFAULT now64(3),
277
+ writer String MATERIALIZED currentUser(),
278
+ -- memhouse's deployment columns, on engine edges only (header)
279
+ r_copied_usage Array(String) DEFAULT [], -- the child's usage_ids that copy the origin's: not billed again
280
+ r_copied_messages UInt32 DEFAULT 0, -- the verified copied prefix, in messages
281
+ r_unverified String DEFAULT '' -- why the edge is not verified ('' when it is)
282
+ ) ENGINE = ReplacingMergeTree(rev, is_deleted)
283
+ ORDER BY (host, harness, harness_path, session_id, incarnation, owner, kind, other_host, other_harness_path, other_harness, other_session_id, other_incarnation, at);
284
+
285
+ CREATE TABLE IF NOT EXISTS {db}.{m}_session_lifecycle (
286
+ host LowCardinality(String),
287
+ CONSTRAINT host_form CHECK match(host, '^([rs]-)?[0-9a-f]{32}$') OR adapter = 'legacy',
288
+ harness LowCardinality(String),
289
+ harness_path LowCardinality(String),
290
+ session_id String,
291
+ incarnation String,
292
+ lifecycle_id String,
293
+ op LowCardinality(String),
294
+ target_kind LowCardinality(String),
295
+ target_id String DEFAULT '',
296
+ target_from String DEFAULT '',
297
+ target_through String DEFAULT '',
298
+ at String DEFAULT '',
299
+ ts Nullable(DateTime64(3, 'UTC')),
300
+ evidence LowCardinality(String) DEFAULT 'explicit',
301
+ content_sha FixedString(64),
302
+ adapter LowCardinality(String),
303
+ adapter_ver LowCardinality(String),
304
+ model_version LowCardinality(String),
305
+ rev UInt64,
306
+ is_deleted UInt8 DEFAULT 0,
307
+ removed UInt8 DEFAULT 0,
308
+ ingested_at DateTime64(3, 'UTC') DEFAULT now64(3),
309
+ writer String MATERIALIZED currentUser()
310
+ ) ENGINE = ReplacingMergeTree(rev, is_deleted)
311
+ ORDER BY (host, harness, harness_path, session_id, incarnation, lifecycle_id);
312
+
313
+ -- memhouse's derived convenience: one row per tool call (spec §3 allows it).
314
+ CREATE TABLE IF NOT EXISTS {db}.{m}_calls (
315
+ host LowCardinality(String),
316
+ CONSTRAINT host_form CHECK match(host, '^([rs]-)?[0-9a-f]{32}$') OR adapter = 'legacy',
317
+ harness LowCardinality(String),
318
+ harness_path LowCardinality(String),
319
+ session_id String,
320
+ incarnation String,
321
+ call_id String,
322
+ message_id String,
323
+ agent_id String DEFAULT '',
324
+ ts Nullable(DateTime64(3, 'UTC')),
325
+ first_seen DateTime64(3, 'UTC'),
326
+ tool_name LowCardinality(String),
327
+ args String DEFAULT '',
328
+ result_ts Nullable(DateTime64(3, 'UTC')),
329
+ is_error Nullable(Bool),
330
+ output_len Nullable(UInt64),
331
+ model LowCardinality(String) DEFAULT '',
332
+ folder String DEFAULT '',
333
+ project String DEFAULT '',
334
+ content_sha FixedString(64), -- the tool_call part's sha (as session_parts)
335
+ adapter LowCardinality(String),
336
+ adapter_ver LowCardinality(String),
337
+ rev UInt64,
338
+ is_deleted UInt8 DEFAULT 0,
339
+ removed UInt8 DEFAULT 0,
340
+ ingested_at DateTime64(3, 'UTC') DEFAULT now64(3),
341
+ writer String MATERIALIZED currentUser(),
342
+ INDEX idx_first_seen first_seen TYPE minmax GRANULARITY 1
343
+ ) ENGINE = ReplacingMergeTree(rev, is_deleted)
344
+ ORDER BY (host, harness, harness_path, session_id, incarnation, call_id);
345
+
346
+ CREATE TABLE IF NOT EXISTS {db}_archive.{m}_session_units (
347
+ host LowCardinality(String),
348
+ CONSTRAINT host_form CHECK match(host, '^([rs]-)?[0-9a-f]{32}$'),
349
+ harness LowCardinality(String),
350
+ harness_path LowCardinality(String),
351
+ session_id String,
352
+ incarnation String,
353
+ source_key String,
354
+ generation UInt32,
355
+ unit_key String,
356
+ chunk UInt16 DEFAULT 0,
357
+ sha256 FixedString(64),
358
+ enc LowCardinality(String), -- a registry (spec §2, principle 7): any value is kept
359
+ orig_sha256 Nullable(FixedString(64)), -- null when the unit has none (harness-sessions-sql §4.1)
360
+ redacted Bool DEFAULT false,
361
+ continuity LowCardinality(String),
362
+ raw String CODEC(ZSTD(3)),
363
+ adapter LowCardinality(String),
364
+ adapter_ver LowCardinality(String),
365
+ captured_at DateTime64(3, 'UTC') DEFAULT now64(3),
366
+ writer String MATERIALIZED currentUser()
367
+ ) ENGINE = ReplacingMergeTree
368
+ ORDER BY (host, harness, harness_path, session_id, incarnation, source_key, generation, unit_key, chunk, sha256);
369
+
370
+ CREATE TABLE IF NOT EXISTS {db}_archive.{m}_session_sources (
371
+ host LowCardinality(String),
372
+ CONSTRAINT host_form CHECK match(host, '^([rs]-)?[0-9a-f]{32}$'),
373
+ harness LowCardinality(String),
374
+ harness_path LowCardinality(String),
375
+ session_id String,
376
+ incarnation String,
377
+ source_key String,
378
+ mode LowCardinality(String),
379
+ position String DEFAULT '',
380
+ generation UInt32,
381
+ progress UInt64,
382
+ state String DEFAULT '' CODEC(ZSTD(1)),
383
+ state_ver UInt32,
384
+ adapter LowCardinality(String),
385
+ adapter_ver LowCardinality(String),
386
+ committed_at DateTime64(3, 'UTC') DEFAULT now64(3),
387
+ ver UInt64 MATERIALIZED (toUInt64(generation) * 1099511627776) + progress,
388
+ writer String MATERIALIZED currentUser(),
389
+ -- memhouse's bookkeeping
390
+ role LowCardinality(String) DEFAULT 'main', -- main | agent | side | legacy | inherited
391
+ agent_id String DEFAULT '',
392
+ agent_type LowCardinality(String) DEFAULT '',
393
+ agent_desc String DEFAULT '',
394
+ known String DEFAULT '' CODEC(ZSTD(1)), -- delta sources: the current unit keys and shas
395
+ contrib String DEFAULT '{}' CODEC(ZSTD(1)), -- this source's exact share of the session totals
396
+ is_deleted UInt8 DEFAULT 0
397
+ ) ENGINE = ReplacingMergeTree(ver)
398
+ ORDER BY (host, harness, harness_path, session_id, incarnation, source_key);
@@ -0,0 +1,267 @@
1
+ // Schema v3 (source identity, docs/design/source-identity.md draft 5): the five tables of
2
+ // one member, in two databases.
3
+ //
4
+ // <db>.<m>_turns, <m>_calls, <m>_sessions conversation layer — harness-agnostic, read
5
+ // by everything, under the member's existing
6
+ // `<db>.<m>_*` grant
7
+ // <db>_archive.<m>_records, <m>_files capture layer — the shipper only; SELECT and
8
+ // INSERT for the member, no grant option, and
9
+ // nothing in <db>_archive is granted to anyone else
10
+ //
11
+ // The DDL lives in schema-v3.sql.tpl, rendered here with {db} and {m}. Every statement is
12
+ // CREATE … IF NOT EXISTS, so creating a member's tables is idempotent and safe to repeat.
13
+ //
14
+ // Transport-free, like migrate.js: every entry point takes `q` — `sql(text, settings)` and
15
+ // `rows(text, settings) -> object[]`. The CLI builds one over HTTP; qFromClient() below
16
+ // builds one over @clickhouse/client for the shipper. Settings follow ensureSchema's
17
+ // conventions: async_insert = 0 on everything (user_id is MATERIALIZED currentUser(), and an
18
+ // async insert can land it empty), allow_experimental_full_text_index = 1 on CREATE (25.11
19
+ // still gates the text index behind it), and the pre-25.10 text-index grammar as a fallback.
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const {
24
+ assertUsableName, legacyTextIndexDialect, isTextIndexGrammarRefusal, textIndexDialectFor,
25
+ } = require('./house');
26
+
27
+ const TEMPLATE = path.join(__dirname, 'schema-v3.sql.tpl');
28
+
29
+ const CONVERSATION_TYPES = ['turns', 'calls', 'sessions'];
30
+ const CAPTURE_TYPES = ['records', 'files'];
31
+ const V3_TYPES = [...CONVERSATION_TYPES, ...CAPTURE_TYPES];
32
+ // The legacy rooms, renamed into the archive database at migration (draft 5 §7 step 7) — in
33
+ // the first build they are restored there directly from the production backup.
34
+ const LEGACY_TYPES = ['messages', 'tool_calls', 'sessions'];
35
+ const LEGACY_SUFFIX = 'pre_v3';
36
+
37
+ /**
38
+ * The sorting key of each table exactly as `system.tables.sorting_key` reports it (layout 3.2:
39
+ * the session's store — host, harness_path, incarnation — in every key). The 3.1 keys were
40
+ * measured on 25.11.9.34, 26.7.4.58 and 26.8.16.41; 3.2 is checked by the house test. Canonical here so a verifier, doctor and a test
41
+ * all compare against one copy (the lesson of ROOM_KEYS in house.js).
42
+ */
43
+ const V3_KEYS = {
44
+ turns: 'toDate(ts), harness, host, harness_path, session_id, incarnation, ts, message_id, part_id',
45
+ calls: 'toDate(ts), harness, host, harness_path, session_id, incarnation, ts, call_id',
46
+ sessions: 'harness, host, harness_path, session_id, incarnation',
47
+ records: 'harness, host, harness_path, session_id, incarnation, file_key, fgen, pos, chunk, sha',
48
+ files: 'harness, host, harness_path, session_id, incarnation, file_key',
49
+ };
50
+
51
+ /**
52
+ * Columns the server fills and a writer must never send: MATERIALIZED ones (refused, or —
53
+ * under insert_allow_materialized_columns — forged) and the ingest clock. The build contract's
54
+ * "row shape" is the template's columns minus these.
55
+ */
56
+ const SERVER_COLUMNS = ['user_id', 'ingested_at', 'text_ngram', 'text_word', 'ver', 'captured_at', 'committed_at'];
57
+
58
+ function archiveDb(db) { return `${db}_archive`; }
59
+
60
+ /** Every fully-qualified name this schema touches for one member. The only place they are built. */
61
+ function v3Names(db, member) {
62
+ assertUsableName(db, 'house');
63
+ assertUsableName(member, 'member');
64
+ const a = archiveDb(db);
65
+ const out = { db, archive: a, member };
66
+ for (const t of CONVERSATION_TYPES) out[t] = `${db}.${member}_${t}`;
67
+ for (const t of CAPTURE_TYPES) out[t] = `${a}.${member}_${t}`;
68
+ out.legacy = {};
69
+ for (const t of LEGACY_TYPES) out.legacy[t] = `${a}.${member}_${t}_${LEGACY_SUFFIX}`;
70
+ // The member's own record of itself, unchanged from 0.18 (house.js META_TYPES).
71
+ out.meta = `${db}.${member}_meta`;
72
+ out.events = `${db}.${member}_events`;
73
+ return out;
74
+ }
75
+
76
+ function readTemplate() { return fs.readFileSync(TEMPLATE, 'utf-8'); }
77
+
78
+ /** The template with comments removed — line comments only; the template has no `--` inside strings. */
79
+ function stripComments(tpl) {
80
+ return tpl.split('\n').map((l) => l.replace(/--.*$/, '')).join('\n');
81
+ }
82
+
83
+ /**
84
+ * The CREATE statements for one member, in template order:
85
+ * [{ type: 'turns', database: 'mem', table: 'dana_turns', fq: 'mem.dana_turns', sql }]
86
+ * Comments are stripped BEFORE the ';' split — the template's comments may contain ';'.
87
+ */
88
+ function renderStatements(tpl, { db, member }) {
89
+ assertUsableName(db, 'house');
90
+ assertUsableName(member, 'member');
91
+ const body = stripComments(tpl).replaceAll('{db}', db).replaceAll('{m}', member);
92
+ const out = [];
93
+ for (const raw of body.split(';')) {
94
+ const sql = raw.trim();
95
+ if (!sql) continue;
96
+ const m = /^CREATE TABLE IF NOT EXISTS ([A-Za-z0-9_]+)\.([A-Za-z0-9_]+)\s*\(/.exec(sql);
97
+ if (!m) throw new Error(`schema-v3 template: unexpected statement: ${sql.slice(0, 80)}`);
98
+ const type = m[2].startsWith(`${member}_`) ? m[2].slice(member.length + 1) : m[2];
99
+ if (!V3_TYPES.includes(type)) throw new Error(`schema-v3 template declares an unknown table '${m[2]}'`);
100
+ out.push({ type, database: m[1], table: m[2], fq: `${m[1]}.${m[2]}`, sql });
101
+ }
102
+ const missing = V3_TYPES.filter((t) => !out.some((s) => s.type === t));
103
+ if (missing.length) throw new Error(`schema-v3 template is missing: ${missing.join(', ')}`);
104
+ return out;
105
+ }
106
+
107
+ /**
108
+ * The columns each table declares, as [{ name, type }] — type text includes DEFAULT /
109
+ * MATERIALIZED / CODEC. A Nested column is ONE entry here (`usage`, type `Nested(…)`);
110
+ * on the server it becomes `usage.hour`, `usage.model`, … arrays.
111
+ */
112
+ function templateColumns(tpl) {
113
+ const out = {};
114
+ for (const s of renderStatements(tpl, { db: 'db', member: 'm' })) {
115
+ const inner = s.sql.slice(s.sql.indexOf('(') + 1, s.sql.search(/\)\s*ENGINE\b/));
116
+ const cols = [];
117
+ let depth = 0; let buf = '';
118
+ for (const ch of inner + ',') {
119
+ if (ch === '(') depth++;
120
+ if (ch === ')') depth--;
121
+ if (ch === ',' && depth === 0) {
122
+ const decl = buf.replace(/\s+/g, ' ').trim();
123
+ buf = '';
124
+ if (!decl || /^(INDEX|CONSTRAINT|PROJECTION)\b/i.test(decl)) continue;
125
+ const sp = decl.indexOf(' ');
126
+ cols.push({ name: decl.slice(0, sp), type: decl.slice(sp + 1) });
127
+ continue;
128
+ }
129
+ buf += ch;
130
+ }
131
+ out[s.type] = cols;
132
+ }
133
+ return out;
134
+ }
135
+
136
+ /** What a writer sends for a table: the template's columns minus the server-filled ones. */
137
+ function rowColumns(tpl, type) {
138
+ return (templateColumns(tpl)[type] || []).map((c) => c.name).filter((n) => !SERVER_COLUMNS.includes(n));
139
+ }
140
+
141
+ /** What is wrong with a v3 table's sorting key, or null. */
142
+ function keyProblem(type, sortingKey) {
143
+ const want = V3_KEYS[type];
144
+ if (!want) return `'${type}' is not a v3 table`;
145
+ const have = String(sortingKey || '').trim();
146
+ if (!have) return 'not a MergeTree';
147
+ return have === want ? null : `(${have}) — expected (${want})`;
148
+ }
149
+
150
+ /**
151
+ * The capture layer's grants for a member (draft 5 §1): SELECT and INSERT on their own two
152
+ * tables, WITHOUT grant option — a member must not be able to hand the exact copy of their
153
+ * transcripts to a share. The conversation tables need nothing new: they sit inside the
154
+ * member's existing `<db>.<m>_*` wildcard.
155
+ */
156
+ function captureGrants(db, member) {
157
+ const n = v3Names(db, member);
158
+ return CAPTURE_TYPES.map((t) => `GRANT SELECT, INSERT ON ${n[t]} TO ${member}`);
159
+ }
160
+
161
+ const DDL_SETTINGS = { async_insert: 0, allow_experimental_full_text_index: 1 };
162
+
163
+ function isDenied(m) { return /Not enough privileges|ACCESS_DENIED/i.test(m); }
164
+ function isExists(m) { return /ALREADY_EXISTS|already exists/i.test(m); }
165
+ function msg(e) { return e && e.message ? e.message : String(e); }
166
+
167
+ /**
168
+ * Create one member's v3 tables. Idempotent.
169
+ *
170
+ * createDatabases also CREATE DATABASE IF NOT EXISTS <db> and <db>_archive (admin)
171
+ * grantCapture also grant the member SELECT, INSERT on the two capture tables (admin)
172
+ *
173
+ * Mirrors ensureSchema: a privilege denial is counted and skipped (a member re-running this
174
+ * on tables an admin made must not fail), a lost create race is the outcome we wanted. Then
175
+ * the tables are VERIFIED against V3_KEYS — the rooms are the truth, not the statements.
176
+ *
177
+ * @returns {{ version, dialect, created: string[], denied: string[], grants: string[],
178
+ * grant_failures: string[], tables: Object<type, {fq, exists, problem}>, ok: boolean }}
179
+ */
180
+ async function ensureV3Schema(q, { db, member, createDatabases = false, grantCapture = false, tpl = null } = {}, ui = null) {
181
+ const n = v3Names(db, member);
182
+ const say = (m) => ui && ui.ok && ui.ok(m);
183
+ const version = String(((await q.rows('SELECT version() AS v', { final: 0 }))[0] || {}).v || '');
184
+ const dialect = textIndexDialectFor(version);
185
+ const out = { version, dialect, created: [], denied: [], grants: [], grant_failures: [], tables: {}, ok: false };
186
+
187
+ if (createDatabases) {
188
+ for (const d of [n.db, n.archive]) {
189
+ try { await q.sql(`CREATE DATABASE IF NOT EXISTS ${d}`, DDL_SETTINGS); }
190
+ catch (e) {
191
+ const m = msg(e);
192
+ if (isDenied(m)) out.denied.push(`CREATE DATABASE ${d}`);
193
+ else if (!isExists(m)) throw e;
194
+ }
195
+ }
196
+ }
197
+
198
+ for (const s of renderStatements(tpl || readTemplate(), { db, member })) {
199
+ const modern = s.sql;
200
+ const legacy = legacyTextIndexDialect(modern);
201
+ const [first, second] = dialect === 'legacy' ? [legacy, modern] : [modern, legacy];
202
+ try {
203
+ try { await q.sql(first, DDL_SETTINGS); }
204
+ catch (e) {
205
+ if (!(first !== second && isTextIndexGrammarRefusal(msg(e)))) throw e;
206
+ await q.sql(second, DDL_SETTINGS);
207
+ }
208
+ out.created.push(s.fq);
209
+ } catch (e) {
210
+ const m = msg(e);
211
+ if (isDenied(m)) { out.denied.push(`CREATE TABLE ${s.fq}`); continue; }
212
+ if (isExists(m)) { out.created.push(s.fq); continue; }
213
+ throw new Error(`${s.fq}: ${m.split('\n')[0]}`);
214
+ }
215
+ }
216
+
217
+ if (grantCapture) {
218
+ // Never fatal: the tables are what this function guarantees. A grant can fail for a
219
+ // reason that is not a privilege — the member has no ClickHouse user of that name yet —
220
+ // and the caller reports it.
221
+ for (const g of captureGrants(db, member)) {
222
+ try { await q.sql(g, DDL_SETTINGS); out.grants.push(g); }
223
+ catch (e) {
224
+ const m = msg(e);
225
+ if (isDenied(m)) out.denied.push(g);
226
+ else out.grant_failures.push(`${g}: ${m.split('\n')[0]}`);
227
+ }
228
+ }
229
+ }
230
+
231
+ const rows = await q.rows(
232
+ `SELECT database, name, sorting_key FROM system.tables
233
+ WHERE (database = '${n.db}' AND name IN (${CONVERSATION_TYPES.map((t) => `'${member}_${t}'`).join(', ')}))
234
+ OR (database = '${n.archive}' AND name IN (${CAPTURE_TYPES.map((t) => `'${member}_${t}'`).join(', ')}))`,
235
+ { final: 0 });
236
+ let ok = true;
237
+ for (const t of V3_TYPES) {
238
+ const fq = n[t];
239
+ const r = rows.find((x) => `${x.database}.${x.name}` === fq);
240
+ const problem = r ? keyProblem(t, r.sorting_key) : 'missing';
241
+ if (problem) ok = false;
242
+ out.tables[t] = { fq, exists: Boolean(r), problem };
243
+ }
244
+ out.ok = ok;
245
+ if (ok) say(`v3 tables for ${member}: ${V3_TYPES.map((t) => n[t]).join(', ')}`);
246
+ return out;
247
+ }
248
+
249
+ /**
250
+ * A `q` over @clickhouse/client — the shipper's transport. Same settings in, same rows out.
251
+ */
252
+ function qFromClient(client) {
253
+ return {
254
+ sql: async (text, settings = {}) => { await client.command({ query: text, clickhouse_settings: settings }); },
255
+ rows: async (text, settings = {}) => {
256
+ const rs = await client.query({ query: text, format: 'JSONEachRow', clickhouse_settings: settings });
257
+ return rs.json();
258
+ },
259
+ };
260
+ }
261
+
262
+ module.exports = {
263
+ TEMPLATE, CONVERSATION_TYPES, CAPTURE_TYPES, V3_TYPES, LEGACY_TYPES, LEGACY_SUFFIX,
264
+ V3_KEYS, SERVER_COLUMNS,
265
+ archiveDb, v3Names, readTemplate, stripComments, renderStatements, templateColumns, rowColumns,
266
+ keyProblem, captureGrants, ensureV3Schema, qFromClient, DDL_SETTINGS,
267
+ };