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
@@ -40,40 +40,45 @@ alone.
40
40
 
41
41
  ## What you should see
42
42
 
43
- Trimmed from a run on a Linux VM with Docker, ClickHouse 26.7 and memhouse 0.18.9:
43
+ Trimmed from a run on a lab host (Linux, Docker 29), ClickHouse 26.7 and memhouse
44
+ 0.20.0-dev.15, as root in a container, with `HOUSE_PORT=18129`:
44
45
 
45
46
  ```
46
47
  == 1. a house: ClickHouse in a container, bound to loopback, plus the install
47
- ✓ ClickHouse starting via docker on http://localhost:8123 (loopback only)
48
+ ✓ ClickHouse starting via docker on http://localhost:18129 (loopback only)
48
49
  ✓ ClickHouse ready
49
- ✓ member 'sam' holds mem.sam_* and nothing else; 'memhouse_root' administers the house
50
- ✓ rooms for 'sam': sam_sessions, sam_messages, sam_tool_calls
50
+ ✓ member 'root' holds mem.root_* and nothing else; 'memhouse_root' administers the house
51
+ ✓ schema 3: mem.root_turns, mem.root_calls, mem.root_sessions, mem_archive.root_records, mem_archive.root_files; capture tables granted to 'root'
51
52
  ✓ installed
52
- ✓ shipper started in the background (pid 617049) — loading your history now
53
+ ✓ shipper started in the background (pid 281) — loading your history now
53
54
 
54
55
  == 2. one shipping pass
55
- [memhouse] shipped 2 sessions (0 skipped) → 6 msg rows, 2 tool rows in 0.7s
56
+ [memhouse] v3 capture: 2/2 files changed in 2 sessions → 2 files rows, 10 archive rows (0.0 MiB), 10 turns, 2 calls, 0 tombstones, 2 sessions [new 2]
56
57
 
57
58
  == 3. what the house holds
58
- ✓ connected: http://localhost:8123 / mem as sam
59
- ✓ house: 2 sessions, 6 messages (freshest ingest 2026-10-07 21:08:06 UTC)
59
+ ✓ connected: http://localhost:18129 / mem as root
60
+ ✓ schema 3 — tables for 'root': mem.root_turns, mem.root_calls, mem.root_sessions
61
+ ✓ house: 2 sessions, 6 messages (freshest ingest 2026-10-22 15:34:18 UTC)
60
62
  ✓ fleet: 1 active writer(s)
61
- sam@linux-vm-1f2e3d4c 0.18.9 last ship 0m ago
62
- ✓ shipper: running — daemon (pid 617049)
63
+ ✓ shipper: running — daemon (pid 281)
63
64
 
64
65
  == 4. find a past conversation
65
- claude-code:421460e4-5e66-4c1c-afa4-f1b198c28c25 claude-code alpha 2026-10-07 19:07 (1 hit)
66
- The deploy fails with ClickHouse error ACCESS_DENIED on INSERT. Why?
66
+ claude-code:f7204114-477a-4fcc-8bd0-353ed6426ae8~WyJy… alpha 2026-10-22 13:34 (1 hit · The deploy fails with ClickHouse error ACCESS_DENIED on INSE)
67
+ user u1: The deploy fails with ClickHouse error ACCESS_DENIED on INSERT. Why?
68
+
69
+ read a hit with: memhouse q window <ref> --at <message_id>
67
70
  ```
68
71
 
69
- Three messages per session in the house, not four: the tool result is folded into the
70
- tool call it answers.
72
+ Ten turns for six messages: a turn is one part of a message (its text, a thinking
73
+ block, a tool call or a tool result), and `memhouse status` counts messages. The ref
74
+ in step 4 names the session's store after the `~`; `memhouse q session <ref>` reads it.
71
75
 
72
- To look around, start the dashboard in the example's home (pick another port if 4640 is
73
- taken by a real install):
76
+ To look around, start the dashboard in the example's home, then open it (when 4640 is taken
77
+ by a real install, the dashboard takes the next free port and says which):
74
78
 
75
79
  ```bash
76
- MEMHOUSE_HOME=~/.memhouse-example-01 MEMHOUSE_PORT=4641 memhouse start
80
+ MEMHOUSE_HOME=~/.memhouse-example-01 memhouse start
81
+ MEMHOUSE_HOME=~/.memhouse-example-01 memhouse dashboard
77
82
  ```
78
83
 
79
84
  ## Teardown
@@ -95,5 +100,5 @@ memhouse deploy --local
95
100
  ```
96
101
 
97
102
  Every editor memhouse knows is read (`memhouse discover` lists what it finds first). Then
98
- `memhouse start` for the dashboard and `memhouse plugins install claude` to give your agents
99
- `/mem:recall` and the other skills.
103
+ `memhouse start` and `memhouse dashboard` for the dashboard, and `memhouse plugins install
104
+ claude` to give your agents `/mem:recall` and the other skills.
@@ -57,5 +57,5 @@ echo "== 4. find a past conversation"
57
57
  mh search ACCESS_DENIED
58
58
 
59
59
  echo
60
- echo "Done. Open the dashboard: MEMHOUSE_HOME=$MEMHOUSE_HOME $MH start (then http://localhost:4640)"
60
+ echo "Done. Open the dashboard: MEMHOUSE_HOME=$MEMHOUSE_HOME $MH start, then MEMHOUSE_HOME=$MEMHOUSE_HOME $MH dashboard"
61
61
  echo "Remove everything: $0 --teardown"
@@ -10,12 +10,15 @@ and keeps only the member's credential on disk.
10
10
  1. `memhouse whoami --admin` checks that the admin credential really can create users and
11
11
  grant, before anything is changed.
12
12
  2. `memhouse install --admin-user …` creates the member (default: your login name), grants
13
- it `mem.<member>_*` and nothing else, creates its rooms, and verifies the install by
14
- connecting **as the member**. The admin credential is used for this one command and not
13
+ it `mem.<member>_*`, builds its schema-3 tables (the six conversation tables in `mem`,
14
+ `<member>_session_parts`, `_sessions` and four more, and its capture archive
15
+ `<member>_session_units` and `_session_sources` in `mem_archive`, granted to it and nothing
16
+ more), and verifies the install by connecting **as the
17
+ member**. The admin credential is used for this one command and not
15
18
  saved (add `--keep-admin` if this machine should be able to invite people later).
16
- 3. `memhouse ship --ensure-schema` creates any room or stat table that is missing — safe to
17
- re-run, and the thing to run after an upgrade that adds one — then `memhouse ship`
18
- ships two made-up sessions.
19
+ 3. `memhouse ship --ensure-schema` checks the member's five tables, as the member, and
20
+ creates what it may — safe to re-run — then `memhouse ship` ships two made-up
21
+ sessions.
19
22
  4. `memhouse whoami` and `memhouse status` show the credential this machine now keeps and
20
23
  what the house holds.
21
24
 
@@ -66,31 +69,34 @@ printf '%s' "$PASSWORD" | memhouse install --url "$MEMHOUSE_URL" --admin-user de
66
69
 
67
70
  ## What you should see
68
71
 
69
- Trimmed from a run against ClickHouse 26.7.1.1315 with memhouse 0.18.9 (the test endpoint is shown as `ch.example.com`):
72
+ Trimmed from a run against a lab ClickHouse 26.7.4.58 with memhouse 0.20.0-dev.15, member
73
+ `ex02`:
70
74
 
71
75
  ```
72
76
  == 1. whoami --admin: is this credential really an administrator?
73
- ✓ admin at https://ch.example.com:8443 — administrator
77
+ ✓ ex_admin at https://ch.example.com:8443 — administrator
74
78
  credential from: MEMHOUSE_ADMIN_* environment
75
79
  may create users: yes
76
80
 
77
- == 2. install: build member 'sam' with the admin credential, then verify as the member
78
- ✓ created ClickHouse user 'sam'
79
- ✓ granted 'sam' mem.sam_* — their rooms and nothing else; async_insert pinned
80
- ✓ verified as 'sam' with the member credential
81
+ == 2. install: build member 'ex02' with the admin credential, then verify as the member
82
+ ✓ created ClickHouse user 'ex02'
83
+ ✓ granted 'ex02' mem.ex02_* — their tables (the capture archive is granted below); async_insert pinned
84
+ ✓ schema 3: mem.ex02_turns, mem.ex02_calls, mem.ex02_sessions, mem_archive.ex02_records, mem_archive.ex02_files; capture tables granted to 'ex02'
85
+ ✓ verified as 'ex02' with the member credential
81
86
  ✓ installed
82
87
  the admin credential was used for this install and not saved. To invite later:
83
- with-secret MEMHOUSE_ADMIN_PASSWORD=<reference> -- memhouse invite <name> --url … --admin-user admin
88
+ with-secret MEMHOUSE_ADMIN_PASSWORD=<reference> -- memhouse invite <name> --url … --admin-user ex_admin
84
89
 
85
- == 3. make sure every room and stat table exists, then ship once
86
- [memhouse] schema ensured (5 statements)
87
- [memhouse] shipped 2 sessions (0 skipped) → 6 msg rows, 2 tool rows in 0.2s
90
+ == 3. make sure the member's tables exist, then ship once
91
+ [memhouse] schema ensured (v3: mem.ex02_turns, mem.ex02_calls, mem.ex02_sessions, mem_archive.ex02_records, mem_archive.ex02_files)
92
+ [memhouse] v3 capture: 2/2 files changed in 2 sessions → 2 files rows, 10 archive rows (0.0 MiB), 10 turns, 2 calls, 0 tombstones, 2 sessions [new 2]
88
93
 
89
94
  == 4. whoami: the credential this machine keeps, and what it may do
90
- ✓ sam at https://ch.example.com:8443 — member
95
+ ✓ ex02 at https://ch.example.com:8443 — member
91
96
  may create users: no
92
97
 
93
98
  == 5. status
99
+ ✓ schema 3 — tables for 'ex02': mem.ex02_turns, mem.ex02_calls, mem.ex02_sessions
94
100
  ✓ house: 2 sessions, 6 messages
95
101
  ✓ fleet: 1 active writer(s)
96
102
  ```
@@ -104,18 +110,20 @@ Step 2 generates the member password and writes it only to the home's `env` file
104
110
  ./run.sh --teardown
105
111
  ```
106
112
 
107
- Deletes the example's home. The member and its rooms stay in the house: dropping them is
113
+ Deletes the example's home. The member and its tables stay in the house: dropping them is
108
114
  an administrator's decision. As the admin:
109
115
 
110
116
  ```sql
111
- DROP USER sam;
112
- -- then each of mem.sam_sessions, _messages, _tool_calls, _meta, _events,
113
- -- _session_stats, _session_model_stats, _session_tool_stats
117
+ DROP USER ex02;
118
+ -- then each of mem.ex02_session_parts, _session_usage, _session_links, _session_lifecycle,
119
+ -- _calls, _sessions, _meta, _events, and mem_archive.ex02_session_units, _session_sources
120
+ -- (docs/guides/operate.md, "Removing a member's tables", lists them by exact name)
114
121
  ```
115
122
 
116
123
  ## For real
117
124
 
118
125
  Leave out the two fixture flags (`--editors claude --claude-roots …`) and the
119
126
  `EXAMPLE_HOME`, and every editor on the machine ships into the house. Your other machines
120
- join the same rooms by installing with the same member credential (`memhouse install --url
121
- … --user <member> --password …`, or an invite file — see [03](../03-team-house/)).
127
+ join the same tables by installing with the same member credential (`memhouse install --url
128
+ … --user <member>` with the password in `MEMHOUSE_PASSWORD` or `--password-file`, or an
129
+ invite file — see [03](../03-team-house/)).
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env bash
2
2
  # Example 02 — point memhouse at a ClickHouse you already run.
3
3
  #
4
- # The admin credential builds a member account (rooms mem.<member>_*, one grant on exactly
5
- # those), the install is verified AS that member, and the admin credential is then dropped:
4
+ # The admin credential builds a member account (its tables mem.<member>_* and its capture
5
+ # archive in mem_archive, grants on exactly those), the install is verified AS that member,
6
+ # and the admin credential is then dropped:
6
7
  # what stays on disk is the member's. The admin password never touches the command line.
7
8
  #
8
9
  # with-secret MEMHOUSE_ADMIN_PASSWORD=<reference> -- ./run.sh
@@ -25,7 +26,7 @@ export MEMHOUSE_HOME=${EXAMPLE_HOME:-$HOME/.memhouse-example-02}
25
26
  if [ "${1:-}" = "--teardown" ]; then
26
27
  mh stop || true
27
28
  rm -rf "$MEMHOUSE_HOME"
28
- echo "example 02 home removed. The member and its rooms stay in the house; dropping them is an admin's call."
29
+ echo "example 02 home removed. The member and its tables stay in the house; dropping them is an admin's call."
29
30
  exit 0
30
31
  fi
31
32
 
@@ -56,7 +57,7 @@ mh install --url "$MEMHOUSE_URL" --admin-user "$MEMHOUSE_ADMIN_USER" --member "$
56
57
  unset MEMHOUSE_ADMIN_USER MEMHOUSE_ADMIN_PASSWORD
57
58
 
58
59
  echo
59
- echo "== 3. make sure every room and stat table exists, then ship once"
60
+ echo "== 3. make sure the member's tables exist, then ship once"
60
61
  # --ensure-schema only creates what is missing, as the member, and ships nothing; it is
61
62
  # safe to re-run, and the thing to run after an upgrade that adds a table.
62
63
  mh ship --ensure-schema
@@ -7,14 +7,16 @@ exactly who can see what. Finally she withdraws both shares.
7
7
 
8
8
  ## The model in four lines
9
9
 
10
- - Every member owns `mem.<member>_*`: five rooms (sessions, messages, tool_calls, meta,
11
- events) and three stat tables. One grant on that pattern is their whole access.
12
- - Housemates cannot read, list, or touch each other's rooms.
13
- - A **full share** (`memhouse share bob`) grants `SELECT ON mem.alice_*`: every room,
14
- every project, every machine, and anything ever pasted into a session.
15
- - A **scoped share** (`memhouse share carol --only project=alpha`) grants the three
16
- transcript rooms only, each behind a row policy. The stat tables, meta and events are
17
- not shared at all, because they carry no per-session filter.
10
+ - Every member owns `mem.<member>_*` (`session_parts`, `session_usage`, `session_links`,
11
+ `session_lifecycle`, `calls`, `sessions`, `meta`, `events`) and two tables in the capture
12
+ archive, `mem_archive.<member>_session_units` and `_session_sources`. Those grants are their
13
+ whole access.
14
+ - Housemates cannot read, list, or touch each other's tables.
15
+ - A **full share** (`memhouse share bob`) grants `SELECT` on exactly alice's six
16
+ conversation tables: every session, every project, every machine, and anything ever pasted
17
+ into one. Not the archive, not `meta` or `events`.
18
+ - A **scoped share** (`memhouse share carol --only project=alpha`) grants the same six
19
+ tables, each behind a row policy, written before the grant.
18
20
 
19
21
  ## What it does
20
22
 
@@ -31,7 +33,7 @@ exactly who can see what. Finally she withdraws both shares.
31
33
  5. alice: `memhouse share bob --revoke`, `memhouse share carol --revoke`; the reads are
32
34
  denied again.
33
35
 
34
- At exit the script drops the three users and their rooms (`KEEP=1` keeps them).
36
+ At exit the script drops the three users and their tables (`KEEP=1` keeps them).
35
37
 
36
38
  ## Needs
37
39
 
@@ -65,11 +67,11 @@ wrong, because their `localhost` is their own machine.
65
67
  | `MEMHOUSE_ADMIN_USER` / `MEMHOUSE_ADMIN_PASSWORD` | (required) | the administrator |
66
68
  | `MEMHOUSE_BIN` | `memhouse` | the command to run |
67
69
  | `EXAMPLE_DIR` | a new temp dir | where the three homes live |
68
- | `KEEP` | `0` | `1` keeps the members and their rooms |
70
+ | `KEEP` | `0` | `1` keeps the members and their tables |
69
71
 
70
72
  ## What you should see
71
73
 
72
- From a run against ClickHouse 26.7.1.1315, memhouse 0.18.9 (temp paths shortened):
74
+ From a run against a lab ClickHouse 26.7.4.58, memhouse 0.20.0-dev.15 (temp paths shortened):
73
75
 
74
76
  ```
75
77
  == 1. the admin invites three members
@@ -81,38 +83,43 @@ From a run against ClickHouse 26.7.1.1315, memhouse 0.18.9 (temp paths shortened
81
83
  alice: ✓ installed
82
84
  alice: ✓ password rotated — this credential is now yours alone
83
85
  alice: ✓ removed the spent invite file …/alice.env
84
- alice: [memhouse] shipped 2 sessions (0 skipped) → 6 msg rows, 2 tool rows in 0.3s
86
+ alice: [memhouse] v3 capture: 2/2 files changed in 2 sessions → 2 files rows, 10 archive rows (0.0 MiB), 10 turns, 2 calls, 0 tombstones, 2 sessions [new 2]
85
87
  (bob and carol the same)
86
88
 
87
89
  == 3. alice shares: everything with bob, project alpha only with carol
88
- ✓ 'bob' can now read every session in your rooms (mem.alice_*)
89
- ✓ 'carol' can read your rooms in 'mem' where project=alpha
90
- who can read your rooms in 'mem' — memhouse's own record, not ClickHouse's grant table:
91
- bob granted 2026-10-07 (all your rooms)
92
- carol granted 2026-10-07 scope=project=alpha
93
- alice_messages project = 'alpha'
90
+ ✓ 'bob' can now read every session in mem.alice_turns, mem.alice_calls, mem.alice_sessions
91
+ Those three tables only: not the capture archive, not meta or events.
92
+ ✓ 'carol' can read your sessions where project=alpha
93
+ turns 5 of 10 rows
94
+ calls 1 of 2 rows
95
+ sessions 1 of 2 rows
96
+ who can read your sessions in 'mem' — memhouse's own record, not ClickHouse's grant table:
97
+ bob granted 2026-10-22 (every session: turns, calls, sessions)
98
+ carol granted 2026-10-22 scope=project=alpha
99
+ alice_calls project = 'alpha'
94
100
  alice_sessions project = 'alpha'
95
- alice_tool_calls project = 'alpha'
101
+ alice_turns project = 'alpha'
96
102
 
97
103
  == 4. who reads what — each with their own credential
98
- alice, her own messages by project alpha,beta 6
99
- bob (full share), alice's messages alpha,beta 6
100
- carol (alpha only), alice's messages alpha 3
101
- bob, alice's session_stats 2 rows
102
- carol, alice's session_stats denied
103
- carol, alice's meta denied
104
- carol, bob's messages (never shared) denied
104
+ alice, her own turns by project alpha,beta 10
105
+ bob (full share), alice's turns alpha,beta 10
106
+ carol (alpha only), alice's turns alpha 5
107
+ bob, alice's meta denied
108
+ bob, alice's capture archive denied
109
+ carol, alice's sessions (alpha only) 1
110
+ carol, bob's turns (never shared) denied
105
111
 
106
112
  == 5. alice withdraws both shares
107
- ✓ revoked 'bob' — SELECT withdrawn and 3 row policies dropped
108
- ✓ revoked 'carol' — SELECT withdrawn and 3 row policies dropped
109
- bob, alice's messages denied
110
- carol, alice's messages denied
111
- removed alice, bob, carol, their rooms and their homes
113
+ ✓ revoked 'bob' — SELECT withdrawn from mem.alice_turns, mem.alice_calls, mem.alice_sessions and its row policies dropped
114
+ ✓ revoked 'carol' — SELECT withdrawn from mem.alice_turns, mem.alice_calls, mem.alice_sessions and its row policies dropped
115
+ bob, alice's turns denied
116
+ carol, alice's turns denied
117
+ removed alice, bob, carol, their tables and their homes
112
118
  ```
113
119
 
114
- The revoke line for bob counts the three policy drops memhouse attempts on every revoke;
115
- a full share has no policies, so none existed to drop.
120
+ On a house still on the 0.18 rooms the same script's reads would fail: they name the
121
+ schema-3 tables. An invite follows the house's layout, so a team house made with 0.20 is
122
+ schema 3 throughout.
116
123
 
117
124
  ## For real
118
125
 
@@ -123,7 +130,7 @@ a full share has no policies, so none existed to drop.
123
130
  - **Each member** runs `memhouse install --env <name>.env` on their laptop, without the
124
131
  fixture flags, then `memhouse start` (or `memhouse service install`) to keep shipping.
125
132
  - **Sharing** is the member's own decision and needs no admin: `memhouse share <user>`,
126
- `--only project=<p>` (also `session=`, `host=`, `source=`, `folder=`, `since=`,
127
- `until=`), `--revoke`, `--list`.
128
- - A grantee reads the shared rooms with their own credential: `/mem:recall` and the other
129
- agent skills, or SQL against `mem.<owner>_messages`.
133
+ `--only project=<p>` (also `session=`, `host=`, `harness=`, `folder=`, `since=`,
134
+ `until=`), `--revoke`, `--list`. See [Share and revoke](../../docs/guides/share.md).
135
+ - A grantee reads the shared tables with their own credential: `/mem:sql`, or SQL against
136
+ `mem.<owner>_session_parts` and `mem.<owner>_sessions FINAL`.
@@ -11,7 +11,7 @@
11
11
  # if users named alice, bob or carol already exist.
12
12
  #
13
13
  # with-secret MEMHOUSE_ADMIN_PASSWORD=<reference> -- ./run.sh
14
- # KEEP=1 ... keep the three members and their rooms afterwards (default: removed)
14
+ # KEEP=1 ... keep the three members and their tables afterwards (default: removed)
15
15
  #
16
16
  # Environment:
17
17
  # MEMHOUSE_URL required the house's HTTP endpoint, as the members will reach it
@@ -46,28 +46,41 @@ as() {
46
46
  | curl -sS -K - --data-binary "$sql" "$URL/" 2>&1 | head -1
47
47
  }
48
48
  denied() { case "$1" in *ACCESS_DENIED*|*"Not enough privileges"*) return 0;; *) return 1;; esac; }
49
+ # A "denied" means something only for a table that exists: a query on a missing table is refused
50
+ # too (UNKNOWN_TABLE, or ACCESS_DENIED on a server that checks grants first), and would read as
51
+ # a share that holds. So every table a check reads is first proved to exist, as the admin.
52
+ need() {
53
+ local db=${1%%.*} t=${1#*.} n
54
+ n=$(admin_q "SELECT count() FROM system.tables WHERE database = '$db' AND name = '$t' FORMAT TSV")
55
+ [ "$n" = "1" ] || { echo "MISSING TABLE $1: this check would read a refusal as a closed door. Nothing it says can be trusted; stopping." >&2; exit 1; }
56
+ }
57
+ # What a user gets from one table: the result, or "denied".
58
+ reads() { local r; r=$(as "$1" "SELECT count() FROM $2 FORMAT TSV"); if denied "$r"; then echo denied; else echo "$r rows"; fi; }
59
+ # One line of the table below. `need` runs here, in the script's own shell, so a missing table
60
+ # stops the script (inside $(…) its exit would end only the subshell).
61
+ show_reads() { need "$3"; show "$1" "$(reads "$2" "$3")"; }
49
62
 
50
63
  cleanup() {
51
- [ "${KEEP:-0}" = "1" ] && { echo "KEEP=1: members and rooms left in place; homes in $WORK"; return; }
64
+ [ "${KEEP:-0}" = "1" ] && { echo "KEEP=1: members and tables left in place; homes in $WORK"; return; }
52
65
  [ "${CREATED:-0}" = "1" ] || return 0
53
66
  for p in $PEOPLE; do
54
67
  admin_q "DROP USER IF EXISTS $p" >/dev/null 2>&1 || true
55
- for t in $(admin_q "SELECT name FROM system.tables WHERE database = 'mem' AND startsWith(name, '${p}_') FORMAT TSV" 2>/dev/null); do
56
- admin_q "DROP TABLE IF EXISTS mem.$t SYNC" >/dev/null 2>&1 || true
68
+ for t in $(admin_q "SELECT database || '.' || name FROM system.tables WHERE database IN ('mem', 'mem_archive') AND startsWith(name, '${p}_') FORMAT TSV" 2>/dev/null); do
69
+ admin_q "DROP TABLE IF EXISTS $t SYNC" >/dev/null 2>&1 || true
57
70
  done
58
71
  done
59
72
  rm -rf "$WORK"
60
- echo "removed alice, bob, carol, their rooms and their homes"
73
+ echo "removed alice, bob, carol, their tables and their homes"
61
74
  }
62
75
  trap cleanup EXIT
63
76
 
64
- # Refuse anything this run did not make: the users, and any table under their room prefixes
65
- # (orphaned rooms can outlive their users). The cleanup below drops by those prefixes, so it
77
+ # Refuse anything this run did not make: the users, and any table under their prefixes
78
+ # (orphaned tables can outlive their users). The cleanup below drops by those prefixes, so it
66
79
  # may only run when this check proved them empty first.
67
80
  existing=$(admin_q "SELECT count() FROM system.users WHERE name IN ('alice','bob','carol') FORMAT TSV")
68
81
  [ "$existing" = "0" ] || { echo "refusing: users alice/bob/carol already exist on this server — use a throwaway ClickHouse"; exit 1; }
69
- rooms=$(admin_q "SELECT count() FROM system.tables WHERE database = 'mem' AND (startsWith(name, 'alice_') OR startsWith(name, 'bob_') OR startsWith(name, 'carol_')) FORMAT TSV")
70
- [ "$rooms" = "0" ] || { echo "refusing: mem.alice_*/bob_*/carol_* tables already exist ($rooms) — use a throwaway ClickHouse"; exit 1; }
82
+ tables=$(admin_q "SELECT count() FROM system.tables WHERE database IN ('mem', 'mem_archive') AND (startsWith(name, 'alice_') OR startsWith(name, 'bob_') OR startsWith(name, 'carol_')) FORMAT TSV")
83
+ [ "$tables" = "0" ] || { echo "refusing: alice_*/bob_*/carol_* tables already exist ($tables) — use a throwaway ClickHouse"; exit 1; }
71
84
  CREATED=1
72
85
 
73
86
  # A loopback URL is right only through a tunnel; invite refuses one unless told so.
@@ -107,18 +120,22 @@ mh share --list
107
120
  echo
108
121
  echo "== 4. who reads what — each with their own credential"
109
122
  show() { printf ' %-44s %s\n' "$1" "$2"; }
110
- show "alice, her own messages by project" "$(as alice "SELECT arrayStringConcat(arraySort(groupUniqArray(project)), ',') || ' ' || toString(count()) FROM mem.alice_messages FORMAT TSV")"
111
- show "bob (full share), alice's messages" "$(as bob "SELECT arrayStringConcat(arraySort(groupUniqArray(project)), ',') || ' ' || toString(count()) FROM mem.alice_messages FORMAT TSV")"
112
- show "carol (alpha only), alice's messages" "$(as carol "SELECT arrayStringConcat(arraySort(groupUniqArray(project)), ',') || ' ' || toString(count()) FROM mem.alice_messages FORMAT TSV")"
113
- r=$(as bob "SELECT count() FROM mem.alice_session_stats FORMAT TSV"); show "bob, alice's session_stats" "$(denied "$r" && echo denied || echo "$r rows")"
114
- r=$(as carol "SELECT count() FROM mem.alice_session_stats FORMAT TSV"); show "carol, alice's session_stats" "$(denied "$r" && echo denied || echo "$r rows")"
115
- r=$(as carol "SELECT count() FROM mem.alice_meta FORMAT TSV"); show "carol, alice's meta" "$(denied "$r" && echo denied || echo "$r rows")"
116
- r=$(as carol "SELECT count() FROM mem.bob_messages FORMAT TSV"); show "carol, bob's messages (never shared)" "$(denied "$r" && echo denied || echo "$r rows")"
123
+ parts() { as "$1" "SELECT arrayStringConcat(arraySort(groupUniqArray(project)), ',') || ' ' || toString(count()) FROM mem.$2_session_parts FINAL WHERE removed = 0 FORMAT TSV"; }
124
+ need mem.alice_session_parts
125
+ show "alice, her own message parts by project" "$(parts alice alice)"
126
+ show "bob (full share), alice's message parts" "$(parts bob alice)"
127
+ show "carol (alpha only), alice's message parts" "$(parts carol alice)"
128
+ # A share is SELECT on exactly alice's six conversation tables: full or scoped, nothing else.
129
+ show_reads "bob, alice's meta" bob mem.alice_meta
130
+ show_reads "bob, alice's capture archive" bob mem_archive.alice_session_units
131
+ need mem.alice_sessions
132
+ show "carol, alice's sessions (alpha only)" "$(as carol "SELECT count() FROM mem.alice_sessions FINAL FORMAT TSV")"
133
+ show_reads "carol, bob's message parts (never shared)" carol mem.bob_session_parts
117
134
 
118
135
  echo
119
136
  echo "== 5. alice withdraws both shares"
120
137
  mh share bob --revoke --yes
121
138
  mh share carol --revoke --yes
122
- r=$(as bob "SELECT count() FROM mem.alice_messages FORMAT TSV"); show "bob, alice's messages" "$(denied "$r" && echo denied || echo "$r rows")"
123
- r=$(as carol "SELECT count() FROM mem.alice_messages FORMAT TSV"); show "carol, alice's messages" "$(denied "$r" && echo denied || echo "$r rows")"
139
+ show_reads "bob, alice's message parts" bob mem.alice_session_parts
140
+ show_reads "carol, alice's message parts" carol mem.alice_session_parts
124
141
  unset MEMHOUSE_HOME
@@ -15,7 +15,7 @@ with members shipping from their own laptops. One compose file holds all of it.
15
15
  |---|---|---|
16
16
  | `clickhouse` | `clickhouse/clickhouse-server:26.7.1.1315` | the house. No published port; reachable only through Caddy. |
17
17
  | `caddy` | `caddy:2.10` | TLS on 443: a Let's Encrypt certificate for your domain, or Caddy's own CA. Port 80 answers the ACME challenge and redirects. |
18
- | `backup` | the ClickHouse image | at `BACKUP_AT` (UTC) writes the house **and its accounts** to the `backups` volume; keeps the newest `BACKUP_KEEP` (14). |
18
+ | `backup` | the ClickHouse image | at `BACKUP_AT` (UTC) writes the house, its capture archive **and its accounts** to the `backups` volume; keeps the newest `BACKUP_KEEP` (14). |
19
19
 
20
20
  ClickHouse is configured inline (`configs:` in [compose.yml](compose.yml)): the `backups`
21
21
  disk, and `users_without_row_policies_can_read_rows` set explicitly to `true`, which
@@ -81,7 +81,7 @@ memhouse plugins install claude # /mem:recall and the other agent ski
81
81
  ```
82
82
 
83
83
  `memhouse service status` shows the unit; `memhouse status` shows freshness and every
84
- machine writing to her rooms. Without a service, `memhouse start` runs the shipper and the
84
+ machine writing to her tables. Without a service, `memhouse start` runs the shipper and the
85
85
  dashboard as background processes until logout.
86
86
 
87
87
  ## Smoke test
@@ -89,11 +89,12 @@ dashboard as background processes until logout.
89
89
  `./smoke.sh` brings the stack up with `.env`, then, through the proxy only: checks the
90
90
  certificate, that the `default` user is gone, invites a member `smoke`, installs it from
91
91
  the invite file, ships two made-up sessions, reads them back as the member, backs up and
92
- restores the member's messages, and checks the nightly job. At exit it drops `smoke` and
93
- its rooms (`KEEP=1` keeps them). `SKIP_UP=1` tests a stack that is already running; see
92
+ restores the member's message parts and archive units, and checks the nightly job. At exit it drops `smoke` and
93
+ its tables (`KEEP=1` keeps them). `SKIP_UP=1` tests a stack that is already running; see
94
94
  the top of the script.
95
95
 
96
- From a run with `MEMHOUSE_TLS=internal` on a Linux VM with Docker:
96
+ From a run with `MEMHOUSE_TLS=internal` on a lab host (Linux, Docker 29, Compose 5.3),
97
+ memhouse 0.20.0-dev.15:
97
98
 
98
99
  ```
99
100
  smoke test against https://localhost:18443
@@ -102,28 +103,36 @@ smoke test against https://localhost:18443
102
103
  ok admin reaches ClickHouse 26.7.1.1315
103
104
  ok admin invited member 'smoke'
104
105
  ok the member installed from the invite file, over https
105
- ok shipped: shipped 2 sessions (0 skipped) → 6 msg rows, 2 tool rows in 0.6s
106
- ok the member reads its 6 messages back through the proxy
106
+ ok shipped: v3 capture: 2/2 files changed in 2 sessions → 2 files rows, 10 archive rows (0.0 MiB), 10 turns, 2 calls, 0 tombstones, 2 sessions [new 2]
107
+ ok the member reads its 2 sessions back through the proxy
107
108
  ok the member cannot create tables outside mem.smoke_*
108
109
  ok BACKUP to the backups disk
109
110
  ok RESTORE from it
110
- ok the restored copy holds the same 6 rows
111
- ok the backup service has written a nightly backup since the server started
111
+ ok the restored copy holds the same 10 turns
112
+ info no nightly backup yet this server lifetime (BACKUP_ON_START=1 runs one at start)
112
113
 
113
- 12 passed, 0 failed
114
+ 11 passed, 0 failed
114
115
  ```
115
116
 
117
+ The nightly job was then run at once (`BACKUP_ON_START=1 docker compose up -d
118
+ --force-recreate backup`): `backup written: mem-2026-10-22-15_38.zip`, and `mem_archive`
119
+ restored from that file under another name held the member's 10 capture records, as
120
+ the live one did.
121
+
116
122
  ## Backups and restore
117
123
 
118
124
  Every night the `backup` service runs
119
125
 
120
126
  ```sql
121
- BACKUP DATABASE mem, TABLE system.users, TABLE system.roles, TABLE system.row_policies,
127
+ BACKUP DATABASE mem, DATABASE mem_archive,
128
+ TABLE system.users, TABLE system.roles, TABLE system.row_policies,
122
129
  TABLE system.settings_profiles, TABLE system.quotas
123
130
  TO Disk('backups', 'mem-YYYY-MM-DD-HH_MM.zip')
124
131
  ```
125
132
 
126
- and deletes all but the newest `BACKUP_KEEP`. The accounts are in the same file on
133
+ and deletes all but the newest `BACKUP_KEEP`. `mem_archive` is the capture archive of a
134
+ 0.20 house: every transcript byte as captured, which the sessions in `mem` are derived
135
+ from. It is left out while it does not exist (a house on the 0.18 rooms). The accounts are in the same file on
127
136
  purpose: users, their password hashes, their grants and the row policies of scoped shares
128
137
  live outside the database, and a restored house without them is one nobody can log in to.
129
138
  `docker compose logs backup` shows each run.
@@ -149,11 +158,14 @@ docker compose exec clickhouse chown clickhouse:clickhouse /backups/mem-2026-10-
149
158
  then, as the admin:
150
159
 
151
160
  ```sql
152
- RESTORE DATABASE mem, TABLE system.users, TABLE system.roles, TABLE system.row_policies,
161
+ RESTORE DATABASE mem, DATABASE mem_archive,
162
+ TABLE system.users, TABLE system.roles, TABLE system.row_policies,
153
163
  TABLE system.settings_profiles, TABLE system.quotas
154
164
  FROM Disk('backups', 'mem-2026-10-07-21_17.zip')
155
165
  ```
156
166
 
167
+ (Leave out `DATABASE mem_archive` for a file written before the house had one.)
168
+
157
169
  Members come back with their own passwords and grants, so their machines keep shipping
158
170
  once the DNS name points at the new server. Restoring accounts fails if a user of the same
159
171
  name already exists, which is why this is a restore onto a fresh stack. To look inside a
@@ -161,9 +173,12 @@ backup without touching the live house, restore just the data under another name
161
173
  `RESTORE DATABASE mem AS mem_check FROM Disk('backups', '<file>')`, then
162
174
  `DROP DATABASE mem_check`.
163
175
 
164
- Tested on ClickHouse 26.7.1.1315: a nightly file copied off the server with the command
176
+ Tested on ClickHouse 26.7.1.1315 with memhouse 0.18.9 (no archive yet): a nightly file copied off the server with the command
165
177
  above, a fresh stack (`down -v`, `up`), the copy put back and restored — the data came
166
178
  back, and the member logged in with its original password.
179
+ With 0.20 the archive's part was checked on its own: `mem_archive` restored from a nightly
180
+ file under another name held the same records as the live one. The full fresh-stack
181
+ restore has not been repeated on 0.20.
167
182
 
168
183
  ## Upgrading ClickHouse
169
184
 
@@ -122,7 +122,8 @@ configs:
122
122
  content: |
123
123
  #!/bin/bash
124
124
  # Nightly backup at BACKUP_AT (UTC), keeping the newest BACKUP_KEEP. One file holds
125
- # the house AND the accounts: users, their grants and the row policies of scoped
125
+ # the house, its capture archive (<db>_archive, schema 3: every transcript byte as
126
+ # captured) AND the accounts: users, their grants and the row policies of scoped
126
127
  # shares live outside the database, and a restore without them is a house nobody
127
128
  # can log in to. README.md has the restore.
128
129
  set -uo pipefail
@@ -133,8 +134,10 @@ configs:
133
134
  return
134
135
  fi
135
136
  name="$$MEMHOUSE_DB-$$(date -u +%Y-%m-%d-%H_%M).zip"
137
+ DBS="DATABASE $$MEMHOUSE_DB"
138
+ [ "$$(clickhouse-client --host clickhouse --query "EXISTS DATABASE $${MEMHOUSE_DB}_archive")" = "1" ] && DBS="$$DBS, DATABASE $${MEMHOUSE_DB}_archive"
136
139
  if clickhouse-client --host clickhouse --query \
137
- "BACKUP DATABASE $$MEMHOUSE_DB, $$ACCESS TO Disk('backups', '$$name')" >/dev/null; then
140
+ "BACKUP $$DBS, $$ACCESS TO Disk('backups', '$$name')" >/dev/null; then
138
141
  echo "$$(date -u +%FT%TZ) backup written: $$name"
139
142
  else
140
143
  echo "$$(date -u +%FT%TZ) backup FAILED: $$name" >&2