llm-cli-gateway 2.13.2 → 2.14.0-rc.1
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 +97 -0
- package/README.md +53 -26
- 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 +2 -0
- package/dist/async-job-manager.js +11 -0
- package/dist/codex-json-parser.d.ts +1 -0
- package/dist/codex-json-parser.js +6 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +47 -0
- 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 +484 -119
- package/dist/job-store.d.ts +45 -7
- package/dist/job-store.js +138 -17
- 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 +327 -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/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.d.ts
CHANGED
|
@@ -214,18 +214,56 @@ export declare class MemoryJobStore implements JobStore {
|
|
|
214
214
|
evictExpired(): number;
|
|
215
215
|
close(): void;
|
|
216
216
|
}
|
|
217
|
-
export declare class PostgresJobStore implements JobStore {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
217
|
+
export declare class PostgresJobStore implements JobStore, ValidationRunStore {
|
|
218
|
+
private logger;
|
|
219
|
+
private worker;
|
|
220
|
+
private tmpDir;
|
|
221
|
+
private nextRequestId;
|
|
222
|
+
private closed;
|
|
223
|
+
constructor(dsn: string, logger?: Logger, options?: {
|
|
224
|
+
retentionMs?: number;
|
|
225
|
+
dedupWindowMs?: number;
|
|
226
|
+
});
|
|
227
|
+
private syncCall;
|
|
228
|
+
recordStart(input: {
|
|
229
|
+
id: string;
|
|
230
|
+
correlationId: string;
|
|
231
|
+
requestKey: string;
|
|
232
|
+
cli: string;
|
|
233
|
+
args: string[];
|
|
234
|
+
outputFormat?: string;
|
|
235
|
+
startedAt: string;
|
|
236
|
+
pid: number | null;
|
|
237
|
+
ownerPrincipal?: string | null;
|
|
238
|
+
transport?: JobTransport;
|
|
239
|
+
payloadJson?: string | null;
|
|
240
|
+
}): void;
|
|
241
|
+
recordOutput(id: string, stdout: string, stderr: string, outputTruncated: boolean): void;
|
|
242
|
+
recordComplete(input: {
|
|
243
|
+
id: string;
|
|
244
|
+
status: Exclude<JobStoreStatus, "running">;
|
|
245
|
+
exitCode: number | null;
|
|
246
|
+
stdout: string;
|
|
247
|
+
stderr: string;
|
|
248
|
+
outputTruncated: boolean;
|
|
249
|
+
error: string | null;
|
|
250
|
+
finishedAt: string;
|
|
251
|
+
httpStatus?: number | null;
|
|
252
|
+
}): void;
|
|
253
|
+
getById(id: string): JobRecord | null;
|
|
254
|
+
findByRequestKey(requestKey: string): JobRecord | null;
|
|
224
255
|
markOrphanedOnStartup(): {
|
|
225
256
|
count: number;
|
|
226
257
|
orphaned: Array<OrphanedJobSnapshot>;
|
|
227
258
|
};
|
|
228
259
|
evictExpired(): number;
|
|
260
|
+
recordValidationRun(run: ValidationRunRecord): void;
|
|
261
|
+
getValidationRun(validationId: string): ValidationRunRecord | null;
|
|
262
|
+
setValidationJudgeLink(validationId: string, judgeLink: ValidationRunLink): void;
|
|
263
|
+
setValidationRunStatus(validationId: string, status: ValidationRunRecord["status"]): void;
|
|
264
|
+
getValidationRunIdByJobId(jobId: string): string | null;
|
|
265
|
+
recordValidationReceipt(receipt: ValidationReceiptRecord): void;
|
|
266
|
+
getValidationReceipt(validationId: string): ValidationReceiptRecord | null;
|
|
229
267
|
close(): void;
|
|
230
268
|
}
|
|
231
269
|
export declare function createJobStore(config: PersistenceConfig, logger?: Logger): JobStore | null;
|
package/dist/job-store.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
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";
|
|
7
9
|
export function resolveJobStoreDbPath() {
|
|
@@ -550,34 +552,153 @@ export class MemoryJobStore {
|
|
|
550
552
|
}
|
|
551
553
|
}
|
|
552
554
|
export class PostgresJobStore {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
555
|
+
logger;
|
|
556
|
+
worker;
|
|
557
|
+
tmpDir;
|
|
558
|
+
nextRequestId = 0;
|
|
559
|
+
closed = false;
|
|
560
|
+
constructor(dsn, logger = noopLogger, options = {}) {
|
|
561
|
+
this.logger = logger;
|
|
562
|
+
if (!dsn) {
|
|
563
|
+
throw new Error("PostgresJobStore requires a non-empty DSN");
|
|
564
|
+
}
|
|
565
|
+
this.tmpDir = mkdtempSync(path.join(os.tmpdir(), "llm-gateway-pg-job-store-"));
|
|
566
|
+
this.worker = new Worker(resolvePostgresWorkerUrl(), {
|
|
567
|
+
execArgv: [],
|
|
568
|
+
workerData: {
|
|
569
|
+
dsn,
|
|
570
|
+
retentionMs: options.retentionMs ?? resolveJobRetentionMs(),
|
|
571
|
+
dedupWindowMs: options.dedupWindowMs ?? resolveDedupWindowMs(),
|
|
572
|
+
farFutureIso: FAR_FUTURE_ISO,
|
|
573
|
+
connectionTimeoutMillis: 5000,
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
try {
|
|
577
|
+
this.syncCall("init");
|
|
578
|
+
}
|
|
579
|
+
catch (err) {
|
|
580
|
+
this.closed = true;
|
|
581
|
+
void this.worker.terminate();
|
|
582
|
+
rmSync(this.tmpDir, { recursive: true, force: true });
|
|
583
|
+
throw err;
|
|
584
|
+
}
|
|
556
585
|
}
|
|
557
|
-
|
|
558
|
-
|
|
586
|
+
syncCall(method, ...args) {
|
|
587
|
+
if (this.closed && method !== "close") {
|
|
588
|
+
throw new Error("PostgresJobStore is closed");
|
|
589
|
+
}
|
|
590
|
+
const shared = new SharedArrayBuffer(4);
|
|
591
|
+
const state = new Int32Array(shared);
|
|
592
|
+
const resultPath = path.join(this.tmpDir, `result-${process.pid}-${++this.nextRequestId}.json`);
|
|
593
|
+
this.worker.postMessage({ method, args, resultPath, shared });
|
|
594
|
+
const wait = Atomics.wait(state, 0, 0, method === "init" ? 10_000 : 30_000);
|
|
595
|
+
if (wait !== "ok" && wait !== "not-equal") {
|
|
596
|
+
throw new Error(`PostgresJobStore ${method} wait failed: ${wait}`);
|
|
597
|
+
}
|
|
598
|
+
if (Atomics.load(state, 0) === 2) {
|
|
599
|
+
throw new Error(`PostgresJobStore ${method} worker could not write its result`);
|
|
600
|
+
}
|
|
601
|
+
let payload;
|
|
602
|
+
try {
|
|
603
|
+
payload = JSON.parse(readFileSync(resultPath, "utf8"));
|
|
604
|
+
}
|
|
605
|
+
finally {
|
|
606
|
+
rmSync(resultPath, { force: true });
|
|
607
|
+
}
|
|
608
|
+
if (!payload.ok) {
|
|
609
|
+
const err = new Error(payload.error.message);
|
|
610
|
+
if (payload.error.stack)
|
|
611
|
+
err.stack = payload.error.stack;
|
|
612
|
+
throw err;
|
|
613
|
+
}
|
|
614
|
+
return payload.value;
|
|
559
615
|
}
|
|
560
|
-
|
|
561
|
-
|
|
616
|
+
recordStart(input) {
|
|
617
|
+
this.syncCall("recordStart", input);
|
|
562
618
|
}
|
|
563
|
-
|
|
564
|
-
|
|
619
|
+
recordOutput(id, stdout, stderr, outputTruncated) {
|
|
620
|
+
this.syncCall("recordOutput", id, stdout, stderr, outputTruncated);
|
|
565
621
|
}
|
|
566
|
-
|
|
567
|
-
|
|
622
|
+
recordComplete(input) {
|
|
623
|
+
this.syncCall("recordComplete", input);
|
|
568
624
|
}
|
|
569
|
-
|
|
570
|
-
|
|
625
|
+
getById(id) {
|
|
626
|
+
const row = this.syncCall("getById", id);
|
|
627
|
+
return row ? rowToRecord(row) : null;
|
|
628
|
+
}
|
|
629
|
+
findByRequestKey(requestKey) {
|
|
630
|
+
const row = this.syncCall("findByRequestKey", requestKey);
|
|
631
|
+
return row ? rowToRecord(row) : null;
|
|
571
632
|
}
|
|
572
633
|
markOrphanedOnStartup() {
|
|
573
|
-
|
|
634
|
+
const result = this.syncCall("markOrphanedOnStartup");
|
|
635
|
+
return {
|
|
636
|
+
count: result.count,
|
|
637
|
+
orphaned: result.orphaned.map(row => ({
|
|
638
|
+
id: row.id,
|
|
639
|
+
correlationId: row.correlation_id,
|
|
640
|
+
startedAt: row.started_at,
|
|
641
|
+
stdout: row.stdout ?? "",
|
|
642
|
+
stderr: row.stderr ?? "",
|
|
643
|
+
exitCode: row.exit_code,
|
|
644
|
+
transport: row.transport ?? "process",
|
|
645
|
+
httpStatus: row.http_status ?? null,
|
|
646
|
+
})),
|
|
647
|
+
};
|
|
574
648
|
}
|
|
575
649
|
evictExpired() {
|
|
576
|
-
|
|
650
|
+
return this.syncCall("evictExpired");
|
|
651
|
+
}
|
|
652
|
+
recordValidationRun(run) {
|
|
653
|
+
this.syncCall("recordValidationRun", run);
|
|
654
|
+
}
|
|
655
|
+
getValidationRun(validationId) {
|
|
656
|
+
const row = this.syncCall("getValidationRun", validationId);
|
|
657
|
+
return row ? rowToValidationRunRecord(row) : null;
|
|
658
|
+
}
|
|
659
|
+
setValidationJudgeLink(validationId, judgeLink) {
|
|
660
|
+
this.syncCall("setValidationJudgeLink", validationId, judgeLink);
|
|
661
|
+
}
|
|
662
|
+
setValidationRunStatus(validationId, status) {
|
|
663
|
+
this.syncCall("setValidationRunStatus", validationId, status);
|
|
664
|
+
}
|
|
665
|
+
getValidationRunIdByJobId(jobId) {
|
|
666
|
+
return this.syncCall("getValidationRunIdByJobId", jobId);
|
|
667
|
+
}
|
|
668
|
+
recordValidationReceipt(receipt) {
|
|
669
|
+
this.syncCall("recordValidationReceipt", receipt);
|
|
670
|
+
}
|
|
671
|
+
getValidationReceipt(validationId) {
|
|
672
|
+
const row = this.syncCall("getValidationReceipt", validationId);
|
|
673
|
+
return row ? rowToValidationReceiptRecord(row) : null;
|
|
577
674
|
}
|
|
578
675
|
close() {
|
|
676
|
+
if (this.closed)
|
|
677
|
+
return;
|
|
678
|
+
try {
|
|
679
|
+
this.syncCall("close");
|
|
680
|
+
}
|
|
681
|
+
catch (err) {
|
|
682
|
+
this.logger.error("PostgresJobStore close failed", err);
|
|
683
|
+
}
|
|
684
|
+
finally {
|
|
685
|
+
this.closed = true;
|
|
686
|
+
void this.worker.terminate();
|
|
687
|
+
rmSync(this.tmpDir, { recursive: true, force: true });
|
|
688
|
+
}
|
|
579
689
|
}
|
|
580
690
|
}
|
|
691
|
+
function resolvePostgresWorkerUrl() {
|
|
692
|
+
const sibling = new URL("./postgres-job-store-worker.js", import.meta.url);
|
|
693
|
+
if (existsSync(fileURLToPath(sibling)))
|
|
694
|
+
return sibling;
|
|
695
|
+
if (import.meta.url.endsWith("/src/job-store.ts")) {
|
|
696
|
+
const built = new URL("../dist/postgres-job-store-worker.js", import.meta.url);
|
|
697
|
+
if (existsSync(fileURLToPath(built)))
|
|
698
|
+
return built;
|
|
699
|
+
}
|
|
700
|
+
throw new Error("PostgresJobStore worker module is missing. Run `npm run build` before using backend = 'postgres'.");
|
|
701
|
+
}
|
|
581
702
|
export function createJobStore(config, logger = noopLogger) {
|
|
582
703
|
const opts = {
|
|
583
704
|
retentionMs: config.retentionDays * 24 * 60 * 60 * 1000,
|
|
@@ -589,7 +710,7 @@ export function createJobStore(config, logger = noopLogger) {
|
|
|
589
710
|
case "memory":
|
|
590
711
|
return new MemoryJobStore(opts);
|
|
591
712
|
case "postgres":
|
|
592
|
-
return new PostgresJobStore(config.dsn ?? "", logger);
|
|
713
|
+
return new PostgresJobStore(config.dsn ?? "", logger, opts);
|
|
593
714
|
case "sqlite":
|
|
594
715
|
default:
|
|
595
716
|
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;
|
package/dist/model-registry.js
CHANGED
|
@@ -139,6 +139,7 @@ function cloneInfo(source) {
|
|
|
139
139
|
aliases: source.aliases ? { ...source.aliases } : undefined,
|
|
140
140
|
modelMetadata: source.modelMetadata ? { ...source.modelMetadata } : {},
|
|
141
141
|
warnings: source.warnings ? [...source.warnings] : [],
|
|
142
|
+
defaultAgent: source.defaultAgent,
|
|
142
143
|
};
|
|
143
144
|
Object.keys(cloned.models).forEach(model => {
|
|
144
145
|
cloned.modelMetadata[model] = cloned.modelMetadata[model] ?? {
|
|
@@ -373,6 +374,7 @@ function applyGeminiOverrides(info) {
|
|
|
373
374
|
}
|
|
374
375
|
function applyGrokOverrides(info) {
|
|
375
376
|
const envDefault = process.env.GROK_DEFAULT_MODEL;
|
|
377
|
+
applyGrokConfig(info);
|
|
376
378
|
addEnvModels(info, "GROK_MODELS");
|
|
377
379
|
addEnvAliases(info, "grok", "GROK_MODEL_ALIASES");
|
|
378
380
|
addGlobalEnvAliases(info, "grok");
|
|
@@ -381,8 +383,48 @@ function applyGrokOverrides(info) {
|
|
|
381
383
|
}
|
|
382
384
|
info.modelOrder = buildOrder(info, info.defaultModel);
|
|
383
385
|
}
|
|
386
|
+
function applyGrokConfig(info) {
|
|
387
|
+
const configPath = process.env.GROK_CONFIG_PATH || path.join(homedir(), ".grok", "config.toml");
|
|
388
|
+
if (!existsSync(configPath)) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
const parsed = parseToml(readFileSync(configPath, "utf-8"));
|
|
393
|
+
const models = readRecordProperty(parsed, "models");
|
|
394
|
+
Object.entries(models).forEach(([key, value]) => {
|
|
395
|
+
if (typeof value !== "string") {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (key === "default") {
|
|
399
|
+
setDefaultModel(info, value, `${configPath} [models].default`, "config");
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
addModel(info, value, `Custom Grok model from config.toml [models].${key}`, {
|
|
403
|
+
source: "config",
|
|
404
|
+
sourceDetail: `${configPath} [models].${key}`,
|
|
405
|
+
confidence: "medium",
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
const ui = readRecordProperty(parsed, "ui");
|
|
409
|
+
const forkModel = readStringProperty(ui, "fork_secondary_model");
|
|
410
|
+
if (forkModel) {
|
|
411
|
+
addModel(info, forkModel, "Configured Grok fork/secondary model", {
|
|
412
|
+
source: "config",
|
|
413
|
+
sourceDetail: `${configPath} [ui].fork_secondary_model`,
|
|
414
|
+
confidence: "medium",
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
420
|
+
addWarning(info, `Could not parse Grok config ${configPath}: ${message}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
384
423
|
function applyMistralOverrides(info) {
|
|
385
424
|
const vibeConfig = readVibeConfig(info);
|
|
425
|
+
if (vibeConfig.defaultAgent) {
|
|
426
|
+
info.defaultAgent = vibeConfig.defaultAgent;
|
|
427
|
+
}
|
|
386
428
|
addVibeModelEntries(info, parseVibeModels(process.env.VIBE_MODELS, "VIBE_MODELS"), "env");
|
|
387
429
|
addVibeModelEntries(info, vibeConfig.models, "config");
|
|
388
430
|
addEnvModels(info, "MISTRAL_MODELS");
|
|
@@ -426,6 +468,10 @@ function readVibeConfig(info) {
|
|
|
426
468
|
if (activeModel) {
|
|
427
469
|
result.activeModel = activeModel.trim();
|
|
428
470
|
}
|
|
471
|
+
const defaultAgent = readStringProperty(parsed, "default_agent");
|
|
472
|
+
if (defaultAgent) {
|
|
473
|
+
result.defaultAgent = defaultAgent.trim();
|
|
474
|
+
}
|
|
429
475
|
result.models = parseVibeModelArray(readRecordOrArrayProperty(parsed, "models"), configPath);
|
|
430
476
|
}
|
|
431
477
|
catch (error) {
|
package/dist/oauth.js
CHANGED
|
@@ -273,7 +273,7 @@ export class OAuthServer {
|
|
|
273
273
|
return {
|
|
274
274
|
resource: joinBaseAndPath(baseUrl, this.opts.protectedPath),
|
|
275
275
|
authorization_servers: [baseUrl],
|
|
276
|
-
scopes_supported: ["mcp", "workspace:admin"],
|
|
276
|
+
scopes_supported: ["mcp", "workspace:admin", "cli:admin"],
|
|
277
277
|
bearer_methods_supported: ["header"],
|
|
278
278
|
};
|
|
279
279
|
}
|
|
@@ -291,7 +291,7 @@ export class OAuthServer {
|
|
|
291
291
|
code_challenge_methods_supported: this.opts.config.allowPlainPkce
|
|
292
292
|
? ["S256", "plain"]
|
|
293
293
|
: ["S256"],
|
|
294
|
-
scopes_supported: ["mcp", "workspace:admin"],
|
|
294
|
+
scopes_supported: ["mcp", "workspace:admin", "cli:admin"],
|
|
295
295
|
};
|
|
296
296
|
}
|
|
297
297
|
registrationAllowedByPolicy(req, params) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
3
|
+
let pool = null;
|
|
4
|
+
function signal(shared, status) {
|
|
5
|
+
const view = new Int32Array(shared);
|
|
6
|
+
Atomics.store(view, 0, status);
|
|
7
|
+
Atomics.notify(view, 0, 1);
|
|
8
|
+
}
|
|
9
|
+
function ok(value) {
|
|
10
|
+
return { ok: true, value };
|
|
11
|
+
}
|
|
12
|
+
function fail(error) {
|
|
13
|
+
return {
|
|
14
|
+
ok: false,
|
|
15
|
+
error: {
|
|
16
|
+
message: error instanceof Error ? error.message : String(error),
|
|
17
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function getPool() {
|
|
22
|
+
if (!pool)
|
|
23
|
+
throw new Error("PostgresJobStore worker pool is not initialized");
|
|
24
|
+
return pool;
|
|
25
|
+
}
|
|
26
|
+
async function withClient(fn) {
|
|
27
|
+
const client = await getPool().connect();
|
|
28
|
+
try {
|
|
29
|
+
return await fn(client);
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
client.release();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function init() {
|
|
36
|
+
let pg;
|
|
37
|
+
try {
|
|
38
|
+
pg = await import("pg");
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
const code = error?.code;
|
|
42
|
+
if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
|
|
43
|
+
throw new Error("Postgres persistence requires optional peer dependency 'pg'. Install it alongside llm-cli-gateway to use backend = 'postgres'.", { cause: error });
|
|
44
|
+
}
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
pool = new pg.Pool({
|
|
48
|
+
connectionString: workerData.dsn,
|
|
49
|
+
connectionTimeoutMillis: workerData.connectionTimeoutMillis,
|
|
50
|
+
});
|
|
51
|
+
await getPool().query("SELECT 1");
|
|
52
|
+
await getPool().query(`
|
|
53
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
54
|
+
id TEXT PRIMARY KEY,
|
|
55
|
+
correlation_id TEXT NOT NULL,
|
|
56
|
+
request_key TEXT NOT NULL,
|
|
57
|
+
cli TEXT NOT NULL,
|
|
58
|
+
args_json TEXT NOT NULL,
|
|
59
|
+
output_format TEXT,
|
|
60
|
+
status TEXT NOT NULL,
|
|
61
|
+
exit_code INTEGER,
|
|
62
|
+
stdout TEXT,
|
|
63
|
+
stderr TEXT,
|
|
64
|
+
output_truncated BOOLEAN NOT NULL DEFAULT FALSE,
|
|
65
|
+
error TEXT,
|
|
66
|
+
started_at TEXT NOT NULL,
|
|
67
|
+
finished_at TEXT,
|
|
68
|
+
pid INTEGER,
|
|
69
|
+
expires_at TEXT NOT NULL,
|
|
70
|
+
owner_principal TEXT,
|
|
71
|
+
transport TEXT NOT NULL DEFAULT 'process',
|
|
72
|
+
http_status INTEGER,
|
|
73
|
+
payload_json TEXT
|
|
74
|
+
);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_request_key ON jobs(request_key);
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_expires_at ON jobs(expires_at);
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_request_key_finished ON jobs(request_key, finished_at);
|
|
79
|
+
|
|
80
|
+
CREATE TABLE IF NOT EXISTS validation_runs (
|
|
81
|
+
validation_id TEXT PRIMARY KEY,
|
|
82
|
+
owner_principal TEXT NOT NULL,
|
|
83
|
+
intent TEXT NOT NULL,
|
|
84
|
+
created_at TEXT NOT NULL,
|
|
85
|
+
request_json TEXT NOT NULL,
|
|
86
|
+
provider_links TEXT NOT NULL,
|
|
87
|
+
judge_link TEXT,
|
|
88
|
+
status TEXT NOT NULL
|
|
89
|
+
);
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_validation_runs_owner ON validation_runs(owner_principal);
|
|
91
|
+
|
|
92
|
+
CREATE TABLE IF NOT EXISTS validation_run_jobs (
|
|
93
|
+
job_id TEXT PRIMARY KEY,
|
|
94
|
+
validation_id TEXT NOT NULL,
|
|
95
|
+
role TEXT NOT NULL
|
|
96
|
+
);
|
|
97
|
+
CREATE INDEX IF NOT EXISTS idx_validation_run_jobs_run ON validation_run_jobs(validation_id);
|
|
98
|
+
|
|
99
|
+
CREATE TABLE IF NOT EXISTS validation_receipts (
|
|
100
|
+
validation_id TEXT PRIMARY KEY,
|
|
101
|
+
owner_principal TEXT NOT NULL,
|
|
102
|
+
minted_at TEXT NOT NULL,
|
|
103
|
+
schema_version TEXT NOT NULL,
|
|
104
|
+
report_json TEXT NOT NULL,
|
|
105
|
+
canonical_sha256 TEXT NOT NULL,
|
|
106
|
+
prev_sha256 TEXT,
|
|
107
|
+
seq INTEGER,
|
|
108
|
+
signature TEXT,
|
|
109
|
+
models TEXT NOT NULL,
|
|
110
|
+
has_material_disagreement BOOLEAN NOT NULL,
|
|
111
|
+
confidence TEXT NOT NULL
|
|
112
|
+
);
|
|
113
|
+
CREATE INDEX IF NOT EXISTS idx_validation_receipts_owner ON validation_receipts(owner_principal);
|
|
114
|
+
`);
|
|
115
|
+
await getPool().query("ALTER TABLE jobs ADD COLUMN IF NOT EXISTS owner_principal TEXT");
|
|
116
|
+
await getPool().query("ALTER TABLE jobs ADD COLUMN IF NOT EXISTS transport TEXT NOT NULL DEFAULT 'process'");
|
|
117
|
+
await getPool().query("ALTER TABLE jobs ADD COLUMN IF NOT EXISTS http_status INTEGER");
|
|
118
|
+
await getPool().query("ALTER TABLE jobs ADD COLUMN IF NOT EXISTS payload_json TEXT");
|
|
119
|
+
}
|
|
120
|
+
async function op(method, args) {
|
|
121
|
+
switch (method) {
|
|
122
|
+
case "init":
|
|
123
|
+
await init();
|
|
124
|
+
return null;
|
|
125
|
+
case "recordStart": {
|
|
126
|
+
const input = args[0];
|
|
127
|
+
await getPool().query(`INSERT INTO jobs (id, correlation_id, request_key, cli, args_json, output_format,
|
|
128
|
+
status, exit_code, stdout, stderr, output_truncated, error,
|
|
129
|
+
started_at, finished_at, pid, expires_at, owner_principal,
|
|
130
|
+
transport, http_status, payload_json)
|
|
131
|
+
VALUES ($1, $2, $3, $4, $5, $6, 'running', NULL, '', '', FALSE, NULL,
|
|
132
|
+
$7, NULL, $8, $9, $10, $11, NULL, $12)`, [
|
|
133
|
+
input.id,
|
|
134
|
+
input.correlationId,
|
|
135
|
+
input.requestKey,
|
|
136
|
+
input.cli,
|
|
137
|
+
JSON.stringify(input.args),
|
|
138
|
+
input.outputFormat ?? null,
|
|
139
|
+
input.startedAt,
|
|
140
|
+
input.pid,
|
|
141
|
+
workerData.farFutureIso,
|
|
142
|
+
input.ownerPrincipal ?? null,
|
|
143
|
+
input.transport ?? "process",
|
|
144
|
+
input.payloadJson ?? null,
|
|
145
|
+
]);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
case "recordOutput":
|
|
149
|
+
await getPool().query("UPDATE jobs SET stdout = $2, stderr = $3, output_truncated = $4 WHERE id = $1", args);
|
|
150
|
+
return null;
|
|
151
|
+
case "recordComplete": {
|
|
152
|
+
const input = args[0];
|
|
153
|
+
const expiresAt = new Date(Date.parse(input.finishedAt) + workerData.retentionMs).toISOString();
|
|
154
|
+
await getPool().query(`UPDATE jobs
|
|
155
|
+
SET status = $2, exit_code = $3, stdout = $4, stderr = $5,
|
|
156
|
+
output_truncated = $6, error = $7, finished_at = $8,
|
|
157
|
+
expires_at = $9, http_status = $10
|
|
158
|
+
WHERE id = $1`, [
|
|
159
|
+
input.id,
|
|
160
|
+
input.status,
|
|
161
|
+
input.exitCode,
|
|
162
|
+
input.stdout,
|
|
163
|
+
input.stderr,
|
|
164
|
+
input.outputTruncated,
|
|
165
|
+
input.error,
|
|
166
|
+
input.finishedAt,
|
|
167
|
+
expiresAt,
|
|
168
|
+
input.httpStatus ?? null,
|
|
169
|
+
]);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
case "getById": {
|
|
173
|
+
const result = await getPool().query("SELECT * FROM jobs WHERE id = $1", [args[0]]);
|
|
174
|
+
return result.rows[0] ?? null;
|
|
175
|
+
}
|
|
176
|
+
case "findByRequestKey": {
|
|
177
|
+
const cutoff = new Date(Date.now() - workerData.dedupWindowMs).toISOString();
|
|
178
|
+
const result = await getPool().query(`SELECT * FROM jobs
|
|
179
|
+
WHERE request_key = $1
|
|
180
|
+
AND started_at >= $2
|
|
181
|
+
AND status IN ('running', 'completed')
|
|
182
|
+
ORDER BY started_at DESC
|
|
183
|
+
LIMIT 1`, [args[0], cutoff]);
|
|
184
|
+
return result.rows[0] ?? null;
|
|
185
|
+
}
|
|
186
|
+
case "markOrphanedOnStartup":
|
|
187
|
+
return await withClient(async (client) => {
|
|
188
|
+
const now = new Date().toISOString();
|
|
189
|
+
const expiresAt = new Date(Date.now() + workerData.retentionMs).toISOString();
|
|
190
|
+
await client.query("BEGIN");
|
|
191
|
+
try {
|
|
192
|
+
const rows = await client.query("SELECT id, correlation_id, started_at, stdout, stderr, exit_code, transport, http_status FROM jobs WHERE status = 'running'");
|
|
193
|
+
const update = await client.query(`UPDATE jobs
|
|
194
|
+
SET status = 'orphaned',
|
|
195
|
+
error = COALESCE(error, 'Gateway restarted while job was running'),
|
|
196
|
+
finished_at = COALESCE(finished_at, $1),
|
|
197
|
+
expires_at = $2
|
|
198
|
+
WHERE status = 'running'`, [now, expiresAt]);
|
|
199
|
+
await client.query("COMMIT");
|
|
200
|
+
return { count: update.rowCount ?? 0, orphaned: rows.rows };
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
await client.query("ROLLBACK");
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
case "evictExpired": {
|
|
208
|
+
const result = await getPool().query("DELETE FROM jobs WHERE expires_at < $1", [
|
|
209
|
+
new Date().toISOString(),
|
|
210
|
+
]);
|
|
211
|
+
return result.rowCount ?? 0;
|
|
212
|
+
}
|
|
213
|
+
case "recordValidationRun": {
|
|
214
|
+
const run = args[0];
|
|
215
|
+
await withClient(async (client) => {
|
|
216
|
+
await client.query("BEGIN");
|
|
217
|
+
try {
|
|
218
|
+
await client.query(`INSERT INTO validation_runs
|
|
219
|
+
(validation_id, owner_principal, intent, created_at, request_json,
|
|
220
|
+
provider_links, judge_link, status)
|
|
221
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
222
|
+
ON CONFLICT (validation_id) DO NOTHING`, [
|
|
223
|
+
run.validationId,
|
|
224
|
+
run.ownerPrincipal,
|
|
225
|
+
run.intent,
|
|
226
|
+
run.createdAt,
|
|
227
|
+
run.requestJson,
|
|
228
|
+
JSON.stringify(run.providerLinks),
|
|
229
|
+
run.judgeLink ? JSON.stringify(run.judgeLink) : null,
|
|
230
|
+
run.status,
|
|
231
|
+
]);
|
|
232
|
+
for (const link of run.providerLinks) {
|
|
233
|
+
await client.query(`INSERT INTO validation_run_jobs (job_id, validation_id, role)
|
|
234
|
+
VALUES ($1, $2, 'provider')
|
|
235
|
+
ON CONFLICT (job_id) DO NOTHING`, [link.jobId, run.validationId]);
|
|
236
|
+
}
|
|
237
|
+
await client.query("COMMIT");
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
await client.query("ROLLBACK");
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
case "getValidationRun": {
|
|
247
|
+
const result = await getPool().query("SELECT * FROM validation_runs WHERE validation_id = $1", [args[0]]);
|
|
248
|
+
return result.rows[0] ?? null;
|
|
249
|
+
}
|
|
250
|
+
case "setValidationJudgeLink": {
|
|
251
|
+
const [validationId, judgeLink] = args;
|
|
252
|
+
await withClient(async (client) => {
|
|
253
|
+
await client.query("BEGIN");
|
|
254
|
+
try {
|
|
255
|
+
await client.query("UPDATE validation_runs SET judge_link = $2 WHERE validation_id = $1", [validationId, JSON.stringify(judgeLink)]);
|
|
256
|
+
await client.query(`INSERT INTO validation_run_jobs (job_id, validation_id, role)
|
|
257
|
+
VALUES ($1, $2, 'judge')
|
|
258
|
+
ON CONFLICT (job_id) DO NOTHING`, [judgeLink.jobId, validationId]);
|
|
259
|
+
await client.query("COMMIT");
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
await client.query("ROLLBACK");
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
case "setValidationRunStatus":
|
|
269
|
+
await getPool().query("UPDATE validation_runs SET status = $2 WHERE validation_id = $1", args);
|
|
270
|
+
return null;
|
|
271
|
+
case "getValidationRunIdByJobId": {
|
|
272
|
+
const result = await getPool().query("SELECT validation_id FROM validation_run_jobs WHERE job_id = $1", [args[0]]);
|
|
273
|
+
return result.rows[0] ? result.rows[0].validation_id : null;
|
|
274
|
+
}
|
|
275
|
+
case "recordValidationReceipt": {
|
|
276
|
+
const receipt = args[0];
|
|
277
|
+
await getPool().query(`INSERT INTO validation_receipts
|
|
278
|
+
(validation_id, owner_principal, minted_at, schema_version, report_json,
|
|
279
|
+
canonical_sha256, prev_sha256, seq, signature, models,
|
|
280
|
+
has_material_disagreement, confidence)
|
|
281
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
|
282
|
+
ON CONFLICT (validation_id) DO NOTHING`, [
|
|
283
|
+
receipt.validationId,
|
|
284
|
+
receipt.ownerPrincipal,
|
|
285
|
+
receipt.mintedAt,
|
|
286
|
+
receipt.schemaVersion,
|
|
287
|
+
receipt.reportJson,
|
|
288
|
+
receipt.canonicalSha256,
|
|
289
|
+
receipt.prevSha256,
|
|
290
|
+
receipt.seq,
|
|
291
|
+
receipt.signature,
|
|
292
|
+
JSON.stringify(receipt.models),
|
|
293
|
+
receipt.hasMaterialDisagreement,
|
|
294
|
+
receipt.confidence,
|
|
295
|
+
]);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
case "getValidationReceipt": {
|
|
299
|
+
const result = await getPool().query("SELECT * FROM validation_receipts WHERE validation_id = $1", [args[0]]);
|
|
300
|
+
return result.rows[0] ?? null;
|
|
301
|
+
}
|
|
302
|
+
case "close":
|
|
303
|
+
if (pool)
|
|
304
|
+
await pool.end();
|
|
305
|
+
pool = null;
|
|
306
|
+
return null;
|
|
307
|
+
default:
|
|
308
|
+
throw new Error(`Unknown PostgresJobStore worker method: ${method}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
parentPort?.on("message", async (message) => {
|
|
312
|
+
const { method, args, resultPath, shared } = message;
|
|
313
|
+
let payload;
|
|
314
|
+
try {
|
|
315
|
+
payload = ok(await op(method, args));
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
payload = fail(error);
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
writeFileSync(resultPath, JSON.stringify(payload));
|
|
322
|
+
signal(shared, 1);
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
signal(shared, 2);
|
|
326
|
+
}
|
|
327
|
+
});
|