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.
- package/.agents/skills/retrospective-walk/SKILL.md +294 -0
- package/CHANGELOG.md +52 -0
- package/README.md +19 -7
- package/dist/async-job-manager.d.ts +36 -1
- package/dist/async-job-manager.js +276 -20
- package/dist/config.d.ts +11 -0
- package/dist/config.js +37 -1
- package/dist/index.js +20 -3
- package/dist/job-store.d.ts +75 -5
- package/dist/job-store.js +248 -39
- package/dist/postgres-job-store-worker.js +124 -7
- package/dist/sqlite-driver.d.ts +1 -1
- package/dist/sqlite-driver.js +2 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
|
@@ -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, '
|
|
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
|
|
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;
|
package/dist/sqlite-driver.d.ts
CHANGED
|
@@ -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) =>
|
|
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;
|
package/dist/sqlite-driver.js
CHANGED
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-cli-gateway",
|
|
3
|
-
"version": "2.14.
|
|
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.
|
|
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.
|
|
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": {
|