@hasna/economy 0.2.41 → 0.2.43
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/CHANGELOG.md +8 -0
- package/dist/cli/commands/extras.d.ts.map +1 -1
- package/dist/cli/index.js +705 -87
- package/dist/db/cloud.d.ts +28 -0
- package/dist/db/cloud.d.ts.map +1 -0
- package/dist/db/database.d.ts +4 -3
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-sync-worker.d.ts +2 -0
- package/dist/db/pg-sync-worker.d.ts.map +1 -0
- package/dist/db/sync-pg.d.ts +23 -0
- package/dist/db/sync-pg.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +299 -49
- package/dist/lib/fleet-sync.d.ts +61 -0
- package/dist/lib/fleet-sync.d.ts.map +1 -0
- package/dist/mcp/index.js +66 -16
- package/dist/openapi.d.ts +3 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/otel/index.js +41 -5
- package/dist/server/index.js +456 -44
- package/dist/server/pg-sync-worker.js +31 -0
- package/dist/server/serve.d.ts +31 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage.js +2746 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +7 -4
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type PeerMergeResult } from './peer-sync.js';
|
|
2
|
+
export interface CommandResult {
|
|
3
|
+
code: number | null;
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
export type FleetCommandRunner = (command: string, args: string[], options: {
|
|
9
|
+
timeoutMs: number;
|
|
10
|
+
}) => CommandResult;
|
|
11
|
+
export interface EconomyFleetSyncOptions {
|
|
12
|
+
machines?: string[];
|
|
13
|
+
localSync?: boolean;
|
|
14
|
+
remoteSync?: boolean;
|
|
15
|
+
cacheDir?: string;
|
|
16
|
+
reportDir?: string;
|
|
17
|
+
remoteSnapshotDir?: string;
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
dryRun?: boolean;
|
|
20
|
+
runner?: FleetCommandRunner;
|
|
21
|
+
now?: Date;
|
|
22
|
+
}
|
|
23
|
+
export interface EconomyFleetSyncMachineResult {
|
|
24
|
+
machine: string;
|
|
25
|
+
status: 'merged' | 'failed' | 'dry_run';
|
|
26
|
+
remote_sync?: CommandResult;
|
|
27
|
+
remote_snapshot?: CommandResult;
|
|
28
|
+
remote_snapshot_path?: string;
|
|
29
|
+
pull?: CommandResult;
|
|
30
|
+
local_integrity?: {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
detail: string;
|
|
33
|
+
};
|
|
34
|
+
merge?: PeerMergeResult;
|
|
35
|
+
error?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface EconomyFleetSyncResult {
|
|
38
|
+
schema: 'open-economy.fleet-sync.v1';
|
|
39
|
+
generated_at: string;
|
|
40
|
+
dry_run: boolean;
|
|
41
|
+
local: {
|
|
42
|
+
status: 'synced' | 'failed' | 'skipped' | 'dry_run';
|
|
43
|
+
sync?: CommandResult;
|
|
44
|
+
db_path: string;
|
|
45
|
+
};
|
|
46
|
+
remote: EconomyFleetSyncMachineResult[];
|
|
47
|
+
summary: {
|
|
48
|
+
machines: number;
|
|
49
|
+
merged: number;
|
|
50
|
+
failed: number;
|
|
51
|
+
dry_run: number;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export declare function runEconomyFleetSync(options?: EconomyFleetSyncOptions): EconomyFleetSyncResult;
|
|
55
|
+
export declare function isSafeRemoteSnapshotPath(path: string, remoteSnapshotDir?: string): boolean;
|
|
56
|
+
export declare function verifySqliteIntegrity(path: string): {
|
|
57
|
+
ok: boolean;
|
|
58
|
+
detail: string;
|
|
59
|
+
};
|
|
60
|
+
export declare function writeFleetSyncReport(reportDir: string, result: EconomyFleetSyncResult): string;
|
|
61
|
+
//# sourceMappingURL=fleet-sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fleet-sync.d.ts","sourceRoot":"","sources":["../../src/lib/fleet-sync.ts"],"names":[],"mappings":"AAMA,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAExE,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,MAAM,kBAAkB,GAAG,CAC/B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,KAC3B,aAAa,CAAA;AAElB,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,MAAM,CAAC,EAAE,kBAAkB,CAAA;IAC3B,GAAG,CAAC,EAAE,IAAI,CAAA;CACX;AAED,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAA;IACvC,WAAW,CAAC,EAAE,aAAa,CAAA;IAC3B,eAAe,CAAC,EAAE,aAAa,CAAA;IAC/B,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,IAAI,CAAC,EAAE,aAAa,CAAA;IACpB,eAAe,CAAC,EAAE;QAChB,EAAE,EAAE,OAAO,CAAA;QACX,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;IACD,KAAK,CAAC,EAAE,eAAe,CAAA;IACvB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,4BAA4B,CAAA;IACpC,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE;QACL,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,CAAA;QACnD,IAAI,CAAC,EAAE,aAAa,CAAA;QACpB,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;IACD,MAAM,EAAE,6BAA6B,EAAE,CAAA;IACvC,OAAO,EAAE;QACP,QAAQ,EAAE,MAAM,CAAA;QAChB,MAAM,EAAE,MAAM,CAAA;QACd,MAAM,EAAE,MAAM,CAAA;QACd,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;CACF;AAKD,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,uBAA4B,GAAG,sBAAsB,CAgDjG;AAiGD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,iBAAiB,SAA8C,GAAG,OAAO,CAQ/H;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAanF;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAK9F"}
|
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 =
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
-
|
|
989
|
+
conditions.push("agent = ?");
|
|
990
|
+
params.push(agent);
|
|
991
|
+
}
|
|
992
|
+
if (since) {
|
|
993
|
+
conditions.push("started_at >= ?");
|
|
994
|
+
params.push(since);
|
|
952
995
|
}
|
|
953
|
-
|
|
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 ?
|
|
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,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAuF/C,CAAA;AACD,eAAe,WAAW,CAAA"}
|
package/dist/otel/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 =
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
}
|