llm-cli-gateway 2.14.0-rc.1 → 2.14.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.
@@ -1,6 +1,7 @@
1
1
  import { writeFileSync } from "node:fs";
2
2
  import { parentPort, workerData } from "node:worker_threads";
3
3
  let pool = null;
4
+ const PG_NOW_MS = "(EXTRACT(EPOCH FROM now()) * 1000)::bigint";
4
5
  function signal(shared, status) {
5
6
  const view = new Int32Array(shared);
6
7
  Atomics.store(view, 0, status);
@@ -70,13 +71,26 @@ async function init() {
70
71
  owner_principal TEXT,
71
72
  transport TEXT NOT NULL DEFAULT 'process',
72
73
  http_status INTEGER,
73
- payload_json TEXT
74
+ payload_json TEXT,
75
+ owner_instance TEXT,
76
+ lease_deadline BIGINT
74
77
  );
75
78
  CREATE INDEX IF NOT EXISTS idx_jobs_request_key ON jobs(request_key);
76
79
  CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
77
80
  CREATE INDEX IF NOT EXISTS idx_jobs_expires_at ON jobs(expires_at);
78
81
  CREATE INDEX IF NOT EXISTS idx_jobs_request_key_finished ON jobs(request_key, finished_at);
79
82
 
83
+ CREATE TABLE IF NOT EXISTS gateway_instances (
84
+ instance_id TEXT PRIMARY KEY,
85
+ role TEXT,
86
+ hostname TEXT,
87
+ pid INTEGER,
88
+ started_at BIGINT NOT NULL,
89
+ last_heartbeat BIGINT NOT NULL
90
+ );
91
+ CREATE INDEX IF NOT EXISTS idx_gateway_instances_heartbeat
92
+ ON gateway_instances(last_heartbeat);
93
+
80
94
  CREATE TABLE IF NOT EXISTS validation_runs (
81
95
  validation_id TEXT PRIMARY KEY,
82
96
  owner_principal TEXT NOT NULL,
@@ -116,6 +130,9 @@ async function init() {
116
130
  await getPool().query("ALTER TABLE jobs ADD COLUMN IF NOT EXISTS transport TEXT NOT NULL DEFAULT 'process'");
117
131
  await getPool().query("ALTER TABLE jobs ADD COLUMN IF NOT EXISTS http_status INTEGER");
118
132
  await getPool().query("ALTER TABLE jobs ADD COLUMN IF NOT EXISTS payload_json TEXT");
133
+ await getPool().query("ALTER TABLE jobs ADD COLUMN IF NOT EXISTS owner_instance TEXT");
134
+ await getPool().query("ALTER TABLE jobs ADD COLUMN IF NOT EXISTS lease_deadline BIGINT");
135
+ await getPool().query("CREATE INDEX IF NOT EXISTS idx_jobs_owner_status ON jobs(owner_instance, status)");
119
136
  }
120
137
  async function op(method, args) {
121
138
  switch (method) {
@@ -127,9 +144,9 @@ async function op(method, args) {
127
144
  await getPool().query(`INSERT INTO jobs (id, correlation_id, request_key, cli, args_json, output_format,
128
145
  status, exit_code, stdout, stderr, output_truncated, error,
129
146
  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)`, [
147
+ transport, http_status, payload_json, owner_instance, lease_deadline)
148
+ VALUES ($1, $2, $3, $4, $5, $6, 'queued', NULL, '', '', FALSE, NULL,
149
+ $7, NULL, $8, $9, $10, $11, NULL, $12, $13, ${PG_NOW_MS} + $14)`, [
133
150
  input.id,
134
151
  input.correlationId,
135
152
  input.requestKey,
@@ -142,9 +159,106 @@ async function op(method, args) {
142
159
  input.ownerPrincipal ?? null,
143
160
  input.transport ?? "process",
144
161
  input.payloadJson ?? null,
162
+ input.ownerInstance ?? null,
163
+ workerData.leaseTtlMs,
145
164
  ]);
146
165
  return null;
147
166
  }
167
+ case "markRunning": {
168
+ const [id, opts] = args;
169
+ const result = await getPool().query(`UPDATE jobs
170
+ SET status = 'running', pid = $2, lease_deadline = ${PG_NOW_MS} + $3::bigint
171
+ WHERE id = $1 AND status = 'queued'`, [id, opts?.pid ?? null, workerData.leaseTtlMs]);
172
+ return (result.rowCount ?? 0) > 0;
173
+ }
174
+ case "registerInstance": {
175
+ const meta = args[0];
176
+ await getPool().query(`INSERT INTO gateway_instances (instance_id, role, hostname, pid, started_at, last_heartbeat)
177
+ VALUES ($1, $2, $3, $4, ${PG_NOW_MS}, ${PG_NOW_MS})
178
+ ON CONFLICT (instance_id) DO UPDATE SET
179
+ role = EXCLUDED.role, hostname = EXCLUDED.hostname, pid = EXCLUDED.pid,
180
+ last_heartbeat = EXCLUDED.last_heartbeat`, [meta.instanceId, meta.role ?? null, meta.hostname ?? null, meta.pid ?? null]);
181
+ return null;
182
+ }
183
+ case "heartbeat": {
184
+ const instanceId = args[0];
185
+ await withClient(async (client) => {
186
+ await client.query("BEGIN");
187
+ try {
188
+ await client.query(`UPDATE gateway_instances SET last_heartbeat = ${PG_NOW_MS} WHERE instance_id = $1`, [instanceId]);
189
+ await client.query(`UPDATE jobs SET lease_deadline = ${PG_NOW_MS} + $2
190
+ WHERE owner_instance = $1 AND status IN ('queued', 'running')`, [instanceId, workerData.leaseTtlMs]);
191
+ await client.query("COMMIT");
192
+ }
193
+ catch (error) {
194
+ await client.query("ROLLBACK");
195
+ throw error;
196
+ }
197
+ });
198
+ return null;
199
+ }
200
+ case "deregisterInstance": {
201
+ await getPool().query("DELETE FROM gateway_instances WHERE instance_id = $1", [args[0]]);
202
+ return null;
203
+ }
204
+ case "selectStaleProcessCandidates": {
205
+ const result = await getPool().query(`SELECT j.id AS id, j.pid AS pid, j.transport AS transport,
206
+ j.owner_instance AS owner_instance, gi.hostname AS hostname
207
+ FROM jobs j
208
+ LEFT JOIN gateway_instances gi ON gi.instance_id = j.owner_instance
209
+ WHERE j.status IN ('queued', 'running')
210
+ AND j.transport = 'process'
211
+ AND j.pid IS NOT NULL
212
+ AND (j.lease_deadline IS NULL OR j.lease_deadline < ${PG_NOW_MS})`);
213
+ return result.rows.map(r => ({
214
+ id: r.id,
215
+ pid: r.pid,
216
+ transport: r.transport ?? "process",
217
+ ownerInstance: r.owner_instance ?? null,
218
+ hostname: r.hostname ?? null,
219
+ }));
220
+ }
221
+ case "recoverStaleJobs": {
222
+ const [leaseTtlMs, httpJobGraceMs, liveConfirmedIds] = args;
223
+ const excludeIds = liveConfirmedIds ?? [];
224
+ const httpGraceCutoffMs = "(" + PG_NOW_MS + " - $1::bigint)";
225
+ return await withClient(async (client) => {
226
+ await client.query("BEGIN");
227
+ try {
228
+ if (excludeIds.length > 0) {
229
+ await client.query(`UPDATE jobs SET lease_deadline = ${PG_NOW_MS} + $1::bigint, pid = NULL
230
+ WHERE status IN ('queued', 'running') AND id = ANY($2::text[])`, [leaseTtlMs, excludeIds]);
231
+ }
232
+ const orphan = await client.query(`UPDATE jobs
233
+ SET status = 'orphaned',
234
+ error = COALESCE(error, 'owning gateway instance is no longer alive'),
235
+ finished_at = COALESCE(finished_at, $2),
236
+ expires_at = $3,
237
+ lease_deadline = NULL
238
+ WHERE status IN ('queued', 'running')
239
+ AND (lease_deadline IS NULL OR lease_deadline < ${PG_NOW_MS})
240
+ AND (transport <> 'http'
241
+ OR (EXTRACT(EPOCH FROM started_at::timestamptz) * 1000)::bigint < ${httpGraceCutoffMs})
242
+ AND NOT (id = ANY($4::text[]))
243
+ RETURNING id, correlation_id, started_at, stdout, stderr, exit_code, transport, http_status`, [
244
+ httpJobGraceMs,
245
+ new Date().toISOString(),
246
+ new Date(Date.now() + workerData.retentionMs).toISOString(),
247
+ excludeIds,
248
+ ]);
249
+ await client.query("COMMIT");
250
+ return { orphaned: orphan.rows };
251
+ }
252
+ catch (error) {
253
+ await client.query("ROLLBACK");
254
+ throw error;
255
+ }
256
+ });
257
+ }
258
+ case "gcInstances": {
259
+ const result = await getPool().query(`DELETE FROM gateway_instances WHERE last_heartbeat < ${PG_NOW_MS} - $1::bigint`, [args[0]]);
260
+ return result.rowCount ?? 0;
261
+ }
148
262
  case "recordOutput":
149
263
  await getPool().query("UPDATE jobs SET stdout = $2, stderr = $3, output_truncated = $4 WHERE id = $1", args);
150
264
  return null;
@@ -154,8 +268,8 @@ async function op(method, args) {
154
268
  await getPool().query(`UPDATE jobs
155
269
  SET status = $2, exit_code = $3, stdout = $4, stderr = $5,
156
270
  output_truncated = $6, error = $7, finished_at = $8,
157
- expires_at = $9, http_status = $10
158
- WHERE id = $1`, [
271
+ expires_at = $9, http_status = $10, lease_deadline = NULL
272
+ WHERE id = $1 AND status IN ('queued', 'running', 'orphaned')`, [
159
273
  input.id,
160
274
  input.status,
161
275
  input.exitCode,
@@ -178,7 +292,10 @@ async function op(method, args) {
178
292
  const result = await getPool().query(`SELECT * FROM jobs
179
293
  WHERE request_key = $1
180
294
  AND started_at >= $2
181
- AND status IN ('running', 'completed')
295
+ AND (
296
+ status IN ('running', 'completed')
297
+ OR (status = 'queued' AND lease_deadline IS NOT NULL AND lease_deadline >= ${PG_NOW_MS})
298
+ )
182
299
  ORDER BY started_at DESC
183
300
  LIMIT 1`, [args[0], cutoff]);
184
301
  return result.rows[0] ?? null;
@@ -9,7 +9,7 @@ export interface GatewayStatement {
9
9
  export interface GatewayDatabase {
10
10
  exec(sql: string): void;
11
11
  prepare(sql: string): GatewayStatement;
12
- withTransaction<A extends unknown[]>(fn: (...args: A) => void): (...args: A) => void;
12
+ withTransaction<A extends unknown[], R = void>(fn: (...args: A) => R): (...args: A) => R;
13
13
  close(): void;
14
14
  }
15
15
  export declare function openDatabase(dbPath: string): GatewayDatabase;
@@ -115,8 +115,9 @@ class GatewayDatabaseImpl {
115
115
  this.db.exec("BEGIN");
116
116
  this.inTransaction = true;
117
117
  try {
118
- fn(...args);
118
+ const result = fn(...args);
119
119
  this.db.exec("COMMIT");
120
+ return result;
120
121
  }
121
122
  catch (error) {
122
123
  try {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "llm-cli-gateway",
3
- "version": "2.14.0-rc.1",
3
+ "version": "2.14.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "llm-cli-gateway",
9
- "version": "2.14.0-rc.1",
9
+ "version": "2.14.1",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@modelcontextprotocol/sdk": "^1.29.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-cli-gateway",
3
- "version": "2.14.0-rc.1",
3
+ "version": "2.14.1",
4
4
  "mcpName": "io.github.verivus-oss/llm-cli-gateway",
5
5
  "description": "Local-first MCP gateway for cross-model review across native coding-agent CLIs and API-token LLMs with sessions, durable jobs, and validation receipts.",
6
6
  "license": "MIT",
@@ -58,6 +58,7 @@
58
58
  ".agents/skills/session-workflow/SKILL.md",
59
59
  ".agents/skills/secure-orchestration/SKILL.md",
60
60
  ".agents/skills/implement-review-fix/SKILL.md",
61
+ ".agents/skills/retrospective-walk/SKILL.md",
61
62
  ".agents/skills/public-demo-session/SKILL.md"
62
63
  ],
63
64
  "scripts": {