@token-dashboard/codex-usage-uploader 0.1.6 → 0.1.7
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/bin/codex-usage-uploader.js +5 -0
- package/dist/cli.mjs +1 -0
- package/package.json +9 -4
- package/bin/codex-usage-uploader.js +0 -9
- package/src/auth.js +0 -56
- package/src/cli.js +0 -759
- package/src/collector.js +0 -676
- package/src/constants.js +0 -44
- package/src/install.js +0 -156
- package/src/launchd.js +0 -170
- package/src/local-usage.js +0 -342
- package/src/parser.js +0 -180
- package/src/runtime-config.js +0 -182
- package/src/state-db.js +0 -325
- package/src/utils.js +0 -68
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
|
-
}
|
package/src/runtime-config.js
DELETED
|
@@ -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
|
-
}
|