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.
Files changed (77) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +53 -26
  3. package/dist/acp/client.d.ts +29 -1
  4. package/dist/acp/client.js +78 -4
  5. package/dist/acp/errors.d.ts +9 -1
  6. package/dist/acp/errors.js +19 -0
  7. package/dist/acp/event-normalizer.d.ts +12 -0
  8. package/dist/acp/event-normalizer.js +16 -0
  9. package/dist/acp/flight-redaction.d.ts +3 -0
  10. package/dist/acp/flight-redaction.js +3 -0
  11. package/dist/acp/permission-bridge.js +11 -5
  12. package/dist/acp/process-manager.d.ts +2 -1
  13. package/dist/acp/process-manager.js +43 -4
  14. package/dist/acp/provider-registry.js +19 -11
  15. package/dist/acp/runtime.d.ts +8 -0
  16. package/dist/acp/runtime.js +47 -4
  17. package/dist/acp/types.d.ts +3083 -55
  18. package/dist/acp/types.js +242 -5
  19. package/dist/async-job-manager.d.ts +2 -0
  20. package/dist/async-job-manager.js +11 -0
  21. package/dist/codex-json-parser.d.ts +1 -0
  22. package/dist/codex-json-parser.js +6 -0
  23. package/dist/config.d.ts +8 -0
  24. package/dist/config.js +47 -0
  25. package/dist/flight-recorder.d.ts +2 -0
  26. package/dist/flight-recorder.js +20 -0
  27. package/dist/gemini-json-parser.d.ts +2 -0
  28. package/dist/gemini-json-parser.js +45 -8
  29. package/dist/grok-json-parser.d.ts +14 -0
  30. package/dist/grok-json-parser.js +156 -0
  31. package/dist/index.d.ts +47 -2
  32. package/dist/index.js +484 -119
  33. package/dist/job-store.d.ts +45 -7
  34. package/dist/job-store.js +138 -17
  35. package/dist/model-registry.d.ts +1 -0
  36. package/dist/model-registry.js +46 -0
  37. package/dist/oauth.js +2 -2
  38. package/dist/postgres-job-store-worker.d.ts +1 -0
  39. package/dist/postgres-job-store-worker.js +327 -0
  40. package/dist/pricing.d.ts +3 -2
  41. package/dist/provider-acp-capabilities.d.ts +52 -0
  42. package/dist/provider-acp-capabilities.js +101 -0
  43. package/dist/provider-admin-tools.d.ts +100 -0
  44. package/dist/provider-admin-tools.js +572 -0
  45. package/dist/provider-capability-cache.d.ts +46 -0
  46. package/dist/provider-capability-cache.js +248 -0
  47. package/dist/provider-capability-discovery.d.ts +85 -0
  48. package/dist/provider-capability-discovery.js +461 -0
  49. package/dist/provider-capability-resolver.d.ts +29 -0
  50. package/dist/provider-capability-resolver.js +92 -0
  51. package/dist/provider-definition-assertions.d.ts +9 -0
  52. package/dist/provider-definition-assertions.js +147 -0
  53. package/dist/provider-definitions.d.ts +127 -0
  54. package/dist/provider-definitions.js +758 -0
  55. package/dist/provider-help-parser.d.ts +34 -0
  56. package/dist/provider-help-parser.js +203 -0
  57. package/dist/provider-model-discovery.d.ts +30 -0
  58. package/dist/provider-model-discovery.js +229 -0
  59. package/dist/provider-output-metadata.d.ts +7 -0
  60. package/dist/provider-output-metadata.js +68 -0
  61. package/dist/provider-schema-builder.d.ts +22 -0
  62. package/dist/provider-schema-builder.js +55 -0
  63. package/dist/provider-surface-generator.d.ts +98 -0
  64. package/dist/provider-surface-generator.js +140 -0
  65. package/dist/provider-tool-capabilities.d.ts +3 -2
  66. package/dist/provider-tool-capabilities.js +77 -62
  67. package/dist/request-helpers.d.ts +37 -4
  68. package/dist/request-helpers.js +134 -0
  69. package/dist/resources.d.ts +6 -1
  70. package/dist/resources.js +64 -192
  71. package/dist/session-manager.js +18 -11
  72. package/dist/stream-json-parser.d.ts +1 -0
  73. package/dist/stream-json-parser.js +3 -0
  74. package/dist/upstream-contracts.d.ts +17 -1
  75. package/dist/upstream-contracts.js +209 -36
  76. package/npm-shrinkwrap.json +2 -2
  77. package/package.json +8 -3
@@ -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
- constructor(_dsn: string, _logger?: Logger);
219
- recordStart(): void;
220
- recordOutput(): void;
221
- recordComplete(): void;
222
- getById(): JobRecord | null;
223
- findByRequestKey(): JobRecord | null;
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
- constructor(_dsn, _logger = noopLogger) {
554
- throw new Error("PostgresJobStore is not yet implemented. Use backend = 'sqlite' (single-instance) or " +
555
- "backend = 'memory' (ephemeral) until the Postgres backend ships.");
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
- recordStart() {
558
- throw new Error("not implemented");
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
- recordOutput() {
561
- throw new Error("not implemented");
616
+ recordStart(input) {
617
+ this.syncCall("recordStart", input);
562
618
  }
563
- recordComplete() {
564
- throw new Error("not implemented");
619
+ recordOutput(id, stdout, stderr, outputTruncated) {
620
+ this.syncCall("recordOutput", id, stdout, stderr, outputTruncated);
565
621
  }
566
- getById() {
567
- throw new Error("not implemented");
622
+ recordComplete(input) {
623
+ this.syncCall("recordComplete", input);
568
624
  }
569
- findByRequestKey() {
570
- throw new Error("not implemented");
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
- throw new Error("not implemented");
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
- throw new Error("not implemented");
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) {
@@ -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;
@@ -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
+ });