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
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
// queries.js — mem-house 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 mem-house typed schema
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// contract) but computed over the mem-house typed schema — one set of rooms per member,
|
|
5
|
+
// `sessions_<m>` / `messages_<m>` / `tool_calls_<m>` (../per-member/schema-member.sql.tpl),
|
|
6
|
+
// plus the session rollup, which is a SAVED QUERY and not an object
|
|
7
|
+
// (../per-member/rooms.js). The dashboard SPA consumes these shapes unchanged.
|
|
7
8
|
//
|
|
8
9
|
// Schema mapping notes (root cache schema → mem-house):
|
|
9
|
-
// chats →
|
|
10
|
-
// chat_stats →
|
|
11
|
-
// messages.content →
|
|
12
|
-
// tool_calls →
|
|
13
|
-
// epoch-ms columns → DateTime64(3,'UTC')
|
|
10
|
+
// chats → sessions_<m> / the rollup (id → session_id, bubble_count → total_msgs)
|
|
11
|
+
// chat_stats → rollup aggregates (token/char sums, models array)
|
|
12
|
+
// messages.content → messages_<m>.text (cache_read → cache_read_tokens, …)
|
|
13
|
+
// tool_calls → tool_calls_<m> (args_json → args, chat_id → session_id)
|
|
14
|
+
// epoch-ms columns → DateTime64(3,'UTC') (converted with toUnixTimestamp64Milli)
|
|
14
15
|
//
|
|
15
16
|
// All reads run with the `final: 1` setting so ReplacingMergeTree collapses
|
|
16
17
|
// duplicate versions at read time (see DESIGN.md data-plane rules).
|
|
17
18
|
|
|
18
19
|
const { createClient } = require('@clickhouse/client');
|
|
19
20
|
const { calculateCost, normalizeModelName } = require('../../pricing');
|
|
21
|
+
const { resolveRooms, READ_SETTINGS } = require('../per-member/rooms');
|
|
20
22
|
|
|
21
23
|
const config = {
|
|
22
24
|
url: process.env.MEMHOUSE_URL || 'http://localhost:8123',
|
|
23
25
|
username: process.env.MEMHOUSE_USER || 'memhouse_root',
|
|
24
26
|
password: process.env.MEMHOUSE_PASSWORD || '',
|
|
25
|
-
database: process.env.MEMHOUSE_DB || '
|
|
27
|
+
database: process.env.MEMHOUSE_DB || 'mem',
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
let _client = null;
|
|
@@ -35,7 +37,7 @@ function getClient() {
|
|
|
35
37
|
database: config.database,
|
|
36
38
|
request_timeout: 60000,
|
|
37
39
|
clickhouse_settings: {
|
|
38
|
-
|
|
40
|
+
...READ_SETTINGS,
|
|
39
41
|
output_format_json_quote_64bit_integers: 0,
|
|
40
42
|
},
|
|
41
43
|
});
|
|
@@ -44,8 +46,38 @@ function getClient() {
|
|
|
44
46
|
|
|
45
47
|
const TZ = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
46
48
|
|
|
49
|
+
// Room routing. Rooms are named for the member
|
|
50
|
+
// (`messages_alice`), so a read layer hardcoding `messages` either hits a permission
|
|
51
|
+
// error or, against a house provisioned for someone else, silently returns the wrong
|
|
52
|
+
// shared-layout data. Every query below names its rooms as `{{sessions_v}}` and friends,
|
|
53
|
+
// and this is the single place they are resolved.
|
|
54
|
+
//
|
|
55
|
+
// `{{name}}` cannot collide with ClickHouse's own `{name:Type}` parameter syntax: double
|
|
56
|
+
// braces, no type.
|
|
57
|
+
const ROOM_TOKEN = /\{\{([a-z_]+)\}\}/g;
|
|
58
|
+
let roomsPromise = null;
|
|
59
|
+
|
|
60
|
+
function rooms() {
|
|
61
|
+
// Resolved once per process. resolveRooms asks the server for currentUser(), so it is a
|
|
62
|
+
// round trip, and every dashboard request would otherwise pay for it.
|
|
63
|
+
if (!roomsPromise) {
|
|
64
|
+
roomsPromise = resolveRooms(getClient()).catch((e) => { roomsPromise = null; throw e; });
|
|
65
|
+
}
|
|
66
|
+
return roomsPromise;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function applyRooms(sql, r) {
|
|
70
|
+
return sql.replace(ROOM_TOKEN, (_, name) => {
|
|
71
|
+
const t = r[name];
|
|
72
|
+
if (!t) throw new Error(`query names unknown room '${name}'`);
|
|
73
|
+
return t;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
47
77
|
async function q(query, params = {}) {
|
|
48
|
-
const rs = await getClient().query({
|
|
78
|
+
const rs = await getClient().query({
|
|
79
|
+
query: applyRooms(query, await rooms()), query_params: params, format: 'JSONEachRow',
|
|
80
|
+
});
|
|
49
81
|
return rs.json();
|
|
50
82
|
}
|
|
51
83
|
async function q1(query, params = {}) { return (await q(query, params))[0]; }
|
|
@@ -101,7 +133,7 @@ function filters(opts, { editorLike = false, folderLike = false } = {}) {
|
|
|
101
133
|
function inSessions(f, col) {
|
|
102
134
|
if (!f.and) return '';
|
|
103
135
|
const alias = col.split('.')[0];
|
|
104
|
-
return ` AND (${alias}.session_id, ${alias}.user_id) IN (SELECT session_id, user_id FROM sessions_v AS c WHERE 1=1${f.and})`;
|
|
136
|
+
return ` AND (${alias}.session_id, ${alias}.user_id) IN (SELECT session_id, user_id FROM {{sessions_v}} AS c WHERE 1=1${f.and})`;
|
|
105
137
|
}
|
|
106
138
|
|
|
107
139
|
// Fold a [{name|model, cnt}] list through normalizeModelName into a freq map.
|
|
@@ -119,20 +151,20 @@ const topN = (freq, n) => Object.entries(freq).sort((a, b) => b[1] - a[1]).slice
|
|
|
119
151
|
// ── overview ────────────────────────────────────────────────────────────────────
|
|
120
152
|
async function getOverview(opts = {}) {
|
|
121
153
|
const f = filters(opts);
|
|
122
|
-
const totalChats = Number((await q1(`SELECT count() AS cnt FROM sessions_v AS c WHERE 1=1${f.and}`, f.params)).cnt);
|
|
154
|
+
const totalChats = Number((await q1(`SELECT count() AS cnt FROM {{sessions_v}} AS c WHERE 1=1${f.and}`, f.params)).cnt);
|
|
123
155
|
|
|
124
156
|
// Root parity: without a folder filter the editor breakdown is global.
|
|
125
157
|
const editors = opts.folder
|
|
126
|
-
? await q(`SELECT source, count() AS count FROM sessions_v AS c WHERE 1=1${f.and} GROUP BY source ORDER BY count DESC`, f.params)
|
|
127
|
-
: await q('SELECT source, count() AS count FROM sessions_v GROUP BY source ORDER BY count DESC');
|
|
158
|
+
? await q(`SELECT source, count() AS count FROM {{sessions_v}} AS c WHERE 1=1${f.and} GROUP BY source ORDER BY count DESC`, f.params)
|
|
159
|
+
: await q('SELECT source, count() AS count FROM {{sessions_v}} GROUP BY source ORDER BY count DESC');
|
|
128
160
|
|
|
129
|
-
const modes = await q(`SELECT mode, count() AS count FROM sessions_v AS c WHERE mode != ''${f.and} GROUP BY mode`, f.params);
|
|
161
|
+
const modes = await q(`SELECT mode, count() AS count FROM {{sessions_v}} AS c WHERE mode != ''${f.and} GROUP BY mode`, f.params);
|
|
130
162
|
const byMode = {};
|
|
131
163
|
for (const m of modes) byMode[m.mode] = Number(m.count);
|
|
132
164
|
|
|
133
165
|
const rows = await q(`
|
|
134
166
|
SELECT formatDateTime(${TS}, '%Y-%m', 'UTC') AS month, source, count() AS count
|
|
135
|
-
FROM sessions_v AS c WHERE 1=1${f.and}
|
|
167
|
+
FROM {{sessions_v}} AS c WHERE 1=1${f.and}
|
|
136
168
|
GROUP BY month, source ORDER BY month`, f.params);
|
|
137
169
|
const monthMap = {};
|
|
138
170
|
for (const r of rows) {
|
|
@@ -143,7 +175,7 @@ async function getOverview(opts = {}) {
|
|
|
143
175
|
const byMonth = Object.keys(monthMap).sort().map(m => ({ month: m, ...monthMap[m] }));
|
|
144
176
|
|
|
145
177
|
const projects = await q(`
|
|
146
|
-
SELECT folder, count() AS count FROM sessions_v AS c
|
|
178
|
+
SELECT folder, count() AS count FROM {{sessions_v}} AS c
|
|
147
179
|
WHERE folder != ''${f.and} GROUP BY folder ORDER BY count DESC LIMIT 20`, f.params);
|
|
148
180
|
const topProjects = projects.map(p => ({
|
|
149
181
|
name: p.folder.split(/[/\\]/).slice(-2).join('/'),
|
|
@@ -151,7 +183,7 @@ async function getOverview(opts = {}) {
|
|
|
151
183
|
count: Number(p.count),
|
|
152
184
|
}));
|
|
153
185
|
|
|
154
|
-
const range = await q1(`SELECT min(${MS}) AS oldest, max(${MS}) AS newest FROM sessions_v AS c WHERE 1=1${f.and}`, f.params);
|
|
186
|
+
const range = await q1(`SELECT min(${MS}) AS oldest, max(${MS}) AS newest FROM {{sessions_v}} AS c WHERE 1=1${f.and}`, f.params);
|
|
155
187
|
|
|
156
188
|
return {
|
|
157
189
|
totalChats,
|
|
@@ -171,7 +203,7 @@ async function getDailyActivity(opts = {}) {
|
|
|
171
203
|
source,
|
|
172
204
|
toHour(${TS}, {tz:String}) AS hour,
|
|
173
205
|
count() AS count
|
|
174
|
-
FROM sessions_v AS c WHERE 1=1${f.and}
|
|
206
|
+
FROM {{sessions_v}} AS c WHERE 1=1${f.and}
|
|
175
207
|
GROUP BY day, source, hour ORDER BY day`, params);
|
|
176
208
|
|
|
177
209
|
const daily = {};
|
|
@@ -193,18 +225,18 @@ async function getDashboardStats(opts = {}) {
|
|
|
193
225
|
|
|
194
226
|
const hourlyRows = await q(`
|
|
195
227
|
SELECT toHour(${TS}, {tz:String}) AS hour, count() AS count
|
|
196
|
-
FROM sessions_v AS c WHERE 1=1${f.and} GROUP BY hour ORDER BY hour`, p);
|
|
228
|
+
FROM {{sessions_v}} AS c WHERE 1=1${f.and} GROUP BY hour ORDER BY hour`, p);
|
|
197
229
|
const hourly = new Array(24).fill(0);
|
|
198
230
|
for (const r of hourlyRows) hourly[Number(r.hour)] = Number(r.count);
|
|
199
231
|
|
|
200
232
|
const weekdayRows = await q(`
|
|
201
233
|
SELECT toDayOfWeek(${TS}, 0, {tz:String}) % 7 AS dow, count() AS count
|
|
202
|
-
FROM sessions_v AS c WHERE 1=1${f.and} GROUP BY dow ORDER BY dow`, p);
|
|
234
|
+
FROM {{sessions_v}} AS c WHERE 1=1${f.and} GROUP BY dow ORDER BY dow`, p);
|
|
203
235
|
const weekdays = new Array(7).fill(0);
|
|
204
236
|
for (const r of weekdayRows) weekdays[Number(r.dow)] = Number(r.count);
|
|
205
237
|
|
|
206
238
|
const depthRows = await q(`
|
|
207
|
-
SELECT total_msgs AS msgs FROM sessions_v AS c WHERE total_msgs > 0${f.and}`, f.params);
|
|
239
|
+
SELECT total_msgs AS msgs FROM {{sessions_v}} AS c WHERE total_msgs > 0${f.and}`, f.params);
|
|
208
240
|
const depthBuckets = { '1': 0, '2-5': 0, '6-10': 0, '11-20': 0, '21-50': 0, '51-100': 0, '100+': 0 };
|
|
209
241
|
for (const r of depthRows) {
|
|
210
242
|
const m = Number(r.msgs);
|
|
@@ -222,11 +254,11 @@ async function getDashboardStats(opts = {}) {
|
|
|
222
254
|
COALESCE(sum(cache_read_tokens), 0) AS cacheRead, COALESCE(sum(cache_write_tokens), 0) AS cacheWrite,
|
|
223
255
|
COALESCE(sum(user_chars), 0) AS userChars, COALESCE(sum(assistant_chars), 0) AS assistantChars,
|
|
224
256
|
count() AS sessions
|
|
225
|
-
FROM sessions_v AS c WHERE 1=1${f.and}`, f.params);
|
|
257
|
+
FROM {{sessions_v}} AS c WHERE 1=1${f.and}`, f.params);
|
|
226
258
|
|
|
227
259
|
const streakRows = await q(`
|
|
228
260
|
SELECT DISTINCT formatDateTime(${TS}, '%Y-%m-%d', {tz:String}) AS day
|
|
229
|
-
FROM sessions_v AS c WHERE 1=1${f.and} ORDER BY day`, p);
|
|
261
|
+
FROM {{sessions_v}} AS c WHERE 1=1${f.and} ORDER BY day`, p);
|
|
230
262
|
let currentStreak = 0, longestStreak = 0, tempStreak = 1;
|
|
231
263
|
const today = new Date().toISOString().split('T')[0];
|
|
232
264
|
for (let i = 1; i < streakRows.length; i++) {
|
|
@@ -249,7 +281,7 @@ async function getDashboardStats(opts = {}) {
|
|
|
249
281
|
|
|
250
282
|
const monthEditorRows = await q(`
|
|
251
283
|
SELECT formatDateTime(${TS}, '%Y-%m', 'UTC') AS month, source, count() AS count
|
|
252
|
-
FROM sessions_v AS c WHERE 1=1${f.and} GROUP BY month, source ORDER BY month`, f.params);
|
|
284
|
+
FROM {{sessions_v}} AS c WHERE 1=1${f.and} GROUP BY month, source ORDER BY month`, f.params);
|
|
253
285
|
const monthEditors = {};
|
|
254
286
|
const allSources = new Set();
|
|
255
287
|
for (const r of monthEditorRows) {
|
|
@@ -261,17 +293,17 @@ async function getDashboardStats(opts = {}) {
|
|
|
261
293
|
const velocityRows = await q(`
|
|
262
294
|
SELECT formatDateTime(c.last_updated_at, '%Y-%m', 'UTC') AS month,
|
|
263
295
|
avg(total_msgs) AS avgMsgs, avg(input_tokens + output_tokens) AS avgTokens
|
|
264
|
-
FROM sessions_v AS c WHERE c.last_updated_at IS NOT NULL${f.and}
|
|
296
|
+
FROM {{sessions_v}} AS c WHERE c.last_updated_at IS NOT NULL${f.and}
|
|
265
297
|
GROUP BY month ORDER BY month`, f.params);
|
|
266
298
|
|
|
267
299
|
const modelRows = await q(`
|
|
268
|
-
SELECT m.model AS model, count() AS cnt FROM messages AS m
|
|
300
|
+
SELECT m.model AS model, count() AS cnt FROM {{messages}} AS m
|
|
269
301
|
WHERE m.model NOT IN ${EXCLUDED_MODELS}${inSessions(f, 'm.session_id')}
|
|
270
302
|
GROUP BY model`, f.params);
|
|
271
303
|
const topModels = topN(normalizedModelFreq(modelRows), 10);
|
|
272
304
|
|
|
273
305
|
const toolRows = await q(`
|
|
274
|
-
SELECT tool_name, count() AS cnt FROM tool_calls AS tc
|
|
306
|
+
SELECT tool_name, count() AS cnt FROM {{tool_calls}} AS tc
|
|
275
307
|
WHERE 1=1${inSessions(f, 'tc.session_id')} GROUP BY tool_name`, f.params);
|
|
276
308
|
let totalToolCalls = 0;
|
|
277
309
|
const toolFreq = {};
|
|
@@ -303,7 +335,7 @@ async function getDashboardStats(opts = {}) {
|
|
|
303
335
|
// ── chats list ──────────────────────────────────────────────────────────────────
|
|
304
336
|
async function countChats(opts = {}) {
|
|
305
337
|
const f = filters(opts, { editorLike: true, folderLike: true });
|
|
306
|
-
let sql = `SELECT count() AS cnt FROM sessions_v AS c WHERE 1=1${f.and}`;
|
|
338
|
+
let sql = `SELECT count() AS cnt FROM {{sessions_v}} AS c WHERE 1=1${f.and}`;
|
|
307
339
|
if (opts.named !== false) sql += " AND (c.name != '' OR c.total_msgs > 0)";
|
|
308
340
|
return Number((await q1(sql, f.params)).cnt);
|
|
309
341
|
}
|
|
@@ -318,7 +350,7 @@ async function getChats(opts = {}) {
|
|
|
318
350
|
c.input_tokens AS _inTok, c.output_tokens AS _outTok,
|
|
319
351
|
c.cache_read_tokens AS _cacheR, c.cache_write_tokens AS _cacheW,
|
|
320
352
|
c.user_chars AS _uChars, c.assistant_chars AS _aChars
|
|
321
|
-
FROM sessions_v AS c WHERE 1=1${f.and}`;
|
|
353
|
+
FROM {{sessions_v}} AS c WHERE 1=1${f.and}`;
|
|
322
354
|
const params = { ...f.params };
|
|
323
355
|
if (opts.named !== false) sql += " AND (c.name != '' OR c.total_msgs > 0)";
|
|
324
356
|
sql += ` ORDER BY ${MS} DESC`;
|
|
@@ -332,7 +364,7 @@ async function getChats(opts = {}) {
|
|
|
332
364
|
const ids = rows.map(r => r.id);
|
|
333
365
|
const tmRows = await q(`
|
|
334
366
|
SELECT session_id, user_id, argMax(model, cnt) AS top_model
|
|
335
|
-
FROM (SELECT session_id, user_id, model, count() AS cnt FROM messages
|
|
367
|
+
FROM (SELECT session_id, user_id, model, count() AS cnt FROM {{messages}}
|
|
336
368
|
WHERE session_id IN {ids:Array(String)} AND model NOT IN ${EXCLUDED_MODELS}
|
|
337
369
|
GROUP BY session_id, user_id, model)
|
|
338
370
|
GROUP BY session_id, user_id`, { ids });
|
|
@@ -381,7 +413,7 @@ async function getChat(id) {
|
|
|
381
413
|
c.user_chars AS user_chars, c.assistant_chars AS assistant_chars,
|
|
382
414
|
c.input_tokens AS input_tokens, c.output_tokens AS output_tokens,
|
|
383
415
|
c.cache_read_tokens AS cache_read_tokens, c.cache_write_tokens AS cache_write_tokens
|
|
384
|
-
FROM sessions_v AS c WHERE session_id = {sid:String}`;
|
|
416
|
+
FROM {{sessions_v}} AS c WHERE session_id = {sid:String}`;
|
|
385
417
|
const params = { sid: sidPart };
|
|
386
418
|
if (uidPart) { sql += ' AND user_id = {uid:String}'; params.uid = uidPart; }
|
|
387
419
|
sql += ' LIMIT 1';
|
|
@@ -398,11 +430,11 @@ async function getChat(id) {
|
|
|
398
430
|
// members share an adapter-local session_id would see their rows merged.
|
|
399
431
|
const messages = await q(`
|
|
400
432
|
SELECT role, text AS content, model, input_tokens, output_tokens
|
|
401
|
-
FROM messages WHERE session_id = {cid:String} AND user_id = {uid:String} ORDER BY seq`,
|
|
433
|
+
FROM {{messages}} WHERE session_id = {cid:String} AND user_id = {uid:String} ORDER BY seq`,
|
|
402
434
|
{ cid: chat.id, uid: chat.user_id });
|
|
403
435
|
|
|
404
436
|
const toolCalls = await q(`
|
|
405
|
-
SELECT tool_name, args FROM tool_calls WHERE session_id = {cid:String} AND user_id = {uid:String} ORDER BY idx`,
|
|
437
|
+
SELECT tool_name, args FROM {{tool_calls}} WHERE session_id = {cid:String} AND user_id = {uid:String} ORDER BY idx`,
|
|
406
438
|
{ cid: chat.id, uid: chat.user_id });
|
|
407
439
|
const toolCallDetails = toolCalls.map(tc => ({ name: tc.tool_name, args: safeParseJson(tc.args) }));
|
|
408
440
|
|
|
@@ -446,7 +478,7 @@ async function getProjects(opts = {}) {
|
|
|
446
478
|
|
|
447
479
|
const perSource = await q(`
|
|
448
480
|
SELECT folder, source, count() AS count, min(${MS}) AS first_seen, max(${MS}) AS last_seen
|
|
449
|
-
FROM sessions_v AS c WHERE folder != ''${df.and}
|
|
481
|
+
FROM {{sessions_v}} AS c WHERE folder != ''${df.and}
|
|
450
482
|
GROUP BY folder, source ORDER BY folder, count DESC`, df.params);
|
|
451
483
|
|
|
452
484
|
const map = {};
|
|
@@ -463,12 +495,12 @@ async function getProjects(opts = {}) {
|
|
|
463
495
|
sum(input_tokens) AS totalInputTokens, sum(output_tokens) AS totalOutputTokens,
|
|
464
496
|
sum(user_chars) AS totalUserChars, sum(assistant_chars) AS totalAssistantChars,
|
|
465
497
|
sum(cache_read_tokens) AS totalCacheRead, sum(cache_write_tokens) AS totalCacheWrite
|
|
466
|
-
FROM sessions_v AS c WHERE folder != ''${df.and} GROUP BY folder`, df.params);
|
|
498
|
+
FROM {{sessions_v}} AS c WHERE folder != ''${df.and} GROUP BY folder`, df.params);
|
|
467
499
|
const sumByFolder = {};
|
|
468
500
|
for (const r of sums) sumByFolder[r.folder] = r;
|
|
469
501
|
|
|
470
502
|
const modelRows = await q(`
|
|
471
|
-
SELECT m.folder AS folder, m.model AS model, count() AS cnt FROM messages AS m
|
|
503
|
+
SELECT m.folder AS folder, m.model AS model, count() AS cnt FROM {{messages}} AS m
|
|
472
504
|
WHERE m.folder != '' AND m.model NOT IN ${EXCLUDED_MODELS}${inSessions(df, 'm.session_id')}
|
|
473
505
|
GROUP BY folder, model`, df.params);
|
|
474
506
|
const modelsByFolder = {};
|
|
@@ -479,7 +511,7 @@ async function getProjects(opts = {}) {
|
|
|
479
511
|
}
|
|
480
512
|
|
|
481
513
|
const toolRows = await q(`
|
|
482
|
-
SELECT tc.folder AS folder, tc.tool_name AS tool_name, count() AS cnt FROM tool_calls AS tc
|
|
514
|
+
SELECT tc.folder AS folder, tc.tool_name AS tool_name, count() AS cnt FROM {{tool_calls}} AS tc
|
|
483
515
|
WHERE tc.folder != ''${inSessions(df, 'tc.session_id')} GROUP BY folder, tool_name`, df.params);
|
|
484
516
|
const toolsByFolder = {};
|
|
485
517
|
for (const r of toolRows) {
|
|
@@ -523,7 +555,7 @@ async function getDeepAnalytics(opts = {}) {
|
|
|
523
555
|
let sql = `
|
|
524
556
|
SELECT c.session_id AS id, c.user_id AS user_id, c.total_msgs AS msgs, c.user_chars AS uc, c.assistant_chars AS ac,
|
|
525
557
|
c.input_tokens AS ti, c.output_tokens AS to_, c.cache_read_tokens AS cr, c.cache_write_tokens AS cw
|
|
526
|
-
FROM sessions_v AS c WHERE 1=1${f.and} ORDER BY ${MS} DESC`;
|
|
558
|
+
FROM {{sessions_v}} AS c WHERE 1=1${f.and} ORDER BY ${MS} DESC`;
|
|
527
559
|
const params = { ...f.params };
|
|
528
560
|
if (opts.limit) { sql += ' LIMIT {limit:UInt64}'; params.limit = opts.limit; }
|
|
529
561
|
const rows = await q(sql, params);
|
|
@@ -548,7 +580,7 @@ async function getDeepAnalytics(opts = {}) {
|
|
|
548
580
|
if (ids.length > 0) {
|
|
549
581
|
const pairs = { ids, users };
|
|
550
582
|
const toolRows = await q(`
|
|
551
|
-
SELECT tool_name, count() AS cnt FROM tool_calls
|
|
583
|
+
SELECT tool_name, count() AS cnt FROM {{tool_calls}}
|
|
552
584
|
WHERE (session_id, user_id) IN arrayZip({ids:Array(String)}, {users:Array(String)})
|
|
553
585
|
GROUP BY tool_name`, pairs);
|
|
554
586
|
const toolFreq = {};
|
|
@@ -556,7 +588,7 @@ async function getDeepAnalytics(opts = {}) {
|
|
|
556
588
|
topTools = topN(toolFreq, 30);
|
|
557
589
|
|
|
558
590
|
const modelRows = await q(`
|
|
559
|
-
SELECT model, count() AS cnt FROM messages
|
|
591
|
+
SELECT model, count() AS cnt FROM {{messages}}
|
|
560
592
|
WHERE (session_id, user_id) IN arrayZip({ids:Array(String)}, {users:Array(String)})
|
|
561
593
|
AND model NOT IN ${EXCLUDED_MODELS} GROUP BY model`, pairs);
|
|
562
594
|
topModels = topN(normalizedModelFreq(modelRows), 20);
|
|
@@ -588,7 +620,7 @@ async function getToolCalls(toolName, opts = {}) {
|
|
|
588
620
|
SELECT tc.tool_name AS tool_name, tc.args AS args, tc.source AS source, tc.folder AS folder,
|
|
589
621
|
toUnixTimestamp64Milli(tc.ts) AS timestamp, s.name AS chat_name,
|
|
590
622
|
concat(tc.session_id, '::', tc.user_id) AS chat_id
|
|
591
|
-
FROM tool_calls AS tc INNER JOIN sessions AS s
|
|
623
|
+
FROM {{tool_calls}} AS tc INNER JOIN {{sessions}} AS s
|
|
592
624
|
ON s.session_id = tc.session_id AND s.user_id = tc.user_id
|
|
593
625
|
WHERE tc.tool_name = {name:String}`;
|
|
594
626
|
const params = { name: toolName, limit };
|
|
@@ -619,7 +651,7 @@ async function sessionDominantMap(f) {
|
|
|
619
651
|
const rows = await q(`
|
|
620
652
|
SELECT session_id, user_id, argMax(model, cnt) AS dominant
|
|
621
653
|
FROM (SELECT m.session_id AS session_id, m.user_id AS user_id, m.model AS model, count() AS cnt
|
|
622
|
-
FROM messages AS m WHERE m.model NOT IN ${EXCLUDED_MODELS}${inSessions(f, 'm.session_id')}
|
|
654
|
+
FROM {{messages}} AS m WHERE m.model NOT IN ${EXCLUDED_MODELS}${inSessions(f, 'm.session_id')}
|
|
623
655
|
GROUP BY session_id, user_id, model)
|
|
624
656
|
GROUP BY session_id, user_id`, f.params);
|
|
625
657
|
const map = {};
|
|
@@ -631,7 +663,7 @@ async function sessionDominantMap(f) {
|
|
|
631
663
|
async function sourceDominantMap(f) {
|
|
632
664
|
const rows = await q(`
|
|
633
665
|
SELECT m.source AS source, m.model AS model, count() AS cnt
|
|
634
|
-
FROM messages AS m WHERE m.model NOT IN ${EXCLUDED_MODELS}${inSessions(f, 'm.session_id')}
|
|
666
|
+
FROM {{messages}} AS m WHERE m.model NOT IN ${EXCLUDED_MODELS}${inSessions(f, 'm.session_id')}
|
|
635
667
|
GROUP BY source, model`, f.params);
|
|
636
668
|
const bySource = {}, globalFreq = {};
|
|
637
669
|
for (const r of rows) {
|
|
@@ -653,14 +685,14 @@ async function estimateCosts(opts = {}) {
|
|
|
653
685
|
const modelTokens = await q(`
|
|
654
686
|
SELECT m.model AS model, sum(m.input_tokens) AS input, sum(m.output_tokens) AS output,
|
|
655
687
|
sum(m.cache_read_tokens) AS cacheRead, sum(m.cache_write_tokens) AS cacheWrite
|
|
656
|
-
FROM messages AS m
|
|
688
|
+
FROM {{messages}} AS m
|
|
657
689
|
WHERE m.model NOT IN ${EXCLUDED_MODELS} AND ${ORPHAN_TOKENS}${inSessions(f, 'm.session_id')}
|
|
658
690
|
GROUP BY model`, f.params);
|
|
659
691
|
|
|
660
692
|
const orphanRows = await q(`
|
|
661
693
|
SELECT m.session_id AS session_id, m.user_id AS user_id, sum(m.input_tokens) AS input, sum(m.output_tokens) AS output,
|
|
662
694
|
sum(m.cache_read_tokens) AS cacheRead, sum(m.cache_write_tokens) AS cacheWrite
|
|
663
|
-
FROM messages AS m
|
|
695
|
+
FROM {{messages}} AS m
|
|
664
696
|
WHERE m.model IN ${EXCLUDED_MODELS} AND ${ORPHAN_TOKENS}${inSessions(f, 'm.session_id')}
|
|
665
697
|
GROUP BY session_id, user_id`, f.params);
|
|
666
698
|
|
|
@@ -682,7 +714,7 @@ async function estimateCosts(opts = {}) {
|
|
|
682
714
|
// Sessions that name models but report zero tokens → estimate from chars.
|
|
683
715
|
const charRows = await q(`
|
|
684
716
|
SELECT c.session_id AS session_id, c.user_id AS user_id, c.user_chars AS userChars, c.assistant_chars AS asstChars
|
|
685
|
-
FROM sessions_v AS c
|
|
717
|
+
FROM {{sessions_v}} AS c
|
|
686
718
|
WHERE notEmpty(c.models) AND c.input_tokens = 0 AND c.output_tokens = 0
|
|
687
719
|
AND (c.user_chars > 0 OR c.assistant_chars > 0)${f.and}`, f.params);
|
|
688
720
|
for (const r of charRows) {
|
|
@@ -694,7 +726,7 @@ async function estimateCosts(opts = {}) {
|
|
|
694
726
|
const unmodeledRows = await q(`
|
|
695
727
|
SELECT c.source AS source, c.input_tokens AS input, c.output_tokens AS output,
|
|
696
728
|
c.cache_read_tokens AS cacheRead, c.cache_write_tokens AS cacheWrite
|
|
697
|
-
FROM sessions_v AS c
|
|
729
|
+
FROM {{sessions_v}} AS c
|
|
698
730
|
WHERE empty(c.models) AND (c.input_tokens > 0 OR c.output_tokens > 0)${f.and}`, f.params);
|
|
699
731
|
if (unmodeledRows.length > 0) {
|
|
700
732
|
const { sourceDominant, globalDominant } = await sourceDominantMap(f);
|
|
@@ -737,7 +769,7 @@ async function computePerChatCosts(f) {
|
|
|
737
769
|
const aRows = await q(`
|
|
738
770
|
SELECT m.session_id AS session_id, m.user_id AS user_id, m.model AS model, sum(m.input_tokens) AS i, sum(m.output_tokens) AS o,
|
|
739
771
|
sum(m.cache_read_tokens) AS cr, sum(m.cache_write_tokens) AS cw
|
|
740
|
-
FROM messages AS m
|
|
772
|
+
FROM {{messages}} AS m
|
|
741
773
|
WHERE m.model NOT IN ${EXCLUDED_MODELS} AND ${ORPHAN_TOKENS}${inSessions(f, 'm.session_id')}
|
|
742
774
|
GROUP BY session_id, user_id, model`, f.params);
|
|
743
775
|
const byChatModel = {};
|
|
@@ -746,7 +778,7 @@ async function computePerChatCosts(f) {
|
|
|
746
778
|
const bRows = await q(`
|
|
747
779
|
SELECT m.session_id AS session_id, m.user_id AS user_id, sum(m.input_tokens) AS i, sum(m.output_tokens) AS o,
|
|
748
780
|
sum(m.cache_read_tokens) AS cr, sum(m.cache_write_tokens) AS cw
|
|
749
|
-
FROM messages AS m
|
|
781
|
+
FROM {{messages}} AS m
|
|
750
782
|
WHERE m.model IN ${EXCLUDED_MODELS} AND ${ORPHAN_TOKENS}${inSessions(f, 'm.session_id')}
|
|
751
783
|
GROUP BY session_id, user_id`, f.params);
|
|
752
784
|
const orphanByChat = {};
|
|
@@ -761,7 +793,7 @@ async function computePerChatCosts(f) {
|
|
|
761
793
|
c.input_tokens AS ti, c.output_tokens AS to_,
|
|
762
794
|
c.cache_read_tokens AS cr, c.cache_write_tokens AS cw,
|
|
763
795
|
formatDateTime(${TS}, '%Y-%m', 'UTC') AS month
|
|
764
|
-
FROM sessions_v AS c WHERE 1=1${f.and}`, f.params);
|
|
796
|
+
FROM {{sessions_v}} AS c WHERE 1=1${f.and}`, f.params);
|
|
765
797
|
|
|
766
798
|
const dominantMap = await sessionDominantMap(f);
|
|
767
799
|
const { sourceDominant, globalDominant } = await sourceDominantMap(f);
|
|
@@ -865,7 +897,25 @@ async function schema() {
|
|
|
865
897
|
SELECT name, type FROM system.columns
|
|
866
898
|
WHERE database = currentDatabase() AND table = {tbl:String} ORDER BY position`, { tbl: name });
|
|
867
899
|
}
|
|
868
|
-
|
|
900
|
+
// The SPA used to infer the house's flavor from `tables.includes('sessions_v')`. That
|
|
901
|
+
// stopped working the moment the rollup became a saved query: a per-member house has
|
|
902
|
+
// three rooms named for the member and no `sessions_v` at all, so the page reported an
|
|
903
|
+
// indeterminate schema and every typed example it might have offered would have failed
|
|
904
|
+
// with UNKNOWN_TABLE. Hand it the resolved names instead of making it guess.
|
|
905
|
+
const r = await rooms();
|
|
906
|
+
return {
|
|
907
|
+
tables: tbls.map((t) => t.name),
|
|
908
|
+
schema: out,
|
|
909
|
+
rooms: {
|
|
910
|
+
member: r.member,
|
|
911
|
+
sessions: r.sessions,
|
|
912
|
+
messages: r.messages,
|
|
913
|
+
tool_calls: r.tool_calls,
|
|
914
|
+
// A parenthesised SELECT, not a name. It drops
|
|
915
|
+
// into a `FROM … AS c` unchanged, which is what the examples do with it.
|
|
916
|
+
sessions_v: r.sessions_v,
|
|
917
|
+
},
|
|
918
|
+
};
|
|
869
919
|
}
|
|
870
920
|
|
|
871
921
|
module.exports = {
|