@token-dashboard/codex-usage-uploader 0.1.6 → 0.1.8

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/src/parser.js DELETED
@@ -1,180 +0,0 @@
1
- import { computeEventUid, deriveRepoName, isoToEpochMs, stableStringify } from './utils.js';
2
-
3
- export class RolloutParser {
4
- constructor(collectorIdentity, relpath, initialState = {}) {
5
- this.collectorIdentity = collectorIdentity;
6
- this.relpath = relpath;
7
- this.currentSession = initialState.current_session ?? null;
8
- this.currentTurn = initialState.current_turn ?? null;
9
- }
10
-
11
- exportState() {
12
- return {
13
- current_session: this.currentSession,
14
- current_turn: this.currentTurn,
15
- };
16
- }
17
-
18
- normalizeSession(payload) {
19
- if (!payload?.id) return null;
20
- const git = payload.git && typeof payload.git === 'object' ? payload.git : {};
21
- const cwd = payload.cwd ?? null;
22
- const repositoryUrl = git.repository_url ?? null;
23
- return {
24
- sessionId: String(payload.id),
25
- sessionTimestamp: isoToEpochMs(payload.timestamp),
26
- cwd,
27
- originator: payload.originator ?? null,
28
- source: payload.source ?? null,
29
- cliVersion: payload.cli_version ?? null,
30
- modelProvider: payload.model_provider ?? null,
31
- repoName: deriveRepoName(repositoryUrl, cwd),
32
- gitBranch: git.branch ?? null,
33
- gitCommitHash: git.commit_hash ?? null,
34
- repositoryUrl,
35
- };
36
- }
37
-
38
- normalizeTurn(payload, timestampMs) {
39
- const turnId = payload?.turn_id;
40
- const sessionId = this.currentSession?.sessionId;
41
- if (!turnId || !sessionId) return null;
42
- const sandbox = payload.sandbox_policy && typeof payload.sandbox_policy === 'object'
43
- ? payload.sandbox_policy
44
- : {};
45
- const writableRoots = Array.isArray(sandbox.writable_roots) ? sandbox.writable_roots : null;
46
- const collaborationMode = payload.collaboration_mode && typeof payload.collaboration_mode === 'object'
47
- ? payload.collaboration_mode.mode
48
- : null;
49
- return {
50
- turnId: String(turnId),
51
- sessionId: String(sessionId),
52
- eventTimestamp: timestampMs,
53
- cwd: payload.cwd ?? null,
54
- currentDate: payload.current_date ?? null,
55
- timezone: payload.timezone ?? null,
56
- approvalPolicy: payload.approval_policy ?? null,
57
- sandboxPolicyType: sandbox.type ?? null,
58
- sandboxNetworkAccess: sandbox.network_access ?? null,
59
- sandboxWritableRootsJson: writableRoots ? stableStringify(writableRoots) : null,
60
- sandboxPolicyJson: Object.keys(sandbox).length ? stableStringify(sandbox) : null,
61
- model: payload.model ?? null,
62
- personality: payload.personality ?? null,
63
- collaborationMode: collaborationMode ?? null,
64
- effort: payload.effort ?? null,
65
- summary: payload.summary ?? null,
66
- truncationPolicyJson: payload.truncation_policy != null
67
- ? stableStringify(payload.truncation_policy)
68
- : null,
69
- };
70
- }
71
-
72
- normalizeTokenEvent(payload, timestampMs, lineNo) {
73
- if (!this.currentSession || timestampMs == null) return null;
74
- const info = payload?.info;
75
- if (!info || typeof info !== 'object') return null;
76
- const totalUsage = info.total_token_usage && typeof info.total_token_usage === 'object'
77
- ? info.total_token_usage
78
- : {};
79
- const lastUsage = info.last_token_usage && typeof info.last_token_usage === 'object'
80
- ? info.last_token_usage
81
- : {};
82
- const rateLimits = payload.rate_limits && typeof payload.rate_limits === 'object'
83
- ? payload.rate_limits
84
- : {};
85
- const primary = rateLimits.primary && typeof rateLimits.primary === 'object' ? rateLimits.primary : {};
86
- const secondary = rateLimits.secondary && typeof rateLimits.secondary === 'object' ? rateLimits.secondary : {};
87
- const session = this.currentSession;
88
- const turn = this.currentTurn ?? {};
89
- return {
90
- eventUid: computeEventUid(this.collectorIdentity.collectorId, this.relpath, lineNo),
91
- sessionId: session.sessionId,
92
- turnId: turn.turnId ?? null,
93
- sourceFileRelpath: this.relpath,
94
- lineNo,
95
- timestamp: timestampMs,
96
- model: turn.model ?? null,
97
- cwd: turn.cwd ?? session.cwd ?? null,
98
- timezone: turn.timezone ?? null,
99
- approvalPolicy: turn.approvalPolicy ?? null,
100
- sandboxPolicyType: turn.sandboxPolicyType ?? null,
101
- source: session.source ?? null,
102
- originator: session.originator ?? null,
103
- cliVersion: session.cliVersion ?? null,
104
- repositoryUrl: session.repositoryUrl ?? null,
105
- repoName: session.repoName ?? null,
106
- gitBranch: session.gitBranch ?? null,
107
- gitCommitHash: session.gitCommitHash ?? null,
108
- totalInputTokens: totalUsage.input_tokens ?? null,
109
- totalCachedInputTokens: totalUsage.cached_input_tokens ?? null,
110
- totalOutputTokens: totalUsage.output_tokens ?? null,
111
- totalReasoningOutputTokens: totalUsage.reasoning_output_tokens ?? null,
112
- totalTokens: totalUsage.total_tokens ?? null,
113
- lastInputTokens: lastUsage.input_tokens ?? null,
114
- lastCachedInputTokens: lastUsage.cached_input_tokens ?? null,
115
- lastOutputTokens: lastUsage.output_tokens ?? null,
116
- lastReasoningOutputTokens: lastUsage.reasoning_output_tokens ?? null,
117
- lastTotalTokens: lastUsage.total_tokens ?? null,
118
- modelContextWindow: info.model_context_window ?? null,
119
- rateLimitPlanType: rateLimits.plan_type ?? null,
120
- primaryUsedPercent: primary.used_percent ?? null,
121
- primaryWindowMinutes: primary.window_minutes ?? null,
122
- primaryResetsAt: primary.resets_at ?? null,
123
- secondaryUsedPercent: secondary.used_percent ?? null,
124
- secondaryWindowMinutes: secondary.window_minutes ?? null,
125
- secondaryResetsAt: secondary.resets_at ?? null,
126
- credits: normalizeCredits(rateLimits.credits),
127
- rawRateLimitsJson: Object.keys(rateLimits).length ? stableStringify(rateLimits) : null,
128
- };
129
- }
130
-
131
- processLine(lineNo, line) {
132
- let record;
133
- try {
134
- record = JSON.parse(line);
135
- } catch {
136
- return { sessions: [], turns: [], events: [] };
137
- }
138
-
139
- const recordType = record.type;
140
- const payload = record.payload ?? {};
141
- const timestampMs = isoToEpochMs(record.timestamp);
142
- const sessions = [];
143
- const turns = [];
144
- const events = [];
145
-
146
- if (recordType === 'session_meta') {
147
- const normalized = this.normalizeSession(payload);
148
- if (normalized) {
149
- this.currentSession = normalized;
150
- sessions.push(normalized);
151
- }
152
- } else if (recordType === 'turn_context') {
153
- const normalized = this.normalizeTurn(payload, timestampMs);
154
- if (normalized) {
155
- this.currentTurn = normalized;
156
- turns.push(normalized);
157
- }
158
- } else if (recordType === 'event_msg' && payload.type === 'token_count') {
159
- const normalized = this.normalizeTokenEvent(payload, timestampMs, lineNo);
160
- if (normalized) {
161
- events.push(normalized);
162
- }
163
- }
164
-
165
- return { sessions, turns, events };
166
- }
167
- }
168
-
169
- function normalizeCredits(value) {
170
- if (typeof value === 'number' && Number.isFinite(value)) {
171
- return value;
172
- }
173
- if (value && typeof value === 'object') {
174
- const balance = value.balance;
175
- if (typeof balance === 'number' && Number.isFinite(balance)) {
176
- return balance;
177
- }
178
- }
179
- return null;
180
- }
@@ -1,182 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import {
5
- CLI_NAME,
6
- DEFAULT_APP_ROOT,
7
- DEFAULT_BACKEND_URL,
8
- DEFAULT_CODEX_AUTH_PATH,
9
- DEFAULT_CONFIG_FILE,
10
- DEFAULT_CURRENT_APP_DIR,
11
- DEFAULT_HOME_BIN_LINK,
12
- DEFAULT_INSTALL_ROOT,
13
- DEFAULT_LAUNCH_AGENT_PATH,
14
- DEFAULT_LAUNCHD_LABEL,
15
- DEFAULT_LOCAL_BIN_DIR,
16
- DEFAULT_LOCAL_BIN_PATH,
17
- DEFAULT_LOG_DIR,
18
- DEFAULT_PLIST_PATH,
19
- DEFAULT_SESSIONS_DIR,
20
- DEFAULT_STATE_DB,
21
- DEFAULT_STDERR_LOG_PATH,
22
- DEFAULT_STDOUT_LOG_PATH,
23
- STATUS_ONLINE_THRESHOLD_SECONDS,
24
- } from './constants.js';
25
- import { stableStringify } from './utils.js';
26
-
27
- const DEFAULT_INSTALL_ROOT_BASE = path.basename(DEFAULT_INSTALL_ROOT);
28
-
29
- export function deriveEnvSuffix(installRoot) {
30
- const base = path.basename(installRoot);
31
- if (!base.startsWith(`${DEFAULT_INSTALL_ROOT_BASE}-`)) return '';
32
- const envName = base.slice(DEFAULT_INSTALL_ROOT_BASE.length + 1);
33
- return envName ? `-${envName}` : '';
34
- }
35
-
36
- export function defaultRuntimeConfig(configFile = DEFAULT_CONFIG_FILE) {
37
- const installRoot = path.dirname(configFile);
38
- const envSuffix = deriveEnvSuffix(installRoot);
39
- const appRoot = path.join(installRoot, 'app');
40
- const currentAppDir = path.join(appRoot, 'current');
41
- const launchdLabel = `${DEFAULT_LAUNCHD_LABEL}${envSuffix}`;
42
- return {
43
- configFile,
44
- installRoot,
45
- appRoot,
46
- currentAppDir,
47
- backendUrl: DEFAULT_BACKEND_URL,
48
- intervalSeconds: 30,
49
- codexAuthPath: DEFAULT_CODEX_AUTH_PATH,
50
- sessionsDir: DEFAULT_SESSIONS_DIR,
51
- stateDbPath: path.join(installRoot, path.basename(DEFAULT_STATE_DB)),
52
- nodePath: process.execPath,
53
- entryFile: '',
54
- packageSpec: '',
55
- localBinDir: path.join(installRoot, 'bin'),
56
- localBinPath: path.join(installRoot, 'bin', CLI_NAME),
57
- homeBinLink: path.join(os.homedir(), 'bin', `${CLI_NAME}${envSuffix}`),
58
- stdoutLogPath: path.join(installRoot, 'logs', path.basename(DEFAULT_STDOUT_LOG_PATH)),
59
- stderrLogPath: path.join(installRoot, 'logs', path.basename(DEFAULT_STDERR_LOG_PATH)),
60
- launchdLabel,
61
- plistPath: path.join(installRoot, 'launchd', `${launchdLabel}.plist`),
62
- launchAgentPath: path.join(
63
- path.dirname(DEFAULT_LAUNCH_AGENT_PATH),
64
- `${launchdLabel}.plist`,
65
- ),
66
- autoStartOnLogin: true,
67
- };
68
- }
69
-
70
- export function loadRuntimeConfig(configFile = DEFAULT_CONFIG_FILE) {
71
- const runtime = defaultRuntimeConfig(configFile);
72
- try {
73
- const payload = JSON.parse(fs.readFileSync(configFile, 'utf8'));
74
- if (!payload || typeof payload !== 'object') return runtime;
75
- return normalizeRuntimeConfig({ ...runtime, ...payload, configFile });
76
- } catch {
77
- return runtime;
78
- }
79
- }
80
-
81
- export function normalizeRuntimeConfig(runtime) {
82
- const installRoot = runtime.installRoot || DEFAULT_INSTALL_ROOT;
83
- const envSuffix = deriveEnvSuffix(installRoot);
84
- const appRoot = runtime.appRoot || path.join(installRoot, 'app');
85
- const currentAppDir = runtime.currentAppDir || path.join(appRoot, 'current');
86
- const localBinDir = runtime.localBinDir || path.join(installRoot, 'bin');
87
- const launchdLabel = runtime.launchdLabel || `${DEFAULT_LAUNCHD_LABEL}${envSuffix}`;
88
- const defaultPlistPath = path.join(installRoot, 'launchd', `${launchdLabel}.plist`);
89
- const defaultLaunchAgentPath = path.join(
90
- path.dirname(DEFAULT_LAUNCH_AGENT_PATH),
91
- `${launchdLabel}.plist`,
92
- );
93
- const legacyLaunchAgentPath = path.join(
94
- path.dirname(DEFAULT_LAUNCH_AGENT_PATH),
95
- `${launchdLabel}.plist`,
96
- );
97
- const rawPlistPath = runtime.plistPath || defaultPlistPath;
98
- return {
99
- ...runtime,
100
- configFile: runtime.configFile || DEFAULT_CONFIG_FILE,
101
- installRoot,
102
- appRoot,
103
- currentAppDir,
104
- backendUrl: runtime.backendUrl?.trim() ? runtime.backendUrl.replace(/\/+$/, '') : null,
105
- intervalSeconds: Number(runtime.intervalSeconds) || 30,
106
- codexAuthPath: runtime.codexAuthPath || DEFAULT_CODEX_AUTH_PATH,
107
- sessionsDir: runtime.sessionsDir || DEFAULT_SESSIONS_DIR,
108
- stateDbPath: runtime.stateDbPath || path.join(installRoot, 'state.sqlite'),
109
- nodePath: runtime.nodePath || process.execPath,
110
- entryFile: runtime.entryFile || '',
111
- packageSpec: runtime.packageSpec || '',
112
- localBinDir,
113
- localBinPath: runtime.localBinPath || path.join(localBinDir, CLI_NAME),
114
- homeBinLink: runtime.homeBinLink || path.join(os.homedir(), 'bin', `${CLI_NAME}${envSuffix}`),
115
- stdoutLogPath: runtime.stdoutLogPath || path.join(installRoot, 'logs', 'stdout.log'),
116
- stderrLogPath: runtime.stderrLogPath || path.join(installRoot, 'logs', 'stderr.log'),
117
- launchdLabel,
118
- plistPath:
119
- rawPlistPath === legacyLaunchAgentPath
120
- ? defaultPlistPath
121
- : rawPlistPath,
122
- launchAgentPath: runtime.launchAgentPath || defaultLaunchAgentPath,
123
- autoStartOnLogin:
124
- runtime.autoStartOnLogin === undefined ? true : Boolean(runtime.autoStartOnLogin),
125
- };
126
- }
127
-
128
- export function saveRuntimeConfig(runtime) {
129
- const normalized = normalizeRuntimeConfig(runtime);
130
- fs.mkdirSync(path.dirname(normalized.configFile), { recursive: true });
131
- fs.mkdirSync(path.dirname(normalized.stdoutLogPath), { recursive: true });
132
- fs.writeFileSync(normalized.configFile, `${stableStringify({
133
- backendUrl: normalized.backendUrl,
134
- intervalSeconds: normalized.intervalSeconds,
135
- codexAuthPath: normalized.codexAuthPath,
136
- sessionsDir: normalized.sessionsDir,
137
- stateDbPath: normalized.stateDbPath,
138
- installRoot: normalized.installRoot,
139
- appRoot: normalized.appRoot,
140
- currentAppDir: normalized.currentAppDir,
141
- nodePath: normalized.nodePath,
142
- entryFile: normalized.entryFile,
143
- packageSpec: normalized.packageSpec,
144
- localBinDir: normalized.localBinDir,
145
- localBinPath: normalized.localBinPath,
146
- homeBinLink: normalized.homeBinLink,
147
- stdoutLogPath: normalized.stdoutLogPath,
148
- stderrLogPath: normalized.stderrLogPath,
149
- launchdLabel: normalized.launchdLabel,
150
- plistPath: normalized.plistPath,
151
- launchAgentPath: normalized.launchAgentPath,
152
- autoStartOnLogin: normalized.autoStartOnLogin,
153
- })}\n`);
154
- return normalized;
155
- }
156
-
157
- export function mergeRuntimeConfig(configFile, overrides = {}) {
158
- const base = loadRuntimeConfig(configFile);
159
- return normalizeRuntimeConfig({ ...base, ...overrides, configFile });
160
- }
161
-
162
- export function formatStatusOutput(runtime, status) {
163
- return {
164
- configExists: fs.existsSync(runtime.configFile),
165
- loaded: status.loaded,
166
- running: status.running,
167
- pid: status.pid,
168
- state: status.state,
169
- lastExitCode: status.lastExitCode,
170
- backendUrl: runtime.backendUrl,
171
- intervalSeconds: runtime.intervalSeconds,
172
- configFile: runtime.configFile,
173
- stateDbPath: runtime.stateDbPath,
174
- stdoutLogPath: runtime.stdoutLogPath,
175
- stderrLogPath: runtime.stderrLogPath,
176
- plistPath: runtime.plistPath,
177
- launchAgentPath: runtime.launchAgentPath,
178
- label: runtime.launchdLabel,
179
- autoStartOnLogin: runtime.autoStartOnLogin,
180
- onlineThresholdSeconds: STATUS_ONLINE_THRESHOLD_SECONDS,
181
- };
182
- }
package/src/state-db.js DELETED
@@ -1,325 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { DatabaseSync } from 'node:sqlite';
4
- import {
5
- MAX_BATCH_BYTES,
6
- MAX_BUFFER_AGE_SECONDS,
7
- MAX_EVENTS_PER_BATCH,
8
- } from './constants.js';
9
- import { nowTs, stableStringify } from './utils.js';
10
-
11
- export class StateDb {
12
- constructor(dbPath) {
13
- this.dbPath = dbPath;
14
- fs.mkdirSync(path.dirname(dbPath), { recursive: true });
15
- this.db = new DatabaseSync(dbPath, { readBigInts: true });
16
- this.initSchema();
17
- }
18
-
19
- close() {
20
- this.db.close();
21
- }
22
-
23
- initSchema() {
24
- this.db.exec(`
25
- CREATE TABLE IF NOT EXISTS identity_config (
26
- key TEXT PRIMARY KEY,
27
- value TEXT NOT NULL,
28
- updated_at REAL NOT NULL
29
- );
30
-
31
- CREATE TABLE IF NOT EXISTS pending_batches (
32
- id INTEGER PRIMARY KEY AUTOINCREMENT,
33
- batch_key TEXT NOT NULL UNIQUE,
34
- status TEXT NOT NULL,
35
- payload_json TEXT NOT NULL,
36
- payload_bytes INTEGER NOT NULL DEFAULT 0,
37
- session_count INTEGER NOT NULL DEFAULT 0,
38
- turn_count INTEGER NOT NULL DEFAULT 0,
39
- event_count INTEGER NOT NULL DEFAULT 0,
40
- attempt_count INTEGER NOT NULL DEFAULT 0,
41
- next_retry_at REAL,
42
- last_error TEXT,
43
- created_at REAL NOT NULL,
44
- updated_at REAL NOT NULL
45
- );
46
-
47
- CREATE TABLE IF NOT EXISTS upload_checkpoint (
48
- key TEXT PRIMARY KEY,
49
- value TEXT NOT NULL,
50
- updated_at REAL NOT NULL
51
- );
52
- `);
53
- this.ensureIngestionFilesTable();
54
- }
55
-
56
- ensureIngestionFilesTable() {
57
- const existing = this.db
58
- .prepare(
59
- "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'ingestion_files'",
60
- )
61
- .get();
62
- if (!existing) {
63
- this.createIngestionFilesTable();
64
- return;
65
- }
66
-
67
- const columns = this.db.prepare('PRAGMA table_info(ingestion_files)').all();
68
- const hasSourceRoot = columns.some((column) => column.name === 'source_root');
69
- if (hasSourceRoot) {
70
- return;
71
- }
72
-
73
- this.db.exec('BEGIN');
74
- try {
75
- this.db.exec('ALTER TABLE ingestion_files RENAME TO ingestion_files_legacy');
76
- this.createIngestionFilesTable();
77
- this.db.prepare(`
78
- INSERT INTO ingestion_files(
79
- source_root, relpath, file_size, file_mtime_ns, last_line_no, state_json, updated_at
80
- )
81
- SELECT 'sessions', relpath, file_size, file_mtime_ns, last_line_no, state_json, updated_at
82
- FROM ingestion_files_legacy
83
- `).run();
84
- this.db.exec('DROP TABLE ingestion_files_legacy');
85
- this.db.exec('COMMIT');
86
- } catch (error) {
87
- this.db.exec('ROLLBACK');
88
- throw error;
89
- }
90
- }
91
-
92
- createIngestionFilesTable() {
93
- this.db.exec(`
94
- CREATE TABLE IF NOT EXISTS ingestion_files (
95
- source_root TEXT NOT NULL,
96
- relpath TEXT NOT NULL,
97
- file_size INTEGER NOT NULL,
98
- file_mtime_ns INTEGER NOT NULL,
99
- last_line_no INTEGER NOT NULL,
100
- state_json TEXT,
101
- updated_at REAL NOT NULL,
102
- PRIMARY KEY (source_root, relpath)
103
- );
104
- `);
105
- }
106
-
107
- getIdentity() {
108
- const rows = this.db.prepare('SELECT key, value FROM identity_config').all();
109
- return Object.fromEntries(rows.map((row) => [row.key, row.value]));
110
- }
111
-
112
- setIdentity(values) {
113
- const ts = nowTs();
114
- const upsert = this.db.prepare(`
115
- INSERT INTO identity_config(key, value, updated_at)
116
- VALUES (?, ?, ?)
117
- ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
118
- `);
119
- const del = this.db.prepare('DELETE FROM identity_config WHERE key = ?');
120
- this.db.exec('BEGIN');
121
- try {
122
- for (const [key, value] of Object.entries(values)) {
123
- if (value == null) {
124
- del.run(key);
125
- } else {
126
- upsert.run(key, value, ts);
127
- }
128
- }
129
- this.db.exec('COMMIT');
130
- } catch (error) {
131
- this.db.exec('ROLLBACK');
132
- throw error;
133
- }
134
- }
135
-
136
- getCheckpoint(key) {
137
- const row = this.db.prepare('SELECT value FROM upload_checkpoint WHERE key = ?').get(key);
138
- return row?.value ?? null;
139
- }
140
-
141
- setCheckpoint(key, value) {
142
- const ts = nowTs();
143
- this.db.prepare(`
144
- INSERT INTO upload_checkpoint(key, value, updated_at)
145
- VALUES (?, ?, ?)
146
- ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
147
- `).run(key, value, ts);
148
- }
149
-
150
- getFileState(sourceRoot, relpath) {
151
- const stmt = this.db.prepare(
152
- 'SELECT * FROM ingestion_files WHERE source_root = ? AND relpath = ?',
153
- );
154
- stmt.setReadBigInts(true);
155
- return stmt.get(sourceRoot, relpath) ?? null;
156
- }
157
-
158
- upsertFileState(sourceRoot, relpath, fileSize, fileMtimeNs, lastLineNo, state) {
159
- this.db.prepare(`
160
- INSERT INTO ingestion_files(
161
- source_root, relpath, file_size, file_mtime_ns, last_line_no, state_json, updated_at
162
- )
163
- VALUES (?, ?, ?, ?, ?, ?, ?)
164
- ON CONFLICT(source_root, relpath) DO UPDATE SET
165
- file_size = excluded.file_size,
166
- file_mtime_ns = excluded.file_mtime_ns,
167
- last_line_no = excluded.last_line_no,
168
- state_json = excluded.state_json,
169
- updated_at = excluded.updated_at
170
- `).run(
171
- sourceRoot,
172
- relpath,
173
- fileSize,
174
- fileMtimeNs,
175
- lastLineNo,
176
- stableStringify(state),
177
- nowTs(),
178
- );
179
- }
180
-
181
- getBufferingBatch() {
182
- const stmt = this.db.prepare(`
183
- SELECT * FROM pending_batches
184
- WHERE status = 'buffering'
185
- ORDER BY id ASC
186
- LIMIT 1
187
- `);
188
- stmt.setReadBigInts(true);
189
- return stmt.get() ?? null;
190
- }
191
-
192
- saveBufferingPayload(payload) {
193
- const existing = this.getBufferingBatch();
194
- const payloadJson = stableStringify(payload);
195
- const payloadBytes = Buffer.byteLength(payloadJson, 'utf8');
196
- const sessionCount = payload.sessions?.length ?? 0;
197
- const turnCount = payload.turns?.length ?? 0;
198
- const eventCount = payload.events?.length ?? 0;
199
- const ts = nowTs();
200
- if (existing) {
201
- this.db.prepare(`
202
- UPDATE pending_batches
203
- SET payload_json = ?, payload_bytes = ?, session_count = ?, turn_count = ?, event_count = ?, updated_at = ?
204
- WHERE id = ?
205
- `).run(payloadJson, payloadBytes, sessionCount, turnCount, eventCount, ts, existing.id);
206
- } else {
207
- this.db.prepare(`
208
- INSERT INTO pending_batches(
209
- batch_key, status, payload_json, payload_bytes, session_count, turn_count, event_count,
210
- attempt_count, created_at, updated_at
211
- ) VALUES (?, 'buffering', ?, ?, ?, ?, ?, 0, ?, ?)
212
- `).run(randomBatchKey(), payloadJson, payloadBytes, sessionCount, turnCount, eventCount, ts, ts);
213
- }
214
- return this.getBufferingBatch();
215
- }
216
-
217
- sealStaleBatches(force = false) {
218
- const selectStmt = this.db.prepare(
219
- "SELECT id, created_at FROM pending_batches WHERE status = 'buffering'",
220
- );
221
- selectStmt.setReadBigInts(true);
222
- const rows = selectStmt.all();
223
- let sealed = 0;
224
- const ts = nowTs();
225
- const updateStmt = this.db.prepare(`
226
- UPDATE pending_batches
227
- SET status = 'pending', updated_at = ?
228
- WHERE id = ?
229
- `);
230
- for (const row of rows) {
231
- if (force || ts - Number(row.created_at) >= MAX_BUFFER_AGE_SECONDS) {
232
- updateStmt.run(ts, row.id);
233
- sealed += 1;
234
- }
235
- }
236
- return sealed;
237
- }
238
-
239
- sealBufferIfThresholdHit() {
240
- const row = this.getBufferingBatch();
241
- if (!row) return false;
242
- const shouldSeal =
243
- Number(row.event_count) >= MAX_EVENTS_PER_BATCH ||
244
- Number(row.payload_bytes) >= MAX_BATCH_BYTES ||
245
- nowTs() - Number(row.created_at) >= MAX_BUFFER_AGE_SECONDS;
246
- if (shouldSeal) {
247
- const ts = nowTs();
248
- this.db.prepare(`
249
- UPDATE pending_batches
250
- SET status = 'pending', updated_at = ?
251
- WHERE id = ?
252
- `).run(ts, row.id);
253
- }
254
- return shouldSeal;
255
- }
256
-
257
- iterPendingBatches() {
258
- const stmt = this.db.prepare(`
259
- SELECT * FROM pending_batches
260
- WHERE status = 'pending'
261
- ORDER BY created_at ASC, id ASC
262
- `);
263
- stmt.setReadBigInts(true);
264
- return stmt.all();
265
- }
266
-
267
- markBatchUploaded(batchId) {
268
- this.db.prepare('DELETE FROM pending_batches WHERE id = ?').run(batchId);
269
- }
270
-
271
- markBatchFailed(batchId, attemptCount, errorMessage) {
272
- this.db.prepare(`
273
- UPDATE pending_batches
274
- SET attempt_count = ?, last_error = ?, updated_at = ?
275
- WHERE id = ?
276
- `).run(attemptCount, String(errorMessage).slice(0, 1000), nowTs(), batchId);
277
- }
278
-
279
- resetBackfillState() {
280
- this.db.exec('BEGIN');
281
- try {
282
- this.db.prepare('DELETE FROM ingestion_files').run();
283
- this.db.prepare('DELETE FROM pending_batches').run();
284
- this.db.prepare('DELETE FROM upload_checkpoint').run();
285
- this.db.exec('COMMIT');
286
- } catch (error) {
287
- this.db.exec('ROLLBACK');
288
- throw error;
289
- }
290
- }
291
-
292
- getQueueStats() {
293
- const stmt = this.db.prepare(`
294
- SELECT
295
- COALESCE(SUM(CASE WHEN status = 'buffering' THEN 1 ELSE 0 END), 0) AS buffering_batch_count,
296
- COALESCE(SUM(CASE WHEN status = 'pending' AND attempt_count = 0 THEN 1 ELSE 0 END), 0) AS pending_batch_count,
297
- COALESCE(SUM(CASE WHEN status = 'pending' AND attempt_count > 0 THEN 1 ELSE 0 END), 0) AS retrying_batch_count,
298
- COALESCE(SUM(session_count), 0) AS queued_sessions,
299
- COALESCE(SUM(turn_count), 0) AS queued_turns,
300
- COALESCE(SUM(event_count), 0) AS queued_events,
301
- MIN(created_at) AS oldest_created_at
302
- FROM pending_batches
303
- `);
304
- stmt.setReadBigInts(true);
305
- const row = stmt.get() ?? {};
306
- const oldestCreatedAt =
307
- row.oldest_created_at == null ? null : Number(row.oldest_created_at);
308
- return {
309
- bufferingBatchCount: Number(row.buffering_batch_count ?? 0),
310
- pendingBatchCount: Number(row.pending_batch_count ?? 0),
311
- retryingBatchCount: Number(row.retrying_batch_count ?? 0),
312
- queuedSessions: Number(row.queued_sessions ?? 0),
313
- queuedTurns: Number(row.queued_turns ?? 0),
314
- queuedEvents: Number(row.queued_events ?? 0),
315
- oldestPendingAgeSeconds:
316
- oldestCreatedAt == null
317
- ? null
318
- : Math.max(0, Math.floor(nowTs() - oldestCreatedAt)),
319
- };
320
- }
321
- }
322
-
323
- function randomBatchKey() {
324
- return `${Date.now().toString(16)}${Math.random().toString(16).slice(2, 14)}`;
325
- }