memhouse 0.4.3 → 0.7.0
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 +244 -114
- package/bin/memhouse.js +1169 -233
- package/editors/base.js +6 -0
- package/editors/claude.js +44 -9
- package/editors/goose.js +4 -0
- package/editors/index.js +0 -0
- package/editors/opencode.js +3 -0
- package/{mem-house → memhouse}/COMPETITION.md +5 -5
- package/{mem-house → memhouse}/DESIGN.md +18 -12
- package/memhouse/RESUME-AND-UPDATE.md +185 -0
- package/{mem-house → memhouse}/delivery/AGENT-INSTALL-PROMPT.md +1 -1
- package/{mem-house → memhouse}/delivery/AGENT-INSTALL.md +9 -7
- package/{mem-house → memhouse}/delivery/PROMPT.md +8 -8
- package/{mem-house → memhouse}/delivery/kernel-install.md +3 -3
- package/{mem-house → memhouse}/delivery/plugin/.claude-plugin/plugin.json +1 -1
- package/{mem-house/delivery/skills/memhouse-search → memhouse/delivery/plugin/skills/search}/SKILL.md +60 -18
- package/memhouse/delivery/plugin/skills/sessions/SKILL.md +177 -0
- package/memhouse/delivery/plugin/skills/sql/SKILL.md +206 -0
- package/memhouse/host.js +101 -0
- package/{mem-house → memhouse}/per-member/INSTALL.md +59 -14
- package/{mem-house → memhouse}/per-member/SCHEMA.md +47 -0
- package/memhouse/per-member/provision.js +265 -0
- package/memhouse/per-member/rooms.js +235 -0
- package/memhouse/per-member/schema-member.sql.tpl +125 -0
- package/{mem-house → memhouse}/per-member/schema-merge.sql.tpl +3 -3
- package/memhouse/resume.js +150 -0
- package/memhouse/self-update.js +150 -0
- package/{mem-house → memhouse}/server/queries.js +80 -34
- package/memhouse/server/server.js +451 -0
- package/{mem-house → memhouse}/service.js +48 -29
- package/memhouse/shipper/ship.js +949 -0
- package/package.json +5 -20
- package/public/assets/{index-CtH-5i95.js → index-CnWzrjF3.js} +10 -7
- package/public/index.html +2 -2
- package/ui/index.html +1 -1
- package/ui/src/App.jsx +1 -1
- package/ui/src/lib/api.js +29 -13
- package/ui/src/pages/Dashboard.jsx +8 -5
- package/ui/src/pages/Sessions.jsx +3 -1
- package/ui/src/pages/SqlViewer.jsx +6 -23
- package/cache.js +0 -850
- package/ch-ingest.js +0 -329
- package/ch-schema.js +0 -152
- package/clickhouse.js +0 -87
- package/index.js +0 -425
- package/mcp-server.js +0 -294
- package/mem-house/delivery/install.sh +0 -183
- package/mem-house/delivery/skills/memhouse-sessions/SKILL.md +0 -125
- package/mem-house/delivery/skills/memhouse-sql/SKILL.md +0 -146
- package/mem-house/per-member/provision.js +0 -142
- package/mem-house/per-member/rooms.js +0 -132
- package/mem-house/per-member/schema-member.sql.tpl +0 -85
- package/mem-house/server/server.js +0 -228
- package/mem-house/shipper/ship.js +0 -504
- package/relay-client.js +0 -306
- package/relay-server.js +0 -577
- package/server.js +0 -860
- package/share-image.js +0 -367
- /package/{mem-house → memhouse}/deploy.js +0 -0
- /package/{mem-house → memhouse}/envfile.js +0 -0
- /package/{mem-house → memhouse}/per-member/PLAN.md +0 -0
- /package/{mem-house → memhouse}/per-member/PROVISIONING.md +0 -0
- /package/{mem-house → memhouse}/per-member/README.md +0 -0
- /package/{mem-house → memhouse}/per-member/SHARING.md +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
// queries.js —
|
|
1
|
+
// queries.js — memhouse read/query layer.
|
|
2
2
|
//
|
|
3
3
|
// Serves the SAME response shapes as the root cache.js (the agentlytics API
|
|
4
|
-
// contract) but computed over the
|
|
4
|
+
// contract) but computed over the memhouse typed schema — one set of rooms per member,
|
|
5
5
|
// `sessions_<m>` / `messages_<m>` / `tool_calls_<m>` (../per-member/schema-member.sql.tpl),
|
|
6
6
|
// plus the session rollup, which is a SAVED QUERY and not an object
|
|
7
7
|
// (../per-member/rooms.js). The dashboard SPA consumes these shapes unchanged.
|
|
8
8
|
//
|
|
9
|
-
// Schema mapping notes (root cache schema →
|
|
9
|
+
// Schema mapping notes (root cache schema → memhouse):
|
|
10
10
|
// chats → sessions_<m> / the rollup (id → session_id, bubble_count → total_msgs)
|
|
11
11
|
// chat_stats → rollup aggregates (token/char sums, models array)
|
|
12
12
|
// messages.content → messages_<m>.text (cache_read → cache_read_tokens, …)
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// All reads run with the `final: 1` setting so ReplacingMergeTree collapses
|
|
17
17
|
// duplicate versions at read time (see DESIGN.md data-plane rules).
|
|
18
18
|
|
|
19
|
-
const { createClient } = require('@clickhouse/client');
|
|
19
|
+
const { createClient, ClickHouseLogLevel } = require('@clickhouse/client');
|
|
20
20
|
const { calculateCost, normalizeModelName } = require('../../pricing');
|
|
21
21
|
const { resolveRooms, READ_SETTINGS } = require('../per-member/rooms');
|
|
22
22
|
|
|
@@ -31,6 +31,9 @@ let _client = null;
|
|
|
31
31
|
function getClient() {
|
|
32
32
|
if (_client) return _client;
|
|
33
33
|
_client = createClient({
|
|
34
|
+
// See the note in shipper/ship.js: the driver's ERROR-level dump lands ahead of every
|
|
35
|
+
// message we print, so a handled refusal reads like a crash. MEMHOUSE_DEBUG=1 restores it.
|
|
36
|
+
log: { level: process.env.MEMHOUSE_DEBUG ? ClickHouseLogLevel.DEBUG : ClickHouseLogLevel.OFF },
|
|
34
37
|
url: config.url,
|
|
35
38
|
username: config.username,
|
|
36
39
|
password: config.password,
|
|
@@ -74,9 +77,10 @@ function applyRooms(sql, r) {
|
|
|
74
77
|
});
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
async function q(query, params = {}) {
|
|
80
|
+
async function q(query, params = {}, settings = undefined) {
|
|
78
81
|
const rs = await getClient().query({
|
|
79
82
|
query: applyRooms(query, await rooms()), query_params: params, format: 'JSONEachRow',
|
|
83
|
+
...(settings ? { clickhouse_settings: settings } : {}),
|
|
80
84
|
});
|
|
81
85
|
return rs.json();
|
|
82
86
|
}
|
|
@@ -145,18 +149,30 @@ function normalizedModelFreq(rows) {
|
|
|
145
149
|
}
|
|
146
150
|
return freq;
|
|
147
151
|
}
|
|
152
|
+
// The same exclusions as EXCLUDED_MODELS, usable in JS.
|
|
153
|
+
const EXCLUDED_MODEL_SET = new Set(['', '<synthetic>']);
|
|
148
154
|
const topN = (freq, n) => Object.entries(freq).sort((a, b) => b[1] - a[1]).slice(0, n)
|
|
149
155
|
.map(([name, count]) => ({ name, count }));
|
|
150
156
|
|
|
151
157
|
// ── overview ────────────────────────────────────────────────────────────────────
|
|
152
158
|
async function getOverview(opts = {}) {
|
|
153
159
|
const f = filters(opts);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
+
// Same definition as getChats(). The nav bar read this and the Sessions page read
|
|
161
|
+
// getChats, so a house with one placeholder session showed "9 sessions" beside
|
|
162
|
+
// "8 sessions" in the same header, and the depth histogram summed to 7. A session row
|
|
163
|
+
// with no name and no messages is a placeholder the shipper keeps to absorb the
|
|
164
|
+
// incremental skip; it is not a conversation, and only one of the two counters knew.
|
|
165
|
+
const totalChats = Number((await q1(
|
|
166
|
+
`SELECT count() AS cnt FROM {{sessions_v}} AS c WHERE 1=1${f.and} AND (c.name != '' OR c.total_msgs > 0)`,
|
|
167
|
+
f.params)).cnt);
|
|
168
|
+
|
|
169
|
+
// Root parity WAS: without a folder filter the editor breakdown is global — which put
|
|
170
|
+
// "34 + 3" chips under a header reading "3 sessions" when an editor filter was applied,
|
|
171
|
+
// and 12 under a header of 11 with no filter at all. Parity with a wrong number is not a
|
|
172
|
+
// feature.
|
|
173
|
+
const editors = await q(
|
|
174
|
+
`SELECT source, count() AS count FROM {{sessions_v}} AS c
|
|
175
|
+
WHERE (c.name != '' OR c.total_msgs > 0)${f.and} GROUP BY source ORDER BY count DESC`, f.params);
|
|
160
176
|
|
|
161
177
|
const modes = await q(`SELECT mode, count() AS count FROM {{sessions_v}} AS c WHERE mode != ''${f.and} GROUP BY mode`, f.params);
|
|
162
178
|
const byMode = {};
|
|
@@ -253,7 +269,11 @@ async function getDashboardStats(opts = {}) {
|
|
|
253
269
|
SELECT COALESCE(sum(input_tokens), 0) AS input, COALESCE(sum(output_tokens), 0) AS output,
|
|
254
270
|
COALESCE(sum(cache_read_tokens), 0) AS cacheRead, COALESCE(sum(cache_write_tokens), 0) AS cacheWrite,
|
|
255
271
|
COALESCE(sum(user_chars), 0) AS userChars, COALESCE(sum(assistant_chars), 0) AS assistantChars,
|
|
256
|
-
|
|
272
|
+
COALESCE(sum(total_msgs), 0) AS messages,
|
|
273
|
+
-- The SAME definition the KPI beside it uses. A plain count() here put a
|
|
274
|
+
-- fourth session number on a screen that already had three: nav 11, chips 12,
|
|
275
|
+
-- avg depth 40/12, histogram 10.
|
|
276
|
+
countIf(c.name != '' OR c.total_msgs > 0) AS sessions
|
|
257
277
|
FROM {{sessions_v}} AS c WHERE 1=1${f.and}`, f.params);
|
|
258
278
|
|
|
259
279
|
const streakRows = await q(`
|
|
@@ -323,7 +343,7 @@ async function getDashboardStats(opts = {}) {
|
|
|
323
343
|
tokens: {
|
|
324
344
|
input: inputTokens, output: outputTokens, cacheRead: Number(tokenRow.cacheRead), cacheWrite: Number(tokenRow.cacheWrite),
|
|
325
345
|
userChars: Number(tokenRow.userChars), assistantChars: Number(tokenRow.assistantChars),
|
|
326
|
-
sessions: Number(tokenRow.sessions), estimated: tokensEstimated,
|
|
346
|
+
sessions: Number(tokenRow.sessions), messages: Number(tokenRow.messages), estimated: tokensEstimated,
|
|
327
347
|
},
|
|
328
348
|
streaks: { current: currentStreak, longest: longestStreak, totalDays: streakRows.length },
|
|
329
349
|
monthlyTrend: { months: Object.keys(monthEditors).sort(), sources: [...allSources], data: monthEditors },
|
|
@@ -359,26 +379,30 @@ async function getChats(opts = {}) {
|
|
|
359
379
|
const rows = await q(sql, params);
|
|
360
380
|
if (rows.length === 0) return [];
|
|
361
381
|
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
382
|
+
// ONE cost implementation, shared with the Costs page.
|
|
383
|
+
//
|
|
384
|
+
// getChats used to compute its own, and the two disagreed three times running: first the
|
|
385
|
+
// whole session priced at its dominant model ($10.02 against $150), then sessions whose
|
|
386
|
+
// rows carry no model at all ($0 against $10.50), then sessions with zero tokens but
|
|
387
|
+
// non-zero chars ($0 against $0.033). Each round fixed the case that had been measured
|
|
388
|
+
// and left the next one. computePerChatCosts already handles all of them — the orphan
|
|
389
|
+
// bucket, the source-dominant and global-dominant fallbacks, the char estimate — so call
|
|
390
|
+
// it instead of growing a second copy of the same logic.
|
|
391
|
+
const perChat = await computePerChatCosts(f);
|
|
392
|
+
const costBySession = {};
|
|
371
393
|
const topModelBySession = {};
|
|
372
|
-
for (const r of
|
|
394
|
+
for (const r of perChat) {
|
|
395
|
+
// computePerChatCosts already emits the composite key as `id`.
|
|
396
|
+
const k = r.id;
|
|
397
|
+
costBySession[k] = { total: r.totalCost, unpriced: !!r.hasUnpriced };
|
|
398
|
+
if (r.byModel[0]) topModelBySession[k] = r.byModel[0].model;
|
|
399
|
+
}
|
|
373
400
|
|
|
374
401
|
return rows.map(r => {
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
outTok = Math.round((r._aChars || 0) / 4);
|
|
380
|
-
}
|
|
381
|
-
const cost = topModel ? (calculateCost(topModel, inTok, outTok, Number(r._cacheR) || 0, Number(r._cacheW) || 0) || 0) : 0;
|
|
402
|
+
const k = `${r.id}::${r.user_id}`;
|
|
403
|
+
const topModel = topModelBySession[k] || null;
|
|
404
|
+
const summed = costBySession[k];
|
|
405
|
+
const cost = summed ? summed.total : 0;
|
|
382
406
|
return {
|
|
383
407
|
// Composite API id: rollups are keyed (session_id, user_id), so the id a
|
|
384
408
|
// client clicks must pin BOTH — otherwise opening one of two colliding
|
|
@@ -388,6 +412,9 @@ async function getChats(opts = {}) {
|
|
|
388
412
|
source: r.source, name: r.name, mode: r.mode, folder: r.folder,
|
|
389
413
|
createdAt: r.created_at, lastUpdatedAt: r.last_updated_at,
|
|
390
414
|
encrypted: false, bubbleCount: Number(r.bubble_count), topModel, cost,
|
|
415
|
+
// Surfaced, not just computed. A session containing an unpriced model reports a cost
|
|
416
|
+
// that silently omits part of itself; the caller can now say so.
|
|
417
|
+
costPartial: !!(summed && summed.unpriced),
|
|
391
418
|
};
|
|
392
419
|
});
|
|
393
420
|
}
|
|
@@ -643,7 +670,7 @@ async function getToolCalls(toolName, opts = {}) {
|
|
|
643
670
|
// ── cost estimation ─────────────────────────────────────────────────────────────
|
|
644
671
|
// Messages with model '' or '<synthetic>' are "orphans": their tokens are
|
|
645
672
|
// attributed to the session's dominant model (root cache.js treats model IS NULL
|
|
646
|
-
// the same way; '' is the
|
|
673
|
+
// the same way; '' is the memhouse null and synthetic models have no pricing).
|
|
647
674
|
const ORPHAN_TOKENS = '(m.input_tokens > 0 OR m.output_tokens > 0 OR m.cache_read_tokens > 0 OR m.cache_write_tokens > 0)';
|
|
648
675
|
|
|
649
676
|
// session_id → dominant model (most frequent across the session's messages).
|
|
@@ -821,15 +848,19 @@ async function computePerChatCosts(f) {
|
|
|
821
848
|
if (srcDom) add(srcDom, c.ti, c.to_, c.cr, c.cw);
|
|
822
849
|
}
|
|
823
850
|
let totalCost = 0; const byModel = [];
|
|
851
|
+
// A model with no price contributes nothing to totalCost, so the number a session
|
|
852
|
+
// reports silently omits part of itself. Carry the fact alongside the number.
|
|
853
|
+
let hasUnpriced = false;
|
|
824
854
|
for (const [model, tok] of Object.entries(tokenMap)) {
|
|
825
855
|
const cost = calculateCost(model, tok.input, tok.output, tok.cacheRead, tok.cacheWrite);
|
|
826
856
|
if (cost !== null) { totalCost += cost; byModel.push({ model, cost }); }
|
|
857
|
+
else hasUnpriced = true;
|
|
827
858
|
}
|
|
828
859
|
byModel.sort((a, b) => b.cost - a.cost);
|
|
829
860
|
out.push({
|
|
830
861
|
id: key, user: c.user_id, source: c.source, name: c.name, folder: c.folder,
|
|
831
862
|
last_updated_at: c.last_updated_at, created_at: c.created_at,
|
|
832
|
-
msgs: Number(c.msgs), month: c.month, totalCost, byModel,
|
|
863
|
+
msgs: Number(c.msgs), month: c.month, totalCost, byModel, hasUnpriced,
|
|
833
864
|
});
|
|
834
865
|
}
|
|
835
866
|
return out;
|
|
@@ -881,10 +912,22 @@ async function getCostAnalytics(opts = {}) {
|
|
|
881
912
|
}
|
|
882
913
|
|
|
883
914
|
// ── raw SQL / schema (SqlViewer) ────────────────────────────────────────────────
|
|
915
|
+
// Bounded at the SERVER, not in JS. `q()` buffers every row into memory, so
|
|
916
|
+
// `SELECT number FROM numbers(2000000000)` killed the whole dashboard process —
|
|
917
|
+
// "FATAL ERROR: Ineffective mark-compacts near heap limit", unauthenticated, on the bound
|
|
918
|
+
// port. Capping after the rows arrive is too late; ClickHouse has to stop sending.
|
|
919
|
+
const RAW_MAX_ROWS = 10000;
|
|
920
|
+
const RAW_MAX_BYTES = 64 * 1024 * 1024;
|
|
884
921
|
async function rawQuery(sql) {
|
|
885
|
-
const rows = await q(sql
|
|
922
|
+
const rows = await q(sql, {}, {
|
|
923
|
+
max_result_rows: RAW_MAX_ROWS,
|
|
924
|
+
max_result_bytes: RAW_MAX_BYTES,
|
|
925
|
+
// Cut the result off rather than failing a query that was legitimately large.
|
|
926
|
+
result_overflow_mode: 'break',
|
|
927
|
+
max_execution_time: 30,
|
|
928
|
+
});
|
|
886
929
|
const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
|
|
887
|
-
return { columns, rows, count: rows.length };
|
|
930
|
+
return { columns, rows, count: rows.length, truncated: rows.length >= RAW_MAX_ROWS };
|
|
888
931
|
}
|
|
889
932
|
|
|
890
933
|
async function schema() {
|
|
@@ -925,4 +968,7 @@ module.exports = {
|
|
|
925
968
|
getProjects, getDeepAnalytics, getToolCalls,
|
|
926
969
|
estimateCosts, getCostAnalytics,
|
|
927
970
|
rawQuery, schema,
|
|
971
|
+
// Used only by the server's own guard to learn which database names are real. Not
|
|
972
|
+
// reachable from any route.
|
|
973
|
+
rawQueryUnguarded: (sql) => q(sql),
|
|
928
974
|
};
|