@h-rig/pi-rig 0.0.6-alpha.7 → 0.0.6-alpha.70
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/dist/src/client.js +238 -7
- package/dist/src/commands.js +33 -3
- package/dist/src/index.js +591 -34
- package/package.json +5 -2
package/dist/src/client.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { existsSync, readFileSync } from "fs";
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
import { dirname, resolve } from "path";
|
|
6
|
+
import { RIG_PROTOCOL_VERSION, RIG_WS_CHANNELS, RIG_WS_METHODS } from "@rig/contracts";
|
|
6
7
|
function cleanString(value) {
|
|
7
8
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
8
9
|
}
|
|
@@ -79,8 +80,9 @@ function createRigContextFromEnv(env = process.env) {
|
|
|
79
80
|
const discovered = discoverRigContext(env);
|
|
80
81
|
const serverUrl = env.RIG_SERVER_URL ?? env.RIG_SERVER_BASE_URL ?? discovered.serverUrl;
|
|
81
82
|
const projectRoot = env.RIG_PROJECT_ROOT ?? env.PROJECT_RIG_ROOT ?? discovered.projectRoot;
|
|
82
|
-
const authToken = env.RIG_AUTH_TOKEN ?? env.
|
|
83
|
+
const authToken = env.RIG_AUTH_TOKEN ?? env.RIG_SERVER_AUTH_TOKEN ?? discovered.authToken;
|
|
83
84
|
const steeringPollMs = cleanNonNegativeInteger(env.RIG_STEERING_POLL_MS);
|
|
85
|
+
const operatorSession = env.RIG_PI_OPERATOR_SESSION === "1" || env.RIG_PI_OPERATOR_SESSION === "true";
|
|
84
86
|
const active = Boolean(runId || taskId || serverUrl || projectRoot);
|
|
85
87
|
if (!active)
|
|
86
88
|
return { active: false };
|
|
@@ -91,7 +93,8 @@ function createRigContextFromEnv(env = process.env) {
|
|
|
91
93
|
...serverUrl ? { serverUrl } : {},
|
|
92
94
|
...projectRoot ? { projectRoot } : {},
|
|
93
95
|
...authToken ? { authToken } : {},
|
|
94
|
-
...steeringPollMs !== null ? { steeringPollMs } : {}
|
|
96
|
+
...steeringPollMs !== null ? { steeringPollMs } : {},
|
|
97
|
+
...operatorSession ? { operatorSession } : {}
|
|
95
98
|
};
|
|
96
99
|
}
|
|
97
100
|
function joinUrl(baseUrl, pathname) {
|
|
@@ -116,6 +119,8 @@ function requireServerUrl(context) {
|
|
|
116
119
|
}
|
|
117
120
|
return context.serverUrl;
|
|
118
121
|
}
|
|
122
|
+
var BRIDGE_REQUEST_TIMEOUT_MS = 30000;
|
|
123
|
+
var PROTOCOL_CHECK_TIMEOUT_MS = 1e4;
|
|
119
124
|
|
|
120
125
|
class RigBridgeClient {
|
|
121
126
|
context;
|
|
@@ -124,18 +129,46 @@ class RigBridgeClient {
|
|
|
124
129
|
this.context = input.context;
|
|
125
130
|
this.fetchImpl = input.fetchImpl ?? fetch;
|
|
126
131
|
}
|
|
127
|
-
async request(pathname, init) {
|
|
132
|
+
async request(pathname, init, timeoutMs = BRIDGE_REQUEST_TIMEOUT_MS) {
|
|
128
133
|
const headers = new Headers(init?.headers);
|
|
129
134
|
if (this.context.authToken && !headers.has("authorization")) {
|
|
130
135
|
headers.set("authorization", `Bearer ${this.context.authToken}`);
|
|
131
136
|
}
|
|
132
|
-
|
|
137
|
+
if (this.context.projectRoot && !headers.has("x-rig-project-root")) {
|
|
138
|
+
headers.set("x-rig-project-root", this.context.projectRoot);
|
|
139
|
+
}
|
|
140
|
+
const signal = init?.signal ?? (timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined);
|
|
141
|
+
const response = await this.fetchImpl(joinUrl(requireServerUrl(this.context), pathname), { ...init, headers, signal });
|
|
133
142
|
return readJsonResponse(response);
|
|
134
143
|
}
|
|
135
|
-
async status() {
|
|
136
|
-
const payload = await this.request("/api/server/status");
|
|
144
|
+
async status(timeoutMs) {
|
|
145
|
+
const payload = await this.request("/api/server/status", undefined, timeoutMs);
|
|
137
146
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
138
147
|
}
|
|
148
|
+
async checkProtocolCompatibility() {
|
|
149
|
+
let payload;
|
|
150
|
+
try {
|
|
151
|
+
payload = await this.status(PROTOCOL_CHECK_TIMEOUT_MS);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return {
|
|
154
|
+
status: "indeterminate",
|
|
155
|
+
serverProtocolVersion: null,
|
|
156
|
+
message: `Rig server protocol check failed: ${error instanceof Error ? error.message : String(error)}`
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const raw = payload.protocolVersion;
|
|
160
|
+
const serverProtocolVersion = typeof raw === "number" && Number.isInteger(raw) && raw >= 0 ? raw : null;
|
|
161
|
+
if (serverProtocolVersion === RIG_PROTOCOL_VERSION) {
|
|
162
|
+
return { status: "compatible", serverProtocolVersion, message: null };
|
|
163
|
+
}
|
|
164
|
+
const serverLabel = serverProtocolVersion === null ? "v0 (no protocolVersion reported)" : `v${serverProtocolVersion}`;
|
|
165
|
+
const updateHint = (serverProtocolVersion ?? 0) < RIG_PROTOCOL_VERSION ? "update the Rig server (upgrade @h-rig/cli / @h-rig/server and restart it)" : "update pi-rig (upgrade @h-rig/pi-rig, or reinstall the extension from this server)";
|
|
166
|
+
return {
|
|
167
|
+
status: "mismatch",
|
|
168
|
+
serverProtocolVersion,
|
|
169
|
+
message: `Rig server speaks protocol ${serverLabel}, this pi-rig speaks v${RIG_PROTOCOL_VERSION} \u2014 ${updateHint}. The Rig bridge is disabled for this session.`
|
|
170
|
+
};
|
|
171
|
+
}
|
|
139
172
|
async listTasks() {
|
|
140
173
|
const payload = await this.request("/api/workspace/tasks");
|
|
141
174
|
return Array.isArray(payload) ? payload.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
@@ -157,6 +190,20 @@ class RigBridgeClient {
|
|
|
157
190
|
const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}`);
|
|
158
191
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { runId };
|
|
159
192
|
}
|
|
193
|
+
async runLogs(runId = this.context.runId, limit = 20) {
|
|
194
|
+
if (!runId)
|
|
195
|
+
throw new Error("runId is required");
|
|
196
|
+
const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/logs?limit=${encodeURIComponent(String(limit))}`);
|
|
197
|
+
const entries = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.entries : null;
|
|
198
|
+
return Array.isArray(entries) ? entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
199
|
+
}
|
|
200
|
+
async runTimeline(runId = this.context.runId, limit = 20) {
|
|
201
|
+
if (!runId)
|
|
202
|
+
throw new Error("runId is required");
|
|
203
|
+
const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/timeline?limit=${encodeURIComponent(String(limit))}`);
|
|
204
|
+
const entries = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.entries : null;
|
|
205
|
+
return Array.isArray(entries) ? entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
206
|
+
}
|
|
160
207
|
async steer(message, runId = this.context.runId) {
|
|
161
208
|
if (!runId)
|
|
162
209
|
throw new Error("runId is required");
|
|
@@ -167,6 +214,16 @@ class RigBridgeClient {
|
|
|
167
214
|
});
|
|
168
215
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
|
|
169
216
|
}
|
|
217
|
+
async stop(runId = this.context.runId) {
|
|
218
|
+
if (!runId)
|
|
219
|
+
throw new Error("runId is required");
|
|
220
|
+
const payload = await this.request("/api/runs/stop", {
|
|
221
|
+
method: "POST",
|
|
222
|
+
headers: { "content-type": "application/json" },
|
|
223
|
+
body: JSON.stringify({ runId })
|
|
224
|
+
});
|
|
225
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true, runId };
|
|
226
|
+
}
|
|
170
227
|
async pollSteering(runId = this.context.runId) {
|
|
171
228
|
if (!runId)
|
|
172
229
|
return [];
|
|
@@ -199,7 +256,181 @@ class RigBridgeClient {
|
|
|
199
256
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
200
257
|
}
|
|
201
258
|
}
|
|
259
|
+
function buildRigWebSocketUrl(serverUrl, authToken) {
|
|
260
|
+
const url = new URL(serverUrl);
|
|
261
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
262
|
+
if (authToken) {
|
|
263
|
+
url.searchParams.set("token", authToken);
|
|
264
|
+
}
|
|
265
|
+
return url.toString();
|
|
266
|
+
}
|
|
267
|
+
function defaultWebSocketFactory() {
|
|
268
|
+
const ctor = globalThis.WebSocket;
|
|
269
|
+
if (typeof ctor !== "function")
|
|
270
|
+
return null;
|
|
271
|
+
return (url) => new ctor(url);
|
|
272
|
+
}
|
|
273
|
+
function webSocketEventText(data) {
|
|
274
|
+
if (typeof data === "string")
|
|
275
|
+
return data;
|
|
276
|
+
if (data instanceof Uint8Array)
|
|
277
|
+
return new TextDecoder().decode(data);
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
class RigBridgeSocket {
|
|
282
|
+
context;
|
|
283
|
+
handlers;
|
|
284
|
+
factory;
|
|
285
|
+
reconnectBaseMs;
|
|
286
|
+
reconnectMaxMs;
|
|
287
|
+
socket = null;
|
|
288
|
+
connectedFlag = false;
|
|
289
|
+
closed = false;
|
|
290
|
+
started = false;
|
|
291
|
+
attempt = 0;
|
|
292
|
+
reconnectTimer = null;
|
|
293
|
+
ackSequence = 0;
|
|
294
|
+
constructor(input) {
|
|
295
|
+
this.context = input.context;
|
|
296
|
+
this.handlers = input.handlers ?? {};
|
|
297
|
+
this.factory = input.webSocketFactory ?? defaultWebSocketFactory();
|
|
298
|
+
this.reconnectBaseMs = input.reconnectBaseMs ?? 1000;
|
|
299
|
+
this.reconnectMaxMs = input.reconnectMaxMs ?? 30000;
|
|
300
|
+
}
|
|
301
|
+
get connected() {
|
|
302
|
+
return this.connectedFlag;
|
|
303
|
+
}
|
|
304
|
+
start() {
|
|
305
|
+
if (this.closed)
|
|
306
|
+
return false;
|
|
307
|
+
if (this.started)
|
|
308
|
+
return true;
|
|
309
|
+
if (!this.context.serverUrl || !this.factory)
|
|
310
|
+
return false;
|
|
311
|
+
this.started = true;
|
|
312
|
+
this.connect();
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
close() {
|
|
316
|
+
this.closed = true;
|
|
317
|
+
this.connectedFlag = false;
|
|
318
|
+
if (this.reconnectTimer) {
|
|
319
|
+
clearTimeout(this.reconnectTimer);
|
|
320
|
+
this.reconnectTimer = null;
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
this.socket?.close();
|
|
324
|
+
} catch {}
|
|
325
|
+
this.socket = null;
|
|
326
|
+
}
|
|
327
|
+
ackSteering(runId, ids) {
|
|
328
|
+
if (!this.connectedFlag || !this.socket || ids.length === 0)
|
|
329
|
+
return;
|
|
330
|
+
try {
|
|
331
|
+
this.socket.send(JSON.stringify({
|
|
332
|
+
id: `pi-rig-steer-ack-${++this.ackSequence}`,
|
|
333
|
+
body: { _tag: RIG_WS_METHODS.ackRunSteering, runId, ids }
|
|
334
|
+
}));
|
|
335
|
+
} catch {}
|
|
336
|
+
}
|
|
337
|
+
connect() {
|
|
338
|
+
if (this.closed || !this.context.serverUrl || !this.factory)
|
|
339
|
+
return;
|
|
340
|
+
let socket;
|
|
341
|
+
try {
|
|
342
|
+
socket = this.factory(buildRigWebSocketUrl(this.context.serverUrl, this.context.authToken));
|
|
343
|
+
} catch {
|
|
344
|
+
this.scheduleReconnect();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
this.socket = socket;
|
|
348
|
+
let gone = false;
|
|
349
|
+
socket.addEventListener("open", () => {
|
|
350
|
+
if (this.closed || gone)
|
|
351
|
+
return;
|
|
352
|
+
this.attempt = 0;
|
|
353
|
+
this.connectedFlag = true;
|
|
354
|
+
this.handlers.onConnect?.();
|
|
355
|
+
});
|
|
356
|
+
const onGone = () => {
|
|
357
|
+
if (gone)
|
|
358
|
+
return;
|
|
359
|
+
gone = true;
|
|
360
|
+
const wasConnected = this.connectedFlag;
|
|
361
|
+
this.connectedFlag = false;
|
|
362
|
+
try {
|
|
363
|
+
socket.close();
|
|
364
|
+
} catch {}
|
|
365
|
+
if (this.socket === socket)
|
|
366
|
+
this.socket = null;
|
|
367
|
+
if (this.closed)
|
|
368
|
+
return;
|
|
369
|
+
if (wasConnected)
|
|
370
|
+
this.handlers.onDisconnect?.();
|
|
371
|
+
this.scheduleReconnect();
|
|
372
|
+
};
|
|
373
|
+
socket.addEventListener("close", onGone);
|
|
374
|
+
socket.addEventListener("error", onGone);
|
|
375
|
+
socket.addEventListener("message", (event) => {
|
|
376
|
+
if (!this.closed)
|
|
377
|
+
this.handleMessage(event);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
scheduleReconnect() {
|
|
381
|
+
if (this.closed || this.reconnectTimer)
|
|
382
|
+
return;
|
|
383
|
+
const delay = Math.min(this.reconnectBaseMs * 2 ** this.attempt, this.reconnectMaxMs);
|
|
384
|
+
this.attempt += 1;
|
|
385
|
+
const timer = setTimeout(() => {
|
|
386
|
+
this.reconnectTimer = null;
|
|
387
|
+
this.connect();
|
|
388
|
+
}, delay);
|
|
389
|
+
this.reconnectTimer = timer;
|
|
390
|
+
if (typeof timer.unref === "function") {
|
|
391
|
+
timer.unref();
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
handleMessage(event) {
|
|
395
|
+
const text = webSocketEventText(event.data);
|
|
396
|
+
if (!text)
|
|
397
|
+
return;
|
|
398
|
+
let parsed;
|
|
399
|
+
try {
|
|
400
|
+
parsed = JSON.parse(text);
|
|
401
|
+
} catch {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
405
|
+
return;
|
|
406
|
+
const record = parsed;
|
|
407
|
+
if (record.type !== "push" || typeof record.channel !== "string")
|
|
408
|
+
return;
|
|
409
|
+
const data = record.data && typeof record.data === "object" && !Array.isArray(record.data) ? record.data : null;
|
|
410
|
+
if (record.channel === RIG_WS_CHANNELS.runSteering) {
|
|
411
|
+
if (!data || !this.context.runId || data.runId !== this.context.runId)
|
|
412
|
+
return;
|
|
413
|
+
const message = data.message && typeof data.message === "object" && !Array.isArray(data.message) ? data.message : null;
|
|
414
|
+
if (message)
|
|
415
|
+
this.handlers.onSteeringMessage?.(message);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (record.channel === RIG_WS_CHANNELS.event) {
|
|
419
|
+
if (data)
|
|
420
|
+
this.handlers.onRigEvent?.(data);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (record.channel === RIG_WS_CHANNELS.snapshotInvalidated) {
|
|
424
|
+
this.handlers.onSnapshotInvalidated?.();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
202
428
|
export {
|
|
203
429
|
createRigContextFromEnv,
|
|
204
|
-
|
|
430
|
+
buildRigWebSocketUrl,
|
|
431
|
+
RigBridgeSocket,
|
|
432
|
+
RigBridgeClient,
|
|
433
|
+
RIG_PROTOCOL_VERSION,
|
|
434
|
+
PROTOCOL_CHECK_TIMEOUT_MS,
|
|
435
|
+
BRIDGE_REQUEST_TIMEOUT_MS
|
|
205
436
|
};
|
package/dist/src/commands.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/pi-rig/src/commands.ts
|
|
3
|
+
function runRecordFromPayload(payload) {
|
|
4
|
+
return payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
|
|
5
|
+
}
|
|
6
|
+
function formatEntry(entry) {
|
|
7
|
+
const type = String(entry.type ?? entry.title ?? "event");
|
|
8
|
+
const text = typeof entry.text === "string" ? entry.text : typeof entry.detail === "string" ? entry.detail : typeof entry.message === "string" ? entry.message : JSON.stringify(entry);
|
|
9
|
+
return `${type}: ${text}`;
|
|
10
|
+
}
|
|
3
11
|
function createRigSlashCommands(input) {
|
|
4
12
|
const notify = input.notify ?? (() => {});
|
|
5
13
|
async function handleRig(args) {
|
|
@@ -28,18 +36,40 @@ function createRigSlashCommands(input) {
|
|
|
28
36
|
}
|
|
29
37
|
if (first === "attach") {
|
|
30
38
|
const run = await input.client.attach(second);
|
|
31
|
-
const runRecord = run
|
|
39
|
+
const runRecord = runRecordFromPayload(run);
|
|
32
40
|
notify(`Attached to ${String(runRecord.runId ?? second ?? input.context.runId ?? "run")}: ${String(runRecord.status ?? "unknown")}`, "info");
|
|
33
41
|
return;
|
|
34
42
|
}
|
|
35
|
-
|
|
43
|
+
if (first === "timeline" || first === "logs") {
|
|
44
|
+
const runId = second || input.context.runId;
|
|
45
|
+
const entries = first === "timeline" ? await input.client.runTimeline(runId, 20) : await input.client.runLogs(runId, 20);
|
|
46
|
+
notify(entries.length > 0 ? entries.slice(-10).map(formatEntry).join(`
|
|
47
|
+
`) : `No ${first} entries for ${runId ?? "run"}.`, "info");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (first === "steer") {
|
|
51
|
+
const message = args.trim().slice("steer".length).trim();
|
|
52
|
+
if (!message) {
|
|
53
|
+
notify("Usage: /rig steer <message>", "error");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
await input.client.steer(message);
|
|
57
|
+
notify("Rig steering message queued.", "info");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (first === "stop") {
|
|
61
|
+
await input.client.stop(second || input.context.runId);
|
|
62
|
+
notify("Rig stop requested.", "info");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
notify("Usage: /rig status | /rig task list | /rig task run [id] | /rig attach [run-id] | /rig timeline [run-id] | /rig logs [run-id] | /rig steer <message> | /rig stop [run-id]", "error");
|
|
36
66
|
} catch (error) {
|
|
37
67
|
notify(error instanceof Error ? error.message : String(error), "error");
|
|
38
68
|
}
|
|
39
69
|
}
|
|
40
70
|
return {
|
|
41
71
|
rig: {
|
|
42
|
-
description: "Rig control-plane commands: status, task list, task run, attach",
|
|
72
|
+
description: "Rig control-plane commands: status, task list, task run, attach, timeline, logs, steer, stop",
|
|
43
73
|
handler: handleRig
|
|
44
74
|
}
|
|
45
75
|
};
|
package/dist/src/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { existsSync, readFileSync } from "fs";
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
import { dirname, resolve } from "path";
|
|
6
|
+
import { RIG_PROTOCOL_VERSION, RIG_WS_CHANNELS, RIG_WS_METHODS } from "@rig/contracts";
|
|
6
7
|
function cleanString(value) {
|
|
7
8
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
8
9
|
}
|
|
@@ -79,8 +80,9 @@ function createRigContextFromEnv(env = process.env) {
|
|
|
79
80
|
const discovered = discoverRigContext(env);
|
|
80
81
|
const serverUrl = env.RIG_SERVER_URL ?? env.RIG_SERVER_BASE_URL ?? discovered.serverUrl;
|
|
81
82
|
const projectRoot = env.RIG_PROJECT_ROOT ?? env.PROJECT_RIG_ROOT ?? discovered.projectRoot;
|
|
82
|
-
const authToken = env.RIG_AUTH_TOKEN ?? env.
|
|
83
|
+
const authToken = env.RIG_AUTH_TOKEN ?? env.RIG_SERVER_AUTH_TOKEN ?? discovered.authToken;
|
|
83
84
|
const steeringPollMs = cleanNonNegativeInteger(env.RIG_STEERING_POLL_MS);
|
|
85
|
+
const operatorSession = env.RIG_PI_OPERATOR_SESSION === "1" || env.RIG_PI_OPERATOR_SESSION === "true";
|
|
84
86
|
const active = Boolean(runId || taskId || serverUrl || projectRoot);
|
|
85
87
|
if (!active)
|
|
86
88
|
return { active: false };
|
|
@@ -91,7 +93,8 @@ function createRigContextFromEnv(env = process.env) {
|
|
|
91
93
|
...serverUrl ? { serverUrl } : {},
|
|
92
94
|
...projectRoot ? { projectRoot } : {},
|
|
93
95
|
...authToken ? { authToken } : {},
|
|
94
|
-
...steeringPollMs !== null ? { steeringPollMs } : {}
|
|
96
|
+
...steeringPollMs !== null ? { steeringPollMs } : {},
|
|
97
|
+
...operatorSession ? { operatorSession } : {}
|
|
95
98
|
};
|
|
96
99
|
}
|
|
97
100
|
function joinUrl(baseUrl, pathname) {
|
|
@@ -116,6 +119,8 @@ function requireServerUrl(context) {
|
|
|
116
119
|
}
|
|
117
120
|
return context.serverUrl;
|
|
118
121
|
}
|
|
122
|
+
var BRIDGE_REQUEST_TIMEOUT_MS = 30000;
|
|
123
|
+
var PROTOCOL_CHECK_TIMEOUT_MS = 1e4;
|
|
119
124
|
|
|
120
125
|
class RigBridgeClient {
|
|
121
126
|
context;
|
|
@@ -124,18 +129,46 @@ class RigBridgeClient {
|
|
|
124
129
|
this.context = input.context;
|
|
125
130
|
this.fetchImpl = input.fetchImpl ?? fetch;
|
|
126
131
|
}
|
|
127
|
-
async request(pathname, init) {
|
|
132
|
+
async request(pathname, init, timeoutMs = BRIDGE_REQUEST_TIMEOUT_MS) {
|
|
128
133
|
const headers = new Headers(init?.headers);
|
|
129
134
|
if (this.context.authToken && !headers.has("authorization")) {
|
|
130
135
|
headers.set("authorization", `Bearer ${this.context.authToken}`);
|
|
131
136
|
}
|
|
132
|
-
|
|
137
|
+
if (this.context.projectRoot && !headers.has("x-rig-project-root")) {
|
|
138
|
+
headers.set("x-rig-project-root", this.context.projectRoot);
|
|
139
|
+
}
|
|
140
|
+
const signal = init?.signal ?? (timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined);
|
|
141
|
+
const response = await this.fetchImpl(joinUrl(requireServerUrl(this.context), pathname), { ...init, headers, signal });
|
|
133
142
|
return readJsonResponse(response);
|
|
134
143
|
}
|
|
135
|
-
async status() {
|
|
136
|
-
const payload = await this.request("/api/server/status");
|
|
144
|
+
async status(timeoutMs) {
|
|
145
|
+
const payload = await this.request("/api/server/status", undefined, timeoutMs);
|
|
137
146
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
138
147
|
}
|
|
148
|
+
async checkProtocolCompatibility() {
|
|
149
|
+
let payload;
|
|
150
|
+
try {
|
|
151
|
+
payload = await this.status(PROTOCOL_CHECK_TIMEOUT_MS);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return {
|
|
154
|
+
status: "indeterminate",
|
|
155
|
+
serverProtocolVersion: null,
|
|
156
|
+
message: `Rig server protocol check failed: ${error instanceof Error ? error.message : String(error)}`
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const raw = payload.protocolVersion;
|
|
160
|
+
const serverProtocolVersion = typeof raw === "number" && Number.isInteger(raw) && raw >= 0 ? raw : null;
|
|
161
|
+
if (serverProtocolVersion === RIG_PROTOCOL_VERSION) {
|
|
162
|
+
return { status: "compatible", serverProtocolVersion, message: null };
|
|
163
|
+
}
|
|
164
|
+
const serverLabel = serverProtocolVersion === null ? "v0 (no protocolVersion reported)" : `v${serverProtocolVersion}`;
|
|
165
|
+
const updateHint = (serverProtocolVersion ?? 0) < RIG_PROTOCOL_VERSION ? "update the Rig server (upgrade @h-rig/cli / @h-rig/server and restart it)" : "update pi-rig (upgrade @h-rig/pi-rig, or reinstall the extension from this server)";
|
|
166
|
+
return {
|
|
167
|
+
status: "mismatch",
|
|
168
|
+
serverProtocolVersion,
|
|
169
|
+
message: `Rig server speaks protocol ${serverLabel}, this pi-rig speaks v${RIG_PROTOCOL_VERSION} \u2014 ${updateHint}. The Rig bridge is disabled for this session.`
|
|
170
|
+
};
|
|
171
|
+
}
|
|
139
172
|
async listTasks() {
|
|
140
173
|
const payload = await this.request("/api/workspace/tasks");
|
|
141
174
|
return Array.isArray(payload) ? payload.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
@@ -157,6 +190,20 @@ class RigBridgeClient {
|
|
|
157
190
|
const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}`);
|
|
158
191
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { runId };
|
|
159
192
|
}
|
|
193
|
+
async runLogs(runId = this.context.runId, limit = 20) {
|
|
194
|
+
if (!runId)
|
|
195
|
+
throw new Error("runId is required");
|
|
196
|
+
const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/logs?limit=${encodeURIComponent(String(limit))}`);
|
|
197
|
+
const entries = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.entries : null;
|
|
198
|
+
return Array.isArray(entries) ? entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
199
|
+
}
|
|
200
|
+
async runTimeline(runId = this.context.runId, limit = 20) {
|
|
201
|
+
if (!runId)
|
|
202
|
+
throw new Error("runId is required");
|
|
203
|
+
const payload = await this.request(`/api/runs/${encodeURIComponent(runId)}/timeline?limit=${encodeURIComponent(String(limit))}`);
|
|
204
|
+
const entries = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.entries : null;
|
|
205
|
+
return Array.isArray(entries) ? entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
206
|
+
}
|
|
160
207
|
async steer(message, runId = this.context.runId) {
|
|
161
208
|
if (!runId)
|
|
162
209
|
throw new Error("runId is required");
|
|
@@ -167,6 +214,16 @@ class RigBridgeClient {
|
|
|
167
214
|
});
|
|
168
215
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true };
|
|
169
216
|
}
|
|
217
|
+
async stop(runId = this.context.runId) {
|
|
218
|
+
if (!runId)
|
|
219
|
+
throw new Error("runId is required");
|
|
220
|
+
const payload = await this.request("/api/runs/stop", {
|
|
221
|
+
method: "POST",
|
|
222
|
+
headers: { "content-type": "application/json" },
|
|
223
|
+
body: JSON.stringify({ runId })
|
|
224
|
+
});
|
|
225
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : { ok: true, runId };
|
|
226
|
+
}
|
|
170
227
|
async pollSteering(runId = this.context.runId) {
|
|
171
228
|
if (!runId)
|
|
172
229
|
return [];
|
|
@@ -199,8 +256,185 @@ class RigBridgeClient {
|
|
|
199
256
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
200
257
|
}
|
|
201
258
|
}
|
|
259
|
+
function buildRigWebSocketUrl(serverUrl, authToken) {
|
|
260
|
+
const url = new URL(serverUrl);
|
|
261
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
262
|
+
if (authToken) {
|
|
263
|
+
url.searchParams.set("token", authToken);
|
|
264
|
+
}
|
|
265
|
+
return url.toString();
|
|
266
|
+
}
|
|
267
|
+
function defaultWebSocketFactory() {
|
|
268
|
+
const ctor = globalThis.WebSocket;
|
|
269
|
+
if (typeof ctor !== "function")
|
|
270
|
+
return null;
|
|
271
|
+
return (url) => new ctor(url);
|
|
272
|
+
}
|
|
273
|
+
function webSocketEventText(data) {
|
|
274
|
+
if (typeof data === "string")
|
|
275
|
+
return data;
|
|
276
|
+
if (data instanceof Uint8Array)
|
|
277
|
+
return new TextDecoder().decode(data);
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
class RigBridgeSocket {
|
|
282
|
+
context;
|
|
283
|
+
handlers;
|
|
284
|
+
factory;
|
|
285
|
+
reconnectBaseMs;
|
|
286
|
+
reconnectMaxMs;
|
|
287
|
+
socket = null;
|
|
288
|
+
connectedFlag = false;
|
|
289
|
+
closed = false;
|
|
290
|
+
started = false;
|
|
291
|
+
attempt = 0;
|
|
292
|
+
reconnectTimer = null;
|
|
293
|
+
ackSequence = 0;
|
|
294
|
+
constructor(input) {
|
|
295
|
+
this.context = input.context;
|
|
296
|
+
this.handlers = input.handlers ?? {};
|
|
297
|
+
this.factory = input.webSocketFactory ?? defaultWebSocketFactory();
|
|
298
|
+
this.reconnectBaseMs = input.reconnectBaseMs ?? 1000;
|
|
299
|
+
this.reconnectMaxMs = input.reconnectMaxMs ?? 30000;
|
|
300
|
+
}
|
|
301
|
+
get connected() {
|
|
302
|
+
return this.connectedFlag;
|
|
303
|
+
}
|
|
304
|
+
start() {
|
|
305
|
+
if (this.closed)
|
|
306
|
+
return false;
|
|
307
|
+
if (this.started)
|
|
308
|
+
return true;
|
|
309
|
+
if (!this.context.serverUrl || !this.factory)
|
|
310
|
+
return false;
|
|
311
|
+
this.started = true;
|
|
312
|
+
this.connect();
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
close() {
|
|
316
|
+
this.closed = true;
|
|
317
|
+
this.connectedFlag = false;
|
|
318
|
+
if (this.reconnectTimer) {
|
|
319
|
+
clearTimeout(this.reconnectTimer);
|
|
320
|
+
this.reconnectTimer = null;
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
this.socket?.close();
|
|
324
|
+
} catch {}
|
|
325
|
+
this.socket = null;
|
|
326
|
+
}
|
|
327
|
+
ackSteering(runId, ids) {
|
|
328
|
+
if (!this.connectedFlag || !this.socket || ids.length === 0)
|
|
329
|
+
return;
|
|
330
|
+
try {
|
|
331
|
+
this.socket.send(JSON.stringify({
|
|
332
|
+
id: `pi-rig-steer-ack-${++this.ackSequence}`,
|
|
333
|
+
body: { _tag: RIG_WS_METHODS.ackRunSteering, runId, ids }
|
|
334
|
+
}));
|
|
335
|
+
} catch {}
|
|
336
|
+
}
|
|
337
|
+
connect() {
|
|
338
|
+
if (this.closed || !this.context.serverUrl || !this.factory)
|
|
339
|
+
return;
|
|
340
|
+
let socket;
|
|
341
|
+
try {
|
|
342
|
+
socket = this.factory(buildRigWebSocketUrl(this.context.serverUrl, this.context.authToken));
|
|
343
|
+
} catch {
|
|
344
|
+
this.scheduleReconnect();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
this.socket = socket;
|
|
348
|
+
let gone = false;
|
|
349
|
+
socket.addEventListener("open", () => {
|
|
350
|
+
if (this.closed || gone)
|
|
351
|
+
return;
|
|
352
|
+
this.attempt = 0;
|
|
353
|
+
this.connectedFlag = true;
|
|
354
|
+
this.handlers.onConnect?.();
|
|
355
|
+
});
|
|
356
|
+
const onGone = () => {
|
|
357
|
+
if (gone)
|
|
358
|
+
return;
|
|
359
|
+
gone = true;
|
|
360
|
+
const wasConnected = this.connectedFlag;
|
|
361
|
+
this.connectedFlag = false;
|
|
362
|
+
try {
|
|
363
|
+
socket.close();
|
|
364
|
+
} catch {}
|
|
365
|
+
if (this.socket === socket)
|
|
366
|
+
this.socket = null;
|
|
367
|
+
if (this.closed)
|
|
368
|
+
return;
|
|
369
|
+
if (wasConnected)
|
|
370
|
+
this.handlers.onDisconnect?.();
|
|
371
|
+
this.scheduleReconnect();
|
|
372
|
+
};
|
|
373
|
+
socket.addEventListener("close", onGone);
|
|
374
|
+
socket.addEventListener("error", onGone);
|
|
375
|
+
socket.addEventListener("message", (event) => {
|
|
376
|
+
if (!this.closed)
|
|
377
|
+
this.handleMessage(event);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
scheduleReconnect() {
|
|
381
|
+
if (this.closed || this.reconnectTimer)
|
|
382
|
+
return;
|
|
383
|
+
const delay = Math.min(this.reconnectBaseMs * 2 ** this.attempt, this.reconnectMaxMs);
|
|
384
|
+
this.attempt += 1;
|
|
385
|
+
const timer = setTimeout(() => {
|
|
386
|
+
this.reconnectTimer = null;
|
|
387
|
+
this.connect();
|
|
388
|
+
}, delay);
|
|
389
|
+
this.reconnectTimer = timer;
|
|
390
|
+
if (typeof timer.unref === "function") {
|
|
391
|
+
timer.unref();
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
handleMessage(event) {
|
|
395
|
+
const text = webSocketEventText(event.data);
|
|
396
|
+
if (!text)
|
|
397
|
+
return;
|
|
398
|
+
let parsed;
|
|
399
|
+
try {
|
|
400
|
+
parsed = JSON.parse(text);
|
|
401
|
+
} catch {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
405
|
+
return;
|
|
406
|
+
const record = parsed;
|
|
407
|
+
if (record.type !== "push" || typeof record.channel !== "string")
|
|
408
|
+
return;
|
|
409
|
+
const data = record.data && typeof record.data === "object" && !Array.isArray(record.data) ? record.data : null;
|
|
410
|
+
if (record.channel === RIG_WS_CHANNELS.runSteering) {
|
|
411
|
+
if (!data || !this.context.runId || data.runId !== this.context.runId)
|
|
412
|
+
return;
|
|
413
|
+
const message = data.message && typeof data.message === "object" && !Array.isArray(data.message) ? data.message : null;
|
|
414
|
+
if (message)
|
|
415
|
+
this.handlers.onSteeringMessage?.(message);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (record.channel === RIG_WS_CHANNELS.event) {
|
|
419
|
+
if (data)
|
|
420
|
+
this.handlers.onRigEvent?.(data);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (record.channel === RIG_WS_CHANNELS.snapshotInvalidated) {
|
|
424
|
+
this.handlers.onSnapshotInvalidated?.();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
202
428
|
|
|
203
429
|
// packages/pi-rig/src/commands.ts
|
|
430
|
+
function runRecordFromPayload(payload) {
|
|
431
|
+
return payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
|
|
432
|
+
}
|
|
433
|
+
function formatEntry(entry) {
|
|
434
|
+
const type = String(entry.type ?? entry.title ?? "event");
|
|
435
|
+
const text = typeof entry.text === "string" ? entry.text : typeof entry.detail === "string" ? entry.detail : typeof entry.message === "string" ? entry.message : JSON.stringify(entry);
|
|
436
|
+
return `${type}: ${text}`;
|
|
437
|
+
}
|
|
204
438
|
function createRigSlashCommands(input) {
|
|
205
439
|
const notify = input.notify ?? (() => {});
|
|
206
440
|
async function handleRig(args) {
|
|
@@ -229,18 +463,40 @@ function createRigSlashCommands(input) {
|
|
|
229
463
|
}
|
|
230
464
|
if (first === "attach") {
|
|
231
465
|
const run = await input.client.attach(second);
|
|
232
|
-
const runRecord = run
|
|
466
|
+
const runRecord = runRecordFromPayload(run);
|
|
233
467
|
notify(`Attached to ${String(runRecord.runId ?? second ?? input.context.runId ?? "run")}: ${String(runRecord.status ?? "unknown")}`, "info");
|
|
234
468
|
return;
|
|
235
469
|
}
|
|
236
|
-
|
|
470
|
+
if (first === "timeline" || first === "logs") {
|
|
471
|
+
const runId = second || input.context.runId;
|
|
472
|
+
const entries = first === "timeline" ? await input.client.runTimeline(runId, 20) : await input.client.runLogs(runId, 20);
|
|
473
|
+
notify(entries.length > 0 ? entries.slice(-10).map(formatEntry).join(`
|
|
474
|
+
`) : `No ${first} entries for ${runId ?? "run"}.`, "info");
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (first === "steer") {
|
|
478
|
+
const message = args.trim().slice("steer".length).trim();
|
|
479
|
+
if (!message) {
|
|
480
|
+
notify("Usage: /rig steer <message>", "error");
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
await input.client.steer(message);
|
|
484
|
+
notify("Rig steering message queued.", "info");
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (first === "stop") {
|
|
488
|
+
await input.client.stop(second || input.context.runId);
|
|
489
|
+
notify("Rig stop requested.", "info");
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
notify("Usage: /rig status | /rig task list | /rig task run [id] | /rig attach [run-id] | /rig timeline [run-id] | /rig logs [run-id] | /rig steer <message> | /rig stop [run-id]", "error");
|
|
237
493
|
} catch (error) {
|
|
238
494
|
notify(error instanceof Error ? error.message : String(error), "error");
|
|
239
495
|
}
|
|
240
496
|
}
|
|
241
497
|
return {
|
|
242
498
|
rig: {
|
|
243
|
-
description: "Rig control-plane commands: status, task list, task run, attach",
|
|
499
|
+
description: "Rig control-plane commands: status, task list, task run, attach, timeline, logs, steer, stop",
|
|
244
500
|
handler: handleRig
|
|
245
501
|
}
|
|
246
502
|
};
|
|
@@ -308,7 +564,8 @@ function createPiRigExtensionState(input = {}) {
|
|
|
308
564
|
const context = createRigContextFromEnv(input.env ?? process.env);
|
|
309
565
|
return {
|
|
310
566
|
...context,
|
|
311
|
-
client: new RigBridgeClient({ context, fetchImpl: input.fetchImpl })
|
|
567
|
+
client: new RigBridgeClient({ context, fetchImpl: input.fetchImpl }),
|
|
568
|
+
...input.webSocketFactory ? { webSocketFactory: input.webSocketFactory } : {}
|
|
312
569
|
};
|
|
313
570
|
}
|
|
314
571
|
function notify(ctx, message, level = "info") {
|
|
@@ -318,6 +575,62 @@ function notify(ctx, message, level = "info") {
|
|
|
318
575
|
notifyFn.call(ui, message, level);
|
|
319
576
|
}
|
|
320
577
|
}
|
|
578
|
+
function canNotify(ctx) {
|
|
579
|
+
const ui = ctx && typeof ctx === "object" ? ctx.ui : null;
|
|
580
|
+
return Boolean(ui && typeof ui === "object" && typeof ui.notify === "function");
|
|
581
|
+
}
|
|
582
|
+
function setWidget(ctx, id, lines) {
|
|
583
|
+
const ui = ctx && typeof ctx === "object" ? ctx.ui : null;
|
|
584
|
+
const setWidgetFn = ui && typeof ui === "object" ? ui.setWidget : null;
|
|
585
|
+
if (typeof setWidgetFn === "function") {
|
|
586
|
+
setWidgetFn.call(ui, id, lines);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function setStatus(ctx, id, text) {
|
|
590
|
+
const ui = ctx && typeof ctx === "object" ? ctx.ui : null;
|
|
591
|
+
const setStatusFn = ui && typeof ui === "object" ? ui.setStatus : null;
|
|
592
|
+
if (typeof setStatusFn === "function") {
|
|
593
|
+
setStatusFn.call(ui, id, text);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function setFooter(ctx, line) {
|
|
597
|
+
const ui = ctx && typeof ctx === "object" ? ctx.ui : null;
|
|
598
|
+
const setFooterFn = ui && typeof ui === "object" ? ui.setFooter : null;
|
|
599
|
+
if (typeof setFooterFn !== "function")
|
|
600
|
+
return;
|
|
601
|
+
setFooterFn.call(ui, () => ({
|
|
602
|
+
render(width) {
|
|
603
|
+
const max = Math.max(0, Math.trunc(width));
|
|
604
|
+
if (max === 0)
|
|
605
|
+
return [""];
|
|
606
|
+
return [line.length > max ? `${line.slice(0, Math.max(0, max - 1))}\u2026` : line];
|
|
607
|
+
},
|
|
608
|
+
invalidate() {}
|
|
609
|
+
}));
|
|
610
|
+
}
|
|
611
|
+
function createBridgeGate(state) {
|
|
612
|
+
let pending = null;
|
|
613
|
+
let warned = false;
|
|
614
|
+
return async (ctx) => {
|
|
615
|
+
if (!state.active || !state.serverUrl)
|
|
616
|
+
return { allowed: true, message: null, status: "indeterminate" };
|
|
617
|
+
pending ??= state.client.checkProtocolCompatibility().then((check2) => {
|
|
618
|
+
if (check2.status === "indeterminate")
|
|
619
|
+
pending = null;
|
|
620
|
+
return check2;
|
|
621
|
+
});
|
|
622
|
+
const check = await pending;
|
|
623
|
+
if (check.status !== "mismatch")
|
|
624
|
+
return { allowed: true, message: null, status: check.status };
|
|
625
|
+
const message = check.message ?? "Rig protocol mismatch \u2014 the Rig bridge is disabled.";
|
|
626
|
+
if (!warned && canNotify(ctx)) {
|
|
627
|
+
warned = true;
|
|
628
|
+
notify(ctx, message, "error");
|
|
629
|
+
setStatus(ctx, "rig", "Rig bridge disabled (protocol mismatch)");
|
|
630
|
+
}
|
|
631
|
+
return { allowed: false, message, status: "mismatch" };
|
|
632
|
+
};
|
|
633
|
+
}
|
|
321
634
|
function steeringText(message) {
|
|
322
635
|
const text = typeof message.message === "string" ? message.message.trim() : "";
|
|
323
636
|
if (!text)
|
|
@@ -326,54 +639,276 @@ function steeringText(message) {
|
|
|
326
639
|
return `[Rig steering from ${actor}]
|
|
327
640
|
${text}`;
|
|
328
641
|
}
|
|
329
|
-
|
|
330
|
-
if (
|
|
642
|
+
function unrefTimer(timer) {
|
|
643
|
+
if (typeof timer.unref === "function") {
|
|
644
|
+
timer.unref();
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
var STEERING_DEDUPE_LIMIT = 500;
|
|
648
|
+
function rememberDeliveredSteeringId(deliveredIds, id) {
|
|
649
|
+
deliveredIds.add(id);
|
|
650
|
+
if (deliveredIds.size > STEERING_DEDUPE_LIMIT) {
|
|
651
|
+
const oldest = deliveredIds.values().next().value;
|
|
652
|
+
if (typeof oldest === "string")
|
|
653
|
+
deliveredIds.delete(oldest);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async function deliverSteeringMessage(pi, deliveredIds, message) {
|
|
657
|
+
if (typeof pi.sendUserMessage !== "function")
|
|
658
|
+
return false;
|
|
659
|
+
const id = typeof message.id === "string" && message.id.trim() ? message.id : null;
|
|
660
|
+
if (id && deliveredIds.has(id))
|
|
661
|
+
return false;
|
|
662
|
+
const text = steeringText(message);
|
|
663
|
+
if (!text)
|
|
664
|
+
return false;
|
|
665
|
+
if (id)
|
|
666
|
+
rememberDeliveredSteeringId(deliveredIds, id);
|
|
667
|
+
await pi.sendUserMessage(text, { deliverAs: "steer", triggerTurn: true });
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
async function consumeQueuedSteering(pi, state, ctx, gate, deliveredIds) {
|
|
671
|
+
if (state.operatorSession || !state.active || !state.runId || typeof pi.sendUserMessage !== "function")
|
|
672
|
+
return;
|
|
673
|
+
if (!(await gate(ctx)).allowed)
|
|
331
674
|
return;
|
|
332
675
|
try {
|
|
333
676
|
const messages = await state.client.consumeSteering(state.runId);
|
|
677
|
+
let deliveredCount = 0;
|
|
334
678
|
for (const message of messages) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
continue;
|
|
338
|
-
await pi.sendUserMessage(text, { deliverAs: "steer", triggerTurn: true });
|
|
679
|
+
if (await deliverSteeringMessage(pi, deliveredIds, message))
|
|
680
|
+
deliveredCount += 1;
|
|
339
681
|
}
|
|
340
|
-
if (
|
|
341
|
-
notify(ctx, `Delivered ${
|
|
682
|
+
if (deliveredCount > 0) {
|
|
683
|
+
notify(ctx, `Delivered ${deliveredCount} Rig steering message${deliveredCount === 1 ? "" : "s"}.`);
|
|
342
684
|
}
|
|
343
685
|
} catch (error) {
|
|
344
686
|
notify(ctx, `Rig steering sync failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
345
687
|
}
|
|
346
688
|
}
|
|
347
|
-
function
|
|
348
|
-
if (!
|
|
689
|
+
function inputText(event) {
|
|
690
|
+
if (!event || typeof event !== "object" || Array.isArray(event))
|
|
691
|
+
return null;
|
|
692
|
+
const text = event.text;
|
|
693
|
+
return typeof text === "string" && text.trim() ? text.trim() : null;
|
|
694
|
+
}
|
|
695
|
+
async function handleOperatorInput(event, state, ctx, gate) {
|
|
696
|
+
if (!state.operatorSession || !state.active || !state.runId)
|
|
697
|
+
return;
|
|
698
|
+
const text = inputText(event);
|
|
699
|
+
if (!text || text.startsWith("/"))
|
|
700
|
+
return;
|
|
701
|
+
if (!(await gate(ctx)).allowed)
|
|
702
|
+
return;
|
|
703
|
+
try {
|
|
704
|
+
await state.client.steer(text, state.runId);
|
|
705
|
+
notify(ctx, "Rig steering message queued.");
|
|
706
|
+
} catch (error) {
|
|
707
|
+
notify(ctx, `Rig steering failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
708
|
+
}
|
|
709
|
+
return { action: "handled" };
|
|
710
|
+
}
|
|
711
|
+
function shortEntry(entry) {
|
|
712
|
+
const type = String(entry.type ?? entry.title ?? "event");
|
|
713
|
+
const text = typeof entry.text === "string" ? entry.text : typeof entry.detail === "string" ? entry.detail : typeof entry.message === "string" ? entry.message : "";
|
|
714
|
+
return `${type}: ${text}`.slice(0, 160);
|
|
715
|
+
}
|
|
716
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
717
|
+
function runLocation(run) {
|
|
718
|
+
const worktree = typeof run.worktreePath === "string" && run.worktreePath.trim() ? run.worktreePath.trim() : null;
|
|
719
|
+
const projectRoot = typeof run.projectRoot === "string" && run.projectRoot.trim() ? run.projectRoot.trim() : null;
|
|
720
|
+
return worktree ?? projectRoot ?? "remote/local worker workspace";
|
|
721
|
+
}
|
|
722
|
+
function runPayload(payload) {
|
|
723
|
+
return payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
|
|
724
|
+
}
|
|
725
|
+
var OPERATOR_WIDGET_WS_FALLBACK_MS = 1e4;
|
|
726
|
+
function startOperatorRunWidget(state, ctx, live) {
|
|
727
|
+
if (!state.operatorSession || !state.active || !state.runId)
|
|
728
|
+
return;
|
|
729
|
+
let inFlight = false;
|
|
730
|
+
let frame = 0;
|
|
731
|
+
let lastRefreshAt = 0;
|
|
732
|
+
const refresh = async () => {
|
|
733
|
+
if (inFlight)
|
|
734
|
+
return;
|
|
735
|
+
inFlight = true;
|
|
736
|
+
lastRefreshAt = Date.now();
|
|
737
|
+
const spinner = SPINNER_FRAMES[frame++ % SPINNER_FRAMES.length] ?? "\u2022";
|
|
738
|
+
try {
|
|
739
|
+
const [runPayloadRecord, timeline] = await Promise.all([
|
|
740
|
+
state.client.attach(state.runId),
|
|
741
|
+
state.client.runTimeline(state.runId, 8).catch(() => [])
|
|
742
|
+
]);
|
|
743
|
+
const run = runPayload(runPayloadRecord);
|
|
744
|
+
const status = String(run.status ?? "unknown");
|
|
745
|
+
const header = `${spinner} Rig ${String(run.runId ?? state.runId)} \xB7 ${status}`;
|
|
746
|
+
const location = runLocation(run);
|
|
747
|
+
const detail = typeof run.statusDetail === "string" && run.statusDetail.trim() ? run.statusDetail.trim() : String(run.title ?? run.taskId ?? "");
|
|
748
|
+
const lines = [
|
|
749
|
+
header,
|
|
750
|
+
`worker: ${location}`.slice(0, 200),
|
|
751
|
+
...detail ? [detail.slice(0, 160)] : [],
|
|
752
|
+
...timeline.slice(-5).map(shortEntry)
|
|
753
|
+
];
|
|
754
|
+
setStatus(ctx, "rig", `${spinner} Rig ${status}`);
|
|
755
|
+
setFooter(ctx, `${spinner} Rig ${String(run.runId ?? state.runId)} \xB7 ${status} \xB7 worker ${location}`);
|
|
756
|
+
setWidget(ctx, "rig-run", lines);
|
|
757
|
+
} catch (error) {
|
|
758
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
759
|
+
setStatus(ctx, "rig", `${spinner} Rig unavailable`);
|
|
760
|
+
setFooter(ctx, `${spinner} Rig ${state.runId} \xB7 unavailable \xB7 ${message}`);
|
|
761
|
+
setWidget(ctx, "rig-run", [`${spinner} Rig ${state.runId} \xB7 unavailable`, message]);
|
|
762
|
+
} finally {
|
|
763
|
+
inFlight = false;
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
refresh();
|
|
767
|
+
const timer = setInterval(() => {
|
|
768
|
+
const triggered = live?.consumePushTrigger() ?? false;
|
|
769
|
+
if ((live?.isConnected() ?? false) && !triggered && Date.now() - lastRefreshAt < OPERATOR_WIDGET_WS_FALLBACK_MS) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
refresh();
|
|
773
|
+
}, 1000);
|
|
774
|
+
unrefTimer(timer);
|
|
775
|
+
}
|
|
776
|
+
function operatorInboxNotification(event) {
|
|
777
|
+
const type = typeof event.type === "string" ? event.type : null;
|
|
778
|
+
if (type !== "rig.approval.requested" && type !== "rig.user-input.requested")
|
|
779
|
+
return null;
|
|
780
|
+
const payload = event.payload && typeof event.payload === "object" && !Array.isArray(event.payload) ? event.payload : {};
|
|
781
|
+
const runId = typeof payload.runId === "string" && payload.runId.trim() ? payload.runId : typeof event.aggregateId === "string" && event.aggregateId.trim() ? event.aggregateId : "unknown";
|
|
782
|
+
const requestId = typeof payload.requestId === "string" && payload.requestId.trim() ? payload.requestId : `${type}:${runId}`;
|
|
783
|
+
const waitingOn = type === "rig.approval.requested" ? "an approval" : "user input";
|
|
784
|
+
return {
|
|
785
|
+
key: requestId,
|
|
786
|
+
text: `Run ${runId} is waiting on ${waitingOn} \u2014 resolve with /rig inbox or \`rig inbox\`.`
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
function startOperatorBridge(state, ctx) {
|
|
790
|
+
if (!state.operatorSession || !state.active)
|
|
349
791
|
return;
|
|
792
|
+
const notifiedRequests = new Set;
|
|
793
|
+
let pushTrigger = false;
|
|
794
|
+
const socket = new RigBridgeSocket({
|
|
795
|
+
context: state,
|
|
796
|
+
webSocketFactory: state.webSocketFactory,
|
|
797
|
+
handlers: {
|
|
798
|
+
onRigEvent: (event) => {
|
|
799
|
+
pushTrigger = true;
|
|
800
|
+
const notification = operatorInboxNotification(event);
|
|
801
|
+
if (!notification || notifiedRequests.has(notification.key))
|
|
802
|
+
return;
|
|
803
|
+
notifiedRequests.add(notification.key);
|
|
804
|
+
if (notifiedRequests.size > 1000) {
|
|
805
|
+
const oldest = notifiedRequests.values().next().value;
|
|
806
|
+
if (typeof oldest === "string")
|
|
807
|
+
notifiedRequests.delete(oldest);
|
|
808
|
+
}
|
|
809
|
+
notify(ctx, notification.text);
|
|
810
|
+
},
|
|
811
|
+
onSnapshotInvalidated: () => {
|
|
812
|
+
pushTrigger = true;
|
|
813
|
+
},
|
|
814
|
+
onDisconnect: () => {
|
|
815
|
+
pushTrigger = true;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
if (!socket.start())
|
|
820
|
+
return;
|
|
821
|
+
return {
|
|
822
|
+
isConnected: () => socket.connected,
|
|
823
|
+
consumePushTrigger: () => {
|
|
824
|
+
const triggered = pushTrigger;
|
|
825
|
+
pushTrigger = false;
|
|
826
|
+
return triggered;
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
function startSteeringBridge(pi, state, ctx, gate, deliveredIds) {
|
|
831
|
+
if (state.operatorSession || !state.active || !state.runId || typeof pi.sendUserMessage !== "function")
|
|
832
|
+
return;
|
|
833
|
+
const runId = state.runId;
|
|
834
|
+
const socket = new RigBridgeSocket({
|
|
835
|
+
context: state,
|
|
836
|
+
webSocketFactory: state.webSocketFactory,
|
|
837
|
+
handlers: {
|
|
838
|
+
onSteeringMessage: (message) => {
|
|
839
|
+
(async () => {
|
|
840
|
+
try {
|
|
841
|
+
if (!await deliverSteeringMessage(pi, deliveredIds, message))
|
|
842
|
+
return;
|
|
843
|
+
const id = typeof message.id === "string" && message.id.trim() ? message.id : null;
|
|
844
|
+
if (id)
|
|
845
|
+
socket.ackSteering(runId, [id]);
|
|
846
|
+
notify(ctx, "Delivered 1 Rig steering message.");
|
|
847
|
+
} catch (error) {
|
|
848
|
+
notify(ctx, `Rig steering sync failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
849
|
+
}
|
|
850
|
+
})();
|
|
851
|
+
},
|
|
852
|
+
onConnect: () => {
|
|
853
|
+
consumeQueuedSteering(pi, state, ctx, gate, deliveredIds);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
(async () => {
|
|
858
|
+
const gateResult = await gate(ctx);
|
|
859
|
+
if (!gateResult.allowed)
|
|
860
|
+
return;
|
|
861
|
+
if (gateResult.status === "compatible") {
|
|
862
|
+
socket.start();
|
|
863
|
+
}
|
|
864
|
+
})();
|
|
350
865
|
const intervalMs = state.steeringPollMs ?? 1000;
|
|
351
866
|
if (intervalMs <= 0)
|
|
352
867
|
return;
|
|
868
|
+
const WS_CONNECTED_SWEEP_MS = 1e4;
|
|
353
869
|
let inFlight = false;
|
|
870
|
+
let lastSweepAt = 0;
|
|
354
871
|
const timer = setInterval(() => {
|
|
355
872
|
if (inFlight)
|
|
356
873
|
return;
|
|
874
|
+
if (socket.connected && Date.now() - lastSweepAt < WS_CONNECTED_SWEEP_MS)
|
|
875
|
+
return;
|
|
357
876
|
inFlight = true;
|
|
358
|
-
|
|
877
|
+
lastSweepAt = Date.now();
|
|
878
|
+
consumeQueuedSteering(pi, state, ctx, gate, deliveredIds).finally(() => {
|
|
359
879
|
inFlight = false;
|
|
360
880
|
});
|
|
361
881
|
}, intervalMs);
|
|
362
|
-
|
|
363
|
-
timer.unref();
|
|
364
|
-
}
|
|
882
|
+
unrefTimer(timer);
|
|
365
883
|
}
|
|
366
884
|
function createPiRigExtension(pi, options = {}) {
|
|
367
885
|
const state = options.state ?? createPiRigExtensionState();
|
|
886
|
+
const gate = createBridgeGate(state);
|
|
887
|
+
const deliveredSteeringIds = new Set;
|
|
368
888
|
const commands = createRigSlashCommands({
|
|
369
889
|
context: state,
|
|
370
890
|
client: state.client,
|
|
371
891
|
notify: (message, level) => notify(globalThis, message, level)
|
|
372
892
|
});
|
|
893
|
+
const tryRegister = (label, register) => {
|
|
894
|
+
try {
|
|
895
|
+
register();
|
|
896
|
+
} catch (error) {
|
|
897
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
898
|
+
if (/conflict|already|duplicate/i.test(message))
|
|
899
|
+
return;
|
|
900
|
+
throw error;
|
|
901
|
+
}
|
|
902
|
+
};
|
|
373
903
|
for (const [name, command] of Object.entries(commands)) {
|
|
374
|
-
pi.registerCommand?.(name, {
|
|
904
|
+
tryRegister(`command:${name}`, () => pi.registerCommand?.(name, {
|
|
375
905
|
description: command.description,
|
|
376
906
|
handler: async (args, ctx) => {
|
|
907
|
+
const gateResult = await gate(ctx);
|
|
908
|
+
if (!gateResult.allowed) {
|
|
909
|
+
notify(ctx, gateResult.message ?? "Rig bridge disabled (protocol mismatch).", "error");
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
377
912
|
const nextCommands = createRigSlashCommands({
|
|
378
913
|
context: state,
|
|
379
914
|
client: state.client,
|
|
@@ -381,26 +916,48 @@ function createPiRigExtension(pi, options = {}) {
|
|
|
381
916
|
});
|
|
382
917
|
await nextCommands.rig.handler(args, ctx);
|
|
383
918
|
}
|
|
384
|
-
});
|
|
919
|
+
}));
|
|
385
920
|
}
|
|
386
921
|
if (state.active && state.runId) {
|
|
387
922
|
for (const tool of createRigTools({ context: state, client: state.client })) {
|
|
388
|
-
pi.registerTool?.(
|
|
923
|
+
tryRegister(`tool:${String(tool.name ?? "rig-tool")}`, () => pi.registerTool?.({
|
|
924
|
+
...tool,
|
|
925
|
+
execute: async (toolCallId, params) => {
|
|
926
|
+
const gateResult = await gate(globalThis);
|
|
927
|
+
if (!gateResult.allowed) {
|
|
928
|
+
return { content: [{ type: "text", text: gateResult.message ?? "Rig bridge disabled (protocol mismatch)." }], isError: true };
|
|
929
|
+
}
|
|
930
|
+
return tool.execute(toolCallId, params);
|
|
931
|
+
}
|
|
932
|
+
}));
|
|
389
933
|
}
|
|
390
|
-
|
|
934
|
+
startSteeringBridge(pi, state, globalThis, gate, deliveredSteeringIds);
|
|
391
935
|
}
|
|
936
|
+
pi.on?.("input", async (event, ctx) => handleOperatorInput(event, state, ctx, gate));
|
|
392
937
|
pi.on?.("session_start", async (_event, ctx) => {
|
|
393
938
|
if (!state.active || !state.runId)
|
|
394
939
|
return;
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
940
|
+
setStatus(ctx, "rig", `Rig ${state.runId} \xB7 connecting\u2026`);
|
|
941
|
+
if (state.operatorSession) {
|
|
942
|
+
setFooter(ctx, `Rig ${state.runId} \xB7 connecting to ${state.serverUrl ?? "Rig server"}\u2026`);
|
|
943
|
+
setWidget(ctx, "rig-run", [`Rig ${state.runId} \xB7 connecting\u2026`]);
|
|
944
|
+
}
|
|
945
|
+
const gateResult = await gate(ctx);
|
|
946
|
+
if (!gateResult.allowed) {
|
|
947
|
+
if (state.operatorSession) {
|
|
948
|
+
const message = gateResult.message ?? "Rig bridge disabled (protocol mismatch).";
|
|
949
|
+
setFooter(ctx, `Rig ${state.runId} \xB7 bridge disabled (protocol mismatch)`);
|
|
950
|
+
setWidget(ctx, "rig-run", [message]);
|
|
951
|
+
}
|
|
952
|
+
return;
|
|
399
953
|
}
|
|
400
|
-
|
|
954
|
+
setStatus(ctx, "rig", `Rig ${state.runId}`);
|
|
955
|
+
const live = gateResult.status === "compatible" ? startOperatorBridge(state, ctx) : undefined;
|
|
956
|
+
startOperatorRunWidget(state, ctx, live);
|
|
957
|
+
await consumeQueuedSteering(pi, state, ctx, gate, deliveredSteeringIds);
|
|
401
958
|
});
|
|
402
959
|
pi.on?.("turn_end", async (_event, ctx) => {
|
|
403
|
-
await consumeQueuedSteering(pi, state, ctx);
|
|
960
|
+
await consumeQueuedSteering(pi, state, ctx, gate, deliveredSteeringIds);
|
|
404
961
|
});
|
|
405
962
|
}
|
|
406
963
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@h-rig/pi-rig",
|
|
3
|
-
"version": "0.0.6-alpha.
|
|
3
|
+
"version": "0.0.6-alpha.70",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Rig package",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -32,8 +32,11 @@
|
|
|
32
32
|
"./dist/src/index.js"
|
|
33
33
|
]
|
|
34
34
|
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.70"
|
|
37
|
+
},
|
|
35
38
|
"peerDependencies": {
|
|
36
|
-
"@earendil-works/pi-coding-agent": "
|
|
39
|
+
"@earendil-works/pi-coding-agent": ">=0.79.0",
|
|
37
40
|
"typebox": "*"
|
|
38
41
|
}
|
|
39
42
|
}
|