@hasna/economy 0.2.40 → 0.2.42

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/dist/mcp/index.js CHANGED
@@ -573,6 +573,32 @@ function getDbPath() {
573
573
  return process.env["ECONOMY_DB"];
574
574
  return join(getDataDir(), "economy.db");
575
575
  }
576
+ function isSqliteBusyError(error) {
577
+ const candidate = error;
578
+ const code = typeof candidate.code === "string" ? candidate.code : "";
579
+ const message = typeof candidate.message === "string" ? candidate.message : String(error);
580
+ return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_RECOVERY" || /database is locked|SQLITE_BUSY/i.test(message);
581
+ }
582
+ function retryDelayMs(attempt) {
583
+ return Math.min(1000, 50 * 2 ** attempt);
584
+ }
585
+ function withSqliteBusyRetry(operation, context) {
586
+ const maxAttempts = 8;
587
+ let lastError;
588
+ for (let attempt = 0;attempt < maxAttempts; attempt++) {
589
+ try {
590
+ return operation();
591
+ } catch (error) {
592
+ if (!isSqliteBusyError(error))
593
+ throw error;
594
+ lastError = error;
595
+ if (attempt === maxAttempts - 1)
596
+ break;
597
+ Bun.sleepSync(retryDelayMs(attempt));
598
+ }
599
+ }
600
+ throw new Error(`SQLite database is locked after ${maxAttempts} attempts while ${context}. Another economy sync/merge may be recovering the database; retry shortly.`, { cause: lastError });
601
+ }
576
602
  function openDatabase(dbPath, skipSeed = false) {
577
603
  const path = dbPath ?? getDbPath();
578
604
  if (path !== ":memory:") {
@@ -580,11 +606,21 @@ function openDatabase(dbPath, skipSeed = false) {
580
606
  if (dir && !existsSync(dir))
581
607
  mkdirSync(dir, { recursive: true });
582
608
  }
583
- const db = new Database(path);
584
- db.exec("PRAGMA journal_mode = WAL");
585
- db.exec("PRAGMA busy_timeout = 5000");
586
- db.exec("PRAGMA foreign_keys = ON");
587
- initSchema(db);
609
+ const db = withSqliteBusyRetry(() => {
610
+ const opened = new Database(path);
611
+ try {
612
+ opened.exec("PRAGMA busy_timeout = 10000");
613
+ opened.exec("PRAGMA journal_mode = WAL");
614
+ opened.exec("PRAGMA foreign_keys = ON");
615
+ initSchema(opened);
616
+ return opened;
617
+ } catch (error) {
618
+ try {
619
+ opened.close();
620
+ } catch {}
621
+ throw error;
622
+ }
623
+ }, `opening ${path}`);
588
624
  if (!skipSeed) {
589
625
  Promise.resolve().then(() => (init_pricing(), exports_pricing)).then(({ ensurePricingSeeded: ensurePricingSeeded2 }) => ensurePricingSeeded2(db)).catch(() => {});
590
626
  }
@@ -946,36 +982,50 @@ function querySessions(db, filter = {}) {
946
982
  SELECT * FROM sessions ${where} ORDER BY started_at DESC LIMIT ? OFFSET ?
947
983
  `).all(...params, limit, offset);
948
984
  }
949
- function queryTopSessions(db, n = 10, agent) {
985
+ function queryTopSessions(db, n = 10, agent, since) {
986
+ const conditions = [];
987
+ const params = [];
950
988
  if (agent) {
951
- return db.prepare(`SELECT * FROM sessions WHERE agent = ? ORDER BY total_cost_usd DESC LIMIT ?`).all(agent, n);
989
+ conditions.push("agent = ?");
990
+ params.push(agent);
991
+ }
992
+ if (since) {
993
+ conditions.push("started_at >= ?");
994
+ params.push(since);
952
995
  }
953
- return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
996
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
997
+ return db.prepare(`SELECT * FROM sessions ${where} ORDER BY total_cost_usd DESC LIMIT ?`).all(...params, n);
954
998
  }
955
- function querySummary(db, period, machine, allMachines = false) {
999
+ function querySummary(db, period, machine, allMachines = false, agent) {
956
1000
  const rWhere = periodWhere(period);
957
1001
  const sWhere = sessionPeriodWhere(period);
958
- const machineClause = !allMachines && machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
1002
+ const machineClause = !allMachines && machine ? " AND machine_id = ?" : "";
1003
+ const agentClause = agent ? " AND agent = ?" : "";
1004
+ const params = [];
1005
+ if (!allMachines && machine)
1006
+ params.push(machine);
1007
+ if (agent)
1008
+ params.push(agent);
959
1009
  const r = db.prepare(`
960
1010
  SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
961
1011
  COUNT(*) as requests,
962
1012
  COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
963
- FROM requests WHERE ${rWhere}${machineClause}
964
- `).get();
1013
+ FROM requests WHERE ${rWhere}${machineClause}${agentClause}
1014
+ `).get(...params);
965
1015
  const codexTotals = db.prepare(`
966
1016
  SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
967
1017
  COALESCE(SUM(total_tokens), 0) as tokens,
968
1018
  COALESCE(SUM(request_count), 0) as requests,
969
1019
  COUNT(*) as sessions
970
1020
  FROM sessions
971
- WHERE ${sWhere}${machineClause}
1021
+ WHERE ${sWhere}${machineClause}${agentClause}
972
1022
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
973
- `).get();
1023
+ `).get(...params);
974
1024
  const requestSessionCount = db.prepare(`
975
1025
  SELECT COUNT(DISTINCT session_id) as sessions
976
1026
  FROM requests
977
- WHERE ${rWhere}${machineClause}
978
- `).get();
1027
+ WHERE ${rWhere}${machineClause}${agentClause}
1028
+ `).get(...params);
979
1029
  return {
980
1030
  total_usd: r.total_usd + codexTotals.cost_usd,
981
1031
  requests: r.requests + codexTotals.requests,
@@ -573,6 +573,32 @@ function getDbPath() {
573
573
  return process.env["ECONOMY_DB"];
574
574
  return join(getDataDir(), "economy.db");
575
575
  }
576
+ function isSqliteBusyError(error) {
577
+ const candidate = error;
578
+ const code = typeof candidate.code === "string" ? candidate.code : "";
579
+ const message = typeof candidate.message === "string" ? candidate.message : String(error);
580
+ return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_RECOVERY" || /database is locked|SQLITE_BUSY/i.test(message);
581
+ }
582
+ function retryDelayMs(attempt) {
583
+ return Math.min(1000, 50 * 2 ** attempt);
584
+ }
585
+ function withSqliteBusyRetry(operation, context) {
586
+ const maxAttempts = 8;
587
+ let lastError;
588
+ for (let attempt = 0;attempt < maxAttempts; attempt++) {
589
+ try {
590
+ return operation();
591
+ } catch (error) {
592
+ if (!isSqliteBusyError(error))
593
+ throw error;
594
+ lastError = error;
595
+ if (attempt === maxAttempts - 1)
596
+ break;
597
+ Bun.sleepSync(retryDelayMs(attempt));
598
+ }
599
+ }
600
+ throw new Error(`SQLite database is locked after ${maxAttempts} attempts while ${context}. Another economy sync/merge may be recovering the database; retry shortly.`, { cause: lastError });
601
+ }
576
602
  function openDatabase(dbPath, skipSeed = false) {
577
603
  const path = dbPath ?? getDbPath();
578
604
  if (path !== ":memory:") {
@@ -580,11 +606,21 @@ function openDatabase(dbPath, skipSeed = false) {
580
606
  if (dir && !existsSync(dir))
581
607
  mkdirSync(dir, { recursive: true });
582
608
  }
583
- const db = new Database(path);
584
- db.exec("PRAGMA journal_mode = WAL");
585
- db.exec("PRAGMA busy_timeout = 5000");
586
- db.exec("PRAGMA foreign_keys = ON");
587
- initSchema(db);
609
+ const db = withSqliteBusyRetry(() => {
610
+ const opened = new Database(path);
611
+ try {
612
+ opened.exec("PRAGMA busy_timeout = 10000");
613
+ opened.exec("PRAGMA journal_mode = WAL");
614
+ opened.exec("PRAGMA foreign_keys = ON");
615
+ initSchema(opened);
616
+ return opened;
617
+ } catch (error) {
618
+ try {
619
+ opened.close();
620
+ } catch {}
621
+ throw error;
622
+ }
623
+ }, `opening ${path}`);
588
624
  if (!skipSeed) {
589
625
  Promise.resolve().then(() => (init_pricing(), exports_pricing)).then(({ ensurePricingSeeded: ensurePricingSeeded2 }) => ensurePricingSeeded2(db)).catch(() => {});
590
626
  }
@@ -573,6 +573,32 @@ function getDbPath() {
573
573
  return process.env["ECONOMY_DB"];
574
574
  return join(getDataDir(), "economy.db");
575
575
  }
576
+ function isSqliteBusyError(error) {
577
+ const candidate = error;
578
+ const code = typeof candidate.code === "string" ? candidate.code : "";
579
+ const message = typeof candidate.message === "string" ? candidate.message : String(error);
580
+ return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_RECOVERY" || /database is locked|SQLITE_BUSY/i.test(message);
581
+ }
582
+ function retryDelayMs(attempt) {
583
+ return Math.min(1000, 50 * 2 ** attempt);
584
+ }
585
+ function withSqliteBusyRetry(operation, context) {
586
+ const maxAttempts = 8;
587
+ let lastError;
588
+ for (let attempt = 0;attempt < maxAttempts; attempt++) {
589
+ try {
590
+ return operation();
591
+ } catch (error) {
592
+ if (!isSqliteBusyError(error))
593
+ throw error;
594
+ lastError = error;
595
+ if (attempt === maxAttempts - 1)
596
+ break;
597
+ Bun.sleepSync(retryDelayMs(attempt));
598
+ }
599
+ }
600
+ throw new Error(`SQLite database is locked after ${maxAttempts} attempts while ${context}. Another economy sync/merge may be recovering the database; retry shortly.`, { cause: lastError });
601
+ }
576
602
  function openDatabase(dbPath, skipSeed = false) {
577
603
  const path = dbPath ?? getDbPath();
578
604
  if (path !== ":memory:") {
@@ -580,11 +606,21 @@ function openDatabase(dbPath, skipSeed = false) {
580
606
  if (dir && !existsSync(dir))
581
607
  mkdirSync(dir, { recursive: true });
582
608
  }
583
- const db = new Database(path);
584
- db.exec("PRAGMA journal_mode = WAL");
585
- db.exec("PRAGMA busy_timeout = 5000");
586
- db.exec("PRAGMA foreign_keys = ON");
587
- initSchema(db);
609
+ const db = withSqliteBusyRetry(() => {
610
+ const opened = new Database(path);
611
+ try {
612
+ opened.exec("PRAGMA busy_timeout = 10000");
613
+ opened.exec("PRAGMA journal_mode = WAL");
614
+ opened.exec("PRAGMA foreign_keys = ON");
615
+ initSchema(opened);
616
+ return opened;
617
+ } catch (error) {
618
+ try {
619
+ opened.close();
620
+ } catch {}
621
+ throw error;
622
+ }
623
+ }, `opening ${path}`);
588
624
  if (!skipSeed) {
589
625
  Promise.resolve().then(() => (init_pricing(), exports_pricing)).then(({ ensurePricingSeeded: ensurePricingSeeded2 }) => ensurePricingSeeded2(db)).catch(() => {});
590
626
  }
@@ -946,36 +982,50 @@ function querySessions(db, filter = {}) {
946
982
  SELECT * FROM sessions ${where} ORDER BY started_at DESC LIMIT ? OFFSET ?
947
983
  `).all(...params, limit, offset);
948
984
  }
949
- function queryTopSessions(db, n = 10, agent) {
985
+ function queryTopSessions(db, n = 10, agent, since) {
986
+ const conditions = [];
987
+ const params = [];
950
988
  if (agent) {
951
- return db.prepare(`SELECT * FROM sessions WHERE agent = ? ORDER BY total_cost_usd DESC LIMIT ?`).all(agent, n);
989
+ conditions.push("agent = ?");
990
+ params.push(agent);
952
991
  }
953
- return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
992
+ if (since) {
993
+ conditions.push("started_at >= ?");
994
+ params.push(since);
995
+ }
996
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
997
+ return db.prepare(`SELECT * FROM sessions ${where} ORDER BY total_cost_usd DESC LIMIT ?`).all(...params, n);
954
998
  }
955
- function querySummary(db, period, machine, allMachines = false) {
999
+ function querySummary(db, period, machine, allMachines = false, agent) {
956
1000
  const rWhere = periodWhere(period);
957
1001
  const sWhere = sessionPeriodWhere(period);
958
- const machineClause = !allMachines && machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
1002
+ const machineClause = !allMachines && machine ? " AND machine_id = ?" : "";
1003
+ const agentClause = agent ? " AND agent = ?" : "";
1004
+ const params = [];
1005
+ if (!allMachines && machine)
1006
+ params.push(machine);
1007
+ if (agent)
1008
+ params.push(agent);
959
1009
  const r = db.prepare(`
960
1010
  SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
961
1011
  COUNT(*) as requests,
962
1012
  COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
963
- FROM requests WHERE ${rWhere}${machineClause}
964
- `).get();
1013
+ FROM requests WHERE ${rWhere}${machineClause}${agentClause}
1014
+ `).get(...params);
965
1015
  const codexTotals = db.prepare(`
966
1016
  SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
967
1017
  COALESCE(SUM(total_tokens), 0) as tokens,
968
1018
  COALESCE(SUM(request_count), 0) as requests,
969
1019
  COUNT(*) as sessions
970
1020
  FROM sessions
971
- WHERE ${sWhere}${machineClause}
1021
+ WHERE ${sWhere}${machineClause}${agentClause}
972
1022
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
973
- `).get();
1023
+ `).get(...params);
974
1024
  const requestSessionCount = db.prepare(`
975
1025
  SELECT COUNT(DISTINCT session_id) as sessions
976
1026
  FROM requests
977
- WHERE ${rWhere}${machineClause}
978
- `).get();
1027
+ WHERE ${rWhere}${machineClause}${agentClause}
1028
+ `).get(...params);
979
1029
  return {
980
1030
  total_usd: r.total_usd + codexTotals.cost_usd,
981
1031
  requests: r.requests + codexTotals.requests,
@@ -4314,7 +4364,8 @@ function createHandler(db) {
4314
4364
  if (path === "/api/top" && method === "GET") {
4315
4365
  const n = Number(url.searchParams.get("n") ?? 10);
4316
4366
  const agent = url.searchParams.get("agent") ?? undefined;
4317
- return ok(queryTopSessions(db, n, agent));
4367
+ const since = url.searchParams.get("since") ?? undefined;
4368
+ return ok(queryTopSessions(db, n, agent, since));
4318
4369
  }
4319
4370
  if (path === "/api/models" && method === "GET") {
4320
4371
  return ok(queryModelBreakdown(db));
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAuC7D,UAAU,kBAAkB;IAC1B,EAAE,CAAC,EAAE,QAAQ,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;AAqED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAAE,YAAY,SAAwB,IACzF,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAwB7D;AAQD,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,IACV,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAqX/D;AAED,wBAAgB,WAAW,CAAC,IAAI,SAAO,EAAE,OAAO,GAAE,kBAAuB,GAAG,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAcvG"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAuC7D,UAAU,kBAAkB;IAC1B,EAAE,CAAC,EAAE,QAAQ,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;AAqED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAAE,YAAY,SAAwB,IACzF,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAwB7D;AAQD,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,IACV,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAsX/D;AAED,wBAAgB,WAAW,CAAC,IAAI,SAAO,EAAE,OAAO,GAAE,kBAAuB,GAAG,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAcvG"}