memhouse 0.3.2 → 0.4.3
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/README.md +64 -6
- package/bin/memhouse.js +1014 -41
- package/editors/antigravity.js +11 -2
- package/editors/cursor.js +16 -7
- package/editors/opencode.js +12 -3
- package/editors/zed.js +13 -4
- package/index.js +2 -2
- package/mem-house/COMPETITION.md +217 -0
- package/mem-house/DESIGN.md +61 -32
- package/mem-house/delivery/AGENT-INSTALL-PROMPT.md +58 -0
- package/mem-house/delivery/AGENT-INSTALL.md +12 -4
- package/mem-house/delivery/PROMPT.md +23 -10
- package/mem-house/delivery/install.sh +9 -7
- package/mem-house/delivery/kernel-install.md +56 -25
- package/mem-house/delivery/skills/memhouse-search/SKILL.md +46 -16
- package/mem-house/delivery/skills/memhouse-sessions/SKILL.md +38 -7
- package/mem-house/delivery/skills/memhouse-sql/SKILL.md +48 -17
- package/mem-house/deploy.js +333 -0
- package/mem-house/envfile.js +49 -0
- package/mem-house/per-member/INSTALL.md +183 -0
- package/mem-house/per-member/PLAN.md +174 -0
- package/mem-house/per-member/PROVISIONING.md +177 -0
- package/mem-house/per-member/README.md +48 -0
- package/mem-house/per-member/SCHEMA.md +73 -0
- package/mem-house/per-member/SHARING.md +102 -0
- package/mem-house/per-member/provision.js +142 -0
- package/mem-house/per-member/rooms.js +132 -0
- package/mem-house/per-member/schema-member.sql.tpl +85 -0
- package/mem-house/per-member/schema-merge.sql.tpl +24 -0
- package/mem-house/server/queries.js +103 -53
- package/mem-house/service.js +400 -0
- package/mem-house/shipper/ship.js +61 -18
- package/package.json +3 -3
- package/pricing.js +20 -2
- package/pricing.json +216 -24
- package/public/assets/{index-CCPh7a2Z.js → index-CtH-5i95.js} +5 -5
- package/public/index.html +1 -1
- package/ui/src/App.jsx +3 -2
- package/ui/src/pages/SqlViewer.jsx +39 -21
- package/mem-house/rls.sql +0 -17
- package/mem-house/schema.sql +0 -145
package/public/index.html
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-CtH-5i95.js"></script>
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/assets/index-DV6ONi_F.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
package/ui/src/App.jsx
CHANGED
|
@@ -349,8 +349,9 @@ export default function App() {
|
|
|
349
349
|
|
|
350
350
|
<div className="text-[12px] font-medium mb-1.5" style={{ color: 'var(--c-white)' }}>Team Sharing</div>
|
|
351
351
|
<div className="text-[10px] mb-1" style={{ color: 'var(--c-text3)' }}>
|
|
352
|
-
Relay join is not supported in memhouse — team sharing is kernel-based
|
|
353
|
-
|
|
352
|
+
Relay join is not supported in memhouse — team sharing is kernel-based. Each
|
|
353
|
+
member owns their own rooms and shares by granting SELECT on them.
|
|
354
|
+
See mem-house/delivery/kernel-install.md.
|
|
354
355
|
</div>
|
|
355
356
|
<pre
|
|
356
357
|
className="text-[11px] px-3 py-2 overflow-x-auto"
|
|
@@ -9,22 +9,29 @@ import PageHeader from '../components/PageHeader'
|
|
|
9
9
|
ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, PointElement, LineElement, Filler)
|
|
10
10
|
|
|
11
11
|
// Two backends share this SPA: the typed mem-house house (sessions / messages /
|
|
12
|
-
// tool_calls + the
|
|
13
|
-
// chat_stats). Both are ClickHouse
|
|
14
|
-
//
|
|
12
|
+
// tool_calls + the session rollup) and the legacy agentlytics cache (chats /
|
|
13
|
+
// chat_stats). Both are ClickHouse, and the flavor comes from /api/schema.
|
|
14
|
+
//
|
|
15
|
+
// The typed examples are TEMPLATED, not literal. A per-member house names its rooms for
|
|
16
|
+
// the member and has no `sessions_v` object at all — the rollup is a saved query — so
|
|
17
|
+
// `/api/schema` reports the resolved names and `{{room}}` tokens are substituted here.
|
|
18
|
+
// Detecting the flavor by `tables.includes('sessions_v')` stopped working for the same
|
|
19
|
+
// reason: it reported an indeterminate schema and offered nothing.
|
|
15
20
|
const TYPED_EXAMPLES = [
|
|
16
|
-
{ label: 'Sessions per editor', sql: `SELECT source, count() AS count FROM sessions_v GROUP BY source ORDER BY count DESC` },
|
|
17
|
-
{ label: 'Top 10 projects', sql: `SELECT project, count() AS sessions, sum(total_msgs) AS messages FROM sessions_v WHERE project != '' GROUP BY project ORDER BY sessions DESC LIMIT 10` },
|
|
18
|
-
{ label: 'Messages per day', sql: `SELECT toDate(ts) AS day, count() AS count FROM messages GROUP BY day ORDER BY day` },
|
|
19
|
-
{ label: 'Top models', sql: `SELECT model, count() AS count FROM messages WHERE model NOT IN ('', '<synthetic>') GROUP BY model ORDER BY count DESC LIMIT 10` },
|
|
20
|
-
{ label: 'Top tools', sql: `SELECT tool_name, count() AS count FROM tool_calls GROUP BY tool_name ORDER BY count DESC LIMIT 15` },
|
|
21
|
-
{ label: 'Token usage by editor', sql: `SELECT source, sum(input_tokens) AS input_tokens, sum(output_tokens) AS output_tokens FROM sessions_v GROUP BY source ORDER BY input_tokens DESC` },
|
|
22
|
-
{ label: 'Sessions by mode', sql: `SELECT mode, count() AS count FROM sessions_v WHERE mode != '' GROUP BY mode ORDER BY count DESC` },
|
|
23
|
-
{ label: 'Hourly distribution', sql: `SELECT toHour(ts) AS hour, count() AS count FROM messages GROUP BY hour ORDER BY hour` },
|
|
24
|
-
// Grouped by (session_id, user_id), matching the
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
21
|
+
{ label: 'Sessions per editor', sql: `SELECT source, count() AS count FROM {{sessions_v}} AS c GROUP BY source ORDER BY count DESC` },
|
|
22
|
+
{ label: 'Top 10 projects', sql: `SELECT project, count() AS sessions, sum(total_msgs) AS messages FROM {{sessions_v}} AS c WHERE project != '' GROUP BY project ORDER BY sessions DESC LIMIT 10` },
|
|
23
|
+
{ label: 'Messages per day', sql: `SELECT toDate(ts) AS day, count() AS count FROM {{messages}} GROUP BY day ORDER BY day` },
|
|
24
|
+
{ label: 'Top models', sql: `SELECT model, count() AS count FROM {{messages}} WHERE model NOT IN ('', '<synthetic>') GROUP BY model ORDER BY count DESC LIMIT 10` },
|
|
25
|
+
{ label: 'Top tools', sql: `SELECT tool_name, count() AS count FROM {{tool_calls}} GROUP BY tool_name ORDER BY count DESC LIMIT 15` },
|
|
26
|
+
{ label: 'Token usage by editor', sql: `SELECT source, sum(input_tokens) AS input_tokens, sum(output_tokens) AS output_tokens FROM {{sessions_v}} AS c GROUP BY source ORDER BY input_tokens DESC` },
|
|
27
|
+
{ label: 'Sessions by mode', sql: `SELECT mode, count() AS count FROM {{sessions_v}} AS c WHERE mode != '' GROUP BY mode ORDER BY count DESC` },
|
|
28
|
+
{ label: 'Hourly distribution', sql: `SELECT toHour(ts) AS hour, count() AS count FROM {{messages}} GROUP BY hour ORDER BY hour` },
|
|
29
|
+
// Grouped by (session_id, user_id), matching the room's ORDER BY. `{{messages}}` is
|
|
30
|
+
// the caller's own room, where user_id is constant — but point the same query at a
|
|
31
|
+
// team `Merge` room (all_messages) and two members can carry the same adapter-local
|
|
32
|
+
// session id; grouping on session_id alone would merge them and make any(source) an
|
|
33
|
+
// arbitrary member's.
|
|
34
|
+
{ label: 'Full-text search', sql: `SELECT session_id, user_id, any(source) AS source, count() AS hits FROM {{messages}} WHERE text_ngram LIKE '%clickhouse%' GROUP BY session_id, user_id ORDER BY hits DESC LIMIT 10` },
|
|
28
35
|
]
|
|
29
36
|
const LEGACY_EXAMPLES = [
|
|
30
37
|
{ label: 'Sessions per editor', sql: `SELECT source, count() AS count FROM chats GROUP BY source ORDER BY count DESC` },
|
|
@@ -37,6 +44,12 @@ const LEGACY_EXAMPLES = [
|
|
|
37
44
|
{ label: 'Hourly distribution', sql: `SELECT toHour(toDateTime(intDiv(created_at, 1000))) AS hour, count() AS count FROM chats WHERE created_at IS NOT NULL GROUP BY hour ORDER BY hour` },
|
|
38
45
|
]
|
|
39
46
|
|
|
47
|
+
// `{{room}}` -> the resolved name (or, for the rollup on a per-member house, the whole
|
|
48
|
+
// subquery). Same token shape the server's own query layer uses.
|
|
49
|
+
function applyRooms(sql, rooms) {
|
|
50
|
+
return sql.replace(/\{\{([a-z_]+)\}\}/g, (m, name) => rooms[name] ?? m)
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
export default function SqlViewer() {
|
|
41
54
|
const { dark } = useTheme()
|
|
42
55
|
// Three states, and they must stay distinguishable: 'loading' while /api/schema is
|
|
@@ -58,13 +71,18 @@ export default function SqlViewer() {
|
|
|
58
71
|
useEffect(() => {
|
|
59
72
|
fetchSchema().then(s => {
|
|
60
73
|
setSchema(s)
|
|
61
|
-
// Three outcomes, not two. The typed house
|
|
62
|
-
// legacy one has chats
|
|
63
|
-
// half-applied schema with neither marker — is indeterminate, and
|
|
64
|
-
// either set would hand the user queries that error.
|
|
74
|
+
// Three outcomes, not two. The typed house is whichever one /api/schema resolved
|
|
75
|
+
// rooms for; the legacy one has `chats`. Anything else — an error object with no
|
|
76
|
+
// `tables`, a half-applied schema with neither marker — is indeterminate, and
|
|
77
|
+
// offering either set would hand the user queries that error.
|
|
78
|
+
//
|
|
79
|
+
// `rooms` is the authority, not a table name: a per-member house has no
|
|
80
|
+
// `sessions_v` object, so the old `tables.includes('sessions_v')` test called a
|
|
81
|
+
// perfectly healthy typed house indeterminate.
|
|
65
82
|
const tables = s?.tables
|
|
66
|
-
const
|
|
67
|
-
|
|
83
|
+
const rooms = s?.rooms
|
|
84
|
+
const ex = rooms?.sessions ? TYPED_EXAMPLES.map(e => ({ ...e, sql: applyRooms(e.sql, rooms) }))
|
|
85
|
+
: !Array.isArray(tables) ? null
|
|
68
86
|
: tables.includes('chats') ? LEGACY_EXAMPLES
|
|
69
87
|
: null
|
|
70
88
|
setExamples(ex)
|
package/mem-house/rls.sql
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
-- mem-house — own-only row-level security (the sharing model's isolation half).
|
|
2
|
-
--
|
|
3
|
-
-- Applied by the KERNEL (or the mayor, who has ACCESS MANAGEMENT) — the agency owner
|
|
4
|
-
-- cannot CREATE ROW POLICY by design. Binds to the kernel's `member` role, so every
|
|
5
|
-
-- registered member sees only their own rows; the owner (not a member) sees the whole
|
|
6
|
-
-- house. For a shared team pool instead, apply NO policy — just the owner's GRANTs.
|
|
7
|
-
--
|
|
8
|
-
-- Substitute `memhouse` if the operator named the house differently. Policies bind to
|
|
9
|
-
-- physical tables; views (sessions_v) inherit the querying user's policy via messages/
|
|
10
|
-
-- sessions.
|
|
11
|
-
|
|
12
|
-
CREATE ROW POLICY IF NOT EXISTS own_sessions ON memhouse.sessions
|
|
13
|
-
FOR SELECT USING user_id = currentUser() TO member;
|
|
14
|
-
CREATE ROW POLICY IF NOT EXISTS own_messages ON memhouse.messages
|
|
15
|
-
FOR SELECT USING user_id = currentUser() TO member;
|
|
16
|
-
CREATE ROW POLICY IF NOT EXISTS own_tool_calls ON memhouse.tool_calls
|
|
17
|
-
FOR SELECT USING user_id = currentUser() TO member;
|
package/mem-house/schema.sql
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
-- mem-house HOUSE schema — the typed common session store (see DESIGN.md).
|
|
2
|
-
--
|
|
3
|
-
-- Applied by the agency OWNER (`memhouse_root`) inside its own house; names are
|
|
4
|
-
-- UNQUALIFIED so this works whatever the operator named the house. Requires
|
|
5
|
-
-- ClickHouse >= 26.2 natively; 25.11 also works because the shipper passes
|
|
6
|
-
-- allow_experimental_full_text_index=1 per-query when applying this file
|
|
7
|
-
-- (the messages text indexes are still gated on 25.x; 26.x+ ignores the flag).
|
|
8
|
-
--
|
|
9
|
-
-- Parse-on-client: the shipper runs the 17 editor adapters and ships TYPED rows —
|
|
10
|
-
-- these tables are the contract. `extra JSON` on each table is the escape hatch for
|
|
11
|
-
-- adapter fields not yet normalized (never lose data to the schema).
|
|
12
|
-
--
|
|
13
|
-
-- Versioning/idempotency: ReplacingMergeTree(ingested_at) everywhere. `messages` is
|
|
14
|
-
-- keyed (session_id, user_id, seq): re-shipping a grown or corrected session
|
|
15
|
-
-- REPLACES stale rows (latest-wins) instead of accumulating variants, and the
|
|
16
|
-
-- user_id in every key means two members shipping the same adapter-local
|
|
17
|
-
-- session_id can never collapse or overwrite each other's rows. Identity is
|
|
18
|
-
-- server-stamped (`user_id MATERIALIZED currentUser()`; writers must use
|
|
19
|
-
-- async_insert=0) and is the RLS anchor (rls.sql). Readers should query with the
|
|
20
|
-
-- `final=1` setting.
|
|
21
|
-
--
|
|
22
|
-
-- session_id is CANONICAL and globally unique: '<source>:<adapter-local id>',
|
|
23
|
-
-- stamped by the shipper. Adapter-local ids are only unique within one editor;
|
|
24
|
-
-- the source prefix makes every key, incremental comparison, delete, view join,
|
|
25
|
-
-- and API id collision-free across editors. `source` remains a plain column for
|
|
26
|
-
-- filtering.
|
|
27
|
-
--
|
|
28
|
-
-- BREAKING (pre-release): the user_id-in-key change alters ORDER BY, which
|
|
29
|
-
-- CREATE TABLE IF NOT EXISTS will NOT apply to an existing house — recreate with
|
|
30
|
-
-- `memhouse reset` (or DROP the tables and re-run --ensure-schema). The
|
|
31
|
-
-- source-prefixed session_id change needs no DDL, but rows shipped before it
|
|
32
|
-
-- linger under their old bare ids — run `memhouse reset` once after upgrading.
|
|
33
|
-
|
|
34
|
-
-- One row per session (adapter-level metadata; aggregates live in sessions_v).
|
|
35
|
-
CREATE TABLE IF NOT EXISTS sessions
|
|
36
|
-
(
|
|
37
|
-
session_id String,
|
|
38
|
-
source LowCardinality(String),
|
|
39
|
-
host LowCardinality(String),
|
|
40
|
-
name String DEFAULT '',
|
|
41
|
-
mode LowCardinality(String) DEFAULT '',
|
|
42
|
-
folder String DEFAULT '',
|
|
43
|
-
project String DEFAULT '',
|
|
44
|
-
git_branch String DEFAULT '',
|
|
45
|
-
created_at Nullable(DateTime64(3, 'UTC')),
|
|
46
|
-
last_updated_at Nullable(DateTime64(3, 'UTC')),
|
|
47
|
-
message_count UInt32 DEFAULT 0,
|
|
48
|
-
path String DEFAULT '',
|
|
49
|
-
extra JSON,
|
|
50
|
-
user_id String MATERIALIZED currentUser(),
|
|
51
|
-
ingested_at DateTime64(3, 'UTC') DEFAULT now64(3)
|
|
52
|
-
)
|
|
53
|
-
ENGINE = ReplacingMergeTree(ingested_at)
|
|
54
|
-
ORDER BY (session_id, user_id);
|
|
55
|
-
|
|
56
|
-
-- One row per message. FTS: two lower(text) materialized columns each carry one
|
|
57
|
-
-- text index (ngram for substring/LIKE, word for token match) — query with
|
|
58
|
-
-- lowercased terms, SELECT `text` for display.
|
|
59
|
-
CREATE TABLE IF NOT EXISTS messages
|
|
60
|
-
(
|
|
61
|
-
session_id String,
|
|
62
|
-
seq UInt32,
|
|
63
|
-
source LowCardinality(String),
|
|
64
|
-
host LowCardinality(String),
|
|
65
|
-
ts DateTime64(3, 'UTC'),
|
|
66
|
-
role LowCardinality(String),
|
|
67
|
-
model LowCardinality(String) DEFAULT '',
|
|
68
|
-
input_tokens UInt64 DEFAULT 0,
|
|
69
|
-
output_tokens UInt64 DEFAULT 0,
|
|
70
|
-
cache_read_tokens UInt64 DEFAULT 0,
|
|
71
|
-
cache_write_tokens UInt64 DEFAULT 0,
|
|
72
|
-
text String,
|
|
73
|
-
project String DEFAULT '',
|
|
74
|
-
folder String DEFAULT '',
|
|
75
|
-
is_subagent Bool DEFAULT false,
|
|
76
|
-
extra JSON,
|
|
77
|
-
line_hash UInt64,
|
|
78
|
-
user_id String MATERIALIZED currentUser(),
|
|
79
|
-
ingested_at DateTime64(3, 'UTC') DEFAULT now64(3),
|
|
80
|
-
text_ngram String MATERIALIZED lower(text),
|
|
81
|
-
text_word String MATERIALIZED lower(text),
|
|
82
|
-
INDEX idx_text_ngram text_ngram TYPE text(tokenizer = ngrams(3)) GRANULARITY 1,
|
|
83
|
-
INDEX idx_text_word text_word TYPE text(tokenizer = splitByNonAlpha) GRANULARITY 1
|
|
84
|
-
)
|
|
85
|
-
ENGINE = ReplacingMergeTree(ingested_at)
|
|
86
|
-
ORDER BY (session_id, user_id, seq);
|
|
87
|
-
|
|
88
|
-
-- One row per tool call (memory-house has no equivalent; this powers the tool
|
|
89
|
-
-- analytics). `seq` = owning message's seq; `idx` = call index within the session.
|
|
90
|
-
CREATE TABLE IF NOT EXISTS tool_calls
|
|
91
|
-
(
|
|
92
|
-
session_id String,
|
|
93
|
-
seq UInt32,
|
|
94
|
-
idx UInt32,
|
|
95
|
-
source LowCardinality(String),
|
|
96
|
-
host LowCardinality(String),
|
|
97
|
-
tool_name LowCardinality(String),
|
|
98
|
-
args String DEFAULT '{}',
|
|
99
|
-
ts DateTime64(3, 'UTC'),
|
|
100
|
-
project String DEFAULT '',
|
|
101
|
-
folder String DEFAULT '',
|
|
102
|
-
user_id String MATERIALIZED currentUser(),
|
|
103
|
-
ingested_at DateTime64(3, 'UTC') DEFAULT now64(3)
|
|
104
|
-
)
|
|
105
|
-
ENGINE = ReplacingMergeTree(ingested_at)
|
|
106
|
-
ORDER BY (session_id, user_id, idx);
|
|
107
|
-
|
|
108
|
-
-- Analytics-ready rollup: session metadata + message aggregates. Query with final=1.
|
|
109
|
-
-- LEFT JOIN (not INNER): stable-empty sessions are deliberately stored (the row
|
|
110
|
-
-- absorbs the incremental skip) and must stay visible with zero aggregates, not
|
|
111
|
-
-- vanish from every dashboard count. join_use_nulls makes unmatched message
|
|
112
|
-
-- columns NULL so count(m.seq)/coalesce produce true zeros instead of counting
|
|
113
|
-
-- the placeholder row.
|
|
114
|
-
CREATE OR REPLACE VIEW sessions_v AS
|
|
115
|
-
SELECT
|
|
116
|
-
s.session_id AS session_id,
|
|
117
|
-
any(s.source) AS source,
|
|
118
|
-
any(s.host) AS host,
|
|
119
|
-
any(s.name) AS name,
|
|
120
|
-
any(s.mode) AS mode,
|
|
121
|
-
any(s.folder) AS folder,
|
|
122
|
-
any(s.project) AS project,
|
|
123
|
-
any(s.git_branch) AS git_branch,
|
|
124
|
-
s.user_id AS user_id,
|
|
125
|
-
any(s.created_at) AS created_at,
|
|
126
|
-
any(s.last_updated_at) AS last_updated_at,
|
|
127
|
-
min(m.ts) AS started,
|
|
128
|
-
max(m.ts) AS ended,
|
|
129
|
-
coalesce(dateDiff('second', min(m.ts), max(m.ts)), 0) AS duration_sec,
|
|
130
|
-
count(m.seq) AS total_msgs,
|
|
131
|
-
countIf(m.role = 'user') AS user_msgs,
|
|
132
|
-
countIf(m.role = 'assistant') AS assistant_msgs,
|
|
133
|
-
countIf(m.is_subagent) AS subagent_msgs,
|
|
134
|
-
groupUniqArrayIf(m.model, m.model NOT IN ('', '<synthetic>')) AS models,
|
|
135
|
-
coalesce(sum(m.input_tokens), 0) AS input_tokens,
|
|
136
|
-
coalesce(sum(m.output_tokens), 0) AS output_tokens,
|
|
137
|
-
coalesce(sum(m.cache_read_tokens), 0) AS cache_read_tokens,
|
|
138
|
-
coalesce(sum(m.cache_write_tokens), 0) AS cache_write_tokens,
|
|
139
|
-
coalesce(sumIf(length(m.text), m.role = 'user'), 0) AS user_chars,
|
|
140
|
-
coalesce(sumIf(length(m.text), m.role = 'assistant'), 0) AS assistant_chars,
|
|
141
|
-
coalesce(substring(argMinIf(m.text, m.seq, m.role = 'user' AND m.text != ''), 1, 200), '') AS first_prompt
|
|
142
|
-
FROM sessions AS s
|
|
143
|
-
LEFT JOIN messages AS m ON m.session_id = s.session_id AND m.user_id = s.user_id
|
|
144
|
-
GROUP BY s.session_id, s.user_id
|
|
145
|
-
SETTINGS join_use_nulls = 1;
|