llm-cli-gateway 2.13.2 → 2.14.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/CHANGELOG.md +139 -0
- package/README.md +68 -29
- package/dist/acp/client.d.ts +29 -1
- package/dist/acp/client.js +78 -4
- package/dist/acp/errors.d.ts +9 -1
- package/dist/acp/errors.js +19 -0
- package/dist/acp/event-normalizer.d.ts +12 -0
- package/dist/acp/event-normalizer.js +16 -0
- package/dist/acp/flight-redaction.d.ts +3 -0
- package/dist/acp/flight-redaction.js +3 -0
- package/dist/acp/permission-bridge.js +11 -5
- package/dist/acp/process-manager.d.ts +2 -1
- package/dist/acp/process-manager.js +43 -4
- package/dist/acp/provider-registry.js +19 -11
- package/dist/acp/runtime.d.ts +8 -0
- package/dist/acp/runtime.js +47 -4
- package/dist/acp/types.d.ts +3083 -55
- package/dist/acp/types.js +242 -5
- package/dist/async-job-manager.d.ts +38 -1
- package/dist/async-job-manager.js +287 -20
- package/dist/codex-json-parser.d.ts +1 -0
- package/dist/codex-json-parser.js +6 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +84 -1
- package/dist/flight-recorder.d.ts +2 -0
- package/dist/flight-recorder.js +20 -0
- package/dist/gemini-json-parser.d.ts +2 -0
- package/dist/gemini-json-parser.js +45 -8
- package/dist/grok-json-parser.d.ts +14 -0
- package/dist/grok-json-parser.js +156 -0
- package/dist/index.d.ts +47 -2
- package/dist/index.js +504 -122
- package/dist/job-store.d.ts +119 -11
- package/dist/job-store.js +372 -42
- package/dist/model-registry.d.ts +1 -0
- package/dist/model-registry.js +46 -0
- package/dist/oauth.js +2 -2
- package/dist/postgres-job-store-worker.d.ts +1 -0
- package/dist/postgres-job-store-worker.js +444 -0
- package/dist/pricing.d.ts +3 -2
- package/dist/provider-acp-capabilities.d.ts +52 -0
- package/dist/provider-acp-capabilities.js +101 -0
- package/dist/provider-admin-tools.d.ts +100 -0
- package/dist/provider-admin-tools.js +572 -0
- package/dist/provider-capability-cache.d.ts +46 -0
- package/dist/provider-capability-cache.js +248 -0
- package/dist/provider-capability-discovery.d.ts +85 -0
- package/dist/provider-capability-discovery.js +461 -0
- package/dist/provider-capability-resolver.d.ts +29 -0
- package/dist/provider-capability-resolver.js +92 -0
- package/dist/provider-definition-assertions.d.ts +9 -0
- package/dist/provider-definition-assertions.js +147 -0
- package/dist/provider-definitions.d.ts +127 -0
- package/dist/provider-definitions.js +758 -0
- package/dist/provider-help-parser.d.ts +34 -0
- package/dist/provider-help-parser.js +203 -0
- package/dist/provider-model-discovery.d.ts +30 -0
- package/dist/provider-model-discovery.js +229 -0
- package/dist/provider-output-metadata.d.ts +7 -0
- package/dist/provider-output-metadata.js +68 -0
- package/dist/provider-schema-builder.d.ts +22 -0
- package/dist/provider-schema-builder.js +55 -0
- package/dist/provider-surface-generator.d.ts +98 -0
- package/dist/provider-surface-generator.js +140 -0
- package/dist/provider-tool-capabilities.d.ts +3 -2
- package/dist/provider-tool-capabilities.js +77 -62
- package/dist/request-helpers.d.ts +37 -4
- package/dist/request-helpers.js +134 -0
- package/dist/resources.d.ts +6 -1
- package/dist/resources.js +64 -192
- package/dist/session-manager.js +18 -11
- package/dist/sqlite-driver.d.ts +1 -1
- package/dist/sqlite-driver.js +2 -1
- package/dist/stream-json-parser.d.ts +1 -0
- package/dist/stream-json-parser.js +3 -0
- package/dist/upstream-contracts.d.ts +17 -1
- package/dist/upstream-contracts.js +209 -36
- package/npm-shrinkwrap.json +2 -2
- package/package.json +8 -3
package/dist/job-store.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { chmodSync } from "fs";
|
|
1
|
+
import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync } from "fs";
|
|
2
2
|
import os from "os";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { createHash } from "crypto";
|
|
5
|
+
import { Worker } from "worker_threads";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
5
7
|
import { openDatabase } from "./sqlite-driver.js";
|
|
6
8
|
import { noopLogger } from "./logger.js";
|
|
9
|
+
import { DEFAULT_INSTANCE_LEASE_TTL_MS, DEFAULT_HTTP_JOB_GRACE_MS } from "./config.js";
|
|
7
10
|
export function resolveJobStoreDbPath() {
|
|
8
11
|
const configured = process.env.LLM_GATEWAY_JOBS_DB ?? process.env.LLM_GATEWAY_LOGS_DB;
|
|
9
12
|
if (configured !== undefined) {
|
|
@@ -17,6 +20,7 @@ export function resolveJobStoreDbPath() {
|
|
|
17
20
|
}
|
|
18
21
|
const DEFAULT_RETENTION_DAYS = 30;
|
|
19
22
|
const FAR_FUTURE_ISO = "9999-12-31T23:59:59.999Z";
|
|
23
|
+
const SQLITE_NOW_MS = "CAST(ROUND((julianday('now') - 2440587.5) * 86400000.0) AS INTEGER)";
|
|
20
24
|
export function resolveJobRetentionMs() {
|
|
21
25
|
const raw = process.env.LLM_GATEWAY_JOB_RETENTION_DAYS;
|
|
22
26
|
const days = raw ? Number(raw) : DEFAULT_RETENTION_DAYS;
|
|
@@ -61,6 +65,8 @@ function rowToRecord(row) {
|
|
|
61
65
|
transport: row.transport ?? "process",
|
|
62
66
|
httpStatus: row.http_status ?? null,
|
|
63
67
|
payloadJson: row.payload_json ?? null,
|
|
68
|
+
ownerInstance: row.owner_instance ?? null,
|
|
69
|
+
leaseDeadline: row.lease_deadline == null ? null : Number(row.lease_deadline),
|
|
64
70
|
};
|
|
65
71
|
}
|
|
66
72
|
function ensureJobsOwnerColumn(db) {
|
|
@@ -83,6 +89,16 @@ function ensureJobsTransportColumns(db) {
|
|
|
83
89
|
db.exec("ALTER TABLE jobs ADD COLUMN payload_json TEXT");
|
|
84
90
|
}
|
|
85
91
|
}
|
|
92
|
+
function ensureJobsLeaseColumns(db) {
|
|
93
|
+
const cols = db.prepare("PRAGMA table_info(jobs)").all();
|
|
94
|
+
const names = new Set(cols.map(col => col?.name));
|
|
95
|
+
if (!names.has("owner_instance")) {
|
|
96
|
+
db.exec("ALTER TABLE jobs ADD COLUMN owner_instance TEXT");
|
|
97
|
+
}
|
|
98
|
+
if (!names.has("lease_deadline")) {
|
|
99
|
+
db.exec("ALTER TABLE jobs ADD COLUMN lease_deadline INTEGER");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
86
102
|
export function isValidationRunStore(store) {
|
|
87
103
|
return (typeof store === "object" &&
|
|
88
104
|
store !== null &&
|
|
@@ -95,6 +111,7 @@ export class SqliteJobStore {
|
|
|
95
111
|
db;
|
|
96
112
|
retentionMs;
|
|
97
113
|
dedupWindowMs;
|
|
114
|
+
leaseTtlMs;
|
|
98
115
|
insertStmt;
|
|
99
116
|
updateOutputStmt;
|
|
100
117
|
updateCompleteStmt;
|
|
@@ -103,11 +120,21 @@ export class SqliteJobStore {
|
|
|
103
120
|
selectRunningOrphansStmt;
|
|
104
121
|
markOrphanedStmt;
|
|
105
122
|
deleteExpiredStmt;
|
|
123
|
+
markRunningStmt;
|
|
124
|
+
registerInstanceStmt;
|
|
125
|
+
heartbeatInstanceStmt;
|
|
126
|
+
heartbeatJobsStmt;
|
|
127
|
+
deregisterInstanceStmt;
|
|
128
|
+
selectStaleCandidatesStmt;
|
|
129
|
+
orphanExpiredStmt;
|
|
130
|
+
advanceLeaseStmt;
|
|
131
|
+
gcInstancesStmt;
|
|
106
132
|
constructor(dbPath, logger = noopLogger, options = {}) {
|
|
107
133
|
this.logger = logger;
|
|
108
134
|
this.db = openDatabase(dbPath);
|
|
109
135
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
110
136
|
this.db.exec("PRAGMA synchronous = NORMAL");
|
|
137
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
111
138
|
this.db.exec(`
|
|
112
139
|
CREATE TABLE IF NOT EXISTS jobs (
|
|
113
140
|
id TEXT PRIMARY KEY,
|
|
@@ -129,12 +156,25 @@ export class SqliteJobStore {
|
|
|
129
156
|
owner_principal TEXT,
|
|
130
157
|
transport TEXT NOT NULL DEFAULT 'process',
|
|
131
158
|
http_status INTEGER,
|
|
132
|
-
payload_json TEXT
|
|
159
|
+
payload_json TEXT,
|
|
160
|
+
owner_instance TEXT,
|
|
161
|
+
lease_deadline INTEGER
|
|
133
162
|
);
|
|
134
163
|
CREATE INDEX IF NOT EXISTS idx_jobs_request_key ON jobs(request_key);
|
|
135
164
|
CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
|
|
136
165
|
CREATE INDEX IF NOT EXISTS idx_jobs_expires_at ON jobs(expires_at);
|
|
137
166
|
CREATE INDEX IF NOT EXISTS idx_jobs_request_key_finished ON jobs(request_key, finished_at);
|
|
167
|
+
|
|
168
|
+
CREATE TABLE IF NOT EXISTS gateway_instances (
|
|
169
|
+
instance_id TEXT PRIMARY KEY,
|
|
170
|
+
role TEXT,
|
|
171
|
+
hostname TEXT,
|
|
172
|
+
pid INTEGER,
|
|
173
|
+
started_at INTEGER NOT NULL,
|
|
174
|
+
last_heartbeat INTEGER NOT NULL
|
|
175
|
+
);
|
|
176
|
+
CREATE INDEX IF NOT EXISTS idx_gateway_instances_heartbeat
|
|
177
|
+
ON gateway_instances(last_heartbeat);
|
|
138
178
|
`);
|
|
139
179
|
this.db.exec(`
|
|
140
180
|
CREATE TABLE IF NOT EXISTS validation_runs (
|
|
@@ -174,6 +214,8 @@ export class SqliteJobStore {
|
|
|
174
214
|
`);
|
|
175
215
|
ensureJobsOwnerColumn(this.db);
|
|
176
216
|
ensureJobsTransportColumns(this.db);
|
|
217
|
+
ensureJobsLeaseColumns(this.db);
|
|
218
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_jobs_owner_status ON jobs(owner_instance, status)");
|
|
177
219
|
if (process.platform !== "win32") {
|
|
178
220
|
try {
|
|
179
221
|
chmodSync(dbPath, 0o600);
|
|
@@ -183,15 +225,17 @@ export class SqliteJobStore {
|
|
|
183
225
|
}
|
|
184
226
|
this.retentionMs = options.retentionMs ?? resolveJobRetentionMs();
|
|
185
227
|
this.dedupWindowMs = options.dedupWindowMs ?? resolveDedupWindowMs();
|
|
228
|
+
this.leaseTtlMs = options.leaseTtlMs ?? DEFAULT_INSTANCE_LEASE_TTL_MS;
|
|
186
229
|
this.insertStmt = this.db.prepare(`
|
|
187
230
|
INSERT INTO jobs (id, correlation_id, request_key, cli, args_json, output_format,
|
|
188
231
|
status, exit_code, stdout, stderr, output_truncated, error,
|
|
189
232
|
started_at, finished_at, pid, expires_at, owner_principal,
|
|
190
|
-
transport, http_status, payload_json)
|
|
233
|
+
transport, http_status, payload_json, owner_instance, lease_deadline)
|
|
191
234
|
VALUES (@id, @correlation_id, @request_key, @cli, @args_json, @output_format,
|
|
192
|
-
|
|
235
|
+
'queued', @exit_code, @stdout, @stderr, @output_truncated, @error,
|
|
193
236
|
@started_at, @finished_at, @pid, @expires_at, @owner_principal,
|
|
194
|
-
@transport, @http_status, @payload_json
|
|
237
|
+
@transport, @http_status, @payload_json, @owner_instance,
|
|
238
|
+
${SQLITE_NOW_MS} + @lease_ttl_ms)
|
|
195
239
|
`);
|
|
196
240
|
this.updateOutputStmt = this.db.prepare(`
|
|
197
241
|
UPDATE jobs SET stdout = @stdout, stderr = @stderr, output_truncated = @output_truncated
|
|
@@ -201,15 +245,18 @@ export class SqliteJobStore {
|
|
|
201
245
|
UPDATE jobs SET status = @status, exit_code = @exit_code, stdout = @stdout, stderr = @stderr,
|
|
202
246
|
output_truncated = @output_truncated, error = @error,
|
|
203
247
|
finished_at = @finished_at, expires_at = @expires_at,
|
|
204
|
-
http_status = @http_status
|
|
205
|
-
WHERE id = @id
|
|
248
|
+
http_status = @http_status, lease_deadline = NULL
|
|
249
|
+
WHERE id = @id AND status IN ('queued', 'running', 'orphaned')
|
|
206
250
|
`);
|
|
207
251
|
this.getByIdStmt = this.db.prepare(`SELECT * FROM jobs WHERE id = ?`);
|
|
208
252
|
this.findByRequestKeyStmt = this.db.prepare(`
|
|
209
253
|
SELECT * FROM jobs
|
|
210
254
|
WHERE request_key = ?
|
|
211
255
|
AND started_at >= ?
|
|
212
|
-
AND
|
|
256
|
+
AND (
|
|
257
|
+
status IN ('running', 'completed')
|
|
258
|
+
OR (status = 'queued' AND lease_deadline IS NOT NULL AND lease_deadline >= ${SQLITE_NOW_MS})
|
|
259
|
+
)
|
|
213
260
|
ORDER BY started_at DESC
|
|
214
261
|
LIMIT 1
|
|
215
262
|
`);
|
|
@@ -226,6 +273,56 @@ export class SqliteJobStore {
|
|
|
226
273
|
WHERE status = 'running'
|
|
227
274
|
`);
|
|
228
275
|
this.deleteExpiredStmt = this.db.prepare(`DELETE FROM jobs WHERE expires_at < ?`);
|
|
276
|
+
this.markRunningStmt = this.db.prepare(`
|
|
277
|
+
UPDATE jobs
|
|
278
|
+
SET status = 'running', pid = @pid, lease_deadline = ${SQLITE_NOW_MS} + @lease_ttl_ms
|
|
279
|
+
WHERE id = @id AND status = 'queued'
|
|
280
|
+
`);
|
|
281
|
+
this.registerInstanceStmt = this.db.prepare(`
|
|
282
|
+
INSERT INTO gateway_instances (instance_id, role, hostname, pid, started_at, last_heartbeat)
|
|
283
|
+
VALUES (@instance_id, @role, @hostname, @pid, ${SQLITE_NOW_MS}, ${SQLITE_NOW_MS})
|
|
284
|
+
ON CONFLICT(instance_id) DO UPDATE SET
|
|
285
|
+
role = excluded.role, hostname = excluded.hostname, pid = excluded.pid,
|
|
286
|
+
last_heartbeat = excluded.last_heartbeat
|
|
287
|
+
`);
|
|
288
|
+
this.heartbeatInstanceStmt = this.db.prepare(`
|
|
289
|
+
UPDATE gateway_instances SET last_heartbeat = ${SQLITE_NOW_MS} WHERE instance_id = @instance_id
|
|
290
|
+
`);
|
|
291
|
+
this.heartbeatJobsStmt = this.db.prepare(`
|
|
292
|
+
UPDATE jobs SET lease_deadline = ${SQLITE_NOW_MS} + @lease_ttl_ms
|
|
293
|
+
WHERE owner_instance = @instance_id AND status IN ('queued', 'running')
|
|
294
|
+
`);
|
|
295
|
+
this.deregisterInstanceStmt = this.db.prepare(`DELETE FROM gateway_instances WHERE instance_id = @instance_id`);
|
|
296
|
+
this.selectStaleCandidatesStmt = this.db.prepare(`
|
|
297
|
+
SELECT j.id AS id, j.pid AS pid, j.transport AS transport,
|
|
298
|
+
j.owner_instance AS owner_instance, gi.hostname AS hostname
|
|
299
|
+
FROM jobs j
|
|
300
|
+
LEFT JOIN gateway_instances gi ON gi.instance_id = j.owner_instance
|
|
301
|
+
WHERE j.status IN ('queued', 'running')
|
|
302
|
+
AND j.transport = 'process'
|
|
303
|
+
AND j.pid IS NOT NULL
|
|
304
|
+
AND (j.lease_deadline IS NULL OR j.lease_deadline < ${SQLITE_NOW_MS})
|
|
305
|
+
`);
|
|
306
|
+
this.orphanExpiredStmt = this.db.prepare(`
|
|
307
|
+
UPDATE jobs
|
|
308
|
+
SET status = 'orphaned',
|
|
309
|
+
error = COALESCE(error, 'owning gateway instance is no longer alive'),
|
|
310
|
+
finished_at = COALESCE(finished_at, @now_iso),
|
|
311
|
+
expires_at = @expires_iso,
|
|
312
|
+
lease_deadline = NULL
|
|
313
|
+
WHERE status IN ('queued', 'running')
|
|
314
|
+
AND (lease_deadline IS NULL OR lease_deadline < ${SQLITE_NOW_MS})
|
|
315
|
+
AND (transport <> 'http'
|
|
316
|
+
OR started_at < strftime('%Y-%m-%dT%H:%M:%fZ', 'now', @http_grace_modifier))
|
|
317
|
+
AND id NOT IN (SELECT value FROM json_each(@exclude_json))
|
|
318
|
+
RETURNING id, correlation_id, started_at, stdout, stderr, exit_code, transport, http_status
|
|
319
|
+
`);
|
|
320
|
+
this.advanceLeaseStmt = this.db.prepare(`
|
|
321
|
+
UPDATE jobs SET lease_deadline = ${SQLITE_NOW_MS} + @lease_ttl_ms, pid = NULL
|
|
322
|
+
WHERE status IN ('queued', 'running')
|
|
323
|
+
AND id IN (SELECT value FROM json_each(@ids_json))
|
|
324
|
+
`);
|
|
325
|
+
this.gcInstancesStmt = this.db.prepare(`DELETE FROM gateway_instances WHERE last_heartbeat < ${SQLITE_NOW_MS} - @gc_ms`);
|
|
229
326
|
}
|
|
230
327
|
recordStart(input) {
|
|
231
328
|
this.insertStmt.run({
|
|
@@ -235,7 +332,6 @@ export class SqliteJobStore {
|
|
|
235
332
|
cli: input.cli,
|
|
236
333
|
args_json: JSON.stringify(input.args),
|
|
237
334
|
output_format: input.outputFormat ?? null,
|
|
238
|
-
status: "running",
|
|
239
335
|
exit_code: null,
|
|
240
336
|
stdout: "",
|
|
241
337
|
stderr: "",
|
|
@@ -249,7 +345,74 @@ export class SqliteJobStore {
|
|
|
249
345
|
transport: input.transport ?? "process",
|
|
250
346
|
http_status: null,
|
|
251
347
|
payload_json: input.payloadJson ?? null,
|
|
348
|
+
owner_instance: input.ownerInstance ?? null,
|
|
349
|
+
lease_ttl_ms: this.leaseTtlMs,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
markRunning(id, opts) {
|
|
353
|
+
const result = this.markRunningStmt.run({
|
|
354
|
+
id,
|
|
355
|
+
pid: opts.pid,
|
|
356
|
+
lease_ttl_ms: this.leaseTtlMs,
|
|
252
357
|
});
|
|
358
|
+
return Number(result.changes) > 0;
|
|
359
|
+
}
|
|
360
|
+
registerInstance(meta) {
|
|
361
|
+
this.registerInstanceStmt.run({
|
|
362
|
+
instance_id: meta.instanceId,
|
|
363
|
+
role: meta.role ?? null,
|
|
364
|
+
hostname: meta.hostname ?? null,
|
|
365
|
+
pid: meta.pid ?? null,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
heartbeat(instanceId) {
|
|
369
|
+
this.heartbeatInstanceStmt.run({ instance_id: instanceId });
|
|
370
|
+
this.heartbeatJobsStmt.run({ instance_id: instanceId, lease_ttl_ms: this.leaseTtlMs });
|
|
371
|
+
}
|
|
372
|
+
deregisterInstance(instanceId) {
|
|
373
|
+
this.deregisterInstanceStmt.run({ instance_id: instanceId });
|
|
374
|
+
}
|
|
375
|
+
selectStaleProcessCandidates(_leaseTtlMs, _httpJobGraceMs) {
|
|
376
|
+
const rows = this.selectStaleCandidatesStmt.all();
|
|
377
|
+
return rows.map(r => ({
|
|
378
|
+
id: r.id,
|
|
379
|
+
pid: r.pid,
|
|
380
|
+
transport: r.transport ?? "process",
|
|
381
|
+
ownerInstance: r.owner_instance ?? null,
|
|
382
|
+
hostname: r.hostname ?? null,
|
|
383
|
+
}));
|
|
384
|
+
}
|
|
385
|
+
recoverStaleJobs(leaseTtlMs, httpJobGraceMs, liveConfirmedIds = []) {
|
|
386
|
+
const excludeJson = JSON.stringify(liveConfirmedIds);
|
|
387
|
+
const httpGraceModifier = `-${httpJobGraceMs / 1000} seconds`;
|
|
388
|
+
const run = this.db.withTransaction(() => {
|
|
389
|
+
if (liveConfirmedIds.length > 0) {
|
|
390
|
+
this.advanceLeaseStmt.run({ ids_json: excludeJson, lease_ttl_ms: leaseTtlMs });
|
|
391
|
+
}
|
|
392
|
+
const nowIso = new Date().toISOString();
|
|
393
|
+
const expiresAt = new Date(Date.now() + this.retentionMs).toISOString();
|
|
394
|
+
const rows = this.orphanExpiredStmt.all({
|
|
395
|
+
now_iso: nowIso,
|
|
396
|
+
expires_iso: expiresAt,
|
|
397
|
+
http_grace_modifier: httpGraceModifier,
|
|
398
|
+
exclude_json: excludeJson,
|
|
399
|
+
});
|
|
400
|
+
return rows.map(r => ({
|
|
401
|
+
id: r.id,
|
|
402
|
+
correlationId: r.correlation_id,
|
|
403
|
+
startedAt: r.started_at,
|
|
404
|
+
stdout: r.stdout ?? "",
|
|
405
|
+
stderr: r.stderr ?? "",
|
|
406
|
+
exitCode: r.exit_code,
|
|
407
|
+
transport: r.transport ?? "process",
|
|
408
|
+
httpStatus: r.http_status ?? null,
|
|
409
|
+
}));
|
|
410
|
+
});
|
|
411
|
+
return run();
|
|
412
|
+
}
|
|
413
|
+
gcInstances(instanceGcMs) {
|
|
414
|
+
const result = this.gcInstancesStmt.run({ gc_ms: instanceGcMs });
|
|
415
|
+
return Number(result.changes);
|
|
253
416
|
}
|
|
254
417
|
recordOutput(id, stdout, stderr, outputTruncated) {
|
|
255
418
|
this.updateOutputStmt.run({
|
|
@@ -284,21 +447,8 @@ export class SqliteJobStore {
|
|
|
284
447
|
return row ? rowToRecord(row) : null;
|
|
285
448
|
}
|
|
286
449
|
markOrphanedOnStartup() {
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
const rows = this.selectRunningOrphansStmt.all();
|
|
290
|
-
const orphaned = rows.map(row => ({
|
|
291
|
-
id: row.id,
|
|
292
|
-
correlationId: row.correlation_id,
|
|
293
|
-
startedAt: row.started_at,
|
|
294
|
-
stdout: row.stdout ?? "",
|
|
295
|
-
stderr: row.stderr ?? "",
|
|
296
|
-
exitCode: row.exit_code,
|
|
297
|
-
transport: row.transport ?? "process",
|
|
298
|
-
httpStatus: row.http_status ?? null,
|
|
299
|
-
}));
|
|
300
|
-
const result = this.markOrphanedStmt.run(now, expiresAt);
|
|
301
|
-
return { count: Number(result.changes), orphaned };
|
|
450
|
+
const orphaned = this.recoverStaleJobs(this.leaseTtlMs, DEFAULT_HTTP_JOB_GRACE_MS);
|
|
451
|
+
return { count: orphaned.length, orphaned };
|
|
302
452
|
}
|
|
303
453
|
evictExpired() {
|
|
304
454
|
const now = new Date().toISOString();
|
|
@@ -460,9 +610,11 @@ export class MemoryJobStore {
|
|
|
460
610
|
rows = new Map();
|
|
461
611
|
retentionMs;
|
|
462
612
|
dedupWindowMs;
|
|
613
|
+
leaseTtlMs;
|
|
463
614
|
constructor(options = {}) {
|
|
464
615
|
this.retentionMs = options.retentionMs ?? resolveJobRetentionMs();
|
|
465
616
|
this.dedupWindowMs = options.dedupWindowMs ?? resolveDedupWindowMs();
|
|
617
|
+
this.leaseTtlMs = options.leaseTtlMs ?? DEFAULT_INSTANCE_LEASE_TTL_MS;
|
|
466
618
|
}
|
|
467
619
|
recordStart(input) {
|
|
468
620
|
this.rows.set(input.id, {
|
|
@@ -472,7 +624,7 @@ export class MemoryJobStore {
|
|
|
472
624
|
cli: input.cli,
|
|
473
625
|
argsJson: JSON.stringify(input.args),
|
|
474
626
|
outputFormat: input.outputFormat ?? null,
|
|
475
|
-
status: "
|
|
627
|
+
status: "queued",
|
|
476
628
|
exitCode: null,
|
|
477
629
|
stdout: "",
|
|
478
630
|
stderr: "",
|
|
@@ -486,8 +638,39 @@ export class MemoryJobStore {
|
|
|
486
638
|
transport: input.transport ?? "process",
|
|
487
639
|
httpStatus: null,
|
|
488
640
|
payloadJson: input.payloadJson ?? null,
|
|
641
|
+
ownerInstance: input.ownerInstance ?? null,
|
|
642
|
+
leaseDeadline: Date.now() + this.leaseTtlMs,
|
|
489
643
|
});
|
|
490
644
|
}
|
|
645
|
+
markRunning(id, opts) {
|
|
646
|
+
const row = this.rows.get(id);
|
|
647
|
+
if (!row || row.status !== "queued")
|
|
648
|
+
return false;
|
|
649
|
+
row.status = "running";
|
|
650
|
+
row.pid = opts.pid;
|
|
651
|
+
row.leaseDeadline = Date.now() + this.leaseTtlMs;
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
registerInstance(_meta) { }
|
|
655
|
+
heartbeat(instanceId) {
|
|
656
|
+
const deadline = Date.now() + this.leaseTtlMs;
|
|
657
|
+
for (const row of this.rows.values()) {
|
|
658
|
+
if (row.ownerInstance === instanceId &&
|
|
659
|
+
(row.status === "queued" || row.status === "running")) {
|
|
660
|
+
row.leaseDeadline = deadline;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
deregisterInstance(_instanceId) { }
|
|
665
|
+
selectStaleProcessCandidates(_leaseTtlMs, _httpJobGraceMs) {
|
|
666
|
+
return [];
|
|
667
|
+
}
|
|
668
|
+
recoverStaleJobs(_leaseTtlMs, _httpJobGraceMs, _liveConfirmedIds) {
|
|
669
|
+
return [];
|
|
670
|
+
}
|
|
671
|
+
gcInstances(_instanceGcMs) {
|
|
672
|
+
return 0;
|
|
673
|
+
}
|
|
491
674
|
recordOutput(id, stdout, stderr, outputTruncated) {
|
|
492
675
|
const row = this.rows.get(id);
|
|
493
676
|
if (!row)
|
|
@@ -500,6 +683,8 @@ export class MemoryJobStore {
|
|
|
500
683
|
const row = this.rows.get(input.id);
|
|
501
684
|
if (!row)
|
|
502
685
|
return;
|
|
686
|
+
if (row.status !== "queued" && row.status !== "running" && row.status !== "orphaned")
|
|
687
|
+
return;
|
|
503
688
|
row.status = input.status;
|
|
504
689
|
row.exitCode = input.exitCode;
|
|
505
690
|
row.stdout = input.stdout;
|
|
@@ -508,6 +693,7 @@ export class MemoryJobStore {
|
|
|
508
693
|
row.error = input.error;
|
|
509
694
|
row.finishedAt = input.finishedAt;
|
|
510
695
|
row.expiresAt = new Date(Date.parse(input.finishedAt) + this.retentionMs).toISOString();
|
|
696
|
+
row.leaseDeadline = null;
|
|
511
697
|
if (input.httpStatus !== undefined)
|
|
512
698
|
row.httpStatus = input.httpStatus;
|
|
513
699
|
}
|
|
@@ -517,11 +703,15 @@ export class MemoryJobStore {
|
|
|
517
703
|
}
|
|
518
704
|
findByRequestKey(requestKey) {
|
|
519
705
|
const cutoffMs = Date.now() - this.dedupWindowMs;
|
|
706
|
+
const nowMs = Date.now();
|
|
520
707
|
let best = null;
|
|
521
708
|
for (const row of this.rows.values()) {
|
|
522
709
|
if (row.requestKey !== requestKey)
|
|
523
710
|
continue;
|
|
524
|
-
|
|
711
|
+
const reusable = row.status === "running" ||
|
|
712
|
+
row.status === "completed" ||
|
|
713
|
+
(row.status === "queued" && row.leaseDeadline !== null && row.leaseDeadline >= nowMs);
|
|
714
|
+
if (!reusable)
|
|
525
715
|
continue;
|
|
526
716
|
if (Date.parse(row.startedAt) < cutoffMs)
|
|
527
717
|
continue;
|
|
@@ -550,38 +740,178 @@ export class MemoryJobStore {
|
|
|
550
740
|
}
|
|
551
741
|
}
|
|
552
742
|
export class PostgresJobStore {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
743
|
+
logger;
|
|
744
|
+
worker;
|
|
745
|
+
tmpDir;
|
|
746
|
+
nextRequestId = 0;
|
|
747
|
+
closed = false;
|
|
748
|
+
constructor(dsn, logger = noopLogger, options = {}) {
|
|
749
|
+
this.logger = logger;
|
|
750
|
+
if (!dsn) {
|
|
751
|
+
throw new Error("PostgresJobStore requires a non-empty DSN");
|
|
752
|
+
}
|
|
753
|
+
this.tmpDir = mkdtempSync(path.join(os.tmpdir(), "llm-gateway-pg-job-store-"));
|
|
754
|
+
this.worker = new Worker(resolvePostgresWorkerUrl(), {
|
|
755
|
+
execArgv: [],
|
|
756
|
+
workerData: {
|
|
757
|
+
dsn,
|
|
758
|
+
retentionMs: options.retentionMs ?? resolveJobRetentionMs(),
|
|
759
|
+
dedupWindowMs: options.dedupWindowMs ?? resolveDedupWindowMs(),
|
|
760
|
+
leaseTtlMs: options.leaseTtlMs ?? DEFAULT_INSTANCE_LEASE_TTL_MS,
|
|
761
|
+
farFutureIso: FAR_FUTURE_ISO,
|
|
762
|
+
connectionTimeoutMillis: 5000,
|
|
763
|
+
},
|
|
764
|
+
});
|
|
765
|
+
try {
|
|
766
|
+
this.syncCall("init");
|
|
767
|
+
}
|
|
768
|
+
catch (err) {
|
|
769
|
+
this.closed = true;
|
|
770
|
+
void this.worker.terminate();
|
|
771
|
+
rmSync(this.tmpDir, { recursive: true, force: true });
|
|
772
|
+
throw err;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
syncCall(method, ...args) {
|
|
776
|
+
if (this.closed && method !== "close") {
|
|
777
|
+
throw new Error("PostgresJobStore is closed");
|
|
778
|
+
}
|
|
779
|
+
const shared = new SharedArrayBuffer(4);
|
|
780
|
+
const state = new Int32Array(shared);
|
|
781
|
+
const resultPath = path.join(this.tmpDir, `result-${process.pid}-${++this.nextRequestId}.json`);
|
|
782
|
+
this.worker.postMessage({ method, args, resultPath, shared });
|
|
783
|
+
const wait = Atomics.wait(state, 0, 0, method === "init" ? 10_000 : 30_000);
|
|
784
|
+
if (wait !== "ok" && wait !== "not-equal") {
|
|
785
|
+
throw new Error(`PostgresJobStore ${method} wait failed: ${wait}`);
|
|
786
|
+
}
|
|
787
|
+
if (Atomics.load(state, 0) === 2) {
|
|
788
|
+
throw new Error(`PostgresJobStore ${method} worker could not write its result`);
|
|
789
|
+
}
|
|
790
|
+
let payload;
|
|
791
|
+
try {
|
|
792
|
+
payload = JSON.parse(readFileSync(resultPath, "utf8"));
|
|
793
|
+
}
|
|
794
|
+
finally {
|
|
795
|
+
rmSync(resultPath, { force: true });
|
|
796
|
+
}
|
|
797
|
+
if (!payload.ok) {
|
|
798
|
+
const err = new Error(payload.error.message);
|
|
799
|
+
if (payload.error.stack)
|
|
800
|
+
err.stack = payload.error.stack;
|
|
801
|
+
throw err;
|
|
802
|
+
}
|
|
803
|
+
return payload.value;
|
|
804
|
+
}
|
|
805
|
+
recordStart(input) {
|
|
806
|
+
this.syncCall("recordStart", input);
|
|
807
|
+
}
|
|
808
|
+
markRunning(id, opts) {
|
|
809
|
+
return this.syncCall("markRunning", id, opts);
|
|
810
|
+
}
|
|
811
|
+
registerInstance(meta) {
|
|
812
|
+
this.syncCall("registerInstance", meta);
|
|
556
813
|
}
|
|
557
|
-
|
|
558
|
-
|
|
814
|
+
heartbeat(instanceId) {
|
|
815
|
+
this.syncCall("heartbeat", instanceId);
|
|
559
816
|
}
|
|
560
|
-
|
|
561
|
-
|
|
817
|
+
deregisterInstance(instanceId) {
|
|
818
|
+
this.syncCall("deregisterInstance", instanceId);
|
|
819
|
+
}
|
|
820
|
+
selectStaleProcessCandidates(leaseTtlMs, httpJobGraceMs) {
|
|
821
|
+
return this.syncCall("selectStaleProcessCandidates", leaseTtlMs, httpJobGraceMs);
|
|
822
|
+
}
|
|
823
|
+
recoverStaleJobs(leaseTtlMs, httpJobGraceMs, liveConfirmedIds = []) {
|
|
824
|
+
const result = this.syncCall("recoverStaleJobs", leaseTtlMs, httpJobGraceMs, liveConfirmedIds);
|
|
825
|
+
return result.orphaned.map(row => ({
|
|
826
|
+
id: row.id,
|
|
827
|
+
correlationId: row.correlation_id,
|
|
828
|
+
startedAt: row.started_at,
|
|
829
|
+
stdout: row.stdout ?? "",
|
|
830
|
+
stderr: row.stderr ?? "",
|
|
831
|
+
exitCode: row.exit_code,
|
|
832
|
+
transport: row.transport ?? "process",
|
|
833
|
+
httpStatus: row.http_status ?? null,
|
|
834
|
+
}));
|
|
835
|
+
}
|
|
836
|
+
gcInstances(instanceGcMs) {
|
|
837
|
+
return this.syncCall("gcInstances", instanceGcMs);
|
|
838
|
+
}
|
|
839
|
+
recordOutput(id, stdout, stderr, outputTruncated) {
|
|
840
|
+
this.syncCall("recordOutput", id, stdout, stderr, outputTruncated);
|
|
562
841
|
}
|
|
563
|
-
recordComplete() {
|
|
564
|
-
|
|
842
|
+
recordComplete(input) {
|
|
843
|
+
this.syncCall("recordComplete", input);
|
|
565
844
|
}
|
|
566
|
-
getById() {
|
|
567
|
-
|
|
845
|
+
getById(id) {
|
|
846
|
+
const row = this.syncCall("getById", id);
|
|
847
|
+
return row ? rowToRecord(row) : null;
|
|
568
848
|
}
|
|
569
|
-
findByRequestKey() {
|
|
570
|
-
|
|
849
|
+
findByRequestKey(requestKey) {
|
|
850
|
+
const row = this.syncCall("findByRequestKey", requestKey);
|
|
851
|
+
return row ? rowToRecord(row) : null;
|
|
571
852
|
}
|
|
572
853
|
markOrphanedOnStartup() {
|
|
573
|
-
|
|
854
|
+
const orphaned = this.recoverStaleJobs(DEFAULT_INSTANCE_LEASE_TTL_MS, DEFAULT_HTTP_JOB_GRACE_MS);
|
|
855
|
+
return { count: orphaned.length, orphaned };
|
|
574
856
|
}
|
|
575
857
|
evictExpired() {
|
|
576
|
-
|
|
858
|
+
return this.syncCall("evictExpired");
|
|
859
|
+
}
|
|
860
|
+
recordValidationRun(run) {
|
|
861
|
+
this.syncCall("recordValidationRun", run);
|
|
862
|
+
}
|
|
863
|
+
getValidationRun(validationId) {
|
|
864
|
+
const row = this.syncCall("getValidationRun", validationId);
|
|
865
|
+
return row ? rowToValidationRunRecord(row) : null;
|
|
866
|
+
}
|
|
867
|
+
setValidationJudgeLink(validationId, judgeLink) {
|
|
868
|
+
this.syncCall("setValidationJudgeLink", validationId, judgeLink);
|
|
869
|
+
}
|
|
870
|
+
setValidationRunStatus(validationId, status) {
|
|
871
|
+
this.syncCall("setValidationRunStatus", validationId, status);
|
|
872
|
+
}
|
|
873
|
+
getValidationRunIdByJobId(jobId) {
|
|
874
|
+
return this.syncCall("getValidationRunIdByJobId", jobId);
|
|
875
|
+
}
|
|
876
|
+
recordValidationReceipt(receipt) {
|
|
877
|
+
this.syncCall("recordValidationReceipt", receipt);
|
|
878
|
+
}
|
|
879
|
+
getValidationReceipt(validationId) {
|
|
880
|
+
const row = this.syncCall("getValidationReceipt", validationId);
|
|
881
|
+
return row ? rowToValidationReceiptRecord(row) : null;
|
|
577
882
|
}
|
|
578
883
|
close() {
|
|
884
|
+
if (this.closed)
|
|
885
|
+
return;
|
|
886
|
+
try {
|
|
887
|
+
this.syncCall("close");
|
|
888
|
+
}
|
|
889
|
+
catch (err) {
|
|
890
|
+
this.logger.error("PostgresJobStore close failed", err);
|
|
891
|
+
}
|
|
892
|
+
finally {
|
|
893
|
+
this.closed = true;
|
|
894
|
+
void this.worker.terminate();
|
|
895
|
+
rmSync(this.tmpDir, { recursive: true, force: true });
|
|
896
|
+
}
|
|
579
897
|
}
|
|
580
898
|
}
|
|
899
|
+
function resolvePostgresWorkerUrl() {
|
|
900
|
+
const sibling = new URL("./postgres-job-store-worker.js", import.meta.url);
|
|
901
|
+
if (existsSync(fileURLToPath(sibling)))
|
|
902
|
+
return sibling;
|
|
903
|
+
if (import.meta.url.endsWith("/src/job-store.ts")) {
|
|
904
|
+
const built = new URL("../dist/postgres-job-store-worker.js", import.meta.url);
|
|
905
|
+
if (existsSync(fileURLToPath(built)))
|
|
906
|
+
return built;
|
|
907
|
+
}
|
|
908
|
+
throw new Error("PostgresJobStore worker module is missing. Run `npm run build` before using backend = 'postgres'.");
|
|
909
|
+
}
|
|
581
910
|
export function createJobStore(config, logger = noopLogger) {
|
|
582
911
|
const opts = {
|
|
583
912
|
retentionMs: config.retentionDays * 24 * 60 * 60 * 1000,
|
|
584
913
|
dedupWindowMs: config.dedupWindowMs,
|
|
914
|
+
leaseTtlMs: config.instanceLeaseTtlMs,
|
|
585
915
|
};
|
|
586
916
|
switch (config.backend) {
|
|
587
917
|
case "none":
|
|
@@ -589,7 +919,7 @@ export function createJobStore(config, logger = noopLogger) {
|
|
|
589
919
|
case "memory":
|
|
590
920
|
return new MemoryJobStore(opts);
|
|
591
921
|
case "postgres":
|
|
592
|
-
return new PostgresJobStore(config.dsn ?? "", logger);
|
|
922
|
+
return new PostgresJobStore(config.dsn ?? "", logger, opts);
|
|
593
923
|
case "sqlite":
|
|
594
924
|
default:
|
|
595
925
|
if (!config.path) {
|
package/dist/model-registry.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface CliInfo {
|
|
|
17
17
|
aliases?: Record<string, string>;
|
|
18
18
|
modelMetadata?: Record<string, ModelMetadata>;
|
|
19
19
|
warnings?: string[];
|
|
20
|
+
defaultAgent?: string;
|
|
20
21
|
}
|
|
21
22
|
export type CliInfoMap = Record<CliType, CliInfo>;
|
|
22
23
|
export declare function getCliInfo(forceRefresh?: boolean): CliInfoMap;
|