@kitsunemori96/opencode-session-metrics 0.1.0 → 0.2.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 +69 -8
- package/bin/osm.js +88 -5
- package/package.json +3 -1
- package/server/dist/index.js +830 -91
- package/web/dist/assets/index-CXuzIF_g.css +1 -0
- package/web/dist/assets/index-Dr7sP5of.js +228 -0
- package/web/dist/assets/index-Dr7sP5of.js.map +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-CxDo3sN-.css +0 -1
- package/web/dist/assets/index-a_KpAtqb.js +0 -221
- package/web/dist/assets/index-a_KpAtqb.js.map +0 -1
package/server/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import express2 from "express";
|
|
3
|
-
import { existsSync as
|
|
4
|
-
import
|
|
3
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
4
|
+
import path3 from "node:path";
|
|
5
5
|
|
|
6
6
|
// src/config.ts
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
10
11
|
var ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
11
12
|
var INSTALLED = `${path.sep}node_modules${path.sep}`;
|
|
@@ -14,8 +15,28 @@ var USER_DATA_DIR = path.join(
|
|
|
14
15
|
"opencode-session-metrics"
|
|
15
16
|
);
|
|
16
17
|
var DATA_DIR = process.env.OSM_DATA_DIR ?? (ROOT.includes(INSTALLED) ? USER_DATA_DIR : path.join(ROOT, ".data"));
|
|
17
|
-
var METRICS_DB = path.join(DATA_DIR, "metrics.db");
|
|
18
18
|
var SOURCE_DB = process.env.OSM_SOURCE_DB ?? path.join(os.homedir(), ".local", "share", "opencode", "opencode.db");
|
|
19
|
+
var QODER_HOME = process.env.OSM_QODER_HOME ?? path.join(os.homedir(), ".qoder");
|
|
20
|
+
var QODER_PROJECTS = path.join(QODER_HOME, "projects");
|
|
21
|
+
var QODER_LOGS = path.join(QODER_HOME, "logs", "sessions");
|
|
22
|
+
var SOURCE_KINDS = ["opencode", "qoder"];
|
|
23
|
+
function resolveSourceKind() {
|
|
24
|
+
const explicit = process.env.OSM_SOURCE?.trim();
|
|
25
|
+
if (explicit === "qoder" || explicit === "opencode") return explicit;
|
|
26
|
+
if (process.env.OSM_SOURCE_DB) return "opencode";
|
|
27
|
+
if (existsSync(SOURCE_DB)) return "opencode";
|
|
28
|
+
if (existsSync(QODER_PROJECTS)) return "qoder";
|
|
29
|
+
return "opencode";
|
|
30
|
+
}
|
|
31
|
+
var SOURCE_KIND = resolveSourceKind();
|
|
32
|
+
var METRICS_DB_BY_KIND = {
|
|
33
|
+
opencode: path.join(DATA_DIR, "metrics.db"),
|
|
34
|
+
qoder: path.join(DATA_DIR, "metrics-qoder.db")
|
|
35
|
+
};
|
|
36
|
+
var metricsDbPath = (kind) => METRICS_DB_BY_KIND[kind];
|
|
37
|
+
var METRICS_DB = METRICS_DB_BY_KIND.opencode;
|
|
38
|
+
var sourceProbe = (kind) => kind === "qoder" ? QODER_PROJECTS : SOURCE_DB;
|
|
39
|
+
var sourceAvailable = (kind) => existsSync(sourceProbe(kind));
|
|
19
40
|
var MODEL_CATALOG = path.join(DATA_DIR, "model-catalog.json");
|
|
20
41
|
var MODEL_OVERRIDES = path.join(DATA_DIR, "model-overrides.json");
|
|
21
42
|
var HOST = process.env.OSM_HOST ?? "127.0.0.1";
|
|
@@ -420,9 +441,9 @@ function rawHourSource(slice) {
|
|
|
420
441
|
|
|
421
442
|
// src/metrics/schema.ts
|
|
422
443
|
import { mkdirSync } from "node:fs";
|
|
423
|
-
function openMetrics() {
|
|
444
|
+
function openMetrics(file = METRICS_DB) {
|
|
424
445
|
mkdirSync(DATA_DIR, { recursive: true });
|
|
425
|
-
const db = new DatabaseSync(
|
|
446
|
+
const db = new DatabaseSync(file);
|
|
426
447
|
db.exec("PRAGMA journal_mode = WAL");
|
|
427
448
|
db.exec("PRAGMA foreign_keys = OFF");
|
|
428
449
|
ensureIndexes(db);
|
|
@@ -447,6 +468,7 @@ CREATE TABLE IF NOT EXISTS turns (
|
|
|
447
468
|
role TEXT, provider TEXT, model TEXT, variant TEXT, agent TEXT,
|
|
448
469
|
input INTEGER, output INTEGER, reasoning INTEGER,
|
|
449
470
|
cache_read INTEGER, cache_write INTEGER, cost REAL,
|
|
471
|
+
credits_micro INTEGER DEFAULT 0,
|
|
450
472
|
prompt_total INTEGER, occupancy INTEGER, context_window INTEGER,
|
|
451
473
|
fill REAL, fill_after REAL,
|
|
452
474
|
bucket INTEGER, day INTEGER, hour INTEGER,
|
|
@@ -498,7 +520,7 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
498
520
|
agent TEXT, provider TEXT, model TEXT, variant TEXT, parent_id TEXT, depth INTEGER,
|
|
499
521
|
created INTEGER, updated INTEGER,
|
|
500
522
|
t_input INTEGER, t_output INTEGER, t_reasoning INTEGER, t_cache_read INTEGER, t_cache_write INTEGER,
|
|
501
|
-
cost REAL, turns INTEGER, tool_calls INTEGER, tool_errors INTEGER,
|
|
523
|
+
cost REAL, credits_micro INTEGER DEFAULT 0, turns INTEGER, tool_calls INTEGER, tool_errors INTEGER,
|
|
502
524
|
compactions INTEGER, retries INTEGER, loops INTEGER,
|
|
503
525
|
additions INTEGER, deletions INTEGER, files_changed INTEGER,
|
|
504
526
|
peak_fill REAL, avg_fill REAL, eff_score REAL, tokens_per_line REAL,
|
|
@@ -1339,19 +1361,19 @@ function toolDetail(db, raw, name) {
|
|
|
1339
1361
|
let chars = 0;
|
|
1340
1362
|
let failures = 0;
|
|
1341
1363
|
for (const r of rows) {
|
|
1342
|
-
const
|
|
1364
|
+
const ms2 = num(r.ms);
|
|
1343
1365
|
const isError = r.status === "error";
|
|
1344
1366
|
const b = buckets[idx(num(r.started) || slice.to - 1)];
|
|
1345
1367
|
b.calls++;
|
|
1346
1368
|
if (isError) b.errors++;
|
|
1347
|
-
b.totalMs +=
|
|
1369
|
+
b.totalMs += ms2;
|
|
1348
1370
|
if (isError) failures++;
|
|
1349
1371
|
additions += num(r.adds);
|
|
1350
1372
|
deletions += num(r.dels);
|
|
1351
1373
|
files += num(r.files);
|
|
1352
1374
|
chars += num(r.chars);
|
|
1353
1375
|
for (let k = 0; k < LAT_BINS.length; k++) {
|
|
1354
|
-
if (
|
|
1376
|
+
if (ms2 >= LAT_BINS[k][1] && ms2 < LAT_BINS[k][2]) {
|
|
1355
1377
|
latency[k].count++;
|
|
1356
1378
|
break;
|
|
1357
1379
|
}
|
|
@@ -1463,10 +1485,20 @@ function facets(db) {
|
|
|
1463
1485
|
// src/api.ts
|
|
1464
1486
|
function createApp(deps) {
|
|
1465
1487
|
const app2 = express();
|
|
1466
|
-
app2.use(cors());
|
|
1488
|
+
app2.use(cors({ exposedHeaders: ["x-osm-source"] }));
|
|
1467
1489
|
app2.use(express.json());
|
|
1468
|
-
const
|
|
1469
|
-
|
|
1490
|
+
const kindOf = (req) => {
|
|
1491
|
+
const raw = String(req.get("x-osm-source") ?? req.query.src ?? "").trim().toLowerCase();
|
|
1492
|
+
return raw === "qoder" || raw === "opencode" ? raw : SOURCE_KIND;
|
|
1493
|
+
};
|
|
1494
|
+
const storeOf = (req, res) => {
|
|
1495
|
+
const kind = kindOf(req);
|
|
1496
|
+
if (deps.hasStore(kind)) return deps.db(kind);
|
|
1497
|
+
res.status(503).json({ error: "source not materialized", source: kind });
|
|
1498
|
+
return null;
|
|
1499
|
+
};
|
|
1500
|
+
const q = (db, sql, ...args) => db.prepare(sql).all(...args);
|
|
1501
|
+
const one = (db, sql, ...args) => db.prepare(sql).get(...args);
|
|
1470
1502
|
const cutoffOf = (req) => {
|
|
1471
1503
|
const raw = Number(req.query.from ?? 0);
|
|
1472
1504
|
return Number.isFinite(raw) && raw > 0 ? raw : 0;
|
|
@@ -1489,15 +1521,44 @@ function createApp(deps) {
|
|
|
1489
1521
|
gran: isGranularity(gran) ? gran : null
|
|
1490
1522
|
};
|
|
1491
1523
|
};
|
|
1492
|
-
app2.get("/api/status", (
|
|
1493
|
-
const
|
|
1494
|
-
|
|
1524
|
+
app2.get("/api/status", (req, res) => {
|
|
1525
|
+
const kind = kindOf(req);
|
|
1526
|
+
if (!deps.hasStore(kind)) {
|
|
1527
|
+
return res.json({
|
|
1528
|
+
ready: false,
|
|
1529
|
+
sourceDb: sourceProbe(kind),
|
|
1530
|
+
sourceSchema: null,
|
|
1531
|
+
sourceKind: kind,
|
|
1532
|
+
unit: null,
|
|
1533
|
+
tokenClasses: false,
|
|
1534
|
+
sourceMaxTime: 0,
|
|
1535
|
+
materializedAt: 0,
|
|
1536
|
+
mode: null,
|
|
1537
|
+
bucketMs: BUCKET_MS,
|
|
1538
|
+
extent: { from: 0, to: Date.now() },
|
|
1539
|
+
counts: { sessions: 0, turns: 0, tools: 0, parts: 0, events: 0, models: 0 }
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
const db = deps.db(kind);
|
|
1543
|
+
const count = (t) => one(db, `SELECT COUNT(*) c FROM ${t}`)?.c ?? 0;
|
|
1495
1544
|
const extent = one(
|
|
1545
|
+
db,
|
|
1496
1546
|
`SELECT MIN(created) AS from_ts, MAX(COALESCE(updated, created)) AS to_ts FROM sessions`
|
|
1497
1547
|
) ?? { from_ts: null, to_ts: null };
|
|
1548
|
+
const tokenClasses = !!one(
|
|
1549
|
+
db,
|
|
1550
|
+
`SELECT 1 AS hit FROM turns
|
|
1551
|
+
WHERE role = 'assistant'
|
|
1552
|
+
AND (output > 0 OR cache_read > 0 OR cache_write > 0 OR reasoning > 0)
|
|
1553
|
+
LIMIT 1`
|
|
1554
|
+
);
|
|
1498
1555
|
res.json({
|
|
1556
|
+
ready: true,
|
|
1499
1557
|
sourceDb: getMeta(db, "sourceDb"),
|
|
1500
1558
|
sourceSchema: getMeta(db, "sourceSchema"),
|
|
1559
|
+
sourceKind: getMeta(db, "sourceKind") ?? kind,
|
|
1560
|
+
unit: getMeta(db, "unit") ?? "usd",
|
|
1561
|
+
tokenClasses,
|
|
1501
1562
|
sourceMaxTime: getMetaNumber(db, "sourceMaxTime"),
|
|
1502
1563
|
materializedAt: getMetaNumber(db, "materializedAt"),
|
|
1503
1564
|
mode: getMeta(db, "mode"),
|
|
@@ -1513,18 +1574,57 @@ function createApp(deps) {
|
|
|
1513
1574
|
}
|
|
1514
1575
|
});
|
|
1515
1576
|
});
|
|
1577
|
+
app2.get("/api/sources", (_req, res) => {
|
|
1578
|
+
const describe = (kind) => {
|
|
1579
|
+
const available = sourceAvailable(kind);
|
|
1580
|
+
const ready2 = deps.hasStore(kind);
|
|
1581
|
+
const base = {
|
|
1582
|
+
kind,
|
|
1583
|
+
label: kind,
|
|
1584
|
+
available,
|
|
1585
|
+
ready: ready2,
|
|
1586
|
+
path: sourceProbe(kind),
|
|
1587
|
+
store: metricsDbPath(kind)
|
|
1588
|
+
};
|
|
1589
|
+
if (!ready2) return { ...base, unit: null, tokenClasses: null, sourceSchema: null, materializedAt: 0, counts: null };
|
|
1590
|
+
const db = deps.db(kind);
|
|
1591
|
+
const count = (t) => one(db, `SELECT COUNT(*) c FROM ${t}`)?.c ?? 0;
|
|
1592
|
+
return {
|
|
1593
|
+
...base,
|
|
1594
|
+
unit: getMeta(db, "unit") ?? "usd",
|
|
1595
|
+
// Same probe as `/api/status`, answered here as well so the shell knows the
|
|
1596
|
+
// store's shape before it draws: the Overview's default stack depends on it,
|
|
1597
|
+
// and the source list is resolved before the first frame.
|
|
1598
|
+
tokenClasses: !!one(
|
|
1599
|
+
db,
|
|
1600
|
+
`SELECT 1 AS hit FROM turns
|
|
1601
|
+
WHERE role = 'assistant'
|
|
1602
|
+
AND (output > 0 OR cache_read > 0 OR cache_write > 0 OR reasoning > 0)
|
|
1603
|
+
LIMIT 1`
|
|
1604
|
+
),
|
|
1605
|
+
sourceSchema: getMeta(db, "sourceSchema"),
|
|
1606
|
+
materializedAt: getMetaNumber(db, "materializedAt"),
|
|
1607
|
+
counts: { sessions: count("sessions"), turns: count("turns"), tools: count("tools") }
|
|
1608
|
+
};
|
|
1609
|
+
};
|
|
1610
|
+
res.json({ default: SOURCE_KIND, sources: SOURCE_KINDS.map(describe) });
|
|
1611
|
+
});
|
|
1516
1612
|
app2.post("/api/refresh", async (req, res) => {
|
|
1517
1613
|
try {
|
|
1518
1614
|
const full = req.body?.full === true;
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1615
|
+
const kind = kindOf(req);
|
|
1616
|
+
const report = await deps.refresh(kind, full);
|
|
1617
|
+
res.json({ ok: true, full, source: kind, report });
|
|
1521
1618
|
} catch (e) {
|
|
1522
1619
|
res.status(500).json({ ok: false, error: e.message });
|
|
1523
1620
|
}
|
|
1524
1621
|
});
|
|
1525
1622
|
app2.get("/api/top/session/:id/messages", (req, res) => {
|
|
1623
|
+
const db = storeOf(req, res);
|
|
1624
|
+
if (!db) return;
|
|
1526
1625
|
const id = req.params.id;
|
|
1527
1626
|
const session = one(
|
|
1627
|
+
db,
|
|
1528
1628
|
`SELECT id, parent_id, project, title, agent, model, provider, created, updated,
|
|
1529
1629
|
t_input, t_output, t_reasoning, t_cache_read, t_cache_write, cost, turns, first_prompt
|
|
1530
1630
|
FROM sessions WHERE id = ?`,
|
|
@@ -1532,6 +1632,7 @@ function createApp(deps) {
|
|
|
1532
1632
|
);
|
|
1533
1633
|
if (!session) return res.status(404).json({ error: "unknown session" });
|
|
1534
1634
|
const turns = q(
|
|
1635
|
+
db,
|
|
1535
1636
|
`SELECT id, seq, model, provider, agent, created, completed, input, output, reasoning,
|
|
1536
1637
|
cache_read, cache_write, cost, dur_ms, finish, tool_calls, tool_errors,
|
|
1537
1638
|
project, occupancy, context_window, fill, fill_after, additions, deletions, write_lines, has_error
|
|
@@ -1540,12 +1641,14 @@ function createApp(deps) {
|
|
|
1540
1641
|
id
|
|
1541
1642
|
);
|
|
1542
1643
|
const prompts = q(
|
|
1644
|
+
db,
|
|
1543
1645
|
`SELECT id, created, text FROM turns
|
|
1544
1646
|
WHERE session_id = ? AND role = 'user' AND length(trim(coalesce(text, ''))) > 0
|
|
1545
1647
|
ORDER BY created ASC, seq ASC`,
|
|
1546
1648
|
id
|
|
1547
1649
|
);
|
|
1548
1650
|
const parts = q(
|
|
1651
|
+
db,
|
|
1549
1652
|
`SELECT turn_id, ordinal, kind, text, name, call_id, status, title, input, output,
|
|
1550
1653
|
exit_code, truncated, started, ended, chars
|
|
1551
1654
|
FROM parts WHERE session_id = ? ORDER BY turn_id, ordinal, id`,
|
|
@@ -1621,24 +1724,30 @@ function createApp(deps) {
|
|
|
1621
1724
|
prompts: prompts.map((p) => ({ id: p.id, created: Number(p.created ?? 0), text: p.text ?? "" }))
|
|
1622
1725
|
});
|
|
1623
1726
|
});
|
|
1624
|
-
app2.get("/api/top/facets", (
|
|
1625
|
-
|
|
1727
|
+
app2.get("/api/top/facets", (req, res) => {
|
|
1728
|
+
const db = storeOf(req, res);
|
|
1729
|
+
if (!db) return;
|
|
1730
|
+
res.json(facets(db));
|
|
1626
1731
|
});
|
|
1627
1732
|
app2.get("/api/top/analytics", (req, res) => {
|
|
1628
|
-
|
|
1733
|
+
const db = storeOf(req, res);
|
|
1734
|
+
if (!db) return;
|
|
1735
|
+
res.json(dashboard(db, sliceOf(req)));
|
|
1629
1736
|
});
|
|
1630
1737
|
app2.get("/api/top/sessions", (req, res) => {
|
|
1738
|
+
const db = storeOf(req, res);
|
|
1739
|
+
if (!db) return;
|
|
1631
1740
|
const focus = str(req, "focus");
|
|
1632
1741
|
const slice = sliceOf(req);
|
|
1633
1742
|
if (focus) {
|
|
1634
|
-
const rows = sessionContext(
|
|
1743
|
+
const rows = sessionContext(db, slice, focus);
|
|
1635
1744
|
if (!rows.length) return res.status(404).json({ error: "session not in this window" });
|
|
1636
1745
|
return res.json({ rows, matching: rows.length, roots: 0, next: null, totals: null });
|
|
1637
1746
|
}
|
|
1638
1747
|
const order = str(req, "order");
|
|
1639
1748
|
const limit = Number(req.query.limit ?? MAX_PAGE_ROOTS);
|
|
1640
1749
|
res.json(
|
|
1641
|
-
sessionsPage(
|
|
1750
|
+
sessionsPage(db, slice, {
|
|
1642
1751
|
q: str(req, "q"),
|
|
1643
1752
|
order: SESSION_ORDERS.includes(order ?? "") ? order : "updated",
|
|
1644
1753
|
dir: req.query.dir === "asc" ? "asc" : "desc",
|
|
@@ -1648,17 +1757,22 @@ function createApp(deps) {
|
|
|
1648
1757
|
);
|
|
1649
1758
|
});
|
|
1650
1759
|
app2.get("/api/top/tool", (req, res) => {
|
|
1760
|
+
const db = storeOf(req, res);
|
|
1761
|
+
if (!db) return;
|
|
1651
1762
|
const name = str(req, "name");
|
|
1652
1763
|
if (!name) return res.status(400).json({ error: "name is required" });
|
|
1653
|
-
const detail = toolDetail(
|
|
1764
|
+
const detail = toolDetail(db, sliceOf(req), name);
|
|
1654
1765
|
if (!detail.totals || detail.totals.calls === 0) return res.status(404).json({ error: "no calls for that tool in this window" });
|
|
1655
1766
|
res.json(detail);
|
|
1656
1767
|
});
|
|
1657
1768
|
app2.get("/api/top/search", (req, res) => {
|
|
1769
|
+
const db = storeOf(req, res);
|
|
1770
|
+
if (!db) return;
|
|
1658
1771
|
const raw = typeof req.query.q === "string" ? req.query.q.trim() : "";
|
|
1659
1772
|
if (raw.length < 2) return res.json({ query: raw, hits: [] });
|
|
1660
1773
|
const like = `%${raw.replace(/[\\%_]/g, (c) => `\\${c}`)}%`;
|
|
1661
1774
|
const rows = q(
|
|
1775
|
+
db,
|
|
1662
1776
|
`SELECT s.id, s.title, s.project, s.updated,
|
|
1663
1777
|
(SELECT COUNT(*) FROM parts p WHERE p.session_id = s.id
|
|
1664
1778
|
AND (p.text LIKE ? ESCAPE '\\' OR p.name LIKE ? ESCAPE '\\'
|
|
@@ -1695,7 +1809,7 @@ function createApp(deps) {
|
|
|
1695
1809
|
|
|
1696
1810
|
// src/metrics/store.ts
|
|
1697
1811
|
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
1698
|
-
import { existsSync as
|
|
1812
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, renameSync, rmSync, statSync as statSync2 } from "node:fs";
|
|
1699
1813
|
|
|
1700
1814
|
// src/source.ts
|
|
1701
1815
|
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
@@ -1802,9 +1916,13 @@ function hashSig(name, input) {
|
|
|
1802
1916
|
}
|
|
1803
1917
|
var SESSION_ID_RE = /ses_[A-Za-z0-9]{4,}/;
|
|
1804
1918
|
var SourceReader = class {
|
|
1919
|
+
kind = "opencode";
|
|
1920
|
+
unit = "usd";
|
|
1921
|
+
path;
|
|
1805
1922
|
db;
|
|
1806
1923
|
schema;
|
|
1807
1924
|
constructor(dbPath) {
|
|
1925
|
+
this.path = dbPath;
|
|
1808
1926
|
this.db = new DatabaseSync2(dbPath, { readOnly: true });
|
|
1809
1927
|
const hasV2 = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='session_v2'").get();
|
|
1810
1928
|
const rows = hasV2 ? this.db.prepare("SELECT COUNT(*) AS c FROM session_v2").get().c : 0;
|
|
@@ -2237,8 +2355,596 @@ var SourceReader = class {
|
|
|
2237
2355
|
}
|
|
2238
2356
|
};
|
|
2239
2357
|
|
|
2358
|
+
// src/source-qoder.ts
|
|
2359
|
+
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2360
|
+
import path2 from "node:path";
|
|
2361
|
+
var QODER_MODEL_WINDOW = {
|
|
2362
|
+
qfmodel: 18e4,
|
|
2363
|
+
dfmodel: 1e6
|
|
2364
|
+
};
|
|
2365
|
+
var QODER_DEFAULT_WINDOW = 2e5;
|
|
2366
|
+
var COMPACTION_DROP_TOKENS = 5e4;
|
|
2367
|
+
var INTERNAL_PATH = /(^|\/)\.(qoder|opencode|osm)\//;
|
|
2368
|
+
var NOT_A_MODEL = /* @__PURE__ */ new Set(["", "<synthetic>", "unknown"]);
|
|
2369
|
+
var QODER_MODELS = {
|
|
2370
|
+
qfmodel: "Qwen3.8-Flash",
|
|
2371
|
+
dfmodel: "DeepSeek-Flash"
|
|
2372
|
+
};
|
|
2373
|
+
function modelWindow(model) {
|
|
2374
|
+
if (!model) return QODER_DEFAULT_WINDOW;
|
|
2375
|
+
const alias = QODER_MODEL_WINDOW[model] !== void 0 ? model : Object.keys(QODER_MODELS).find((a) => QODER_MODELS[a] === model);
|
|
2376
|
+
return QODER_MODEL_WINDOW[alias ?? model] ?? QODER_DEFAULT_WINDOW;
|
|
2377
|
+
}
|
|
2378
|
+
var num3 = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
2379
|
+
function ms(value) {
|
|
2380
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2381
|
+
if (typeof value === "string") {
|
|
2382
|
+
const t = Date.parse(value);
|
|
2383
|
+
return Number.isFinite(t) ? t : 0;
|
|
2384
|
+
}
|
|
2385
|
+
return 0;
|
|
2386
|
+
}
|
|
2387
|
+
var micro = (credits) => Math.round(credits * 1e6);
|
|
2388
|
+
function lineCount(body) {
|
|
2389
|
+
if (typeof body !== "string" || !body) return 0;
|
|
2390
|
+
return body.split("\n").length;
|
|
2391
|
+
}
|
|
2392
|
+
function resultText(content) {
|
|
2393
|
+
if (typeof content === "string") return content;
|
|
2394
|
+
if (Array.isArray(content)) {
|
|
2395
|
+
return content.map((c) => typeof c === "string" ? c : c?.type === "text" ? String(c.text ?? "") : "").join("\n");
|
|
2396
|
+
}
|
|
2397
|
+
return "";
|
|
2398
|
+
}
|
|
2399
|
+
function toolTitle(name, input) {
|
|
2400
|
+
if (!input || typeof input !== "object") return null;
|
|
2401
|
+
const raw = input.description ?? input.file_path ?? input.path ?? input.toolName ?? (typeof input.command === "string" ? input.command.split("\n")[0] : null) ?? (typeof input.query === "string" ? input.query : null) ?? (typeof input.subject === "string" ? input.subject : null);
|
|
2402
|
+
if (typeof raw !== "string" || !raw.trim()) return null;
|
|
2403
|
+
return raw.trim().slice(0, 160);
|
|
2404
|
+
}
|
|
2405
|
+
var QoderSource = class {
|
|
2406
|
+
kind = "qoder";
|
|
2407
|
+
unit = "credit";
|
|
2408
|
+
schema = "qoder-jsonl";
|
|
2409
|
+
path;
|
|
2410
|
+
cache = /* @__PURE__ */ new Map();
|
|
2411
|
+
evidence = /* @__PURE__ */ new Map();
|
|
2412
|
+
logDirs = null;
|
|
2413
|
+
constructor(root = QODER_PROJECTS) {
|
|
2414
|
+
this.path = root;
|
|
2415
|
+
}
|
|
2416
|
+
close() {
|
|
2417
|
+
this.cache.clear();
|
|
2418
|
+
this.evidence.clear();
|
|
2419
|
+
}
|
|
2420
|
+
/**
|
|
2421
|
+
* Session id → its transcript files.
|
|
2422
|
+
*
|
|
2423
|
+
* A session owns `<root>/<encoded-cwd>/<sessionId>.jsonl` and, when it spawned
|
|
2424
|
+
* subagents, `<root>/<encoded-cwd>/<sessionId>/subagents/agent-*.jsonl`. Those
|
|
2425
|
+
* subagent rows carry the parent id in `sessionId` and bill to it, so they are
|
|
2426
|
+
* merged into the parent session rather than counted as sessions of their own.
|
|
2427
|
+
* In the corpus at hand that is 8 extra files, 131 turns and 8.7 credits — 1.3%
|
|
2428
|
+
* of the total, which silently missing would understate every session that
|
|
2429
|
+
* ever delegated work.
|
|
2430
|
+
*/
|
|
2431
|
+
index() {
|
|
2432
|
+
const out = /* @__PURE__ */ new Map();
|
|
2433
|
+
if (!existsSync2(this.path)) return out;
|
|
2434
|
+
const add = (id, file) => {
|
|
2435
|
+
const list = out.get(id);
|
|
2436
|
+
if (list) list.push(file);
|
|
2437
|
+
else out.set(id, [file]);
|
|
2438
|
+
};
|
|
2439
|
+
for (const dir of readdirSync(this.path, { withFileTypes: true })) {
|
|
2440
|
+
if (!dir.isDirectory()) continue;
|
|
2441
|
+
const abs = path2.join(this.path, dir.name);
|
|
2442
|
+
for (const entry of readdirSync(abs, { withFileTypes: true })) {
|
|
2443
|
+
if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
2444
|
+
add(entry.name.slice(0, -".jsonl".length), path2.join(abs, entry.name));
|
|
2445
|
+
continue;
|
|
2446
|
+
}
|
|
2447
|
+
if (!entry.isDirectory()) continue;
|
|
2448
|
+
const subs = path2.join(abs, entry.name, "subagents");
|
|
2449
|
+
if (!existsSync2(subs)) continue;
|
|
2450
|
+
for (const sub of readdirSync(subs)) if (sub.endsWith(".jsonl")) add(entry.name, path2.join(subs, sub));
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
return out;
|
|
2454
|
+
}
|
|
2455
|
+
stamp(file) {
|
|
2456
|
+
try {
|
|
2457
|
+
const st = statSync(file);
|
|
2458
|
+
return { mtime: st.mtimeMs, size: st.size };
|
|
2459
|
+
} catch {
|
|
2460
|
+
return null;
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
maxMessageTime() {
|
|
2464
|
+
let max = 0;
|
|
2465
|
+
for (const files of this.index().values())
|
|
2466
|
+
for (const f of files) max = Math.max(max, this.stamp(f)?.mtime ?? 0);
|
|
2467
|
+
return max;
|
|
2468
|
+
}
|
|
2469
|
+
changedSessions(since) {
|
|
2470
|
+
const out = /* @__PURE__ */ new Set();
|
|
2471
|
+
if (since <= 0) return out;
|
|
2472
|
+
for (const [id, files] of this.index()) {
|
|
2473
|
+
for (const f of files) {
|
|
2474
|
+
const st = this.stamp(f);
|
|
2475
|
+
if (st && st.mtime > since) {
|
|
2476
|
+
out.add(id);
|
|
2477
|
+
break;
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
return out;
|
|
2482
|
+
}
|
|
2483
|
+
all(sessionIds) {
|
|
2484
|
+
const out = [];
|
|
2485
|
+
for (const [id, files] of this.index()) {
|
|
2486
|
+
if (sessionIds && !sessionIds.has(id)) continue;
|
|
2487
|
+
const p = this.load(id, files);
|
|
2488
|
+
if (p) out.push(p);
|
|
2489
|
+
}
|
|
2490
|
+
return out;
|
|
2491
|
+
}
|
|
2492
|
+
sessions() {
|
|
2493
|
+
return this.all().map((p) => this.sessionOf(p));
|
|
2494
|
+
}
|
|
2495
|
+
turns(sessionIds) {
|
|
2496
|
+
return this.all(sessionIds).flatMap((p) => p.turns);
|
|
2497
|
+
}
|
|
2498
|
+
events(sessionIds) {
|
|
2499
|
+
return this.all(sessionIds).flatMap((p) => p.compactions);
|
|
2500
|
+
}
|
|
2501
|
+
/** Files touched by edit/write calls; `build` uses these to infer the project lane. */
|
|
2502
|
+
patchFileCounts(sessionIds) {
|
|
2503
|
+
const out = /* @__PURE__ */ new Map();
|
|
2504
|
+
for (const p of this.all(sessionIds)) {
|
|
2505
|
+
if (p.paths.length) out.set(p.id, { files: p.paths.length, paths: p.paths });
|
|
2506
|
+
}
|
|
2507
|
+
return out;
|
|
2508
|
+
}
|
|
2509
|
+
sessionOf(p) {
|
|
2510
|
+
const model = this.displayModel(p.id, p.models);
|
|
2511
|
+
return {
|
|
2512
|
+
id: p.id,
|
|
2513
|
+
parentId: null,
|
|
2514
|
+
project: projectName(p.directory),
|
|
2515
|
+
directory: p.directory,
|
|
2516
|
+
title: p.title,
|
|
2517
|
+
agent: null,
|
|
2518
|
+
provider: "qoder",
|
|
2519
|
+
model,
|
|
2520
|
+
variant: null,
|
|
2521
|
+
created: p.created,
|
|
2522
|
+
updated: p.updated,
|
|
2523
|
+
cost: p.credits,
|
|
2524
|
+
tInput: p.tInput,
|
|
2525
|
+
tOutput: 0,
|
|
2526
|
+
tReasoning: 0,
|
|
2527
|
+
tCacheRead: 0,
|
|
2528
|
+
tCacheWrite: 0,
|
|
2529
|
+
additions: p.additions,
|
|
2530
|
+
deletions: p.deletions,
|
|
2531
|
+
files: p.files,
|
|
2532
|
+
creditsMicro: p.creditsMicro
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
/**
|
|
2536
|
+
* Session-level model naming. The alias and the shipped name never appear in the
|
|
2537
|
+
* same telemetry event, so this is inference:
|
|
2538
|
+
* - one alias + one real name → the real name;
|
|
2539
|
+
* - one alias + no evidence → the global map;
|
|
2540
|
+
* - several aliases (a mixed session) → all aliases, never a guess.
|
|
2541
|
+
*/
|
|
2542
|
+
displayModel(id, models) {
|
|
2543
|
+
const aliases = [...new Set(models.filter((m) => !!m && !NOT_A_MODEL.has(m)))];
|
|
2544
|
+
if (aliases.length === 0) return "unknown";
|
|
2545
|
+
if (aliases.length > 1) return aliases.sort().join(" + ");
|
|
2546
|
+
const alias = aliases[0];
|
|
2547
|
+
const real = this.realModels(id);
|
|
2548
|
+
if (real.size === 1) return [...real][0];
|
|
2549
|
+
if (real.size === 0 && QODER_MODELS[alias]) return QODER_MODELS[alias];
|
|
2550
|
+
return alias;
|
|
2551
|
+
}
|
|
2552
|
+
/** Shipped names reported by hooks for this session (`hook_input.model`). */
|
|
2553
|
+
realModels(sessionId) {
|
|
2554
|
+
const hit = this.evidence.get(sessionId);
|
|
2555
|
+
if (hit) return hit;
|
|
2556
|
+
const out = /* @__PURE__ */ new Set();
|
|
2557
|
+
for (const enc of this.telemetryDirs()) {
|
|
2558
|
+
const dir = path2.join(QODER_LOGS, enc, sessionId, "segments");
|
|
2559
|
+
if (!existsSync2(dir)) continue;
|
|
2560
|
+
for (const f of readdirSync(dir)) {
|
|
2561
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
2562
|
+
let body = "";
|
|
2563
|
+
try {
|
|
2564
|
+
body = readFileSync(path2.join(dir, f), "utf8");
|
|
2565
|
+
} catch {
|
|
2566
|
+
continue;
|
|
2567
|
+
}
|
|
2568
|
+
for (const line of body.split("\n")) {
|
|
2569
|
+
if (!line.includes("hook_input")) continue;
|
|
2570
|
+
try {
|
|
2571
|
+
const e = JSON.parse(line);
|
|
2572
|
+
const name = e?.data?.hook_input?.model;
|
|
2573
|
+
if (typeof name === "string" && name && name !== "unknown") out.add(name);
|
|
2574
|
+
} catch {
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
this.evidence.set(sessionId, out);
|
|
2580
|
+
return out;
|
|
2581
|
+
}
|
|
2582
|
+
telemetryDirs() {
|
|
2583
|
+
if (this.logDirs) return this.logDirs;
|
|
2584
|
+
this.logDirs = existsSync2(QODER_LOGS) ? readdirSync(QODER_LOGS, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name) : [];
|
|
2585
|
+
return this.logDirs;
|
|
2586
|
+
}
|
|
2587
|
+
/** Parse every file of a session and merge them; cached by the files' stamps. */
|
|
2588
|
+
load(id, files) {
|
|
2589
|
+
const stamped = files.map((f) => ({ f, st: this.stamp(f) })).filter((x) => !!x.st).sort((a, b) => a.f.localeCompare(b.f));
|
|
2590
|
+
if (!stamped.length) return null;
|
|
2591
|
+
const sig = stamped.map((x) => `${x.f}:${x.st.mtime}:${x.st.size}`).join("|");
|
|
2592
|
+
const hit = this.cache.get(id);
|
|
2593
|
+
if (hit && hit.sig === sig) return hit.parsed;
|
|
2594
|
+
const parts = [];
|
|
2595
|
+
for (const { f, st } of stamped) {
|
|
2596
|
+
let body = "";
|
|
2597
|
+
try {
|
|
2598
|
+
body = readFileSync(f, "utf8");
|
|
2599
|
+
} catch {
|
|
2600
|
+
continue;
|
|
2601
|
+
}
|
|
2602
|
+
parts.push(this.parse(id, f, st.mtime, body));
|
|
2603
|
+
}
|
|
2604
|
+
if (!parts.length) return null;
|
|
2605
|
+
const parsed = parts.length === 1 ? parts[0] : this.merge(id, parts);
|
|
2606
|
+
const name = this.displayModel(id, parsed.models);
|
|
2607
|
+
if (name !== "unknown" && !name.includes(" + ")) {
|
|
2608
|
+
for (const t of parsed.turns) if (t.role === "assistant" && t.model) t.model = name;
|
|
2609
|
+
}
|
|
2610
|
+
if (this.cache.size > 64) this.cache.clear();
|
|
2611
|
+
this.cache.set(id, { sig, parsed });
|
|
2612
|
+
return parsed;
|
|
2613
|
+
}
|
|
2614
|
+
/** A parent transcript plus its subagent transcripts, as one session. */
|
|
2615
|
+
merge(id, parts) {
|
|
2616
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2617
|
+
const turns = parts.flatMap((p) => p.turns).sort((a, b) => a.created - b.created).filter((t) => seen.has(t.id) ? false : (seen.add(t.id), true)).map((t, i) => ({ ...t, seq: i }));
|
|
2618
|
+
const paths = new Set(parts.flatMap((p) => p.paths));
|
|
2619
|
+
const sum = (pick) => parts.reduce((a, p) => a + pick(p), 0);
|
|
2620
|
+
const created = parts.map((p) => p.created).filter((t) => t > 0);
|
|
2621
|
+
return {
|
|
2622
|
+
id,
|
|
2623
|
+
file: parts[0].file,
|
|
2624
|
+
mtime: Math.max(...parts.map((p) => p.mtime)),
|
|
2625
|
+
directory: parts.find((p) => p.directory !== "/")?.directory ?? "/",
|
|
2626
|
+
title: parts.find((p) => p.title)?.title ?? null,
|
|
2627
|
+
created: created.length ? Math.min(...created) : 0,
|
|
2628
|
+
updated: Math.max(...parts.map((p) => p.updated)),
|
|
2629
|
+
turns,
|
|
2630
|
+
compactions: parts.flatMap((p) => p.compactions).sort((a, b) => a.at - b.at),
|
|
2631
|
+
paths: [...paths],
|
|
2632
|
+
models: parts.flatMap((p) => p.models),
|
|
2633
|
+
credits: sum((p) => p.credits),
|
|
2634
|
+
creditsMicro: sum((p) => p.creditsMicro),
|
|
2635
|
+
tInput: sum((p) => p.tInput),
|
|
2636
|
+
additions: sum((p) => p.additions),
|
|
2637
|
+
deletions: sum((p) => p.deletions),
|
|
2638
|
+
files: sum((p) => p.files)
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
parse(id, file, mtime, body) {
|
|
2642
|
+
const order = [];
|
|
2643
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2644
|
+
const prompts = [];
|
|
2645
|
+
const results = /* @__PURE__ */ new Map();
|
|
2646
|
+
const paths = /* @__PURE__ */ new Set();
|
|
2647
|
+
let title = null;
|
|
2648
|
+
let lastPrompt = null;
|
|
2649
|
+
let directory = "";
|
|
2650
|
+
let explicit = 0;
|
|
2651
|
+
let created = 0;
|
|
2652
|
+
let updated = 0;
|
|
2653
|
+
const touch = (ts) => {
|
|
2654
|
+
if (!ts) return;
|
|
2655
|
+
if (!created || ts < created) created = ts;
|
|
2656
|
+
if (ts > updated) updated = ts;
|
|
2657
|
+
};
|
|
2658
|
+
for (const line of body.split("\n")) {
|
|
2659
|
+
if (!line.trim()) continue;
|
|
2660
|
+
let e;
|
|
2661
|
+
try {
|
|
2662
|
+
e = JSON.parse(line);
|
|
2663
|
+
} catch {
|
|
2664
|
+
continue;
|
|
2665
|
+
}
|
|
2666
|
+
const type = e?.type;
|
|
2667
|
+
if (typeof e?.cwd === "string" && e.cwd) directory = e.cwd;
|
|
2668
|
+
touch(ms(e?.timestamp));
|
|
2669
|
+
if (type === "assistant") {
|
|
2670
|
+
const m = e.message ?? {};
|
|
2671
|
+
const mid = typeof m.id === "string" && m.id ? m.id : e.uuid;
|
|
2672
|
+
if (!mid) continue;
|
|
2673
|
+
let entry = byId.get(mid);
|
|
2674
|
+
if (!entry) {
|
|
2675
|
+
entry = {
|
|
2676
|
+
id: mid,
|
|
2677
|
+
ts: ms(e.timestamp),
|
|
2678
|
+
model: m.model ?? null,
|
|
2679
|
+
stop: m.stop_reason ?? null,
|
|
2680
|
+
window: explicit,
|
|
2681
|
+
blocks: [],
|
|
2682
|
+
usage: null
|
|
2683
|
+
};
|
|
2684
|
+
byId.set(mid, entry);
|
|
2685
|
+
order.push(entry);
|
|
2686
|
+
}
|
|
2687
|
+
if (m.stop_reason) entry.stop = m.stop_reason;
|
|
2688
|
+
if (m.model) entry.model = m.model;
|
|
2689
|
+
if (Array.isArray(m.content)) {
|
|
2690
|
+
for (const b of m.content) if (b && typeof b === "object" && b.type) entry.blocks.push(b);
|
|
2691
|
+
}
|
|
2692
|
+
if (m.usage && typeof m.usage === "object") {
|
|
2693
|
+
entry.usage = {
|
|
2694
|
+
credits: num3(m.usage.credits),
|
|
2695
|
+
original: num3(m.usage.original_credits ?? m.usage.credits),
|
|
2696
|
+
ratio: typeof m.usage.context_usage_ratio === "number" ? m.usage.context_usage_ratio : null,
|
|
2697
|
+
request: typeof m.usage.request_id === "string" ? m.usage.request_id : null
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
continue;
|
|
2701
|
+
}
|
|
2702
|
+
if (type === "user") {
|
|
2703
|
+
const m = e.message ?? {};
|
|
2704
|
+
const content = m.content;
|
|
2705
|
+
const ts = ms(e.timestamp);
|
|
2706
|
+
if (typeof content === "string") {
|
|
2707
|
+
if (content.trim()) prompts.push({ id: String(e.uuid ?? `u${prompts.length}`), ts, text: content });
|
|
2708
|
+
continue;
|
|
2709
|
+
}
|
|
2710
|
+
if (!Array.isArray(content)) continue;
|
|
2711
|
+
let text = "";
|
|
2712
|
+
for (const b of content) {
|
|
2713
|
+
if (!b || typeof b !== "object") continue;
|
|
2714
|
+
if (b.type === "text" && typeof b.text === "string") text += b.text;
|
|
2715
|
+
if (b.type === "tool_result" && typeof b.tool_use_id === "string") {
|
|
2716
|
+
results.set(b.tool_use_id, {
|
|
2717
|
+
text: resultText(b.content),
|
|
2718
|
+
error: b.is_error === true,
|
|
2719
|
+
ts
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
if (text.trim()) prompts.push({ id: String(e.uuid ?? `u${prompts.length}`), ts, text });
|
|
2724
|
+
continue;
|
|
2725
|
+
}
|
|
2726
|
+
if (type === "runtime-config") {
|
|
2727
|
+
explicit = typeof e.contextWindow === "number" && e.contextWindow > 0 ? e.contextWindow : 0;
|
|
2728
|
+
continue;
|
|
2729
|
+
}
|
|
2730
|
+
if (type === "ai-title" && typeof e.aiTitle === "string") {
|
|
2731
|
+
title = e.aiTitle;
|
|
2732
|
+
continue;
|
|
2733
|
+
}
|
|
2734
|
+
if (type === "workspace-directories" && Array.isArray(e.directories) && e.directories[0]) {
|
|
2735
|
+
directory = String(e.directories[0]);
|
|
2736
|
+
continue;
|
|
2737
|
+
}
|
|
2738
|
+
if (type === "last-prompt" && typeof e.lastPrompt === "string") lastPrompt = e.lastPrompt;
|
|
2739
|
+
}
|
|
2740
|
+
const items = [];
|
|
2741
|
+
const models = [];
|
|
2742
|
+
let credits = 0;
|
|
2743
|
+
let tInput = 0;
|
|
2744
|
+
let additions = 0;
|
|
2745
|
+
let deletions = 0;
|
|
2746
|
+
let files = 0;
|
|
2747
|
+
const ratios = [];
|
|
2748
|
+
for (const entry of order) {
|
|
2749
|
+
const usage = entry.usage;
|
|
2750
|
+
const ratio = usage?.ratio ?? null;
|
|
2751
|
+
const contextWindow = entry.window || modelWindow(entry.model);
|
|
2752
|
+
const input = ratio == null ? 0 : Math.round(ratio * contextWindow);
|
|
2753
|
+
const toolCalls = [];
|
|
2754
|
+
const parts = [];
|
|
2755
|
+
const seenCalls = /* @__PURE__ */ new Set();
|
|
2756
|
+
let text = "";
|
|
2757
|
+
let add = 0;
|
|
2758
|
+
let del = 0;
|
|
2759
|
+
let fileCount = 0;
|
|
2760
|
+
let writeLines = 0;
|
|
2761
|
+
for (const b of entry.blocks) {
|
|
2762
|
+
if (b.type === "text") {
|
|
2763
|
+
const value = String(b.text ?? "");
|
|
2764
|
+
text += value;
|
|
2765
|
+
if (value.trim()) parts.push(bodyPart("text", value));
|
|
2766
|
+
continue;
|
|
2767
|
+
}
|
|
2768
|
+
if (b.type === "thinking") {
|
|
2769
|
+
const value = String(b.thinking ?? b.text ?? "");
|
|
2770
|
+
if (value.trim()) parts.push(bodyPart("reasoning", value));
|
|
2771
|
+
continue;
|
|
2772
|
+
}
|
|
2773
|
+
if (b.type !== "tool_use") continue;
|
|
2774
|
+
const callId = String(b.id ?? `${entry.id}:${toolCalls.length}`);
|
|
2775
|
+
if (seenCalls.has(callId)) continue;
|
|
2776
|
+
seenCalls.add(callId);
|
|
2777
|
+
const name = String(b.name ?? "unknown");
|
|
2778
|
+
const inputJson = b.input ?? {};
|
|
2779
|
+
const result = results.get(callId);
|
|
2780
|
+
const status = result ? result.error ? "error" : "completed" : "pending";
|
|
2781
|
+
const filePath = typeof inputJson?.file_path === "string" ? inputJson.file_path : null;
|
|
2782
|
+
const touched = pathsOf(inputJson, null);
|
|
2783
|
+
for (const p of touched) if (!INTERNAL_PATH.test(p)) paths.add(p);
|
|
2784
|
+
const toolAdd = name === "Edit" ? lineCount(inputJson?.new_string) : 0;
|
|
2785
|
+
const toolDel = name === "Edit" ? lineCount(inputJson?.old_string) : 0;
|
|
2786
|
+
const write = name === "Write" ? lineCount(inputJson?.content) : 0;
|
|
2787
|
+
add += toolAdd;
|
|
2788
|
+
del += toolDel;
|
|
2789
|
+
writeLines += write;
|
|
2790
|
+
if (filePath) fileCount += 1;
|
|
2791
|
+
toolCalls.push({
|
|
2792
|
+
turnId: entry.id,
|
|
2793
|
+
sessionId: id,
|
|
2794
|
+
callId,
|
|
2795
|
+
name,
|
|
2796
|
+
status,
|
|
2797
|
+
started: entry.ts,
|
|
2798
|
+
ended: result?.ts ?? null,
|
|
2799
|
+
title: toolTitle(name, inputJson),
|
|
2800
|
+
outputChars: result?.text.length ?? 0,
|
|
2801
|
+
additions: toolAdd,
|
|
2802
|
+
deletions: toolDel,
|
|
2803
|
+
files: filePath ? 1 : 0,
|
|
2804
|
+
writeLines: write,
|
|
2805
|
+
error: result?.error ? result.text.slice(0, 500) || "tool error" : null,
|
|
2806
|
+
sig: hashSig(name, inputJson),
|
|
2807
|
+
inputPreview: inputJson ? JSON.stringify(inputJson).slice(0, 400) : null,
|
|
2808
|
+
targetSession: null,
|
|
2809
|
+
paths: touched.filter((p) => !INTERNAL_PATH.test(p))
|
|
2810
|
+
});
|
|
2811
|
+
parts.push({
|
|
2812
|
+
kind: "tool",
|
|
2813
|
+
text: "",
|
|
2814
|
+
name,
|
|
2815
|
+
callId,
|
|
2816
|
+
status,
|
|
2817
|
+
title: toolTitle(name, inputJson),
|
|
2818
|
+
input: (inputJson ? JSON.stringify(inputJson) : "").slice(0, PART_CAP.input),
|
|
2819
|
+
output: (result?.text ?? "").slice(0, PART_CAP.output),
|
|
2820
|
+
exitCode: null,
|
|
2821
|
+
truncated: (result?.text.length ?? 0) > PART_CAP.output,
|
|
2822
|
+
started: entry.ts,
|
|
2823
|
+
ended: result?.ts ?? null,
|
|
2824
|
+
chars: result?.text.length ?? 0
|
|
2825
|
+
});
|
|
2826
|
+
}
|
|
2827
|
+
const turnCredits = usage?.credits ?? 0;
|
|
2828
|
+
const synthetic = entry.model === "<synthetic>";
|
|
2829
|
+
const turn = {
|
|
2830
|
+
id: entry.id,
|
|
2831
|
+
sessionId: id,
|
|
2832
|
+
seq: 0,
|
|
2833
|
+
created: entry.ts,
|
|
2834
|
+
completed: entry.ts,
|
|
2835
|
+
role: synthetic ? "system" : "assistant",
|
|
2836
|
+
provider: "qoder",
|
|
2837
|
+
model: entry.model && !NOT_A_MODEL.has(entry.model) ? entry.model : null,
|
|
2838
|
+
variant: null,
|
|
2839
|
+
agent: null,
|
|
2840
|
+
input,
|
|
2841
|
+
output: 0,
|
|
2842
|
+
reasoning: 0,
|
|
2843
|
+
cacheRead: 0,
|
|
2844
|
+
cacheWrite: 0,
|
|
2845
|
+
cost: turnCredits,
|
|
2846
|
+
finish: entry.stop,
|
|
2847
|
+
text,
|
|
2848
|
+
tools: toolCalls,
|
|
2849
|
+
parts,
|
|
2850
|
+
prompt: null,
|
|
2851
|
+
creditsMicro: micro(turnCredits),
|
|
2852
|
+
contextWindow,
|
|
2853
|
+
_add: add,
|
|
2854
|
+
_del: del,
|
|
2855
|
+
_files: fileCount,
|
|
2856
|
+
_write: writeLines
|
|
2857
|
+
};
|
|
2858
|
+
items.push({ ts: entry.ts, turn });
|
|
2859
|
+
if (entry.model) models.push(entry.model);
|
|
2860
|
+
credits += turnCredits;
|
|
2861
|
+
tInput += input;
|
|
2862
|
+
additions += add;
|
|
2863
|
+
deletions += del;
|
|
2864
|
+
files += fileCount;
|
|
2865
|
+
if (ratio != null) ratios.push({ ts: entry.ts, tokens: input });
|
|
2866
|
+
}
|
|
2867
|
+
for (const p of prompts) {
|
|
2868
|
+
if (!p.ts) continue;
|
|
2869
|
+
items.push({
|
|
2870
|
+
ts: p.ts,
|
|
2871
|
+
turn: {
|
|
2872
|
+
id: p.id,
|
|
2873
|
+
sessionId: id,
|
|
2874
|
+
seq: 0,
|
|
2875
|
+
created: p.ts,
|
|
2876
|
+
completed: p.ts,
|
|
2877
|
+
role: "user",
|
|
2878
|
+
provider: null,
|
|
2879
|
+
model: null,
|
|
2880
|
+
variant: null,
|
|
2881
|
+
agent: null,
|
|
2882
|
+
input: 0,
|
|
2883
|
+
output: 0,
|
|
2884
|
+
reasoning: 0,
|
|
2885
|
+
cacheRead: 0,
|
|
2886
|
+
cacheWrite: 0,
|
|
2887
|
+
cost: 0,
|
|
2888
|
+
finish: null,
|
|
2889
|
+
text: p.text,
|
|
2890
|
+
tools: [],
|
|
2891
|
+
parts: [],
|
|
2892
|
+
prompt: p.text.slice(0, 400),
|
|
2893
|
+
creditsMicro: 0
|
|
2894
|
+
}
|
|
2895
|
+
});
|
|
2896
|
+
}
|
|
2897
|
+
items.sort((a, b) => a.ts - b.ts);
|
|
2898
|
+
const turns = items.map((it, i) => ({ ...it.turn, seq: i }));
|
|
2899
|
+
const compactions = [];
|
|
2900
|
+
let prev = null;
|
|
2901
|
+
for (const r of ratios.sort((a, b) => a.ts - b.ts)) {
|
|
2902
|
+
if (prev && r.tokens < prev.tokens - COMPACTION_DROP_TOKENS) {
|
|
2903
|
+
compactions.push({
|
|
2904
|
+
sessionId: id,
|
|
2905
|
+
turnId: null,
|
|
2906
|
+
type: "compaction",
|
|
2907
|
+
// Sit between the two turns: `build` derives before/after from the turns
|
|
2908
|
+
// around the event, and the drop itself happened in that gap.
|
|
2909
|
+
at: Math.round((prev.ts + r.ts) / 2),
|
|
2910
|
+
reason: "auto",
|
|
2911
|
+
detail: "context reset (context drop)",
|
|
2912
|
+
tokensBefore: prev.tokens,
|
|
2913
|
+
tokensAfter: r.tokens,
|
|
2914
|
+
model: null
|
|
2915
|
+
});
|
|
2916
|
+
}
|
|
2917
|
+
prev = r;
|
|
2918
|
+
}
|
|
2919
|
+
return {
|
|
2920
|
+
id,
|
|
2921
|
+
file,
|
|
2922
|
+
mtime,
|
|
2923
|
+
directory: directory || "/",
|
|
2924
|
+
title: title ?? (lastPrompt ? lastPrompt.split("\n")[0].slice(0, 120) : null),
|
|
2925
|
+
created,
|
|
2926
|
+
updated,
|
|
2927
|
+
turns,
|
|
2928
|
+
compactions,
|
|
2929
|
+
paths: [...paths],
|
|
2930
|
+
models,
|
|
2931
|
+
credits,
|
|
2932
|
+
creditsMicro: micro(credits),
|
|
2933
|
+
tInput,
|
|
2934
|
+
additions,
|
|
2935
|
+
deletions,
|
|
2936
|
+
files
|
|
2937
|
+
};
|
|
2938
|
+
}
|
|
2939
|
+
};
|
|
2940
|
+
|
|
2941
|
+
// src/source-select.ts
|
|
2942
|
+
function openSource(kind = SOURCE_KIND) {
|
|
2943
|
+
return kind === "qoder" ? new QoderSource(QODER_PROJECTS) : new SourceReader(SOURCE_DB);
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2240
2946
|
// src/metrics/catalog.ts
|
|
2241
|
-
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2947
|
+
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3 } from "node:fs";
|
|
2242
2948
|
var FALLBACK_CONTEXT = 128e3;
|
|
2243
2949
|
var CATALOG_TTL_MS = 7 * 864e5;
|
|
2244
2950
|
var catalogKey = (provider, model) => `${provider ?? "unknown"}/${model ?? "unknown"}`;
|
|
@@ -2269,9 +2975,9 @@ function normalize(modelID, providerID, m) {
|
|
|
2269
2975
|
};
|
|
2270
2976
|
}
|
|
2271
2977
|
function readCache() {
|
|
2272
|
-
if (!
|
|
2978
|
+
if (!existsSync3(MODEL_CATALOG)) return { fetchedAt: 0, models: {} };
|
|
2273
2979
|
try {
|
|
2274
|
-
const parsed = JSON.parse(
|
|
2980
|
+
const parsed = JSON.parse(readFileSync2(MODEL_CATALOG, "utf8"));
|
|
2275
2981
|
if (parsed && typeof parsed === "object" && "models" in parsed && parsed.models) {
|
|
2276
2982
|
return { fetchedAt: Number(parsed.fetchedAt ?? 0), models: parsed.models };
|
|
2277
2983
|
}
|
|
@@ -2281,9 +2987,9 @@ function readCache() {
|
|
|
2281
2987
|
}
|
|
2282
2988
|
}
|
|
2283
2989
|
function readOverrides() {
|
|
2284
|
-
if (!
|
|
2990
|
+
if (!existsSync3(MODEL_OVERRIDES)) return {};
|
|
2285
2991
|
try {
|
|
2286
|
-
return JSON.parse(
|
|
2992
|
+
return JSON.parse(readFileSync2(MODEL_OVERRIDES, "utf8"));
|
|
2287
2993
|
} catch {
|
|
2288
2994
|
return {};
|
|
2289
2995
|
}
|
|
@@ -2393,13 +3099,13 @@ function inferProject(paths, fallback, accounts) {
|
|
|
2393
3099
|
for (const [label, v] of votes) if (v > bestV) bestV = v, best = label;
|
|
2394
3100
|
return best;
|
|
2395
3101
|
}
|
|
2396
|
-
function accountOf(
|
|
2397
|
-
return (
|
|
3102
|
+
function accountOf(path4) {
|
|
3103
|
+
return (path4 ?? "").replace(/^\/mnt\/[a-zA-Z]\//, "/").match(/^\/(?:home|Users)\/([^/]+)$/)?.[1] ?? null;
|
|
2398
3104
|
}
|
|
2399
3105
|
async function build(db, opts = {}) {
|
|
2400
3106
|
const t0 = Date.now();
|
|
2401
3107
|
const mode = opts.mode ?? "full";
|
|
2402
|
-
const src =
|
|
3108
|
+
const src = openSource(opts.kind);
|
|
2403
3109
|
try {
|
|
2404
3110
|
initSchema(db, { drop: mode === "full" });
|
|
2405
3111
|
ensureRollups(db);
|
|
@@ -2454,7 +3160,7 @@ async function build(db, opts = {}) {
|
|
|
2454
3160
|
const m = info(t.provider, t.model);
|
|
2455
3161
|
const promptTotal = t.input + t.cacheRead + t.cacheWrite;
|
|
2456
3162
|
const occupancy = promptTotal + t.output + t.reasoning;
|
|
2457
|
-
const contextWindow = m?.context ?? 128e3;
|
|
3163
|
+
const contextWindow = t.contextWindow ?? m?.context ?? 128e3;
|
|
2458
3164
|
const costComputed = m ? (t.input * m.costInput + t.output * m.costOutput + t.reasoning * m.costOutput + t.cacheRead * m.costCacheRead + t.cacheWrite * m.costCacheWrite) / 1e6 : 0;
|
|
2459
3165
|
return {
|
|
2460
3166
|
...t,
|
|
@@ -2529,6 +3235,7 @@ async function build(db, opts = {}) {
|
|
|
2529
3235
|
"cache_read",
|
|
2530
3236
|
"cache_write",
|
|
2531
3237
|
"cost",
|
|
3238
|
+
"credits_micro",
|
|
2532
3239
|
"prompt_total",
|
|
2533
3240
|
"occupancy",
|
|
2534
3241
|
"context_window",
|
|
@@ -2577,6 +3284,7 @@ async function build(db, opts = {}) {
|
|
|
2577
3284
|
t.cacheRead,
|
|
2578
3285
|
t.cacheWrite,
|
|
2579
3286
|
t.costComputed,
|
|
3287
|
+
t.creditsMicro ?? 0,
|
|
2580
3288
|
t.promptTotal,
|
|
2581
3289
|
t.occupancy,
|
|
2582
3290
|
t.contextWindow,
|
|
@@ -2687,6 +3395,7 @@ async function build(db, opts = {}) {
|
|
|
2687
3395
|
"t_cache_read",
|
|
2688
3396
|
"t_cache_write",
|
|
2689
3397
|
"cost",
|
|
3398
|
+
"credits_micro",
|
|
2690
3399
|
"turns",
|
|
2691
3400
|
"tool_calls",
|
|
2692
3401
|
"tool_errors",
|
|
@@ -2743,6 +3452,7 @@ async function build(db, opts = {}) {
|
|
|
2743
3452
|
const compactions = sessionEvents.filter((e) => e.type === "compaction").length;
|
|
2744
3453
|
const retries = sessionEvents.filter((e) => e.type === "retry").length;
|
|
2745
3454
|
const loops = sessionEvents.filter((e) => e.type === "loop").length;
|
|
3455
|
+
const creditsMicro = s.creditsMicro || ts.reduce((a, t) => a + (t.creditsMicro ?? 0), 0);
|
|
2746
3456
|
insSession.run(
|
|
2747
3457
|
s.id,
|
|
2748
3458
|
s.project,
|
|
@@ -2762,6 +3472,7 @@ async function build(db, opts = {}) {
|
|
|
2762
3472
|
tCacheRead,
|
|
2763
3473
|
tCacheWrite,
|
|
2764
3474
|
Math.max(s.cost, ts.reduce((a, t) => a + t.costComputed, 0)),
|
|
3475
|
+
creditsMicro,
|
|
2765
3476
|
ts.length,
|
|
2766
3477
|
toolCalls,
|
|
2767
3478
|
toolErrors,
|
|
@@ -2793,8 +3504,10 @@ async function build(db, opts = {}) {
|
|
|
2793
3504
|
upModel.run(key, m.provider, m.label, m.context, m.costInput, m.costOutput, m.costCacheRead, m.costCacheWrite, m.source);
|
|
2794
3505
|
}
|
|
2795
3506
|
refreshRollups(db, idList, [...affectedDays], mode === "full");
|
|
2796
|
-
setMeta(db, "sourceDb",
|
|
3507
|
+
setMeta(db, "sourceDb", src.path);
|
|
2797
3508
|
setMeta(db, "sourceSchema", src.schema);
|
|
3509
|
+
setMeta(db, "sourceKind", src.kind);
|
|
3510
|
+
setMeta(db, "unit", src.unit);
|
|
2798
3511
|
setMeta(db, "sourceMaxTime", String(src.maxMessageTime()));
|
|
2799
3512
|
setMeta(db, "materializedAt", String(Date.now()));
|
|
2800
3513
|
setMeta(db, "bucketMs", String(BUCKET_MS));
|
|
@@ -3014,42 +3727,54 @@ function recomputeHeat(db, days) {
|
|
|
3014
3727
|
}
|
|
3015
3728
|
|
|
3016
3729
|
// src/metrics/store.ts
|
|
3017
|
-
var
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3730
|
+
var slots = /* @__PURE__ */ new Map();
|
|
3731
|
+
function slotOf(kind) {
|
|
3732
|
+
let slot = slots.get(kind);
|
|
3733
|
+
if (!slot) {
|
|
3734
|
+
slot = { handle: null, stat: null };
|
|
3735
|
+
slots.set(kind, slot);
|
|
3736
|
+
}
|
|
3737
|
+
return slot;
|
|
3738
|
+
}
|
|
3739
|
+
function fileIdentity(kind) {
|
|
3021
3740
|
try {
|
|
3022
|
-
const st =
|
|
3741
|
+
const st = statSync2(metricsDbPath(kind));
|
|
3023
3742
|
return `${st.dev}:${st.ino}`;
|
|
3024
3743
|
} catch {
|
|
3025
3744
|
return "missing";
|
|
3026
3745
|
}
|
|
3027
3746
|
}
|
|
3028
|
-
|
|
3747
|
+
var hasStore = (kind = SOURCE_KIND) => existsSync4(metricsDbPath(kind));
|
|
3748
|
+
function current(kind = SOURCE_KIND) {
|
|
3029
3749
|
mkdirSync2(DATA_DIR, { recursive: true });
|
|
3030
|
-
const
|
|
3031
|
-
|
|
3032
|
-
|
|
3750
|
+
const slot = slotOf(kind);
|
|
3751
|
+
const id = fileIdentity(kind);
|
|
3752
|
+
if (slot.handle && slot.stat !== id) {
|
|
3753
|
+
closeReader(kind);
|
|
3033
3754
|
}
|
|
3034
|
-
if (!handle) {
|
|
3035
|
-
handle = openMetrics();
|
|
3036
|
-
|
|
3755
|
+
if (!slot.handle) {
|
|
3756
|
+
slot.handle = openMetrics(metricsDbPath(kind));
|
|
3757
|
+
slot.stat = id;
|
|
3037
3758
|
}
|
|
3038
|
-
return handle;
|
|
3759
|
+
return slot.handle;
|
|
3039
3760
|
}
|
|
3040
|
-
function closeReader() {
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3761
|
+
function closeReader(kind) {
|
|
3762
|
+
const kinds = kind ? [kind] : [...slots.keys()];
|
|
3763
|
+
for (const k of kinds) {
|
|
3764
|
+
const slot = slotOf(k);
|
|
3765
|
+
try {
|
|
3766
|
+
slot.handle?.close();
|
|
3767
|
+
} catch {
|
|
3768
|
+
}
|
|
3769
|
+
slot.handle = null;
|
|
3770
|
+
slot.stat = null;
|
|
3044
3771
|
}
|
|
3045
|
-
handle = null;
|
|
3046
|
-
handleStat = null;
|
|
3047
3772
|
}
|
|
3048
|
-
function sourceChanged() {
|
|
3049
|
-
if (!
|
|
3050
|
-
const prev = getMetaNumber(current(), "sourceMaxTime");
|
|
3773
|
+
function sourceChanged(kind = SOURCE_KIND) {
|
|
3774
|
+
if (!hasStore(kind)) return true;
|
|
3775
|
+
const prev = getMetaNumber(current(kind), "sourceMaxTime");
|
|
3051
3776
|
if (prev <= 0) return true;
|
|
3052
|
-
const src =
|
|
3777
|
+
const src = openSource(kind);
|
|
3053
3778
|
try {
|
|
3054
3779
|
return src.maxMessageTime() > prev;
|
|
3055
3780
|
} catch {
|
|
@@ -3058,37 +3783,40 @@ function sourceChanged() {
|
|
|
3058
3783
|
src.close();
|
|
3059
3784
|
}
|
|
3060
3785
|
}
|
|
3061
|
-
async function rebuild(opts = {}) {
|
|
3786
|
+
async function rebuild(kind = SOURCE_KIND, opts = {}) {
|
|
3062
3787
|
mkdirSync2(DATA_DIR, { recursive: true });
|
|
3063
|
-
if (opts.full === true || !
|
|
3064
|
-
return incrementalRebuild();
|
|
3788
|
+
if (opts.full === true || !hasStore(kind) || schemaStale(kind)) return fullRebuild(kind);
|
|
3789
|
+
return incrementalRebuild(kind);
|
|
3065
3790
|
}
|
|
3066
|
-
function schemaStale() {
|
|
3791
|
+
function schemaStale(kind) {
|
|
3067
3792
|
try {
|
|
3068
|
-
return getMeta(current(), "schemaSig") !== SCHEMA_SIG;
|
|
3793
|
+
return getMeta(current(kind), "schemaSig") !== SCHEMA_SIG;
|
|
3069
3794
|
} catch {
|
|
3070
3795
|
return true;
|
|
3071
3796
|
}
|
|
3072
3797
|
}
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
const
|
|
3798
|
+
var nextFile = (kind) => `${metricsDbPath(kind)}.next`;
|
|
3799
|
+
async function fullRebuild(kind) {
|
|
3800
|
+
const next = nextFile(kind);
|
|
3801
|
+
for (const suffix of ["", "-wal", "-shm"]) if (existsSync4(next + suffix)) rmSync(next + suffix, { force: true });
|
|
3802
|
+
const writer = new DatabaseSync3(next);
|
|
3076
3803
|
writer.exec("PRAGMA journal_mode = WAL");
|
|
3077
3804
|
let report;
|
|
3078
3805
|
try {
|
|
3079
|
-
report = await build(writer, { mode: "full" });
|
|
3806
|
+
report = await build(writer, { mode: "full", kind });
|
|
3080
3807
|
} finally {
|
|
3081
3808
|
writer.close();
|
|
3082
3809
|
}
|
|
3083
|
-
closeReader();
|
|
3084
|
-
|
|
3085
|
-
|
|
3810
|
+
closeReader(kind);
|
|
3811
|
+
const target = metricsDbPath(kind);
|
|
3812
|
+
for (const suffix of ["", "-wal", "-shm"]) rmSync(target + suffix, { force: true });
|
|
3813
|
+
renameSync(next, target);
|
|
3086
3814
|
return report;
|
|
3087
3815
|
}
|
|
3088
|
-
async function incrementalRebuild() {
|
|
3089
|
-
const db = current();
|
|
3816
|
+
async function incrementalRebuild(kind) {
|
|
3817
|
+
const db = current(kind);
|
|
3090
3818
|
const prevWatermark = getMetaNumber(db, "sourceMaxTime");
|
|
3091
|
-
const src =
|
|
3819
|
+
const src = openSource(kind);
|
|
3092
3820
|
let changed;
|
|
3093
3821
|
try {
|
|
3094
3822
|
changed = prevWatermark > 0 ? src.changedSessions(prevWatermark) : /* @__PURE__ */ new Set();
|
|
@@ -3111,43 +3839,54 @@ async function incrementalRebuild() {
|
|
|
3111
3839
|
ms: 0
|
|
3112
3840
|
};
|
|
3113
3841
|
}
|
|
3114
|
-
return build(db, { mode: "incremental", sessionIds: changed });
|
|
3842
|
+
return build(db, { mode: "incremental", sessionIds: changed, kind });
|
|
3115
3843
|
}
|
|
3116
3844
|
|
|
3117
3845
|
// src/index.ts
|
|
3118
3846
|
var REFRESH_MS = 12e4;
|
|
3119
3847
|
var queue = Promise.resolve();
|
|
3120
|
-
function materialize(opts = {}) {
|
|
3121
|
-
const run = queue.then(() => doMaterialize(opts), () => doMaterialize(opts));
|
|
3848
|
+
function materialize(kind, opts = {}) {
|
|
3849
|
+
const run = queue.then(() => doMaterialize(kind, opts), () => doMaterialize(kind, opts));
|
|
3122
3850
|
queue = run.catch(() => void 0);
|
|
3123
3851
|
return run;
|
|
3124
3852
|
}
|
|
3125
|
-
async function doMaterialize({ force = false, full = false }) {
|
|
3126
|
-
if (!
|
|
3127
|
-
console.log(`[osm] ${full ? "full rebuild requested" : "no
|
|
3128
|
-
const r2 = await rebuild({ full: true });
|
|
3129
|
-
console.log(
|
|
3853
|
+
async function doMaterialize(kind, { force = false, full = false }) {
|
|
3854
|
+
if (!hasStore(kind) || full) {
|
|
3855
|
+
console.log(`[osm] ${kind}: ${full ? "full rebuild requested" : "no store yet"} \u2014 materializing\u2026`);
|
|
3856
|
+
const r2 = await rebuild(kind, { full: true });
|
|
3857
|
+
console.log(
|
|
3858
|
+
`[osm] ${kind}: built ${r2.sessions} sessions / ${r2.turns} turns / ${r2.tools} tools / ${r2.parts} parts in ${r2.ms}ms`
|
|
3859
|
+
);
|
|
3130
3860
|
return r2;
|
|
3131
3861
|
}
|
|
3132
|
-
if (!force && !sourceChanged()) return void 0;
|
|
3133
|
-
const r = await rebuild();
|
|
3862
|
+
if (!force && !sourceChanged(kind)) return void 0;
|
|
3863
|
+
const r = await rebuild(kind);
|
|
3134
3864
|
console.log(
|
|
3135
|
-
`[osm] ${r.mode} sync
|
|
3865
|
+
`[osm] ${kind}: ${r.mode} sync ${r.sessions} sessions / ${r.turns} turns / ${r.parts} parts / ${r.events} events in ${r.ms}ms`
|
|
3136
3866
|
);
|
|
3137
3867
|
return r;
|
|
3138
3868
|
}
|
|
3139
|
-
var app = createApp({
|
|
3140
|
-
|
|
3141
|
-
|
|
3869
|
+
var app = createApp({
|
|
3870
|
+
db: (kind) => current(kind),
|
|
3871
|
+
hasStore: (kind) => hasStore(kind),
|
|
3872
|
+
refresh: (kind, full) => materialize(kind, { force: true, full })
|
|
3873
|
+
});
|
|
3874
|
+
var webDist = path3.join(ROOT, "web", "dist");
|
|
3875
|
+
if (existsSync5(webDist)) {
|
|
3142
3876
|
app.use(express2.static(webDist));
|
|
3143
3877
|
app.get("*splat", (req, res) => {
|
|
3144
|
-
if (
|
|
3145
|
-
res.sendFile(
|
|
3878
|
+
if (path3.extname(req.path)) return res.status(404).type("text/plain").send("not found");
|
|
3879
|
+
res.sendFile(path3.join(webDist, "index.html"));
|
|
3146
3880
|
});
|
|
3147
3881
|
console.log(`[osm] serving built web from ${webDist}`);
|
|
3148
3882
|
}
|
|
3149
3883
|
app.listen(PORT, HOST, async () => {
|
|
3150
|
-
console.log(`[osm] api on http://${HOST}:${PORT}`);
|
|
3151
|
-
await materialize();
|
|
3152
|
-
setInterval(() =>
|
|
3884
|
+
console.log(`[osm] api on http://${HOST}:${PORT} (default source: ${SOURCE_KIND})`);
|
|
3885
|
+
await materialize(SOURCE_KIND);
|
|
3886
|
+
setInterval(() => {
|
|
3887
|
+
for (const kind of SOURCE_KINDS) {
|
|
3888
|
+
if (!hasStore(kind)) continue;
|
|
3889
|
+
void materialize(kind).catch((e) => console.error(`[osm] ${kind} refresh failed:`, e.message));
|
|
3890
|
+
}
|
|
3891
|
+
}, REFRESH_MS);
|
|
3153
3892
|
});
|