@dexcost/sdk 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/LICENSE +21 -0
- package/README.md +210 -0
- package/dist/adapters/_netbytes.d.ts +31 -0
- package/dist/adapters/_netbytes.d.ts.map +1 -0
- package/dist/adapters/_netbytes.js +154 -0
- package/dist/adapters/_netbytes.js.map +1 -0
- package/dist/adapters/aws-lambda.d.ts +41 -0
- package/dist/adapters/aws-lambda.d.ts.map +1 -0
- package/dist/adapters/aws-lambda.js +65 -0
- package/dist/adapters/aws-lambda.js.map +1 -0
- package/dist/adapters/browser.d.ts +52 -0
- package/dist/adapters/browser.d.ts.map +1 -0
- package/dist/adapters/browser.js +127 -0
- package/dist/adapters/browser.js.map +1 -0
- package/dist/adapters/compute-wrap.d.ts +33 -0
- package/dist/adapters/compute-wrap.d.ts.map +1 -0
- package/dist/adapters/compute-wrap.js +188 -0
- package/dist/adapters/compute-wrap.js.map +1 -0
- package/dist/adapters/data/aws_lambda_pricing.json +61 -0
- package/dist/adapters/gpu-wrap.d.ts +31 -0
- package/dist/adapters/gpu-wrap.d.ts.map +1 -0
- package/dist/adapters/gpu-wrap.js +147 -0
- package/dist/adapters/gpu-wrap.js.map +1 -0
- package/dist/adapters/http.d.ts +58 -0
- package/dist/adapters/http.d.ts.map +1 -0
- package/dist/adapters/http.js +769 -0
- package/dist/adapters/http.js.map +1 -0
- package/dist/adapters/index.d.ts +11 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +12 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/network-accountant.d.ts +63 -0
- package/dist/adapters/network-accountant.d.ts.map +1 -0
- package/dist/adapters/network-accountant.js +153 -0
- package/dist/adapters/network-accountant.js.map +1 -0
- package/dist/cli/index.d.ts +13 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +225 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/scanner.d.ts +39 -0
- package/dist/cli/scanner.d.ts.map +1 -0
- package/dist/cli/scanner.js +480 -0
- package/dist/cli/scanner.js.map +1 -0
- package/dist/clients.d.ts +54 -0
- package/dist/clients.d.ts.map +1 -0
- package/dist/clients.js +240 -0
- package/dist/clients.js.map +1 -0
- package/dist/cloud-detect.d.ts +96 -0
- package/dist/cloud-detect.d.ts.map +1 -0
- package/dist/cloud-detect.js +545 -0
- package/dist/cloud-detect.js.map +1 -0
- package/dist/core/auto-task.d.ts +20 -0
- package/dist/core/auto-task.d.ts.map +1 -0
- package/dist/core/auto-task.js +34 -0
- package/dist/core/auto-task.js.map +1 -0
- package/dist/core/cgroup-reader.d.ts +45 -0
- package/dist/core/cgroup-reader.d.ts.map +1 -0
- package/dist/core/cgroup-reader.js +124 -0
- package/dist/core/cgroup-reader.js.map +1 -0
- package/dist/core/cgroup-walker.d.ts +60 -0
- package/dist/core/cgroup-walker.d.ts.map +1 -0
- package/dist/core/cgroup-walker.js +166 -0
- package/dist/core/cgroup-walker.js.map +1 -0
- package/dist/core/compute-accountant.d.ts +51 -0
- package/dist/core/compute-accountant.d.ts.map +1 -0
- package/dist/core/compute-accountant.js +179 -0
- package/dist/core/compute-accountant.js.map +1 -0
- package/dist/core/compute-runtime.d.ts +42 -0
- package/dist/core/compute-runtime.d.ts.map +1 -0
- package/dist/core/compute-runtime.js +80 -0
- package/dist/core/compute-runtime.js.map +1 -0
- package/dist/core/config.d.ts +44 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +66 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/context.d.ts +76 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +91 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/fargate-metadata.d.ts +27 -0
- package/dist/core/fargate-metadata.d.ts.map +1 -0
- package/dist/core/fargate-metadata.js +102 -0
- package/dist/core/fargate-metadata.js.map +1 -0
- package/dist/core/gpu-accountant.d.ts +104 -0
- package/dist/core/gpu-accountant.d.ts.map +1 -0
- package/dist/core/gpu-accountant.js +383 -0
- package/dist/core/gpu-accountant.js.map +1 -0
- package/dist/core/gpu-runtime.d.ts +58 -0
- package/dist/core/gpu-runtime.d.ts.map +1 -0
- package/dist/core/gpu-runtime.js +131 -0
- package/dist/core/gpu-runtime.js.map +1 -0
- package/dist/core/heuristics.d.ts +74 -0
- package/dist/core/heuristics.d.ts.map +1 -0
- package/dist/core/heuristics.js +182 -0
- package/dist/core/heuristics.js.map +1 -0
- package/dist/core/models.d.ts +149 -0
- package/dist/core/models.d.ts.map +1 -0
- package/dist/core/models.js +226 -0
- package/dist/core/models.js.map +1 -0
- package/dist/core/nvml-reader.d.ts +114 -0
- package/dist/core/nvml-reader.d.ts.map +1 -0
- package/dist/core/nvml-reader.js +323 -0
- package/dist/core/nvml-reader.js.map +1 -0
- package/dist/core/session.d.ts +48 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +123 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/tracker.d.ts +364 -0
- package/dist/core/tracker.d.ts.map +1 -0
- package/dist/core/tracker.js +1073 -0
- package/dist/core/tracker.js.map +1 -0
- package/dist/data/compute_prices.json +180 -0
- package/dist/data/egress_prices.json +418 -0
- package/dist/data/gpu_prices.json +412 -0
- package/dist/data/service_prices.json +2595 -0
- package/dist/dev-console.d.ts +12 -0
- package/dist/dev-console.d.ts.map +1 -0
- package/dist/dev-console.js +60 -0
- package/dist/dev-console.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/instruments/anthropic.d.ts +26 -0
- package/dist/instruments/anthropic.d.ts.map +1 -0
- package/dist/instruments/anthropic.js +242 -0
- package/dist/instruments/anthropic.js.map +1 -0
- package/dist/instruments/bedrock.d.ts +29 -0
- package/dist/instruments/bedrock.d.ts.map +1 -0
- package/dist/instruments/bedrock.js +215 -0
- package/dist/instruments/bedrock.js.map +1 -0
- package/dist/instruments/cohere.d.ts +29 -0
- package/dist/instruments/cohere.d.ts.map +1 -0
- package/dist/instruments/cohere.js +237 -0
- package/dist/instruments/cohere.js.map +1 -0
- package/dist/instruments/gemini.d.ts +30 -0
- package/dist/instruments/gemini.d.ts.map +1 -0
- package/dist/instruments/gemini.js +247 -0
- package/dist/instruments/gemini.js.map +1 -0
- package/dist/instruments/index.d.ts +35 -0
- package/dist/instruments/index.d.ts.map +1 -0
- package/dist/instruments/index.js +54 -0
- package/dist/instruments/index.js.map +1 -0
- package/dist/instruments/mcp.d.ts +24 -0
- package/dist/instruments/mcp.d.ts.map +1 -0
- package/dist/instruments/mcp.js +459 -0
- package/dist/instruments/mcp.js.map +1 -0
- package/dist/instruments/openai.d.ts +26 -0
- package/dist/instruments/openai.d.ts.map +1 -0
- package/dist/instruments/openai.js +221 -0
- package/dist/instruments/openai.js.map +1 -0
- package/dist/instruments/vercel-ai.d.ts +28 -0
- package/dist/instruments/vercel-ai.d.ts.map +1 -0
- package/dist/instruments/vercel-ai.js +192 -0
- package/dist/instruments/vercel-ai.js.map +1 -0
- package/dist/integrations/langchain.d.ts +65 -0
- package/dist/integrations/langchain.d.ts.map +1 -0
- package/dist/integrations/langchain.js +165 -0
- package/dist/integrations/langchain.js.map +1 -0
- package/dist/middleware/express.d.ts +55 -0
- package/dist/middleware/express.d.ts.map +1 -0
- package/dist/middleware/express.js +101 -0
- package/dist/middleware/express.js.map +1 -0
- package/dist/middleware/index.d.ts +6 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +5 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/pricing/compute-pricing.d.ts +57 -0
- package/dist/pricing/compute-pricing.d.ts.map +1 -0
- package/dist/pricing/compute-pricing.js +627 -0
- package/dist/pricing/compute-pricing.js.map +1 -0
- package/dist/pricing/cost_map.json +37665 -0
- package/dist/pricing/egress-pricing.d.ts +55 -0
- package/dist/pricing/egress-pricing.d.ts.map +1 -0
- package/dist/pricing/egress-pricing.js +226 -0
- package/dist/pricing/egress-pricing.js.map +1 -0
- package/dist/pricing/engine.d.ts +24 -0
- package/dist/pricing/engine.d.ts.map +1 -0
- package/dist/pricing/engine.js +148 -0
- package/dist/pricing/engine.js.map +1 -0
- package/dist/pricing/gpu-pricing.d.ts +63 -0
- package/dist/pricing/gpu-pricing.d.ts.map +1 -0
- package/dist/pricing/gpu-pricing.js +484 -0
- package/dist/pricing/gpu-pricing.js.map +1 -0
- package/dist/pricing/rates.d.ts +17 -0
- package/dist/pricing/rates.d.ts.map +1 -0
- package/dist/pricing/rates.js +102 -0
- package/dist/pricing/rates.js.map +1 -0
- package/dist/pricing/service-catalog.d.ts +87 -0
- package/dist/pricing/service-catalog.d.ts.map +1 -0
- package/dist/pricing/service-catalog.js +406 -0
- package/dist/pricing/service-catalog.js.map +1 -0
- package/dist/schema/dexcost-event.v1.json +111 -0
- package/dist/schema/dexcost-task.v1.json +160 -0
- package/dist/schema/validate.d.ts +15 -0
- package/dist/schema/validate.d.ts.map +1 -0
- package/dist/schema/validate.js +87 -0
- package/dist/schema/validate.js.map +1 -0
- package/dist/security/redaction.d.ts +55 -0
- package/dist/security/redaction.d.ts.map +1 -0
- package/dist/security/redaction.js +144 -0
- package/dist/security/redaction.js.map +1 -0
- package/dist/transport/buffer.d.ts +117 -0
- package/dist/transport/buffer.d.ts.map +1 -0
- package/dist/transport/buffer.js +759 -0
- package/dist/transport/buffer.js.map +1 -0
- package/dist/transport/pusher.d.ts +89 -0
- package/dist/transport/pusher.d.ts.map +1 -0
- package/dist/transport/pusher.js +323 -0
- package/dist/transport/pusher.js.map +1 -0
- package/package.json +93 -0
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite-backed event buffer for dexcost.
|
|
3
|
+
*
|
|
4
|
+
* Persists cost events and tasks to a local SQLite database using the exact
|
|
5
|
+
* same schema as the Python SDK. Data survives process restarts.
|
|
6
|
+
*
|
|
7
|
+
* Uses better-sqlite3 for synchronous, high-performance SQLite access.
|
|
8
|
+
*/
|
|
9
|
+
import { createRequire } from "node:module";
|
|
10
|
+
import { mkdirSync } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Helpers
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
function rowToEvent(row) {
|
|
17
|
+
return {
|
|
18
|
+
eventId: row.event_id,
|
|
19
|
+
taskId: row.task_id,
|
|
20
|
+
eventType: row.event_type,
|
|
21
|
+
provider: row.provider ?? undefined,
|
|
22
|
+
model: row.model ?? undefined,
|
|
23
|
+
inputTokens: row.input_tokens ?? undefined,
|
|
24
|
+
outputTokens: row.output_tokens ?? undefined,
|
|
25
|
+
cachedTokens: row.cached_tokens ?? undefined,
|
|
26
|
+
serviceName: row.service_name ?? undefined,
|
|
27
|
+
costUsd: Number(row.cost_usd),
|
|
28
|
+
latencyMs: row.latency_ms ?? undefined,
|
|
29
|
+
costConfidence: row.cost_confidence,
|
|
30
|
+
pricingSource: row.pricing_source ?? undefined,
|
|
31
|
+
pricingVersion: row.pricing_version ?? undefined,
|
|
32
|
+
isRetry: Boolean(row.is_retry),
|
|
33
|
+
retryReason: row.retry_reason ?? undefined,
|
|
34
|
+
retryOf: row.retry_of ?? undefined,
|
|
35
|
+
details: (() => {
|
|
36
|
+
let d = {};
|
|
37
|
+
try {
|
|
38
|
+
d = row.details != null ? JSON.parse(row.details) : {};
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
d = {};
|
|
42
|
+
}
|
|
43
|
+
return d;
|
|
44
|
+
})(),
|
|
45
|
+
occurredAt: new Date(row.timestamp),
|
|
46
|
+
schemaVersion: "1",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function rowToTask(row) {
|
|
50
|
+
return {
|
|
51
|
+
taskId: row.task_id,
|
|
52
|
+
taskType: row.task_type,
|
|
53
|
+
status: row.status,
|
|
54
|
+
startedAt: new Date(row.started_at),
|
|
55
|
+
endedAt: row.ended_at != null ? new Date(row.ended_at) : undefined,
|
|
56
|
+
metadata: (() => {
|
|
57
|
+
let m = {};
|
|
58
|
+
try {
|
|
59
|
+
m = row.metadata != null ? JSON.parse(row.metadata) : {};
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
m = {};
|
|
63
|
+
}
|
|
64
|
+
return m;
|
|
65
|
+
})(),
|
|
66
|
+
llmCostUsd: Number(row.llm_cost_usd ?? "0"),
|
|
67
|
+
externalCostUsd: Number(row.external_cost_usd ?? "0"),
|
|
68
|
+
computeCostUsd: Number(row.compute_cost_usd ?? "0"),
|
|
69
|
+
totalCostUsd: Number(row.total_cost_usd ?? "0"),
|
|
70
|
+
totalInputTokens: row.total_input_tokens ?? 0,
|
|
71
|
+
totalOutputTokens: row.total_output_tokens ?? 0,
|
|
72
|
+
totalCachedTokens: row.total_cached_tokens ?? 0,
|
|
73
|
+
retryCount: row.retry_count ?? 0,
|
|
74
|
+
retryCostUsd: Number(row.retry_cost_usd ?? "0"),
|
|
75
|
+
failureCount: row.failure_count ?? 0,
|
|
76
|
+
customerId: row.customer_id ?? undefined,
|
|
77
|
+
projectId: row.project_id ?? undefined,
|
|
78
|
+
parentTaskId: row.parent_task_id ?? undefined,
|
|
79
|
+
experimentId: row.experiment_id ?? undefined,
|
|
80
|
+
variant: row.variant ?? undefined,
|
|
81
|
+
// Network capture fields default to zero / empty for rows that
|
|
82
|
+
// pre-date the v1 migration. Phase D wires the SQLite columns +
|
|
83
|
+
// serialisation/deserialisation; for now legacy rows read back as
|
|
84
|
+
// fresh, matching Python's from_dict defaults.
|
|
85
|
+
networkBytesIn: 0,
|
|
86
|
+
networkBytesOut: 0,
|
|
87
|
+
networkCallCount: 0,
|
|
88
|
+
networkByHost: { hosts: [] },
|
|
89
|
+
networkCostUsd: 0,
|
|
90
|
+
// Same legacy-row default as the other network/GPU fields: rows
|
|
91
|
+
// pre-dating the GPU columns read back as fresh zero. Matches
|
|
92
|
+
// Python's from_dict default and aligns with the post-Sprint-2
|
|
93
|
+
// 5-subsystem `Task` type.
|
|
94
|
+
gpuCostUsd: 0,
|
|
95
|
+
schemaVersion: "1",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// DDL
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
const CREATE_TASKS = `
|
|
102
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
103
|
+
task_id TEXT PRIMARY KEY,
|
|
104
|
+
task_type TEXT NOT NULL,
|
|
105
|
+
status TEXT NOT NULL,
|
|
106
|
+
started_at TEXT NOT NULL,
|
|
107
|
+
ended_at TEXT,
|
|
108
|
+
metadata TEXT,
|
|
109
|
+
llm_cost_usd TEXT,
|
|
110
|
+
external_cost_usd TEXT,
|
|
111
|
+
compute_cost_usd TEXT,
|
|
112
|
+
total_cost_usd TEXT,
|
|
113
|
+
total_input_tokens INTEGER,
|
|
114
|
+
total_output_tokens INTEGER,
|
|
115
|
+
total_cached_tokens INTEGER,
|
|
116
|
+
retry_count INTEGER DEFAULT 0,
|
|
117
|
+
retry_cost_usd TEXT DEFAULT '0',
|
|
118
|
+
failure_count INTEGER DEFAULT 0,
|
|
119
|
+
customer_id TEXT,
|
|
120
|
+
project_id TEXT,
|
|
121
|
+
parent_task_id TEXT,
|
|
122
|
+
experiment_id TEXT,
|
|
123
|
+
variant TEXT,
|
|
124
|
+
sync_status TEXT NOT NULL DEFAULT 'pending'
|
|
125
|
+
)`;
|
|
126
|
+
const CREATE_EVENTS = `
|
|
127
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
128
|
+
event_id TEXT PRIMARY KEY,
|
|
129
|
+
task_id TEXT NOT NULL,
|
|
130
|
+
event_type TEXT NOT NULL,
|
|
131
|
+
provider TEXT,
|
|
132
|
+
model TEXT,
|
|
133
|
+
input_tokens INTEGER,
|
|
134
|
+
output_tokens INTEGER,
|
|
135
|
+
cached_tokens INTEGER,
|
|
136
|
+
service_name TEXT,
|
|
137
|
+
cost_usd TEXT NOT NULL,
|
|
138
|
+
latency_ms INTEGER,
|
|
139
|
+
cost_confidence TEXT NOT NULL DEFAULT 'exact',
|
|
140
|
+
pricing_source TEXT,
|
|
141
|
+
pricing_version TEXT,
|
|
142
|
+
is_retry INTEGER DEFAULT 0,
|
|
143
|
+
retry_reason TEXT,
|
|
144
|
+
retry_of TEXT,
|
|
145
|
+
details TEXT,
|
|
146
|
+
timestamp TEXT NOT NULL,
|
|
147
|
+
sync_status TEXT NOT NULL DEFAULT 'pending'
|
|
148
|
+
)`;
|
|
149
|
+
const CREATE_SCHEMA_VERSION = `
|
|
150
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
151
|
+
version_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
152
|
+
version_number INTEGER NOT NULL,
|
|
153
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
154
|
+
migration_name TEXT
|
|
155
|
+
)`;
|
|
156
|
+
const INDEXES = [
|
|
157
|
+
`CREATE INDEX IF NOT EXISTS idx_tasks_customer ON tasks(customer_id, started_at)`,
|
|
158
|
+
`CREATE INDEX IF NOT EXISTS idx_tasks_type ON tasks(task_type, started_at)`,
|
|
159
|
+
`CREATE INDEX IF NOT EXISTS idx_tasks_period ON tasks(started_at)`,
|
|
160
|
+
`CREATE INDEX IF NOT EXISTS idx_events_task ON events(task_id)`,
|
|
161
|
+
`CREATE INDEX IF NOT EXISTS idx_events_type ON events(event_type, timestamp)`,
|
|
162
|
+
`CREATE INDEX IF NOT EXISTS idx_events_sync ON events(sync_status, timestamp)`,
|
|
163
|
+
`CREATE INDEX IF NOT EXISTS idx_tasks_sync ON tasks(sync_status, started_at)`,
|
|
164
|
+
];
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// MemoryBufferStore — in-memory fallback when better-sqlite3 is unavailable
|
|
167
|
+
// (Vercel Edge, Cloudflare Workers, Bun without bindings).
|
|
168
|
+
//
|
|
169
|
+
// Sprint 1 Theme B / §2.2.3 (B8 follow-on). The audit-minimum no-op
|
|
170
|
+
// fallback (commit a6eb6db) kept customer apps alive but silently
|
|
171
|
+
// dropped events. This store provides durable in-memory buffering with
|
|
172
|
+
// a hard 10k-entry cap per kind (events, tasks) and FIFO eviction
|
|
173
|
+
// (Map iteration order = insertion order). Events still don't survive
|
|
174
|
+
// process restarts — that's the SQLite path's job — but they're now
|
|
175
|
+
// available to the sync pusher within the process lifetime.
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
const MEM_BUFFER_MAX_EVENTS = 10_000;
|
|
178
|
+
const MEM_BUFFER_MAX_TASKS = 10_000;
|
|
179
|
+
class MemoryBufferStore {
|
|
180
|
+
_events = new Map();
|
|
181
|
+
_tasks = new Map();
|
|
182
|
+
addEvent(event) {
|
|
183
|
+
this._evict(this._events, MEM_BUFFER_MAX_EVENTS);
|
|
184
|
+
// Clone to detach from caller mutations.
|
|
185
|
+
this._events.set(event.eventId, {
|
|
186
|
+
event: { ...event },
|
|
187
|
+
syncStatus: "pending",
|
|
188
|
+
capturedAt: new Date(),
|
|
189
|
+
syncedAt: null,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
updateEvent(event) {
|
|
193
|
+
// Only update if entry exists — matches SQLite's UPDATE semantics
|
|
194
|
+
// (no-op when no row matches).
|
|
195
|
+
const existing = this._events.get(event.eventId);
|
|
196
|
+
if (existing == null)
|
|
197
|
+
return;
|
|
198
|
+
existing.event = { ...event };
|
|
199
|
+
}
|
|
200
|
+
upsertTask(task) {
|
|
201
|
+
const existing = this._tasks.get(task.taskId);
|
|
202
|
+
if (existing != null) {
|
|
203
|
+
existing.task = { ...task };
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
this._evict(this._tasks, MEM_BUFFER_MAX_TASKS);
|
|
207
|
+
this._tasks.set(task.taskId, {
|
|
208
|
+
task: { ...task },
|
|
209
|
+
syncStatus: "pending",
|
|
210
|
+
capturedAt: new Date(),
|
|
211
|
+
syncedAt: null,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
getPendingEvents(limit) {
|
|
215
|
+
const out = [];
|
|
216
|
+
for (const entry of this._events.values()) {
|
|
217
|
+
if (entry.syncStatus !== "pending")
|
|
218
|
+
continue;
|
|
219
|
+
out.push(entry.event);
|
|
220
|
+
if (out.length >= limit)
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
markSynced(eventIds) {
|
|
226
|
+
const now = new Date();
|
|
227
|
+
for (const id of eventIds) {
|
|
228
|
+
const entry = this._events.get(id);
|
|
229
|
+
if (entry != null) {
|
|
230
|
+
entry.syncStatus = "synced";
|
|
231
|
+
entry.syncedAt = now;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
getTask(taskId) {
|
|
236
|
+
return this._tasks.get(taskId)?.task;
|
|
237
|
+
}
|
|
238
|
+
getAllTasks() {
|
|
239
|
+
return Array.from(this._tasks.values(), (e) => e.task);
|
|
240
|
+
}
|
|
241
|
+
getPendingTasks() {
|
|
242
|
+
const out = [];
|
|
243
|
+
for (const entry of this._tasks.values()) {
|
|
244
|
+
if (entry.syncStatus === "pending")
|
|
245
|
+
out.push(entry.task);
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
markTasksSynced(taskIds) {
|
|
250
|
+
const now = new Date();
|
|
251
|
+
for (const id of taskIds) {
|
|
252
|
+
const entry = this._tasks.get(id);
|
|
253
|
+
if (entry != null) {
|
|
254
|
+
entry.syncStatus = "synced";
|
|
255
|
+
entry.syncedAt = now;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
get pendingTaskCount() {
|
|
260
|
+
let n = 0;
|
|
261
|
+
for (const e of this._tasks.values())
|
|
262
|
+
if (e.syncStatus === "pending")
|
|
263
|
+
n += 1;
|
|
264
|
+
return n;
|
|
265
|
+
}
|
|
266
|
+
get pendingCount() {
|
|
267
|
+
let n = 0;
|
|
268
|
+
for (const e of this._events.values())
|
|
269
|
+
if (e.syncStatus === "pending")
|
|
270
|
+
n += 1;
|
|
271
|
+
return n;
|
|
272
|
+
}
|
|
273
|
+
getAllEvents() {
|
|
274
|
+
return Array.from(this._events.values(), (e) => e.event);
|
|
275
|
+
}
|
|
276
|
+
queryEvents(taskId) {
|
|
277
|
+
const out = [];
|
|
278
|
+
for (const entry of this._events.values()) {
|
|
279
|
+
if (entry.event.taskId === taskId)
|
|
280
|
+
out.push(entry.event);
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
purgeSynced(retentionHours) {
|
|
285
|
+
const cutoff = Date.now() - retentionHours * 3600 * 1000;
|
|
286
|
+
let removed = 0;
|
|
287
|
+
for (const [id, entry] of this._events) {
|
|
288
|
+
if (entry.syncStatus === "synced" && entry.syncedAt != null &&
|
|
289
|
+
entry.syncedAt.getTime() < cutoff) {
|
|
290
|
+
this._events.delete(id);
|
|
291
|
+
removed += 1;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return removed;
|
|
295
|
+
}
|
|
296
|
+
purgeOldPending(maxAgeDays) {
|
|
297
|
+
const cutoff = Date.now() - maxAgeDays * 24 * 3600 * 1000;
|
|
298
|
+
let removed = 0;
|
|
299
|
+
for (const [id, entry] of this._events) {
|
|
300
|
+
if (entry.syncStatus === "pending" && entry.capturedAt.getTime() < cutoff) {
|
|
301
|
+
this._events.delete(id);
|
|
302
|
+
removed += 1;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return removed;
|
|
306
|
+
}
|
|
307
|
+
close() {
|
|
308
|
+
this._events.clear();
|
|
309
|
+
this._tasks.clear();
|
|
310
|
+
}
|
|
311
|
+
// Test-only: total entry counts (used by buffer regression tests to
|
|
312
|
+
// exercise the FIFO eviction cap without going through every getter).
|
|
313
|
+
_eventCount() { return this._events.size; }
|
|
314
|
+
_taskCount() { return this._tasks.size; }
|
|
315
|
+
_evict(map, max) {
|
|
316
|
+
while (map.size >= max) {
|
|
317
|
+
// Map iteration order = insertion order, so first key is oldest.
|
|
318
|
+
const oldestKey = map.keys().next().value;
|
|
319
|
+
if (oldestKey === undefined)
|
|
320
|
+
break;
|
|
321
|
+
map.delete(oldestKey);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// EventBuffer
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
/**
|
|
329
|
+
* SQLite-backed buffer that persists events and tasks across process restarts.
|
|
330
|
+
*
|
|
331
|
+
* Schema is identical to the Python SDK so both SDKs can share a database file.
|
|
332
|
+
* Costs are stored as TEXT strings to avoid floating-point precision loss.
|
|
333
|
+
*/
|
|
334
|
+
export class EventBuffer {
|
|
335
|
+
// null when better-sqlite3 is unavailable; in that case `_mem` holds
|
|
336
|
+
// the in-memory fallback store and every method delegates to it.
|
|
337
|
+
_db;
|
|
338
|
+
_mem = null;
|
|
339
|
+
/**
|
|
340
|
+
* Test-only seam. When `true`, the constructor takes the no-binding
|
|
341
|
+
* fallback path without attempting the require — used by
|
|
342
|
+
* tests/runtime-fallback.test.ts to simulate Vercel Edge / Cloudflare
|
|
343
|
+
* Workers behaviour without touching the real native module. Do NOT
|
|
344
|
+
* set this in production code. Sprint 1 Theme B / §2.2.3 (B8).
|
|
345
|
+
*/
|
|
346
|
+
static _forceFallbackForTest = false;
|
|
347
|
+
constructor(dbPath) {
|
|
348
|
+
// Sprint 1 Theme B / §2.2.3 (B8): try to load better-sqlite3
|
|
349
|
+
// dynamically. If the native binding is absent or fails to load
|
|
350
|
+
// (Vercel Edge, Cloudflare Workers, Bun without bindings), fall
|
|
351
|
+
// back to a Map-based in-memory buffer with a 10k-entry cap so
|
|
352
|
+
// init() doesn't crash the customer app and events are still
|
|
353
|
+
// available to the sync pusher within the process lifetime.
|
|
354
|
+
let DatabaseCtor = null;
|
|
355
|
+
if (EventBuffer._forceFallbackForTest) {
|
|
356
|
+
this._db = null;
|
|
357
|
+
this._mem = new MemoryBufferStore();
|
|
358
|
+
console.warn("dexcost: EventBuffer._forceFallbackForTest is set — using in-memory buffer (events do not survive process restart)");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
const require = createRequire(import.meta.url);
|
|
363
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
364
|
+
DatabaseCtor = require("better-sqlite3");
|
|
365
|
+
}
|
|
366
|
+
catch (err) {
|
|
367
|
+
console.warn("dexcost: better-sqlite3 not available in this runtime; falling " +
|
|
368
|
+
"back to in-memory buffer (events do not survive process " +
|
|
369
|
+
"restart, hard cap 10k entries). Install better-sqlite3 as a " +
|
|
370
|
+
"peer dependency for durable buffering. Cause: " +
|
|
371
|
+
(err instanceof Error ? err.message : String(err)));
|
|
372
|
+
this._db = null;
|
|
373
|
+
this._mem = new MemoryBufferStore();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const resolvedPath = dbPath ?? join(homedir(), ".dexcost", "buffer.db");
|
|
377
|
+
try {
|
|
378
|
+
mkdirSync(dirname(resolvedPath), { recursive: true });
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
throw new Error(`Cannot create dexcost storage directory: ${err instanceof Error ? err.message : err}`);
|
|
382
|
+
}
|
|
383
|
+
this._db = new DatabaseCtor(resolvedPath);
|
|
384
|
+
// PRAGMAs and DDL
|
|
385
|
+
try {
|
|
386
|
+
this._db.pragma("journal_mode=WAL");
|
|
387
|
+
this._db.pragma("synchronous=NORMAL");
|
|
388
|
+
this._db.pragma("foreign_keys=ON");
|
|
389
|
+
this._db.exec(CREATE_TASKS);
|
|
390
|
+
this._db.exec(CREATE_EVENTS);
|
|
391
|
+
this._db.exec(CREATE_SCHEMA_VERSION);
|
|
392
|
+
// Migrate older databases that pre-date the tasks.sync_status column.
|
|
393
|
+
// CREATE TABLE IF NOT EXISTS won't add columns to an existing table,
|
|
394
|
+
// so add it explicitly; ignore the "duplicate column" error when the
|
|
395
|
+
// column already exists.
|
|
396
|
+
this._migrateAddColumn("tasks", "sync_status", "TEXT NOT NULL DEFAULT 'pending'");
|
|
397
|
+
for (const idx of INDEXES) {
|
|
398
|
+
this._db.exec(idx);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
catch (err) {
|
|
402
|
+
throw new Error(`Cannot initialize dexcost database: ${err instanceof Error ? err.message : err}`);
|
|
403
|
+
}
|
|
404
|
+
// Seed schema_version if empty
|
|
405
|
+
const versionCount = this._db.prepare("SELECT COUNT(*) AS count FROM schema_version").get().count;
|
|
406
|
+
if (versionCount === 0) {
|
|
407
|
+
try {
|
|
408
|
+
this._db
|
|
409
|
+
.prepare(`INSERT INTO schema_version (version_number, migration_name)
|
|
410
|
+
VALUES (1, 'initial')`)
|
|
411
|
+
.run();
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
// SQLite error — skip seeding, don't crash
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Add `column` to `table` if it does not already exist.
|
|
420
|
+
*
|
|
421
|
+
* SQLite has no `ADD COLUMN IF NOT EXISTS`, so a duplicate-column error
|
|
422
|
+
* is the expected signal that the migration has already been applied and
|
|
423
|
+
* is swallowed. Any other failure is also tolerated so init never crashes.
|
|
424
|
+
*/
|
|
425
|
+
_migrateAddColumn(table, column, definition) {
|
|
426
|
+
if (!this._db)
|
|
427
|
+
return;
|
|
428
|
+
try {
|
|
429
|
+
this._db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
// Column already exists (duplicate column name) or other benign error.
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Add a cost event to the buffer with sync_status = 'pending'.
|
|
437
|
+
*/
|
|
438
|
+
addEvent(event) {
|
|
439
|
+
if (this._mem) {
|
|
440
|
+
this._mem.addEvent(event);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (!this._db)
|
|
444
|
+
return;
|
|
445
|
+
try {
|
|
446
|
+
this._db
|
|
447
|
+
.prepare(`INSERT INTO events (
|
|
448
|
+
event_id, task_id, event_type, provider, model,
|
|
449
|
+
input_tokens, output_tokens, cached_tokens, service_name,
|
|
450
|
+
cost_usd, latency_ms, cost_confidence, pricing_source, pricing_version,
|
|
451
|
+
is_retry, retry_reason, retry_of, details, timestamp, sync_status
|
|
452
|
+
) VALUES (
|
|
453
|
+
?, ?, ?, ?, ?,
|
|
454
|
+
?, ?, ?, ?,
|
|
455
|
+
?, ?, ?, ?, ?,
|
|
456
|
+
?, ?, ?, ?, ?, 'pending'
|
|
457
|
+
)`)
|
|
458
|
+
.run(event.eventId, event.taskId, event.eventType, event.provider ?? null, event.model ?? null, event.inputTokens ?? null, event.outputTokens ?? null, event.cachedTokens ?? null, event.serviceName ?? null, event.costUsd.toString(), event.latencyMs ?? null, event.costConfidence, event.pricingSource ?? null, event.pricingVersion ?? null, event.isRetry ? 1 : 0, event.retryReason ?? null, event.retryOf ?? null, JSON.stringify(event.details), event.occurredAt.toISOString());
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
// SQLite error (disk full, locked) — skip this event, don't crash
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Insert or replace a task in the buffer.
|
|
466
|
+
*
|
|
467
|
+
* The task is (re)marked `sync_status = 'pending'`: an upsert means the
|
|
468
|
+
* task's data changed (new cost rolled up, status flipped, etc.), so it
|
|
469
|
+
* must be re-sent on the next push. `markTasksSynced` flips it to
|
|
470
|
+
* `'synced'` after a successful POST so unchanged tasks are not re-sent.
|
|
471
|
+
*/
|
|
472
|
+
upsertTask(task) {
|
|
473
|
+
if (this._mem) {
|
|
474
|
+
this._mem.upsertTask(task);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (!this._db)
|
|
478
|
+
return;
|
|
479
|
+
try {
|
|
480
|
+
this._db
|
|
481
|
+
.prepare(`INSERT OR REPLACE INTO tasks (
|
|
482
|
+
task_id, task_type, status, started_at, ended_at, metadata,
|
|
483
|
+
llm_cost_usd, external_cost_usd, compute_cost_usd, total_cost_usd,
|
|
484
|
+
total_input_tokens, total_output_tokens, total_cached_tokens,
|
|
485
|
+
retry_count, retry_cost_usd, failure_count,
|
|
486
|
+
customer_id, project_id, parent_task_id, experiment_id, variant,
|
|
487
|
+
sync_status
|
|
488
|
+
) VALUES (
|
|
489
|
+
?, ?, ?, ?, ?, ?,
|
|
490
|
+
?, ?, ?, ?,
|
|
491
|
+
?, ?, ?,
|
|
492
|
+
?, ?, ?,
|
|
493
|
+
?, ?, ?, ?, ?,
|
|
494
|
+
'pending'
|
|
495
|
+
)`)
|
|
496
|
+
.run(task.taskId, task.taskType, task.status, task.startedAt.toISOString(), task.endedAt ? task.endedAt.toISOString() : null, JSON.stringify(task.metadata), task.llmCostUsd.toString(), task.externalCostUsd.toString(), task.computeCostUsd.toString(), task.totalCostUsd.toString(), task.totalInputTokens, task.totalOutputTokens, task.totalCachedTokens, task.retryCount, task.retryCostUsd.toString(), task.failureCount, task.customerId ?? null, task.projectId ?? null, task.parentTaskId ?? null, task.experimentId ?? null, task.variant ?? null);
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
// SQLite error (disk full, locked) — skip this upsert, don't crash
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Return up to `limit` pending events, ordered by timestamp ASC.
|
|
504
|
+
*/
|
|
505
|
+
getPendingEvents(limit = 100) {
|
|
506
|
+
if (this._mem)
|
|
507
|
+
return this._mem.getPendingEvents(limit);
|
|
508
|
+
if (!this._db)
|
|
509
|
+
return [];
|
|
510
|
+
const rows = this._db
|
|
511
|
+
.prepare(`SELECT * FROM events WHERE sync_status = 'pending' ORDER BY timestamp ASC LIMIT ?`)
|
|
512
|
+
.all(limit);
|
|
513
|
+
return rows.map(rowToEvent);
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Mark the given event IDs as synced.
|
|
517
|
+
*/
|
|
518
|
+
markSynced(eventIds) {
|
|
519
|
+
if (this._mem) {
|
|
520
|
+
this._mem.markSynced(eventIds);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (!this._db)
|
|
524
|
+
return;
|
|
525
|
+
if (eventIds.length === 0)
|
|
526
|
+
return;
|
|
527
|
+
try {
|
|
528
|
+
const placeholders = eventIds.map(() => "?").join(", ");
|
|
529
|
+
this._db
|
|
530
|
+
.prepare(`UPDATE events SET sync_status = 'synced' WHERE event_id IN (${placeholders})`)
|
|
531
|
+
.run(...eventIds);
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
// SQLite error (disk full, locked) — skip marking, don't crash
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Retrieve a task by ID, or undefined if not found.
|
|
539
|
+
*/
|
|
540
|
+
getTask(taskId) {
|
|
541
|
+
if (this._mem)
|
|
542
|
+
return this._mem.getTask(taskId);
|
|
543
|
+
if (!this._db)
|
|
544
|
+
return undefined;
|
|
545
|
+
const row = this._db
|
|
546
|
+
.prepare("SELECT * FROM tasks WHERE task_id = ?")
|
|
547
|
+
.get(taskId);
|
|
548
|
+
return row != null ? rowToTask(row) : undefined;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Return all tasks in the buffer.
|
|
552
|
+
*/
|
|
553
|
+
getAllTasks() {
|
|
554
|
+
if (this._mem)
|
|
555
|
+
return this._mem.getAllTasks();
|
|
556
|
+
if (!this._db)
|
|
557
|
+
return [];
|
|
558
|
+
const rows = this._db.prepare("SELECT * FROM tasks").all();
|
|
559
|
+
return rows.map(rowToTask);
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Return all tasks awaiting sync (`sync_status = 'pending'`).
|
|
563
|
+
*
|
|
564
|
+
* The pusher sends only these so unchanged tasks are not re-POSTed on
|
|
565
|
+
* every push cycle.
|
|
566
|
+
*/
|
|
567
|
+
getPendingTasks() {
|
|
568
|
+
if (this._mem)
|
|
569
|
+
return this._mem.getPendingTasks();
|
|
570
|
+
if (!this._db)
|
|
571
|
+
return [];
|
|
572
|
+
const rows = this._db
|
|
573
|
+
.prepare("SELECT * FROM tasks WHERE sync_status = 'pending'")
|
|
574
|
+
.all();
|
|
575
|
+
return rows.map(rowToTask);
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Mark the given task IDs as synced.
|
|
579
|
+
*
|
|
580
|
+
* Called by the pusher after a successful POST so the tasks are excluded
|
|
581
|
+
* from subsequent pushes until they are upserted again.
|
|
582
|
+
*/
|
|
583
|
+
markTasksSynced(taskIds) {
|
|
584
|
+
if (this._mem) {
|
|
585
|
+
this._mem.markTasksSynced(taskIds);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (!this._db)
|
|
589
|
+
return;
|
|
590
|
+
if (taskIds.length === 0)
|
|
591
|
+
return;
|
|
592
|
+
try {
|
|
593
|
+
const placeholders = taskIds.map(() => "?").join(", ");
|
|
594
|
+
this._db
|
|
595
|
+
.prepare(`UPDATE tasks SET sync_status = 'synced' WHERE task_id IN (${placeholders})`)
|
|
596
|
+
.run(...taskIds);
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
// SQLite error (disk full, locked) — skip marking, don't crash
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
/** The number of tasks awaiting sync (`sync_status = 'pending'`). */
|
|
603
|
+
get pendingTaskCount() {
|
|
604
|
+
if (this._mem)
|
|
605
|
+
return this._mem.pendingTaskCount;
|
|
606
|
+
if (!this._db)
|
|
607
|
+
return 0;
|
|
608
|
+
const row = this._db
|
|
609
|
+
.prepare("SELECT COUNT(*) AS count FROM tasks WHERE sync_status = 'pending'")
|
|
610
|
+
.get();
|
|
611
|
+
return row.count;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Return all events in the buffer (including synced).
|
|
615
|
+
*/
|
|
616
|
+
getAllEvents() {
|
|
617
|
+
if (this._mem)
|
|
618
|
+
return this._mem.getAllEvents();
|
|
619
|
+
if (!this._db)
|
|
620
|
+
return [];
|
|
621
|
+
const rows = this._db.prepare("SELECT * FROM events").all();
|
|
622
|
+
return rows.map(rowToEvent);
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Return events for a specific task, ordered by timestamp DESC.
|
|
626
|
+
*/
|
|
627
|
+
queryEvents(taskId) {
|
|
628
|
+
if (this._mem)
|
|
629
|
+
return this._mem.queryEvents(taskId);
|
|
630
|
+
if (!this._db)
|
|
631
|
+
return [];
|
|
632
|
+
const rows = this._db
|
|
633
|
+
.prepare("SELECT * FROM events WHERE task_id = ? ORDER BY timestamp DESC")
|
|
634
|
+
.all(taskId);
|
|
635
|
+
return rows.map(rowToEvent);
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Update all columns of an existing event in-place.
|
|
639
|
+
*/
|
|
640
|
+
updateEvent(event) {
|
|
641
|
+
if (this._mem) {
|
|
642
|
+
this._mem.updateEvent(event);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (!this._db)
|
|
646
|
+
return;
|
|
647
|
+
try {
|
|
648
|
+
this._db
|
|
649
|
+
.prepare(`UPDATE events SET
|
|
650
|
+
task_id = ?,
|
|
651
|
+
event_type = ?,
|
|
652
|
+
provider = ?,
|
|
653
|
+
model = ?,
|
|
654
|
+
input_tokens = ?,
|
|
655
|
+
output_tokens = ?,
|
|
656
|
+
cached_tokens = ?,
|
|
657
|
+
service_name = ?,
|
|
658
|
+
cost_usd = ?,
|
|
659
|
+
latency_ms = ?,
|
|
660
|
+
cost_confidence = ?,
|
|
661
|
+
pricing_source = ?,
|
|
662
|
+
pricing_version = ?,
|
|
663
|
+
is_retry = ?,
|
|
664
|
+
retry_reason = ?,
|
|
665
|
+
retry_of = ?,
|
|
666
|
+
details = ?,
|
|
667
|
+
timestamp = ?
|
|
668
|
+
WHERE event_id = ?`)
|
|
669
|
+
.run(event.taskId, event.eventType, event.provider ?? null, event.model ?? null, event.inputTokens ?? null, event.outputTokens ?? null, event.cachedTokens ?? null, event.serviceName ?? null, event.costUsd.toString(), event.latencyMs ?? null, event.costConfidence, event.pricingSource ?? null, event.pricingVersion ?? null, event.isRetry ? 1 : 0, event.retryReason ?? null, event.retryOf ?? null, JSON.stringify(event.details), event.occurredAt.toISOString(), event.eventId);
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
// SQLite error (disk full, locked) — skip this update, don't crash
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Return the number of pending (unsynced) events.
|
|
677
|
+
*/
|
|
678
|
+
get pendingCount() {
|
|
679
|
+
if (this._mem)
|
|
680
|
+
return this._mem.pendingCount;
|
|
681
|
+
if (!this._db)
|
|
682
|
+
return 0;
|
|
683
|
+
const row = this._db
|
|
684
|
+
.prepare("SELECT COUNT(*) AS count FROM events WHERE sync_status = 'pending'")
|
|
685
|
+
.get();
|
|
686
|
+
return row.count;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Delete synced events older than `retentionHours` and VACUUM.
|
|
690
|
+
*
|
|
691
|
+
* Returns the number of deleted rows.
|
|
692
|
+
*/
|
|
693
|
+
purgeSynced(retentionHours = 48) {
|
|
694
|
+
if (this._mem)
|
|
695
|
+
return this._mem.purgeSynced(retentionHours);
|
|
696
|
+
if (!this._db)
|
|
697
|
+
return 0;
|
|
698
|
+
try {
|
|
699
|
+
const cutoff = new Date(Date.now() - retentionHours * 3_600_000).toISOString();
|
|
700
|
+
const result = this._db
|
|
701
|
+
.prepare(`DELETE FROM events WHERE sync_status = 'synced' AND timestamp < ?`)
|
|
702
|
+
.run(cutoff);
|
|
703
|
+
const deleted = result.changes;
|
|
704
|
+
if (deleted > 0) {
|
|
705
|
+
this._db.pragma("wal_checkpoint(TRUNCATE)");
|
|
706
|
+
this._db.exec("VACUUM");
|
|
707
|
+
}
|
|
708
|
+
return deleted;
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
// SQLite error — skip purge, don't crash
|
|
712
|
+
return 0;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Delete pending events older than `maxAgeDays` and VACUUM.
|
|
717
|
+
*
|
|
718
|
+
* Safety net for events that can never be synced (rejected API key,
|
|
719
|
+
* permanently-down endpoint, etc.) so the local buffer cannot grow
|
|
720
|
+
* unbounded. Mirrors the Python SDK's `purge_old_pending` (default 7
|
|
721
|
+
* days). Returns the number of deleted rows.
|
|
722
|
+
*/
|
|
723
|
+
purgeOldPending(maxAgeDays = 7) {
|
|
724
|
+
if (this._mem)
|
|
725
|
+
return this._mem.purgeOldPending(maxAgeDays);
|
|
726
|
+
if (!this._db)
|
|
727
|
+
return 0;
|
|
728
|
+
try {
|
|
729
|
+
const cutoff = new Date(Date.now() - maxAgeDays * 86_400_000).toISOString();
|
|
730
|
+
const result = this._db
|
|
731
|
+
.prepare(`DELETE FROM events WHERE sync_status = 'pending' AND timestamp < ?`)
|
|
732
|
+
.run(cutoff);
|
|
733
|
+
const deleted = result.changes;
|
|
734
|
+
if (deleted > 0) {
|
|
735
|
+
this._db.pragma("wal_checkpoint(TRUNCATE)");
|
|
736
|
+
this._db.exec("VACUUM");
|
|
737
|
+
}
|
|
738
|
+
return deleted;
|
|
739
|
+
}
|
|
740
|
+
catch {
|
|
741
|
+
// SQLite error — skip purge, don't crash
|
|
742
|
+
return 0;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Close the underlying database connection.
|
|
747
|
+
*/
|
|
748
|
+
close() {
|
|
749
|
+
if (this._mem) {
|
|
750
|
+
this._mem.close();
|
|
751
|
+
this._mem = null;
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
if (!this._db)
|
|
755
|
+
return;
|
|
756
|
+
this._db.close();
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
//# sourceMappingURL=buffer.js.map
|