memhouse 0.18.10 → 0.18.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +10 -11
- package/CHANGELOG.md +153 -13
- package/README.md +2 -2
- package/SECURITY.md +4 -2
- package/bin/memhouse.js +105 -49
- package/docs/guides/admin-password.md +1 -1
- package/docs/guides/configuration.md +2 -2
- package/docs/guides/install.md +1 -1
- package/docs/guides/invite.md +1 -1
- package/docs/guides/storage.md +10 -7
- package/docs/guides/uninstall.md +4 -2
- package/docs/releases/README.md +2 -0
- package/docs/releases/v0.18.10.md +7 -7
- package/docs/releases/v0.18.11.md +187 -0
- package/docs/releases/v0.18.12.md +59 -0
- package/docs/tutorials/01-first-house.md +2 -2
- package/editors/claude.js +70 -12
- package/editors/cursor-agent.js +1 -1
- package/examples/01-local-one-machine/README.md +4 -4
- package/examples/02-existing-clickhouse/README.md +7 -7
- package/memhouse/DESIGN.md +9 -2
- package/memhouse/RESUME-AND-UPDATE.md +1 -1
- package/memhouse/capabilities.js +1 -1
- package/memhouse/convert.js +3 -3
- package/memhouse/delivery/PROMPT.md +1 -1
- package/memhouse/delivery/plugin/.claude-plugin/plugin.json +1 -1
- package/memhouse/delivery/plugin/reference/HOUSE.md +21 -9
- package/memhouse/delivery/plugin/skills/access/SKILL.md +2 -2
- package/memhouse/delivery/plugin/skills/admin/SKILL.md +3 -3
- package/memhouse/delivery/plugin/skills/house/SKILL.md +2 -2
- package/memhouse/delivery/plugin/skills/recall/SKILL.md +36 -16
- package/memhouse/delivery/plugin/skills/sql/SKILL.md +2 -2
- package/memhouse/flags.js +28 -1
- package/memhouse/host.js +11 -6
- package/memhouse/house/HOUSE.md +6 -6
- package/memhouse/house/house.js +31 -5
- package/memhouse/invite-target.js +37 -0
- package/memhouse/self-update.js +1 -1
- package/memhouse/server/queries.js +11 -6
- package/memhouse/shipper/ship.js +148 -31
- package/package.json +3 -3
- package/public/THIRD_PARTY_NOTICES.txt +2114 -0
- package/ui/README.md +16 -0
- package/ui/vite.config.js +4 -1
|
@@ -3,7 +3,7 @@ name: access
|
|
|
3
3
|
description: Bring someone onto this memhouse ClickHouse, or let them read your house — mint a new member with their own house and the env file their install needs, grant a housemate read-only access to yours, list who can currently read it, or revoke. Use when the user says "invite X", "add X to memhouse", "get my friend set up", "share my memory with X", "let X see my sessions", "who can read my memory", "stop sharing with X". Inviting comes BEFORE sharing — you can only grant a user who already exists. Read-only by construction on the sharing side: a share can never let anyone write to or delete your memory.
|
|
4
4
|
user-invocable: true
|
|
5
5
|
argument-hint: "invite <name> | share <name> | list | revoke <name>"
|
|
6
|
-
allowed-tools: Bash
|
|
6
|
+
allowed-tools: Bash(memhouse whoami *), Bash(memhouse status *), Bash(memhouse share --list), Bash(memhouse share --list --json)
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# /mem:access — who exists, and who may read
|
|
@@ -13,7 +13,7 @@ Two things that look alike and are not:
|
|
|
13
13
|
- **Invite** mints a ClickHouse *user* and their own *house*. Needs an administrator.
|
|
14
14
|
- **Share** grants an existing user `SELECT` on *your* house. You can do this yourself.
|
|
15
15
|
|
|
16
|
-
**Read
|
|
16
|
+
**Read `../../reference/HOUSE.md` first** for the connection block. One rule dominates
|
|
17
17
|
everything here: **a password must never enter this conversation.** memhouse ships this
|
|
18
18
|
transcript into the house, and the archive is insert-only — a credential pasted here
|
|
19
19
|
cannot be withdrawn.
|
|
@@ -3,7 +3,7 @@ name: admin
|
|
|
3
3
|
description: Operate the memhouse ClickHouse as its administrator — list and inspect every user and house, provision or remove members, grant and revoke access, and read server-wide health (disk, parts, mutations, running queries). Use when the user asks to "do X as admin", "manage users", "drop a house", "see disk usage", "who is on this server", or when a member-scoped skill refused for want of privileges. Requires an administrator credential in the environment; says so plainly, and how to supply one, when there is not.
|
|
4
4
|
user-invocable: true
|
|
5
5
|
argument-hint: "<what you want done>"
|
|
6
|
-
allowed-tools: Bash
|
|
6
|
+
allowed-tools: Bash(memhouse whoami *), Bash(memhouse status *), Bash(memhouse instance *)
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# /mem:admin — operate the house as its administrator
|
|
@@ -12,7 +12,7 @@ A member owns a house, or rooms in one. An **administrator** owns the ClickHouse
|
|
|
12
12
|
account, every grant. This skill is the second one, and it is the only skill allowed to
|
|
13
13
|
act outside the caller's own house.
|
|
14
14
|
|
|
15
|
-
**Read
|
|
15
|
+
**Read `../../reference/HOUSE.md` first** — connection, schema, and the traps every query
|
|
16
16
|
here inherits.
|
|
17
17
|
|
|
18
18
|
**An admin password must never enter this conversation.** memhouse ships this transcript
|
|
@@ -128,7 +128,7 @@ Before any of them:
|
|
|
128
128
|
|
|
129
129
|
1. **Look at the target and report what is really there** — row counts per room, who
|
|
130
130
|
wrote them, the date span, which machines. A house you expected to be empty may not
|
|
131
|
-
be: one `
|
|
131
|
+
be: one `carol` house looked disposable and held 3,733 messages belonging to somebody
|
|
132
132
|
else entirely.
|
|
133
133
|
2. **Name the collateral.** Dropping a user stops that account's shipper on every machine
|
|
134
134
|
it runs on. Revoking a grant closes a share someone may be mid-query on. Rotating a
|
|
@@ -3,7 +3,7 @@ name: house
|
|
|
3
3
|
description: The state of your memory and who is around it — what memhouse is and how to use it, whether shipping is working, what the house holds (sessions, messages, tokens per editor), how fresh it is, which machines write into it, which houses you can read, and who can read yours. Use for "what is memhouse", "how do I use this", "is my memory working", "how much is stored", "when did it last ship", "which machines are shipping", "who can see my memory", "whose memory can I see", "who else is on this server" — and as the first answer when another /mem skill fails because nothing is configured yet, or before debugging why a search found nothing.
|
|
4
4
|
user-invocable: true
|
|
5
5
|
argument-hint: ""
|
|
6
|
-
allowed-tools: Bash
|
|
6
|
+
allowed-tools: Bash(memhouse instance *), Bash(memhouse whoami *), Bash(memhouse status *), Bash(memhouse stats *), Bash(memhouse doctor *), Bash(memhouse share --list), Bash(memhouse share --list --json)
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# /mem:house — what this is, whether it works, and who is around it
|
|
@@ -11,7 +11,7 @@ allowed-tools: Bash
|
|
|
11
11
|
**Invoking this IS the request. Run the report and present it; there is nothing to
|
|
12
12
|
clarify.** Read-only throughout.
|
|
13
13
|
|
|
14
|
-
**Read
|
|
14
|
+
**Read `../../reference/HOUSE.md` first** — connection, schema, and the epoch filter every
|
|
15
15
|
count below depends on.
|
|
16
16
|
|
|
17
17
|
## Nothing configured yet? Answer that instead
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: recall
|
|
3
|
-
description: Retrieve from past agent conversations and answer from them — search every session ever shipped from all 17 editors (Claude Code, Codex, Cursor, VS Code, Zed, Gemini CLI, …) across every project and machine, read the actual transcripts, and answer with citations. Use whenever the user refers to something not in the current context - "how did I fix X last time", "what approach did we settle on", "why did we choose Z", "find that conversation about Y", "did I ever try W", "what was I working on", "show my recent sessions", "sessions from last week", "remind me how I did it". Reach for this BEFORE saying you do not know about prior work. Also reads a housemate's memory when they name it ("how did
|
|
3
|
+
description: Retrieve from past agent conversations and answer from them — search every session ever shipped from all 17 editors (Claude Code, Codex, Cursor, VS Code, Zed, Gemini CLI, …) across every project and machine, read the actual transcripts, and answer with citations. Use whenever the user refers to something not in the current context - "how did I fix X last time", "what approach did we settle on", "why did we choose Z", "find that conversation about Y", "did I ever try W", "what was I working on", "show my recent sessions", "sessions from last week", "remind me how I did it". Reach for this BEFORE saying you do not know about prior work. Also reads a housemate's memory when they name it ("how did bob fix X"). For numbers rather than transcripts — spend, model mix, tool rankings — use /mem:sql.
|
|
4
4
|
user-invocable: true
|
|
5
5
|
argument-hint: "<question, terms, or 'recent sessions'>"
|
|
6
|
-
allowed-tools: Bash
|
|
6
|
+
allowed-tools: Bash(memhouse search *), Bash(memhouse resume *), Bash(memhouse sessions-query *), Bash(memhouse status *), Bash(memhouse whoami *)
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# /mem:recall — find it, read it, answer from it
|
|
@@ -16,7 +16,7 @@ answer still exists.
|
|
|
16
16
|
path — this skill retrieves rows and YOU read them. Never present a guess as a retrieval:
|
|
17
17
|
if the house has nothing, say so.
|
|
18
18
|
|
|
19
|
-
**Read
|
|
19
|
+
**Read `../../reference/HOUSE.md` first** — connection, schema, and the three traps
|
|
20
20
|
(epoch filtering, `(session_id, user_id)` joins, escaping). Every query below assumes it.
|
|
21
21
|
|
|
22
22
|
## Which shape is being asked for
|
|
@@ -43,7 +43,8 @@ found 8 messages where `%clickhouse%` AND `%ttl%` found 1,461.
|
|
|
43
43
|
```sql
|
|
44
44
|
SELECT session_id, user_id, any(folder) AS folder,
|
|
45
45
|
formatDateTime(max(ts), '%Y-%m-%d') AS day, count() AS hits,
|
|
46
|
-
|
|
46
|
+
min(seq) AS hit_seq,
|
|
47
|
+
substring(argMin(text, seq), 1, 300) AS sample
|
|
47
48
|
FROM messages
|
|
48
49
|
WHERE text_ngram LIKE '%needle%'
|
|
49
50
|
AND (origin != 'ship'
|
|
@@ -55,8 +56,8 @@ ORDER BY hits DESC, max(ts) DESC
|
|
|
55
56
|
LIMIT 8
|
|
56
57
|
```
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
Every row counted here matched, so `sample` is a line that actually matched: the
|
|
60
|
+
session's first hit, at `hit_seq`. Step 2 reads around that position. **Order by `hits`,
|
|
60
61
|
not just recency**: a session mentioning the term 197 times outranks yesterday's passing
|
|
61
62
|
reference.
|
|
62
63
|
|
|
@@ -64,15 +65,27 @@ reference.
|
|
|
64
65
|
the match so you see the resolution, not the complaint:
|
|
65
66
|
|
|
66
67
|
```sql
|
|
67
|
-
SELECT seq, role, substring(text, 1, 2000) AS text
|
|
68
|
-
FROM messages
|
|
69
|
-
WHERE session_id = '<sid>' AND user_id = '<uid>'
|
|
70
|
-
|
|
68
|
+
SELECT seq, role, is_subagent, substring(text, 1, 2000) AS text FROM (
|
|
69
|
+
(SELECT seq, role, is_subagent, text FROM messages FINAL
|
|
70
|
+
WHERE session_id = '<sid>' AND user_id = '<uid>' AND seq < <hit_seq>
|
|
71
|
+
AND (origin != 'ship' OR epoch = (SELECT max(epoch) FROM messages
|
|
72
|
+
WHERE session_id = '<sid>' AND user_id = '<uid>' AND origin = 'ship'))
|
|
73
|
+
ORDER BY seq DESC LIMIT 3)
|
|
74
|
+
UNION ALL
|
|
75
|
+
(SELECT seq, role, is_subagent, text FROM messages FINAL
|
|
76
|
+
WHERE session_id = '<sid>' AND user_id = '<uid>' AND seq >= <hit_seq>
|
|
77
|
+
AND (origin != 'ship' OR epoch = (SELECT max(epoch) FROM messages
|
|
78
|
+
WHERE session_id = '<sid>' AND user_id = '<uid>' AND origin = 'ship'))
|
|
79
|
+
ORDER BY seq LIMIT 13))
|
|
71
80
|
ORDER BY seq
|
|
72
81
|
```
|
|
73
82
|
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
`<sid>`, `<uid>` and `<hit_seq>` come from step 1's row. The window is counted in rows,
|
|
84
|
+
not in `seq` values: three turns before the hit and twelve after. `seq` has gaps. A
|
|
85
|
+
subagent's turns are numbered in a block of their own, far above the parent's, so
|
|
86
|
+
`seq BETWEEN hit_seq - 3 AND hit_seq + 12` could return the hit alone. The epoch filter
|
|
87
|
+
keeps one parse: an unfiltered read of a compacted session interleaves two versions of
|
|
88
|
+
the same conversation. `FINAL` collapses row versions the server has not merged yet.
|
|
76
89
|
|
|
77
90
|
**3. Answer, then cite.** Lead with the answer. Under it list the sessions it came from —
|
|
78
91
|
`session_id`, date, project — so the user can reopen one (`memhouse resume <session_id>`
|
|
@@ -95,13 +108,20 @@ one representative line each. Offer to read the strongest. Do not dump ten rows
|
|
|
95
108
|
prefer it over hand-joining. For a plain list:
|
|
96
109
|
|
|
97
110
|
```sql
|
|
98
|
-
SELECT session_id, name, source, host, project,
|
|
99
|
-
formatDateTime(
|
|
100
|
-
|
|
101
|
-
|
|
111
|
+
SELECT session_id, user_id, name, source, host, project,
|
|
112
|
+
formatDateTime(created_at, '%Y-%m-%d %H:%i') AS started,
|
|
113
|
+
formatDateTime(coalesce(last_updated_at, created_at), '%Y-%m-%d %H:%i') AS last_active,
|
|
114
|
+
message_count
|
|
115
|
+
FROM sessions FINAL
|
|
116
|
+
ORDER BY coalesce(last_updated_at, created_at) DESC
|
|
102
117
|
LIMIT 20
|
|
103
118
|
```
|
|
104
119
|
|
|
120
|
+
`sessions` keeps one row per session and the newest write wins, so read it `FINAL`.
|
|
121
|
+
"Recent" means recently active (`last_updated_at`): a session started last month and
|
|
122
|
+
worked on today is recent. The room has no `started_at`; `created_at` is when the
|
|
123
|
+
session began.
|
|
124
|
+
|
|
105
125
|
Narrow with `WHERE project = '…'`, `source = 'cursor'`, `host = '…'`, or a date range as
|
|
106
126
|
asked. In a shared house every member's sessions are here — add `AND user_id = '<name>'`
|
|
107
127
|
only when the user asks for one person's.
|
|
@@ -3,7 +3,7 @@ name: sql
|
|
|
3
3
|
description: Run read-only SQL over memhouse conversation memory for numbers rather than transcripts — token spend, cost by model or editor or project, tool-call rankings, activity over time, busiest days, cache-hit ratios, session length distributions, or any custom aggregate across the typed sessions/messages/tool_calls tables. Use when the user wants a figure, a ranking, a trend or a breakdown - "what did I spend on Opus", "which tools do I use most", "how many sessions per project", "my busiest week", "compare editors". For finding or answering FROM past conversations, use /mem:recall instead. Works over your own house, or a housemate's when they name it.
|
|
4
4
|
user-invocable: true
|
|
5
5
|
argument-hint: "<the question, or the SQL>"
|
|
6
|
-
allowed-tools: Bash
|
|
6
|
+
allowed-tools: Bash(memhouse sessions-query *), Bash(memhouse stats *), Bash(memhouse status *), Bash(memhouse whoami *)
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# /mem:sql — analytics over the rooms
|
|
@@ -11,7 +11,7 @@ allowed-tools: Bash
|
|
|
11
11
|
For **numbers**. When the answer is a passage from a past conversation, that is
|
|
12
12
|
`/mem:recall` — this skill counts, groups and ranks.
|
|
13
13
|
|
|
14
|
-
**Read
|
|
14
|
+
**Read `../../reference/HOUSE.md` first.** Schema, connection, and the three traps. The epoch
|
|
15
15
|
filter in particular is not optional here: this is the skill most likely to produce a
|
|
16
16
|
figure someone acts on, and omitting it over-counted a real house by **34%**.
|
|
17
17
|
|
package/memhouse/flags.js
CHANGED
|
@@ -75,4 +75,31 @@ function allowedFlags(cmd) {
|
|
|
75
75
|
return new Set([...GLOBAL_FLAGS, ...known]);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
// Options that never take a value. The parser used to read `--x y` as x = "y" for every
|
|
79
|
+
// option, so a switch swallowed the word after it: `update --no-install yes` set
|
|
80
|
+
// no-install to "yes" (not true) and ran the npm install it was asked to skip, and
|
|
81
|
+
// `share --revoke bob` revoked nobody. A switch is true when present, and the next word
|
|
82
|
+
// stays a positional.
|
|
83
|
+
const BOOLEAN_FLAGS = new Set([
|
|
84
|
+
'json', 'yes', 'help', 'version', 'full', 'check', 'migrate', 'no-install', 'all-origins',
|
|
85
|
+
'dry-run', 'print-sql', 'no-ship', 'keep-admin', 'ensure-schema', 'rotate-password', 'force',
|
|
86
|
+
'adopt', 'allow-local', 'local', 'down', 'credentials', 'full-removal', 'insecure-native',
|
|
87
|
+
'keep-shipper', 'guides', 'revoke', 'list', 'admin', 'install', 'all',
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
/** argv (after the command) -> { flags, positional }. */
|
|
91
|
+
function parseArgv(rest) {
|
|
92
|
+
const flags = {}; const positional = [];
|
|
93
|
+
for (let i = 0; i < rest.length; i++) {
|
|
94
|
+
const a = rest[i];
|
|
95
|
+
if (a.startsWith('--')) {
|
|
96
|
+
const key = a.slice(2);
|
|
97
|
+
const next = rest[i + 1];
|
|
98
|
+
if (!BOOLEAN_FLAGS.has(key) && next !== undefined && !next.startsWith('--')) { flags[key] = next; i++; }
|
|
99
|
+
else flags[key] = true;
|
|
100
|
+
} else positional.push(a);
|
|
101
|
+
}
|
|
102
|
+
return { flags, positional };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { GLOBAL_FLAGS, COMMAND_FLAGS, BOOLEAN_FLAGS, parseArgv, unknownFlags, allowedFlags, suggestFlag, editDistance };
|
package/memhouse/host.js
CHANGED
|
@@ -14,13 +14,18 @@
|
|
|
14
14
|
// * One machine splits. Rename the box and every row shipped afterwards lands under a
|
|
15
15
|
// new host, so the machine's own history appears to end and a stranger's to begin.
|
|
16
16
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
17
|
+
// So the fingerprint comes from the OS's own machine id (IOPlatformUUID, /etc/machine-id,
|
|
18
|
+
// MachineGuid), hashed with a fixed salt (machineFingerprint), never from the hostname: two
|
|
19
|
+
// machines cannot share it and a rename cannot move it. Since 0.18.6. Before that it was
|
|
20
|
+
// random, which fixed both of the above but split a machine whenever MEMHOUSE_HOME was
|
|
21
|
+
// wiped: a re-imaged laptop came back as a stranger. Where no machine id can be read it is
|
|
22
|
+
// still random.
|
|
19
23
|
//
|
|
20
|
-
// It
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
+
// It is written once to MEMHOUSE_HOME/host.json and an existing file always wins, so no
|
|
25
|
+
// install changes identity under it. `uninstall --full-removal` drops the file, and the
|
|
26
|
+
// next install derives the same fingerprint again from the same machine. The id also
|
|
27
|
+
// carries the hostname the machine had at first install, so it comes back the same unless
|
|
28
|
+
// the machine was renamed in between, or has no readable machine id.
|
|
24
29
|
|
|
25
30
|
const fs = require('fs');
|
|
26
31
|
const os = require('os');
|
package/memhouse/house/HOUSE.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# The house — a database, and the rooms in it
|
|
2
2
|
|
|
3
3
|
**A house is a ClickHouse database.** Any database, named whatever its people name it —
|
|
4
|
-
`
|
|
4
|
+
`sam`, `team_a`, even `default`. Its rooms are tables: `sessions`, `messages`,
|
|
5
5
|
`tool_calls`. No views, no Merge rooms, no settings profile.
|
|
6
6
|
|
|
7
|
-
**Every member's rooms are named for them, and one grant covers them.** `mem.
|
|
8
|
-
for
|
|
9
|
-
reason. The grant is `GRANT … ON mem.
|
|
7
|
+
**Every member's rooms are named for them, and one grant covers them.** `mem.sam_*`
|
|
8
|
+
for sam, `mem.alice_*` for alice, in the same database — `mem` unless somebody has a
|
|
9
|
+
reason. The grant is `GRANT … ON mem.sam_* TO sam WITH GRANT OPTION`: it lets the
|
|
10
10
|
member create and rebuild their own rooms, share them, and reach nothing else. Nobody is
|
|
11
11
|
granted the database itself. A house with one member is a house of one, not a different
|
|
12
12
|
kind of house — a colleague joins as one more member and nothing about the first changes.
|
|
@@ -17,9 +17,9 @@ Two columns say where every row came from:
|
|
|
17
17
|
| Column | Meaning | Comes from |
|
|
18
18
|
|---|---|---|
|
|
19
19
|
| `user_id` | who wrote it | `String MATERIALIZED currentUser()` — stamped by the server, unforgeable by clients. `async_insert = 0 CONST` is pinned on each user (`ADD SETTING`, which merges — a bare `SETTINGS` clause replaces the user's whole list) because an async flush stores the stamp as the empty string (measured on 25.11.9.34) |
|
|
20
|
-
| `host` | which machine | the
|
|
20
|
+
| `host` | which machine | the machine's fingerprint (`../host.js`) — derived from the OS machine id (random only where none can be read), so two machines with the same hostname cannot collide and renaming one cannot split its history |
|
|
21
21
|
|
|
22
|
-
`WHERE user_id = 'alice'` is one person. `WHERE host = '
|
|
22
|
+
`WHERE user_id = 'alice'` is one person. `WHERE host = 'my-laptop-1f2e3d4c'` is one
|
|
23
23
|
machine. No filter is the whole house — which is what a team dashboard wants.
|
|
24
24
|
|
|
25
25
|
## Joining a house
|
package/memhouse/house/house.js
CHANGED
|
@@ -8,19 +8,19 @@
|
|
|
8
8
|
// clients (async_insert is pinned to 0
|
|
9
9
|
// on the user so the stamp cannot be
|
|
10
10
|
// skipped)
|
|
11
|
-
// host LowCardinality(String) — the machine's fingerprint,
|
|
11
|
+
// host LowCardinality(String) — the machine's fingerprint, derived from its OS machine id
|
|
12
12
|
// per install (../host.js)
|
|
13
13
|
//
|
|
14
14
|
// That pair IS the provenance model.
|
|
15
15
|
//
|
|
16
16
|
// ONE LAYOUT. A house is a database — `mem` unless somebody has a reason — and every
|
|
17
|
-
// member's rooms in it carry the member's name: `mem.alice_messages`, `mem.
|
|
17
|
+
// member's rooms in it carry the member's name: `mem.alice_messages`, `mem.sam_messages`.
|
|
18
18
|
// A member holds one grant, on the wildcard `mem.<name>_*`, and nothing else in the
|
|
19
19
|
// database. That is the whole access model, and roomNames() below is the only place a
|
|
20
20
|
// table name is produced.
|
|
21
21
|
//
|
|
22
|
-
// alone on a laptop mem.
|
|
23
|
-
// a team on one server mem.
|
|
22
|
+
// alone on a laptop mem.sam_* a house of one
|
|
23
|
+
// a team on one server mem.sam_*, mem.alice_*
|
|
24
24
|
// an agency on a kernel the same, in the kernel's `mem`
|
|
25
25
|
//
|
|
26
26
|
// Nobody ever holds ALL ON <db>.*. That grant is dynamic — it covers rooms created later —
|
|
@@ -412,6 +412,31 @@ function statRefreshStatements(rooms, at = 'now64(3)') {
|
|
|
412
412
|
`INSERT INTO ${names[k]} SELECT ${at} AS refreshed_at, * FROM (${bodies[k].body})`);
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
+
/**
|
|
416
|
+
* When the shipper refreshes the stat tables (G11). After every pass that shipped something,
|
|
417
|
+
* as before — and ALSO when the newest generation is older than STATS_MAX_AGE_MS, even if
|
|
418
|
+
* nothing shipped. The tables carry a one-day TTL, so a house whose machines went quiet for
|
|
419
|
+
* a day used to lose every generation and the dashboard went empty while the rooms still
|
|
420
|
+
* held everything. `ageMs` null means "could not tell" (no table, no rights): do not refresh
|
|
421
|
+
* on age then; the read layer is already on the inline rollup.
|
|
422
|
+
*/
|
|
423
|
+
const STATS_MAX_AGE_MS = 60 * 60 * 1000;
|
|
424
|
+
function statsNeedRefresh({ shipped, ageMs, maxAgeMs = STATS_MAX_AGE_MS }) {
|
|
425
|
+
if (shipped > 0) return true;
|
|
426
|
+
return ageMs !== null && ageMs !== undefined && ageMs > maxAgeMs;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* How long a reader may keep its resolved rooms (G11). resolveRooms() decides once whether
|
|
431
|
+
* the stat tables are usable or the inline rollup must answer; a long-running dashboard that
|
|
432
|
+
* never asked again kept reading a stat table after its TTL had emptied it. Re-resolving
|
|
433
|
+
* every few minutes costs one small round trip and lets it follow the house either way.
|
|
434
|
+
*/
|
|
435
|
+
const ROOMS_TTL_MS = 5 * 60 * 1000;
|
|
436
|
+
function roomsCacheStale(resolvedAt, now = Date.now(), ttlMs = ROOMS_TTL_MS) {
|
|
437
|
+
return !resolvedAt || now - resolvedAt > ttlMs;
|
|
438
|
+
}
|
|
439
|
+
|
|
415
440
|
/** The read-side source for one stat table: its newest generation, and nothing else. */
|
|
416
441
|
function latestGeneration(table) {
|
|
417
442
|
return `(SELECT * FROM ${table} WHERE refreshed_at = (SELECT max(refreshed_at) FROM ${table}))`;
|
|
@@ -435,7 +460,7 @@ const READ_SETTINGS = { final: 1, join_use_nulls: 1 };
|
|
|
435
460
|
* The table a room type lands in. THE naming rule, and the only copy of it.
|
|
436
461
|
*
|
|
437
462
|
* ONE LAYOUT. Every member's rooms carry the member's own name: `mem.alice_messages`,
|
|
438
|
-
* and `mem.
|
|
463
|
+
* and `mem.sam_messages` even when sam is the only one in there. A house with one
|
|
439
464
|
* member is a house of one, not a different kind of house — so there is no second case
|
|
440
465
|
* here, no prefix to configure, and nothing for an invite file to carry.
|
|
441
466
|
*
|
|
@@ -598,4 +623,5 @@ module.exports = {
|
|
|
598
623
|
sessionsRollup, currentParse, createStatement, roomNames, currentUser, resolveRooms,
|
|
599
624
|
SESSION_STATS, MODEL_STATS, TOOL_STATS, statTableNames, hasSessionStats, statTables,
|
|
600
625
|
statCreateStatements, statRefreshStatements, latestGeneration,
|
|
626
|
+
STATS_MAX_AGE_MS, statsNeedRefresh, ROOMS_TTL_MS, roomsCacheStale,
|
|
601
627
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Where `invite` may send a credential — and the one place that decides it (G4).
|
|
2
|
+
//
|
|
3
|
+
// `--url` is the address the INVITEE will reach the house at. It goes into the invite file;
|
|
4
|
+
// it is typed on a command line, so it can be a typo, a tunnel, or somebody else's server.
|
|
5
|
+
// A credential this install KEEPS — the member's own, or an admin saved with --keep-admin —
|
|
6
|
+
// belongs to the house it was saved for, and is only ever sent there. Before 0.18.11 the
|
|
7
|
+
// stored credential was sent to `--url`: resolveConfig() lets the flag outrank the env file,
|
|
8
|
+
// so `cfg.url` WAS `--url`, and the reachability probe, the grants read and the provisioning
|
|
9
|
+
// all authenticated against whatever `--url` named.
|
|
10
|
+
//
|
|
11
|
+
// A credential given for THIS command — `--admin-user`, or an exported MEMHOUSE_ADMIN_USER,
|
|
12
|
+
// with a password from a file, stdin, the environment or a prompt — was chosen by the person
|
|
13
|
+
// who also typed `--url`, so it provisions there, as it always has.
|
|
14
|
+
//
|
|
15
|
+
// Pure, so it is unit tested.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {object} o
|
|
19
|
+
* @param {string} o.url the invitee-facing --url
|
|
20
|
+
* @param {string|null} o.storedUrl the house this install is configured for (env or env file)
|
|
21
|
+
* @param {'flag'|'env'|'file'|'member'} o.adminUserSource
|
|
22
|
+
* where the credential's USER came from: --admin-user, an exported MEMHOUSE_ADMIN_USER,
|
|
23
|
+
* the env file's MEMHOUSE_ADMIN_USER, or (no admin at all) the member credential
|
|
24
|
+
* @param {string|null} [o.passwordSource] resolveAdminPassword's `source` ('stored' = the env file)
|
|
25
|
+
* @returns {{ url: string, stored: boolean } | { error: string }}
|
|
26
|
+
*/
|
|
27
|
+
function provisionTarget({ url, storedUrl, adminUserSource, passwordSource = null }) {
|
|
28
|
+
const stored = adminUserSource === 'file' || adminUserSource === 'member' || passwordSource === 'stored';
|
|
29
|
+
if (!stored) return { url, stored: false };
|
|
30
|
+
if (!storedUrl) {
|
|
31
|
+
return { error: 'this install keeps no house URL to send its stored credential to — '
|
|
32
|
+
+ 'give the admin for this command instead (--admin-user, password from MEMHOUSE_ADMIN_PASSWORD or --admin-password-file)' };
|
|
33
|
+
}
|
|
34
|
+
return { url: storedUrl, stored: true };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { provisionTarget };
|
package/memhouse/self-update.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// `npm i -g memhouse@latest` replaces the files under the global prefix and does not touch
|
|
4
4
|
// the running processes, so the shipper keeps parsing with the old adapters and the
|
|
5
5
|
// dashboard keeps serving the old bundle. Nothing reports it: `status` prints the version
|
|
6
|
-
// of the CLI you just typed, not the version the daemon is running. On
|
|
6
|
+
// of the CLI you just typed, not the version the daemon is running. On one machine a shipper
|
|
7
7
|
// ran 1d16h out of a directory that had been MOVED — the fix was live, the machine was not.
|
|
8
8
|
//
|
|
9
9
|
// npm's own `postinstall` is the obvious hook and the wrong one. npm >= 12 blocks install
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
const { createClient, ClickHouseLogLevel } = require('@clickhouse/client');
|
|
20
20
|
const { calculateCost, normalizeModelName } = require('../../pricing');
|
|
21
|
-
const { resolveRooms, READ_SETTINGS } = require('../house/house');
|
|
21
|
+
const { resolveRooms, READ_SETTINGS, roomsCacheStale } = require('../house/house');
|
|
22
22
|
|
|
23
23
|
const config = {
|
|
24
24
|
url: process.env.MEMHOUSE_URL || 'http://localhost:8123',
|
|
@@ -90,11 +90,16 @@ const TZ = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
|
90
90
|
// braces, no type.
|
|
91
91
|
const ROOM_TOKEN = /\{\{([a-z_]+)\}\}/g;
|
|
92
92
|
let roomsPromise = null;
|
|
93
|
+
let roomsAt = 0;
|
|
93
94
|
|
|
94
95
|
function rooms() {
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
96
|
+
// Cached, because resolveRooms asks the server for currentUser() and which stat tables
|
|
97
|
+
// are usable, and every dashboard request would otherwise pay that round trip. But not
|
|
98
|
+
// for the life of the process: whether the stat tables can answer changes — their TTL
|
|
99
|
+
// empties them on a quiet house, a refresh fills them again — and a dashboard that
|
|
100
|
+
// decided once kept reading an emptied table and showed nothing (G11).
|
|
101
|
+
if (!roomsPromise || roomsCacheStale(roomsAt)) {
|
|
102
|
+
roomsAt = Date.now();
|
|
98
103
|
roomsPromise = resolveRooms(getClient()).catch((e) => { roomsPromise = null; throw e; });
|
|
99
104
|
}
|
|
100
105
|
return roomsPromise;
|
|
@@ -473,8 +478,8 @@ async function getChats(opts = {}) {
|
|
|
473
478
|
// per call, three endpoints calling it — and getChats used ~4% of what it asked for.
|
|
474
479
|
// In CHUNKS. The scope travels as `{scopeIds:Array(String)}` query parameters, and the
|
|
475
480
|
// driver puts query parameters in the REQUEST URI — so a page of N sessions builds a URI
|
|
476
|
-
// of roughly 60 bytes per id, twice (ids and users).
|
|
477
|
-
// tunnel
|
|
481
|
+
// of roughly 60 bytes per id, twice (ids and users). A house reached through a Cloudflare
|
|
482
|
+
// tunnel sits behind a proxy that rejects a URI far below ClickHouse's own 1 MiB `http_max_uri_size`: at
|
|
478
483
|
// 925 sessions the request came back 500 with an EMPTY message (the driver reports a
|
|
479
484
|
// transport-level refusal as `new Error('')`), so `/api/chats?limit=1000` — exactly what
|
|
480
485
|
// the Sessions page asks for — returned 500 and the page rendered blank. 900 worked,
|