memhouse 0.20.0 → 0.20.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 (37) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/bin/memhouse.js +209 -30
  3. package/docs/guides/install.md +3 -0
  4. package/docs/guides/invite.md +7 -0
  5. package/docs/guides/search-and-resume.md +30 -0
  6. package/docs/guides/share.md +5 -0
  7. package/docs/releases/README.md +2 -0
  8. package/docs/releases/v0.20.1.md +114 -0
  9. package/docs/releases/v0.20.2.md +134 -0
  10. package/memhouse/codex-plugin.js +194 -0
  11. package/memhouse/delivery/INVITATION.md.tpl +1 -1
  12. package/memhouse/delivery/codex/AGENTS-BLOCK.md +11 -0
  13. package/memhouse/delivery/codex/SKILL.md +50 -0
  14. package/memhouse/delivery/plugin/reference/HOUSE.md +7 -9
  15. package/memhouse/delivery/plugin/skills/access/SKILL.md +1 -1
  16. package/memhouse/flags.js +1 -1
  17. package/memhouse/help.js +37 -0
  18. package/memhouse/house/q-cli.js +14 -1
  19. package/memhouse/house/records-to-profile.js +14 -3
  20. package/memhouse/house/schema-choice.js +15 -6
  21. package/memhouse/house/schema-profile-views.sql.tpl +53 -0
  22. package/memhouse/house/schema-profile.js +46 -6
  23. package/memhouse/house/schema-profile.sql.tpl +8 -1
  24. package/memhouse/invitation.js +8 -2
  25. package/memhouse/plugin-copies.js +13 -1
  26. package/memhouse/release-hint.js +24 -0
  27. package/memhouse/share.js +36 -4
  28. package/memhouse/shipper/capture.js +39 -9
  29. package/memhouse/shipper/log-adapter.js +18 -7
  30. package/memhouse/shipper/normalise-run.js +8 -2
  31. package/memhouse/version-notice.js +66 -0
  32. package/package.json +3 -3
  33. package/public/assets/{index-CQSJ3nhV.js → index-CcNnhfM3.js} +1 -1
  34. package/public/index.html +1 -1
  35. package/ui/package-lock.json +264 -318
  36. package/ui/package.json +5 -4
  37. package/ui/src/lib/dashboard-token.js +7 -2
@@ -128,3 +128,6 @@ The path proves nothing on its own: Homebrew's prefix is `/usr/local` on Intel a
128
128
  - `memhouse plugins install claude`: the `/mem:*` skills, for Claude Code only. Codex and the
129
129
  other editors' sessions ship all the same; an agent there reads the house with the CLI
130
130
  (`memhouse q`, `memhouse room-sql`). See [Search, resume and skills](search-and-resume.md).
131
+ - `memhouse plugins install codex`: for Codex, a short block in its `AGENTS.md` (the rules) and
132
+ a `memhouse` skill (the read recipes). Nothing is written under the Codex home unless you run
133
+ it. See [Codex](search-and-resume.md#codex).
@@ -27,6 +27,13 @@ memhouse invite alice --url https://house.example.com
27
27
  - **Alice installs with `memhouse install --env invite-alice.env`.** It offers to rotate
28
28
  the password to one only she knows, which works because she is granted `ALTER USER` on
29
29
  herself. Then it deletes the file. Once she rotates, the password you saw stops working.
30
+ - **The invite expires.** The password in the file stops working 7 days after the invite
31
+ (`--expires <days>` sets 1 to 365), counted by the server's clock. The output, the file and
32
+ the invitation page say when. Installing lifts the expiry, whether she changes the password
33
+ or keeps it with `--keep-password`. After the date, `install` refuses the file and tells her
34
+ to ask for a new invite. Her account is still there, so remove it first (`DROP USER alice`, as
35
+ the administrator; `invite` says so when it meets it), then invite her again. Her tables are
36
+ kept and reused.
30
37
 
31
38
  ## Which admin credential
32
39
 
@@ -99,6 +99,36 @@ missing skill never announces itself.
99
99
  For an agent that does not use Claude Code skills, `memhouse prompt` prints a snippet for
100
100
  its system prompt: where memory lives and how to query it.
101
101
 
102
+ ### Codex
103
+
104
+ The `/mem` skills are for Claude Code. For Codex:
105
+
106
+ ```bash
107
+ memhouse plugins install codex # under CODEX_HOME, or ~/.codex
108
+ memhouse plugins list # shows the codex copy and the command it runs
109
+ memhouse plugins remove codex
110
+ ```
111
+
112
+ It writes two things, and only when you run it:
113
+
114
+ - **A block at the top of `AGENTS.md`**, between `<!-- memhouse:begin … -->` and
115
+ `<!-- memhouse:end -->`. Codex puts this file in every prompt, so the block holds only what must
116
+ always hold: never read or print `~/.memhouse/env` or a `MEMHOUSE_*` value; `MEMHOUSE_URL` is the
117
+ house and `MEMHOUSE_PORT` the dashboard; questions about past sessions go to the `memhouse`
118
+ skill. The file is created if it is missing. Installing again replaces the block where it
119
+ stands. Removing takes out the block and the blank line after it, so the rest of the file is
120
+ what it was, byte for byte; a file that held only the block is deleted, or, when `AGENTS.md`
121
+ is a symlink, its target is emptied and the link kept. Leave the markers
122
+ alone: with one of them missing, install and remove refuse and change nothing.
123
+ - **A skill, `skills/memhouse/`**, with the read recipes: `memhouse q`, `--member` for a
124
+ housemate's sessions, `memhouse room-sql`. Codex lists it by name and description and reads
125
+ the rest when it uses it. The directory carries `.memhouse.json`; a `skills/memhouse` without
126
+ it is someone else's, and install refuses to replace it and remove leaves it.
127
+
128
+ For an instance with its own home or binary, both name it: `MEMHOUSE_HOME=<home> <binary> q …`.
129
+ After an upgrade, the first `memhouse` command refreshes both, as it does the `/mem` copies; a
130
+ block you took out by hand stays out.
131
+
102
132
  ## Writing your own queries
103
133
 
104
134
  On schema 3 (0.20), the tables are `mem.<you>_session_parts` (one row per part of a message:
@@ -26,6 +26,7 @@ Full or scoped, a share is `SELECT` on exactly your six conversation tables:
26
26
  | rows | all | those matching the scope, through a row policy on each table |
27
27
  | capture archive (`<db>_archive`) | not granted | not granted |
28
28
  | `<you>_meta`, `<you>_events` | not granted | not granted |
29
+ | `<you>_session_turns_view` (your turns, a view) | not granted | not granted |
29
30
  | tables added later | not granted | not granted |
30
31
 
31
32
  ## Scopes
@@ -106,6 +107,10 @@ included. A grant someone made by hand in SQL does not appear there. A member ca
106
107
  `system.grants` or `system.row_policies`; an administrator can, with `SHOW GRANTS FOR <user>`
107
108
  and the queries in [Operate](operate.md#who-is-here).
108
109
 
110
+ It also lists the other direction: each member whose sessions your credential can read, with
111
+ the command that reads them. A scoped share shows only the rows its owner chose, and its filter
112
+ is the owner's to see.
113
+
109
114
  ## Reading what was shared with you
110
115
 
111
116
  Shared tables sit in the same database as yours, under the other member's name, for
@@ -5,6 +5,8 @@ do, how to upgrade, what is deprecated and when it goes, and what was deferred.
5
5
 
6
6
  | version | date | headline |
7
7
  |---|---|---|
8
+ | [0.20.2](v0.20.2.md) | 2026-11-03 | harness-sessions-sql v1.1's columns and a turns view; Cursor sub-agent links; the first capture re-derives every session |
9
+ | [0.20.1](v0.20.1.md) | 2026-11-02 | invites expire; `memhouse plugins install codex`; per-command help and clearer CLI answers |
8
10
  | [0.20.0](v0.20.0.md) | 2026-10-29 | schema 3: a session is its store, nothing cut or lost, forks billed once, ten editors through normalisers; Apache-2.0 |
9
11
  | [0.18.11](v0.18.11.md) | 2026-10-10 | sessions with subagents stop re-shipping; security: no stored credential sent to `invite --url` |
10
12
  | [0.18.10](v0.18.10.md) | 2026-10-08 | security: the admin password never on argv; no member password printed; a scoped share reaches only its scope |
@@ -0,0 +1,114 @@
1
+ # memhouse 0.20.1 — 2026-11-02
2
+
3
+ A patch release on 0.20.0, with no schema change. Install it with `memhouse update`, or
4
+ `npm install -g memhouse`.
5
+
6
+ - **An invite's password expires.** It stops working 7 days after the invite unless it is
7
+ installed first. A file that sits unused, or that was read where it should not have been,
8
+ stops working on its own.
9
+ - **Codex gets memhouse on request.** `memhouse plugins install codex` adds a short rules block
10
+ to Codex's `AGENTS.md` and a `memhouse` skill with the read recipes. `plugins remove codex`
11
+ takes out exactly those.
12
+ - **Clearer answers from the CLI:**
13
+ - `memhouse <command> --help` prints that command's help;
14
+ - a refused credential is named before anything is written;
15
+ - `share --list` also shows the shares you received;
16
+ - `members` tells a share from a member's own tables;
17
+ - `discover` and `update` stop reporting problems that belong to nobody.
18
+ - **A new minor version's first run says so,** and names the release notes to read.
19
+
20
+ ## Read this before upgrading
21
+
22
+ Nothing in the house changes, and there is no migration. Three things behave differently:
23
+
24
+ - **Inviting: the password now expires.** `memhouse invite` sets the expiry on the new account
25
+ using the server's clock. The default is 7 days; `--expires <days>` takes 1 to 365. The
26
+ output, the invite file and `MEMHOUSE-INVITATION.md` all give the date. An invitee who
27
+ installs after that date is refused with "this invite expired at … ask whoever invited you
28
+ for a new one". To invite them again under the same name, remove the unused account first
29
+ with `DROP USER <name>`, as the administrator; their tables are kept and reused. Invites
30
+ made by 0.20.0 or earlier carry no expiry and keep working as before.
31
+ - **The first command after moving to a new minor version prints one line** to stderr. It
32
+ names the version it replaces and the release notes. Patch releases, downgrades and fresh
33
+ installs print nothing.
34
+ - **Codex is opt-in.** Nothing is written under `~/.codex` (or `CODEX_HOME`) unless you run
35
+ `memhouse plugins install codex`.
36
+
37
+ ```bash
38
+ memhouse update # then: memhouse status — every line a check mark
39
+ ```
40
+
41
+ ## Invites
42
+
43
+ - **The expiry is set by the server**, as `VALID UNTIL` on the new account, so it holds no
44
+ matter what the file says. `install --env` also checks the date in the file and refuses
45
+ before contacting the server.
46
+ - **Installing lifts it.** A password change removes the expiry, and so does
47
+ `--keep-password`, which keeps the invited password.
48
+ - **`invite --print-sql`** prints the same `ALTER USER … VALID UNTIL` statement for an
49
+ administrator who runs the plan by hand. The date is left as a placeholder, so the
50
+ statement does not parse until a real date is put in.
51
+ - **Re-inviting an unused name** used to fail with the install message, "give their
52
+ password". It now says the invite was never installed, gives its expiry, and names the
53
+ remedy.
54
+
55
+ ## Codex
56
+
57
+ Codex puts `AGENTS.md` into every prompt and lists each skill by its name and description,
58
+ reading the body only when the skill is used. memhouse uses both, and only when asked:
59
+
60
+ - **The block** in `AGENTS.md` holds what must always apply:
61
+ - never read or print the env file or a `MEMHOUSE_*` value;
62
+ - `MEMHOUSE_URL` is the house and `MEMHOUSE_PORT` the dashboard;
63
+ - questions about past sessions go to the skill.
64
+
65
+ It sits between `<!-- memhouse:begin … -->` and `<!-- memhouse:end -->` markers. A reinstall
66
+ replaces it where it stands. Remove takes it out and leaves the rest of the file
67
+ byte-identical. A file that held only the block is deleted, unless it is a symlink: then its
68
+ target is emptied and the link kept.
69
+ - **The skill**, `skills/memhouse/`, carries the read recipes:
70
+ - `memhouse q` for search, sessions, activity by hour, spend, models, tools and machines;
71
+ - `--member` for a housemate's shared sessions;
72
+ - `memhouse room-sql` for anything else.
73
+
74
+ It is memhouse's only if it holds `.memhouse.json`. A `skills/memhouse` without that file is
75
+ never replaced or removed.
76
+ - **An instance with its own home or binary** is named in both, as `MEMHOUSE_HOME=<home>
77
+ <binary>`.
78
+ - **After an upgrade,** the first command refreshes an installed copy, as it does the `/mem`
79
+ copies. A block you took out by hand stays out.
80
+ - **The `/mem` skills are still for Claude Code.**
81
+
82
+ ## Smaller changes
83
+
84
+ - **`memhouse <command> --help`** prints that command's entry. A command with no entry of its
85
+ own still gets the whole page. `memhouse q --help` lists the named queries.
86
+ - **A refused credential is named up front.** When ClickHouse refuses the credential (code
87
+ 516, or 194 for a missing password), `install`, `invite` and `deploy --local` stop before
88
+ writing anything, with "could not authenticate as '<user>' at <url>".
89
+ - **`share --list` also lists the shares you received,** with the command that reads each
90
+ one. `--json` adds `received`.
91
+ - **`members` tells a share from a member's own tables:**
92
+ - own tables;
93
+ - each member whose sessions this account reads, scoped or full;
94
+ - any other grant.
95
+
96
+ `--json` adds `own`, `shares` and `other`.
97
+ - **`discover`** shows an unreachable `http://localhost:8123` as a plain line when nothing is
98
+ configured to use it, instead of a ✗.
99
+ - **`update` on a machine with no member install** (an administrator's machine) skips the
100
+ schema heal and migration check, instead of printing the default member's authentication
101
+ failures.
102
+ - **`doctor`** names the release channel when it says a release is the latest. Offline, it no
103
+ longer says the install is current.
104
+ - **Dashboard:** a browser that refuses sessionStorage keeps the access token in memory for
105
+ that page load, instead of showing the "open it with `memhouse dashboard`" page.
106
+ - **/mem skills:** the schema-2 raw-SQL recipe runs `memhouse room-sql <<'SQL'` directly. It
107
+ used to wrap the call in a shell function, so every call asked for approval.
108
+
109
+ ## Deprecated
110
+
111
+ Unchanged from 0.20.0: passwords on the command line (`--password`, `--member-password`,
112
+ `--to-password`), and `MEMHOUSE_USER` / `MEMHOUSE_PASSWORD` as the admin for `migrate --v3-*`,
113
+ still work with a warning and are refused from the next minor release. See
114
+ [0.20.0's Deprecated](v0.20.0.md#deprecated).
@@ -0,0 +1,134 @@
1
+ # memhouse 0.20.2 — 2026-11-03
2
+
3
+ A patch release on 0.20.1. The profile tables gain five columns, which the schema step adds in
4
+ place, and the first capture after upgrading re-derives every session from its archive. Install
5
+ it with `memhouse update`, or `npm install -g memhouse`.
6
+
7
+ - **The profile tables gain harness-sessions-sql v1.1's five columns.** Each session and message
8
+ keeps the editor's own fields that its adapter declares, and the archived units it came from,
9
+ so a row joins back to its raw line. These notes claim no v1.1 conformance; the conformance
10
+ run's result is below.
11
+ - **A reading view of turns:** `<m>_session_turns_view`, one row per turn of a session (v1.1's
12
+ `session_turns`, still a plan there).
13
+ - **Cursor: a chat's sub-agents are captured as its links.**
14
+ - **Fixes:**
15
+ - `memhouse room-sql` runs `SHOW ROW POLICIES` on ClickHouse 25.11;
16
+ - a session log memhouse cannot read is never rewritten into an empty one;
17
+ - on the development layout, an adapter upgrade writes above the rows it replaces.
18
+
19
+ ## Read this before upgrading
20
+
21
+ - **The first capture re-derives every session from its archive.** This happens once, on the
22
+ first pass after `memhouse update`. It reads the archive, not the editors' stores, and writes
23
+ every session's rows again, so it takes longer than a usual pass; later passes are as before.
24
+ - Measured on a synthetic house of 2,500 sessions (2,000 Claude Code and 500 Codex; 110 MiB
25
+ of session files, 193,814 message parts), in a 4-core container with 6 GiB of memory, that
26
+ pass took 18 minutes on ClickHouse 26.7.4.58 and 27 minutes on 25.11.9.34: 1.5 and 1.3
27
+ times as long as capturing the same house for the first time. The shipper's memory peaked
28
+ at 348 MB and ClickHouse's at 58 MiB per query, and the next pass took 7 seconds.
29
+ - The time grows with the number of sessions, 0.43 seconds a session on 26.7 and 0.64 on
30
+ 25.11 in that measurement: a house with four times as many sessions takes about four times
31
+ as long.
32
+ - Claude Code and Codex are read through `@agent-realm/harness-session-adapters` 1.1.0, whose
33
+ adapters state `adapter_ver` 0.3.0.
34
+ - Every other editor is read through memhouse's own session logs, whose adapter,
35
+ `memhouse-bridge`, moves to 1.0.2 (the Cursor links below).
36
+ - A new adapter version is an upgrade (harness-sessions-sql §8, "Re-derive from the
37
+ archive"), so each session is replayed under the new version and written above the house's
38
+ revs.
39
+ - **The schema step adds five columns** to an existing house's `<m>_sessions` and
40
+ `<m>_session_parts`, at the end of each table (a new house has them at the specification's
41
+ positions). The member's own credential adds them; the archive tables do not change. A row
42
+ captured before 0.20.2 holds `''` and `[]` there until its session is re-derived, which the
43
+ first capture does.
44
+
45
+ ```bash
46
+ memhouse update # then: memhouse status — every line a check mark
47
+ ```
48
+
49
+ ## The profile tables: harness-sessions-sql v1.1's columns
50
+
51
+ This adds the v1.1 columns and the `session_turns` view (a plan in v1.1).
52
+
53
+ - `sessions.native`, `session_parts.message_native` and `part_native`: the fields of the
54
+ editor's own record that the adapter declares (a Claude Code message's `message.stop_reason`,
55
+ say), as canonical JSON, `''` when there are none.
56
+ - `sessions.units` and `session_parts.message_units`: the `(source_key, generation, unit_key)`
57
+ of each archived unit the record came from, so a row joins back to its raw line in
58
+ `<db>_archive.<m>_session_units`.
59
+ - The legacy conversion leaves the five columns out of its `content_sha` line, as v1.1 asks, so
60
+ a converted session keeps its shas.
61
+
62
+ These notes claim no conformance. The harness-sessions-sql conformance kit, run against this
63
+ release's code on ClickHouse 26.7 and 25.11 with the session adapters 1.1.0:
64
+
65
+ - **Native is declared** passes.
66
+ - **Units resolve** fails. memhouse numbers a source's first archived generation 0, where v1.1
67
+ numbers it 1, so each triple's generation is one lower than the specification's. Every triple
68
+ still names a unit of the session's own archive, so a row joins back to its raw line. memhouse
69
+ has numbered generations this way since 0.20.0. Aligning the two is 0.20.3 work.
70
+ - **Crash-safe** keeps the failure 0.20.0 reported: a stopped read whose source changed before
71
+ the re-run.
72
+
73
+ ## A reading view of turns
74
+
75
+ `<m>_session_turns_view` (harness-sessions-sql v1.1 §6, `session_turns`, still a plan there)
76
+ gives one row per turn of one session, a turn running from a prompt to the next one, with its
77
+ messages, tool calls, failed tool results and the models that served it:
78
+
79
+ ```sql
80
+ SELECT * FROM <db>.<m>_session_turns_view(host = '…', harness = '…', harness_path = '…', session_id = '…', incarnation = '')
81
+ ```
82
+
83
+ - The schema step makes it (`CREATE OR REPLACE`).
84
+ - It runs as its reader (`SQL SECURITY INVOKER`), so it shows nobody a row the tables would not.
85
+ - A share does not reach it: a share is still the six tables.
86
+ - It is not named `<m>_session_turns`, because a table ending in `_turns` reads as a 0.18
87
+ member's room when memhouse looks for colliding member names.
88
+
89
+ ## Cursor sub-agents
90
+
91
+ Cursor names a chat's sub-agents only in the chat's own record (`subagentComposerIds`), never in
92
+ a message, and the profile capture did not read them there, so the links from a chat to its
93
+ sub-agents were missing. `memhouse-bridge` 1.0.2 reads them as links stated on the chat, one per
94
+ sub-agent.
95
+
96
+ - No live Cursor store has shown a sub-agent yet; the tests use synthetic sessions.
97
+ - Each new sub-agent still rewrites the chat's normalised log. Placing it where it appeared
98
+ waits for a live sample (Deferred).
99
+
100
+ ## Smaller changes
101
+
102
+ - **`memhouse room-sql` runs `SHOW ROW POLICIES` on ClickHouse 25.11.** It appended its output
103
+ format to the statement, and 25.11 parses no FORMAT after a bare `SHOW ROW POLICIES` or `SHOW
104
+ POLICIES`. The format now goes as the `default_format` setting; a FORMAT the statement names
105
+ itself still wins.
106
+ - **A session log memhouse cannot read is never rewritten into an empty one.** An old log that
107
+ was there but could not be read (a permission or disk error) counted as having no messages,
108
+ which let an empty read through. It now stops that session's pass with "cannot read the
109
+ existing log (<code>)", reported and retried.
110
+ - **The development layout (`<m>_turns`, `MEMHOUSE_LAYOUT=v3-dev`):** an adapter upgrade writes
111
+ one rev above the highest the house holds for the file, instead of tying with the rows it
112
+ replaces. The profile tables were not affected.
113
+ - **The site** no longer points readers at a repository-only document.
114
+
115
+ ## Deferred
116
+
117
+ - **The normalisers' filesystem errors** (0.20.3): about thirty reads in the editor normalisers
118
+ take any error for "not there", so a permission denial (macOS privacy on
119
+ `~/Library/Application Support`, for one) shows as no sessions. 0.20.3 reports them by editor
120
+ and errno in `status` and `doctor`, with a Full Disk Access hint on macOS.
121
+ - **Generation numbering** (0.20.3): a source's first archived generation is 0 where v1.1 says 1
122
+ (above).
123
+ - **Cursor sub-agents as appended records.** An anchor would place each after the parent's last
124
+ message at or before the sub-agent's own `createdAt`; that waits for a live store to confirm
125
+ that a composer's and its bubbles' `createdAt` share one clock.
126
+ - **`text_len` on the development layout** counts UTF-16 units, not code points.
127
+ - **`memhouse reset` and `relocate` on schema 3**: 0.21, with a design note first.
128
+
129
+ ## Deprecated
130
+
131
+ Unchanged from 0.20.0: passwords on the command line (`--password`, `--member-password`,
132
+ `--to-password`), and `MEMHOUSE_USER` / `MEMHOUSE_PASSWORD` as the admin for `migrate --v3-*`,
133
+ still work with a warning and are refused from the next minor release. See
134
+ [0.20.0's Deprecated](v0.20.0.md#deprecated).
@@ -0,0 +1,194 @@
1
+ // `memhouse plugins install codex`: what Codex reads of memhouse, and only on request.
2
+ //
3
+ // Codex (0.165, checked against the request it sends) puts <codex home>/AGENTS.md into every
4
+ // prompt in full, and lists each skill under <codex home>/skills by its name and description,
5
+ // reading the body only when the skill is used. So the rules that must hold in every session go
6
+ // in a short block in AGENTS.md, and the read recipes in one skill, skills/memhouse.
7
+ //
8
+ // The block is marker-delimited. Install puts it at the top of the file, followed by one blank
9
+ // line, or creates the file with the block alone; a reinstall replaces it where it stands. Remove
10
+ // takes out the block and that one line, so the rest of the file is what it was, byte for byte;
11
+ // a file left empty is deleted, unless it is a symlink: then the target is emptied through it and
12
+ // the link stays. The skill directory carries an ownership file (OWNER_FILE): a
13
+ // skills/memhouse without it is somebody else's and is never replaced or removed.
14
+ //
15
+ // Nothing here writes under the Codex home except install, remove, and the refresh of a copy
16
+ // that install already put there (plugin-copies.js).
17
+
18
+ const fs = require('fs');
19
+ const os = require('os');
20
+ const path = require('path');
21
+
22
+ const SKILL = 'memhouse';
23
+ const OWNER_FILE = '.memhouse.json';
24
+ const BEGIN = '<!-- memhouse:begin';
25
+ const END = '<!-- memhouse:end -->';
26
+ const MARKERS = /^<!-- memhouse:(begin|end)\b[^\n]*$/gm;
27
+
28
+ /** The Codex home: CODEX_HOME when set, else ~/.codex. */
29
+ function codexHome(env = process.env) {
30
+ return env.CODEX_HOME || path.join(os.homedir(), '.codex');
31
+ }
32
+ const agentsFile = (cx) => path.join(cx, 'AGENTS.md');
33
+ const skillDir = (cx) => path.join(cx, 'skills', SKILL);
34
+ const real = (p) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
35
+
36
+ /**
37
+ * Where the block sits in `text`: { start, end, version }, or null when there is none.
38
+ * Throws when the markers do not make exactly one block, so nothing is written over a file
39
+ * whose block was damaged by hand.
40
+ */
41
+ function findBlock(text) {
42
+ const begins = []; const ends = [];
43
+ let m;
44
+ MARKERS.lastIndex = 0;
45
+ while ((m = MARKERS.exec(text))) (m[1] === 'begin' ? begins : ends).push(m.index);
46
+ if (!begins.length && !ends.length) return null;
47
+ if (begins.length !== 1 || ends.length !== 1 || ends[0] < begins[0]) {
48
+ throw new Error(`AGENTS.md holds ${begins.length} memhouse:begin and ${ends.length} memhouse:end markers, not one block — fix it by hand; nothing was changed`);
49
+ }
50
+ const nl = text.indexOf('\n', ends[0]);
51
+ const version = (/^<!-- memhouse:begin (\d+\.\d+\.\d+\S*)/.exec(text.slice(begins[0])) || [])[1] || null;
52
+ return { start: begins[0], end: nl === -1 ? text.length : nl + 1, version };
53
+ }
54
+
55
+ /** `text` with `block` in it: replaced where it stands, or put first with one blank line after. */
56
+ function withBlock(text, block) {
57
+ const b = findBlock(text);
58
+ if (b) return text.slice(0, b.start) + block + text.slice(b.end);
59
+ return text ? `${block}\n${text}` : block;
60
+ }
61
+
62
+ /** `text` without the block, and without the blank line install put after it. */
63
+ function withoutBlock(text) {
64
+ const b = findBlock(text);
65
+ if (!b) return text;
66
+ let rest = text.slice(b.end);
67
+ if (b.start === 0 && rest.startsWith('\n')) rest = rest.slice(1);
68
+ return text.slice(0, b.start) + rest;
69
+ }
70
+
71
+ /** A path for the text, with the home directory as ~. */
72
+ function shown(p) {
73
+ const h = os.homedir();
74
+ return p === h || p.startsWith(h + path.sep) ? `~${p.slice(h.length)}` : p;
75
+ }
76
+
77
+ /** A shell word: as is when it needs no quoting, else single-quoted. */
78
+ function shArg(s) {
79
+ return /^[A-Za-z0-9_@%+=:,./~-]+$/.test(s) ? s : `'${String(s).replace(/'/g, `'\\''`)}'`;
80
+ }
81
+
82
+ /** The `memhouse` on PATH, resolved, or null. */
83
+ function memhouseOnPath(env = process.env) {
84
+ for (const d of String(env.PATH || '').split(path.delimiter).filter(Boolean)) {
85
+ const f = path.join(d, 'memhouse');
86
+ try { fs.accessSync(f, fs.constants.X_OK); return real(f); } catch { /* not here */ }
87
+ }
88
+ return null;
89
+ }
90
+
91
+ /**
92
+ * How the block and the skill name the command: `memhouse` for the default instance, else with
93
+ * MEMHOUSE_HOME and the binary spelled out, each only where it differs from the default.
94
+ */
95
+ function invocation({ home, bin, defaultHome = path.join(os.homedir(), '.memhouse'), pathBin = memhouseOnPath() }) {
96
+ const parts = [];
97
+ if (home && real(home) !== real(defaultHome)) parts.push(`MEMHOUSE_HOME=${shArg(shown(home))}`);
98
+ parts.push(!bin || (pathBin && real(bin) === pathBin) ? 'memhouse' : shArg(shown(bin)));
99
+ return parts.join(' ');
100
+ }
101
+
102
+ function render(tpl, { mh, envFile }) {
103
+ return tpl.replace(/\{\{MH\}\}/g, mh).replace(/\{\{ENV_FILE\}\}/g, envFile);
104
+ }
105
+
106
+ function renderBlock({ deliveryDir, version, mh, envFile }) {
107
+ const body = render(fs.readFileSync(path.join(deliveryDir, 'codex', 'AGENTS-BLOCK.md'), 'utf-8'), { mh, envFile }).trim();
108
+ return `${BEGIN} ${version} — managed by \`memhouse plugins install codex\`; \`memhouse plugins remove codex\` takes it out -->\n${body}\n${END}\n`;
109
+ }
110
+
111
+ /** The skill directory's ownership record, or null when it is not memhouse's (or absent). */
112
+ function readOwner(dir) {
113
+ try {
114
+ const o = JSON.parse(fs.readFileSync(path.join(dir, OWNER_FILE), 'utf-8'));
115
+ return o && o.owner === 'memhouse' ? o : null;
116
+ } catch { return null; }
117
+ }
118
+
119
+ /**
120
+ * Write the block and the skill. Everything is checked before anything is written: a foreign
121
+ * skills/memhouse or a damaged block throws with the Codex home untouched.
122
+ * `onlyWhereInstalled`: the refresh — the block is rewritten only where it already is.
123
+ * @returns {{ file: string, created: boolean, block: boolean, dir: string, mh: string }}
124
+ */
125
+ function install({ codexHome: cx, home, bin, version, deliveryDir, pathBin, onlyWhereInstalled = false }) {
126
+ const dir = skillDir(cx);
127
+ if (fs.existsSync(dir) && !readOwner(dir)) {
128
+ throw new Error(`${shown(dir)} is not memhouse's (it has no ${OWNER_FILE}) — not replacing it; move it aside and install again`);
129
+ }
130
+ const file = agentsFile(cx);
131
+ let text = null;
132
+ try { text = fs.readFileSync(file, 'utf-8'); } catch (e) { if (e.code !== 'ENOENT') throw e; }
133
+ const mh = invocation({ home, bin, ...(pathBin !== undefined ? { pathBin } : {}) });
134
+ const envFile = shown(path.join(home, 'env'));
135
+ const writeBlock = !onlyWhereInstalled || (text !== null && findBlock(text) !== null);
136
+ const next = writeBlock ? withBlock(text || '', renderBlock({ deliveryDir, version, mh, envFile })) : text;
137
+ const skill = render(fs.readFileSync(path.join(deliveryDir, 'codex', 'SKILL.md'), 'utf-8'), { mh, envFile });
138
+
139
+ fs.rmSync(dir, { recursive: true, force: true });
140
+ fs.mkdirSync(dir, { recursive: true });
141
+ fs.writeFileSync(path.join(dir, 'SKILL.md'), skill);
142
+ fs.writeFileSync(path.join(dir, OWNER_FILE), `${JSON.stringify({ owner: 'memhouse', version, home, bin: bin || null }, null, 2)}\n`);
143
+ // In place, not a rename: an AGENTS.md that is a symlink (a dotfiles checkout) stays one.
144
+ if (writeBlock && next !== text) fs.writeFileSync(file, next);
145
+ return { file, created: text === null && writeBlock, block: writeBlock, dir, mh };
146
+ }
147
+
148
+ /**
149
+ * Take out the block and the skill — the skill only when it is memhouse's.
150
+ * @returns {{ block: boolean, fileDeleted: boolean, emptiedLink: boolean, skill: boolean, foreignSkill: string|null }}
151
+ */
152
+ function remove({ codexHome: cx }) {
153
+ const res = { block: false, fileDeleted: false, emptiedLink: false, skill: false, foreignSkill: null };
154
+ const file = agentsFile(cx);
155
+ let text = null;
156
+ try { text = fs.readFileSync(file, 'utf-8'); } catch (e) { if (e.code !== 'ENOENT') throw e; }
157
+ const next = text !== null && findBlock(text) ? withoutBlock(text) : null;
158
+ const dir = skillDir(cx);
159
+ const owned = !!readOwner(dir);
160
+ if (!owned && fs.existsSync(dir)) res.foreignSkill = dir;
161
+ if (next !== null) {
162
+ // Only a regular file is unlinked. Unlinking a symlink (a dotfiles checkout) would leave the
163
+ // block in its target, so the target is written through the link instead.
164
+ if (next === '' && !fs.lstatSync(file).isSymbolicLink()) { fs.rmSync(file); res.fileDeleted = true; }
165
+ else { fs.writeFileSync(file, next); res.emptiedLink = next === ''; }
166
+ res.block = true;
167
+ }
168
+ if (owned) { fs.rmSync(dir, { recursive: true }); res.skill = true; }
169
+ return res;
170
+ }
171
+
172
+ /** What is installed: the skill's owner record (or why not), and whether the block is there. */
173
+ function status({ codexHome: cx }) {
174
+ const dir = skillDir(cx);
175
+ const owner = readOwner(dir);
176
+ let block = null; let damaged = null;
177
+ try { block = findBlock(fs.readFileSync(agentsFile(cx), 'utf-8')); } catch (e) { if (e.code !== 'ENOENT') damaged = e.message; }
178
+ return { codexHome: cx, installed: !!owner, owner, foreignSkill: !owner && fs.existsSync(dir) ? dir : null, block: !!block, blockVersion: block ? block.version : null, damaged };
179
+ }
180
+
181
+ /**
182
+ * The copy to refresh for `version`, or null: installed, memhouse's, bound to `home`, and older.
183
+ */
184
+ function staleCopy({ codexHome: cx, home, version, compareVersions }) {
185
+ const owner = readOwner(skillDir(cx));
186
+ if (!owner || !owner.home || real(owner.home) !== real(home)) return null;
187
+ const c = compareVersions(owner.version, version);
188
+ return c === -1 || c === null ? { codexHome: cx, have: owner.version || null, home: owner.home, bin: owner.bin || null } : null;
189
+ }
190
+
191
+ module.exports = {
192
+ SKILL, OWNER_FILE, BEGIN, END, codexHome, agentsFile, skillDir, findBlock, withBlock, withoutBlock,
193
+ invocation, memhouseOnPath, renderBlock, readOwner, install, remove, status, staleCopy, shown,
194
+ };
@@ -46,7 +46,7 @@ them exist because an agent broke them:
46
46
 
47
47
  ## 0. Find your credential
48
48
 
49
- The install needs `{{FILE}}`, the file delivered with this page. Confirm it is here first —
49
+ {{EXPIRY}}The install needs `{{FILE}}`, the file delivered with this page. Confirm it is here first —
50
50
  run this from the directory this page is in now, and if it prints the filename you are ready:
51
51
 
52
52
  ```
@@ -0,0 +1,11 @@
1
+ ## memhouse: past agent sessions
2
+
3
+ This machine's agent sessions (Codex, Claude Code and other editors) are kept in a memhouse
4
+ house. For a question about earlier work (what was done, when, in which hours, what it cost,
5
+ how something was fixed), use the `memhouse` skill before saying you do not know.
6
+
7
+ - Never read, print or echo `{{ENV_FILE}}`, and never print a `MEMHOUSE_*` value: they hold the
8
+ house's password, and this conversation may be shipped into the house. `{{MH}}` reads its
9
+ own configuration.
10
+ - `MEMHOUSE_URL` is the house (ClickHouse). `MEMHOUSE_PORT` is the local dashboard's port, not
11
+ the house's.
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: memhouse
3
+ description: "Answer from past agent sessions kept in memhouse (Codex, Claude Code and other editors): what was worked on and when, which hours or days, spend and models, tools used, how something was fixed, a past session's transcript, a housemate's shared sessions. Read-only memhouse commands. Use before saying you do not know about earlier work."
4
+ ---
5
+
6
+ # memhouse: read past sessions
7
+
8
+ Every command here only reads. `{{MH}}` reads its own configuration: never read or print
9
+ `{{ENV_FILE}}` or any `MEMHOUSE_*` value.
10
+
11
+ Start with `{{MH}} q schema`. The recipes below are for schema 3. On 2 (a house still on the
12
+ 0.18 rooms), `{{MH}} prompt` prints the recipes for it.
13
+
14
+ ## Named queries
15
+
16
+ `{{MH}} q <name> --json` prints one JSON object. A session is `<harness>:<session_id>`, the
17
+ `ref` every result carries.
18
+
19
+ - Sessions about a topic (every term in one message; a "quoted phrase" as written):
20
+ `{{MH}} q search clickhouse "authentication failed" --json`
21
+ - Around a hit: `{{MH}} q window <ref> --at <message_id> --json`. A whole session:
22
+ `{{MH}} q session <ref> --json`, paged with `--cursor`.
23
+ - Recent sessions: `{{MH}} q recent --limit 20 --since 7d --json`
24
+ - Which hours or days: `{{MH}} q activity --by hour --grain usage --tz <IANA zone> --json`
25
+ counts messages and tokens by the hour they happened; `--by day`, `--by dow`, `--by day,host`.
26
+ - Spend and models: `{{MH}} q spend --by model --since 30d --json`, `{{MH}} q models --json`
27
+ - Tools: `{{MH}} q tools --limit 25 --json`; one tool's calls: `{{MH}} q tool <name> --json`
28
+ - Machines: `{{MH}} q hosts --json`. What the house holds: `{{MH}} q coverage --json`
29
+ - Filters on most names: `--harness`, `--project`, `--folder`, `--host`, `--since`, `--until`.
30
+ `{{MH}} q` lists every name and flag.
31
+
32
+ ## A housemate's sessions
33
+
34
+ Add `--member <name>` to a named query to read sessions a housemate shared with you.
35
+ `{{MH}} share --list` names who shared with you. An empty result means nothing is visible,
36
+ not that nothing happened.
37
+
38
+ ## Raw SQL, when no named query fits
39
+
40
+ `{{MH}} room-sql "<SELECT …>"` runs one read-only statement. The bare names `sessions`,
41
+ `session_parts`, `session_usage` and `calls` resolve to your tables (`--member <name>` for a
42
+ housemate's). Read them `FINAL` with `is_deleted = 0 AND removed = 0`; a session is
43
+ `(host, harness, harness_path, session_id, incarnation)`; take totals from `sessions`, never by
44
+ summing parts or usage.
45
+
46
+ ## Answering
47
+
48
+ Answer from what the commands return, cite the `ref` of each session used, and say so when a
49
+ search finds nothing. `{{MH}} resume <ref>` prints the command that reopens a session. Cost is
50
+ derived from token counts: an unpriced model is unknown, not free.
@@ -20,8 +20,8 @@ memhouse q schema # prints 3 or 2
20
20
  - **3** — the house holds harness-sessions-sql's profile tables (`sessions`, `session_parts`,
21
21
  `session_usage`, `calls`; memhouse 0.20). Read
22
22
  [Schema 3](#schema-3--harness-sessions-sqls-profile) below and use **`memhouse q`**; nothing about
23
- epochs, `seq` or the `q()` rewrite applies. Skip everything from "Connect" on unless you
24
- need raw SQL, and then use `memhouse room-sql`, not `q()`.
23
+ epochs, `seq` or the 0.18 rooms' name rewrite applies. Skip everything from "Connect" on
24
+ unless you need raw SQL, and then use `memhouse room-sql`.
25
25
  - **2** — the 0.18 rooms (`messages`, `tool_calls`, `sessions`). Everything from
26
26
  "What this is" on is for you, unchanged.
27
27
 
@@ -161,8 +161,8 @@ Every member's rooms are named for them — `mem.alice_messages`, `mem.alice_ses
161
161
  `mem.alice_tool_calls` — and one grant covers exactly those: `ON mem.alice_*`. Housemates
162
162
  keep their own rooms beside yours and cannot read them, list them, or grant them onward;
163
163
  you cannot read theirs. `SHOW GRANTS` shows you the one line you hold. **A bare
164
- `FROM messages` names nothing you have** — the `q()` below prefixes room names with your
165
- member name for you, and `memhouse rooms` prints what yours are called.
164
+ `FROM messages` names nothing you have** — `memhouse room-sql` (below) prefixes room names
165
+ with your member name for you, and `memhouse rooms` prints what yours are called.
166
166
 
167
167
  An **operator** owns the ClickHouse itself; an **admin credential** is what proves it.
168
168
  `memhouse whoami` says which you are holding.
@@ -189,13 +189,11 @@ instances on two channels; the top of this file says when to use it instead of p
189
189
  Credentials resolve **flags > exported `MEMHOUSE_*` > `$MEMHOUSE_HOME/env`** (default
190
190
  `~/.memhouse/env`), and the CLI does that itself: you never source the file, and the
191
191
  password never appears in a command. Every read goes through `memhouse room-sql`, with the
192
- SQL on stdin:
192
+ SQL on stdin. Run it as written, not through a shell function: the skills pre-approve
193
+ `memhouse room-sql`, and a wrapper would ask for approval every time.
193
194
 
194
195
  ```bash
195
- q() { # read-only by construction; every read path uses this
196
- memhouse room-sql "$@"
197
- }
198
- q <<'SQL'
196
+ memhouse room-sql <<'SQL'
199
197
  SELECT 1 FORMAT PrettyCompact
200
198
  SQL
201
199
  ```
@@ -116,7 +116,7 @@ each one:
116
116
  memhouse share <user> everything you own
117
117
  memhouse share <user> --only project=memhouse just one project
118
118
  memhouse share <user> --only session=<id> just one conversation
119
- memhouse share --list who can read it, and how much
119
+ memhouse share --list who can read it, how much, and whose you can read
120
120
  memhouse share <user> --revoke withdraw
121
121
  ```
122
122