@h-rig/pi-rig 0.0.6-alpha.63 → 0.0.6-alpha.65
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 +198 -1
- package/dist/src/index.js +390 -25
- 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
|
}
|
|
@@ -138,6 +139,30 @@ class RigBridgeClient {
|
|
|
138
139
|
const payload = await this.request("/api/server/status");
|
|
139
140
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
140
141
|
}
|
|
142
|
+
async checkProtocolCompatibility() {
|
|
143
|
+
let payload;
|
|
144
|
+
try {
|
|
145
|
+
payload = await this.status();
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return {
|
|
148
|
+
status: "indeterminate",
|
|
149
|
+
serverProtocolVersion: null,
|
|
150
|
+
message: `Rig server protocol check failed: ${error instanceof Error ? error.message : String(error)}`
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const raw = payload.protocolVersion;
|
|
154
|
+
const serverProtocolVersion = typeof raw === "number" && Number.isInteger(raw) && raw >= 0 ? raw : null;
|
|
155
|
+
if (serverProtocolVersion === RIG_PROTOCOL_VERSION) {
|
|
156
|
+
return { status: "compatible", serverProtocolVersion, message: null };
|
|
157
|
+
}
|
|
158
|
+
const serverLabel = serverProtocolVersion === null ? "v0 (no protocolVersion reported)" : `v${serverProtocolVersion}`;
|
|
159
|
+
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)";
|
|
160
|
+
return {
|
|
161
|
+
status: "mismatch",
|
|
162
|
+
serverProtocolVersion,
|
|
163
|
+
message: `Rig server speaks protocol ${serverLabel}, this pi-rig speaks v${RIG_PROTOCOL_VERSION} \u2014 ${updateHint}. The Rig bridge is disabled for this session.`
|
|
164
|
+
};
|
|
165
|
+
}
|
|
141
166
|
async listTasks() {
|
|
142
167
|
const payload = await this.request("/api/workspace/tasks");
|
|
143
168
|
return Array.isArray(payload) ? payload.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
@@ -225,7 +250,179 @@ class RigBridgeClient {
|
|
|
225
250
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
226
251
|
}
|
|
227
252
|
}
|
|
253
|
+
function buildRigWebSocketUrl(serverUrl, authToken) {
|
|
254
|
+
const url = new URL(serverUrl);
|
|
255
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
256
|
+
if (authToken) {
|
|
257
|
+
url.searchParams.set("token", authToken);
|
|
258
|
+
}
|
|
259
|
+
return url.toString();
|
|
260
|
+
}
|
|
261
|
+
function defaultWebSocketFactory() {
|
|
262
|
+
const ctor = globalThis.WebSocket;
|
|
263
|
+
if (typeof ctor !== "function")
|
|
264
|
+
return null;
|
|
265
|
+
return (url) => new ctor(url);
|
|
266
|
+
}
|
|
267
|
+
function webSocketEventText(data) {
|
|
268
|
+
if (typeof data === "string")
|
|
269
|
+
return data;
|
|
270
|
+
if (data instanceof Uint8Array)
|
|
271
|
+
return new TextDecoder().decode(data);
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
class RigBridgeSocket {
|
|
276
|
+
context;
|
|
277
|
+
handlers;
|
|
278
|
+
factory;
|
|
279
|
+
reconnectBaseMs;
|
|
280
|
+
reconnectMaxMs;
|
|
281
|
+
socket = null;
|
|
282
|
+
connectedFlag = false;
|
|
283
|
+
closed = false;
|
|
284
|
+
started = false;
|
|
285
|
+
attempt = 0;
|
|
286
|
+
reconnectTimer = null;
|
|
287
|
+
ackSequence = 0;
|
|
288
|
+
constructor(input) {
|
|
289
|
+
this.context = input.context;
|
|
290
|
+
this.handlers = input.handlers ?? {};
|
|
291
|
+
this.factory = input.webSocketFactory ?? defaultWebSocketFactory();
|
|
292
|
+
this.reconnectBaseMs = input.reconnectBaseMs ?? 1000;
|
|
293
|
+
this.reconnectMaxMs = input.reconnectMaxMs ?? 30000;
|
|
294
|
+
}
|
|
295
|
+
get connected() {
|
|
296
|
+
return this.connectedFlag;
|
|
297
|
+
}
|
|
298
|
+
start() {
|
|
299
|
+
if (this.closed)
|
|
300
|
+
return false;
|
|
301
|
+
if (this.started)
|
|
302
|
+
return true;
|
|
303
|
+
if (!this.context.serverUrl || !this.factory)
|
|
304
|
+
return false;
|
|
305
|
+
this.started = true;
|
|
306
|
+
this.connect();
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
close() {
|
|
310
|
+
this.closed = true;
|
|
311
|
+
this.connectedFlag = false;
|
|
312
|
+
if (this.reconnectTimer) {
|
|
313
|
+
clearTimeout(this.reconnectTimer);
|
|
314
|
+
this.reconnectTimer = null;
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
this.socket?.close();
|
|
318
|
+
} catch {}
|
|
319
|
+
this.socket = null;
|
|
320
|
+
}
|
|
321
|
+
ackSteering(runId, ids) {
|
|
322
|
+
if (!this.connectedFlag || !this.socket || ids.length === 0)
|
|
323
|
+
return;
|
|
324
|
+
try {
|
|
325
|
+
this.socket.send(JSON.stringify({
|
|
326
|
+
id: `pi-rig-steer-ack-${++this.ackSequence}`,
|
|
327
|
+
body: { _tag: RIG_WS_METHODS.ackRunSteering, runId, ids }
|
|
328
|
+
}));
|
|
329
|
+
} catch {}
|
|
330
|
+
}
|
|
331
|
+
connect() {
|
|
332
|
+
if (this.closed || !this.context.serverUrl || !this.factory)
|
|
333
|
+
return;
|
|
334
|
+
let socket;
|
|
335
|
+
try {
|
|
336
|
+
socket = this.factory(buildRigWebSocketUrl(this.context.serverUrl, this.context.authToken));
|
|
337
|
+
} catch {
|
|
338
|
+
this.scheduleReconnect();
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
this.socket = socket;
|
|
342
|
+
let gone = false;
|
|
343
|
+
socket.addEventListener("open", () => {
|
|
344
|
+
if (this.closed || gone)
|
|
345
|
+
return;
|
|
346
|
+
this.attempt = 0;
|
|
347
|
+
this.connectedFlag = true;
|
|
348
|
+
this.handlers.onConnect?.();
|
|
349
|
+
});
|
|
350
|
+
const onGone = () => {
|
|
351
|
+
if (gone)
|
|
352
|
+
return;
|
|
353
|
+
gone = true;
|
|
354
|
+
const wasConnected = this.connectedFlag;
|
|
355
|
+
this.connectedFlag = false;
|
|
356
|
+
try {
|
|
357
|
+
socket.close();
|
|
358
|
+
} catch {}
|
|
359
|
+
if (this.socket === socket)
|
|
360
|
+
this.socket = null;
|
|
361
|
+
if (this.closed)
|
|
362
|
+
return;
|
|
363
|
+
if (wasConnected)
|
|
364
|
+
this.handlers.onDisconnect?.();
|
|
365
|
+
this.scheduleReconnect();
|
|
366
|
+
};
|
|
367
|
+
socket.addEventListener("close", onGone);
|
|
368
|
+
socket.addEventListener("error", onGone);
|
|
369
|
+
socket.addEventListener("message", (event) => {
|
|
370
|
+
if (!this.closed)
|
|
371
|
+
this.handleMessage(event);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
scheduleReconnect() {
|
|
375
|
+
if (this.closed || this.reconnectTimer)
|
|
376
|
+
return;
|
|
377
|
+
const delay = Math.min(this.reconnectBaseMs * 2 ** this.attempt, this.reconnectMaxMs);
|
|
378
|
+
this.attempt += 1;
|
|
379
|
+
const timer = setTimeout(() => {
|
|
380
|
+
this.reconnectTimer = null;
|
|
381
|
+
this.connect();
|
|
382
|
+
}, delay);
|
|
383
|
+
this.reconnectTimer = timer;
|
|
384
|
+
if (typeof timer.unref === "function") {
|
|
385
|
+
timer.unref();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
handleMessage(event) {
|
|
389
|
+
const text = webSocketEventText(event.data);
|
|
390
|
+
if (!text)
|
|
391
|
+
return;
|
|
392
|
+
let parsed;
|
|
393
|
+
try {
|
|
394
|
+
parsed = JSON.parse(text);
|
|
395
|
+
} catch {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
399
|
+
return;
|
|
400
|
+
const record = parsed;
|
|
401
|
+
if (record.type !== "push" || typeof record.channel !== "string")
|
|
402
|
+
return;
|
|
403
|
+
const data = record.data && typeof record.data === "object" && !Array.isArray(record.data) ? record.data : null;
|
|
404
|
+
if (record.channel === RIG_WS_CHANNELS.runSteering) {
|
|
405
|
+
if (!data || !this.context.runId || data.runId !== this.context.runId)
|
|
406
|
+
return;
|
|
407
|
+
const message = data.message && typeof data.message === "object" && !Array.isArray(data.message) ? data.message : null;
|
|
408
|
+
if (message)
|
|
409
|
+
this.handlers.onSteeringMessage?.(message);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (record.channel === RIG_WS_CHANNELS.event) {
|
|
413
|
+
if (data)
|
|
414
|
+
this.handlers.onRigEvent?.(data);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (record.channel === RIG_WS_CHANNELS.snapshotInvalidated) {
|
|
418
|
+
this.handlers.onSnapshotInvalidated?.();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
228
422
|
export {
|
|
229
423
|
createRigContextFromEnv,
|
|
230
|
-
|
|
424
|
+
buildRigWebSocketUrl,
|
|
425
|
+
RigBridgeSocket,
|
|
426
|
+
RigBridgeClient,
|
|
427
|
+
RIG_PROTOCOL_VERSION
|
|
231
428
|
};
|
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
|
}
|
|
@@ -138,6 +139,30 @@ class RigBridgeClient {
|
|
|
138
139
|
const payload = await this.request("/api/server/status");
|
|
139
140
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
140
141
|
}
|
|
142
|
+
async checkProtocolCompatibility() {
|
|
143
|
+
let payload;
|
|
144
|
+
try {
|
|
145
|
+
payload = await this.status();
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return {
|
|
148
|
+
status: "indeterminate",
|
|
149
|
+
serverProtocolVersion: null,
|
|
150
|
+
message: `Rig server protocol check failed: ${error instanceof Error ? error.message : String(error)}`
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const raw = payload.protocolVersion;
|
|
154
|
+
const serverProtocolVersion = typeof raw === "number" && Number.isInteger(raw) && raw >= 0 ? raw : null;
|
|
155
|
+
if (serverProtocolVersion === RIG_PROTOCOL_VERSION) {
|
|
156
|
+
return { status: "compatible", serverProtocolVersion, message: null };
|
|
157
|
+
}
|
|
158
|
+
const serverLabel = serverProtocolVersion === null ? "v0 (no protocolVersion reported)" : `v${serverProtocolVersion}`;
|
|
159
|
+
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)";
|
|
160
|
+
return {
|
|
161
|
+
status: "mismatch",
|
|
162
|
+
serverProtocolVersion,
|
|
163
|
+
message: `Rig server speaks protocol ${serverLabel}, this pi-rig speaks v${RIG_PROTOCOL_VERSION} \u2014 ${updateHint}. The Rig bridge is disabled for this session.`
|
|
164
|
+
};
|
|
165
|
+
}
|
|
141
166
|
async listTasks() {
|
|
142
167
|
const payload = await this.request("/api/workspace/tasks");
|
|
143
168
|
return Array.isArray(payload) ? payload.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
@@ -225,6 +250,175 @@ class RigBridgeClient {
|
|
|
225
250
|
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
226
251
|
}
|
|
227
252
|
}
|
|
253
|
+
function buildRigWebSocketUrl(serverUrl, authToken) {
|
|
254
|
+
const url = new URL(serverUrl);
|
|
255
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
256
|
+
if (authToken) {
|
|
257
|
+
url.searchParams.set("token", authToken);
|
|
258
|
+
}
|
|
259
|
+
return url.toString();
|
|
260
|
+
}
|
|
261
|
+
function defaultWebSocketFactory() {
|
|
262
|
+
const ctor = globalThis.WebSocket;
|
|
263
|
+
if (typeof ctor !== "function")
|
|
264
|
+
return null;
|
|
265
|
+
return (url) => new ctor(url);
|
|
266
|
+
}
|
|
267
|
+
function webSocketEventText(data) {
|
|
268
|
+
if (typeof data === "string")
|
|
269
|
+
return data;
|
|
270
|
+
if (data instanceof Uint8Array)
|
|
271
|
+
return new TextDecoder().decode(data);
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
class RigBridgeSocket {
|
|
276
|
+
context;
|
|
277
|
+
handlers;
|
|
278
|
+
factory;
|
|
279
|
+
reconnectBaseMs;
|
|
280
|
+
reconnectMaxMs;
|
|
281
|
+
socket = null;
|
|
282
|
+
connectedFlag = false;
|
|
283
|
+
closed = false;
|
|
284
|
+
started = false;
|
|
285
|
+
attempt = 0;
|
|
286
|
+
reconnectTimer = null;
|
|
287
|
+
ackSequence = 0;
|
|
288
|
+
constructor(input) {
|
|
289
|
+
this.context = input.context;
|
|
290
|
+
this.handlers = input.handlers ?? {};
|
|
291
|
+
this.factory = input.webSocketFactory ?? defaultWebSocketFactory();
|
|
292
|
+
this.reconnectBaseMs = input.reconnectBaseMs ?? 1000;
|
|
293
|
+
this.reconnectMaxMs = input.reconnectMaxMs ?? 30000;
|
|
294
|
+
}
|
|
295
|
+
get connected() {
|
|
296
|
+
return this.connectedFlag;
|
|
297
|
+
}
|
|
298
|
+
start() {
|
|
299
|
+
if (this.closed)
|
|
300
|
+
return false;
|
|
301
|
+
if (this.started)
|
|
302
|
+
return true;
|
|
303
|
+
if (!this.context.serverUrl || !this.factory)
|
|
304
|
+
return false;
|
|
305
|
+
this.started = true;
|
|
306
|
+
this.connect();
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
close() {
|
|
310
|
+
this.closed = true;
|
|
311
|
+
this.connectedFlag = false;
|
|
312
|
+
if (this.reconnectTimer) {
|
|
313
|
+
clearTimeout(this.reconnectTimer);
|
|
314
|
+
this.reconnectTimer = null;
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
this.socket?.close();
|
|
318
|
+
} catch {}
|
|
319
|
+
this.socket = null;
|
|
320
|
+
}
|
|
321
|
+
ackSteering(runId, ids) {
|
|
322
|
+
if (!this.connectedFlag || !this.socket || ids.length === 0)
|
|
323
|
+
return;
|
|
324
|
+
try {
|
|
325
|
+
this.socket.send(JSON.stringify({
|
|
326
|
+
id: `pi-rig-steer-ack-${++this.ackSequence}`,
|
|
327
|
+
body: { _tag: RIG_WS_METHODS.ackRunSteering, runId, ids }
|
|
328
|
+
}));
|
|
329
|
+
} catch {}
|
|
330
|
+
}
|
|
331
|
+
connect() {
|
|
332
|
+
if (this.closed || !this.context.serverUrl || !this.factory)
|
|
333
|
+
return;
|
|
334
|
+
let socket;
|
|
335
|
+
try {
|
|
336
|
+
socket = this.factory(buildRigWebSocketUrl(this.context.serverUrl, this.context.authToken));
|
|
337
|
+
} catch {
|
|
338
|
+
this.scheduleReconnect();
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
this.socket = socket;
|
|
342
|
+
let gone = false;
|
|
343
|
+
socket.addEventListener("open", () => {
|
|
344
|
+
if (this.closed || gone)
|
|
345
|
+
return;
|
|
346
|
+
this.attempt = 0;
|
|
347
|
+
this.connectedFlag = true;
|
|
348
|
+
this.handlers.onConnect?.();
|
|
349
|
+
});
|
|
350
|
+
const onGone = () => {
|
|
351
|
+
if (gone)
|
|
352
|
+
return;
|
|
353
|
+
gone = true;
|
|
354
|
+
const wasConnected = this.connectedFlag;
|
|
355
|
+
this.connectedFlag = false;
|
|
356
|
+
try {
|
|
357
|
+
socket.close();
|
|
358
|
+
} catch {}
|
|
359
|
+
if (this.socket === socket)
|
|
360
|
+
this.socket = null;
|
|
361
|
+
if (this.closed)
|
|
362
|
+
return;
|
|
363
|
+
if (wasConnected)
|
|
364
|
+
this.handlers.onDisconnect?.();
|
|
365
|
+
this.scheduleReconnect();
|
|
366
|
+
};
|
|
367
|
+
socket.addEventListener("close", onGone);
|
|
368
|
+
socket.addEventListener("error", onGone);
|
|
369
|
+
socket.addEventListener("message", (event) => {
|
|
370
|
+
if (!this.closed)
|
|
371
|
+
this.handleMessage(event);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
scheduleReconnect() {
|
|
375
|
+
if (this.closed || this.reconnectTimer)
|
|
376
|
+
return;
|
|
377
|
+
const delay = Math.min(this.reconnectBaseMs * 2 ** this.attempt, this.reconnectMaxMs);
|
|
378
|
+
this.attempt += 1;
|
|
379
|
+
const timer = setTimeout(() => {
|
|
380
|
+
this.reconnectTimer = null;
|
|
381
|
+
this.connect();
|
|
382
|
+
}, delay);
|
|
383
|
+
this.reconnectTimer = timer;
|
|
384
|
+
if (typeof timer.unref === "function") {
|
|
385
|
+
timer.unref();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
handleMessage(event) {
|
|
389
|
+
const text = webSocketEventText(event.data);
|
|
390
|
+
if (!text)
|
|
391
|
+
return;
|
|
392
|
+
let parsed;
|
|
393
|
+
try {
|
|
394
|
+
parsed = JSON.parse(text);
|
|
395
|
+
} catch {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
399
|
+
return;
|
|
400
|
+
const record = parsed;
|
|
401
|
+
if (record.type !== "push" || typeof record.channel !== "string")
|
|
402
|
+
return;
|
|
403
|
+
const data = record.data && typeof record.data === "object" && !Array.isArray(record.data) ? record.data : null;
|
|
404
|
+
if (record.channel === RIG_WS_CHANNELS.runSteering) {
|
|
405
|
+
if (!data || !this.context.runId || data.runId !== this.context.runId)
|
|
406
|
+
return;
|
|
407
|
+
const message = data.message && typeof data.message === "object" && !Array.isArray(data.message) ? data.message : null;
|
|
408
|
+
if (message)
|
|
409
|
+
this.handlers.onSteeringMessage?.(message);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (record.channel === RIG_WS_CHANNELS.event) {
|
|
413
|
+
if (data)
|
|
414
|
+
this.handlers.onRigEvent?.(data);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (record.channel === RIG_WS_CHANNELS.snapshotInvalidated) {
|
|
418
|
+
this.handlers.onSnapshotInvalidated?.();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
228
422
|
|
|
229
423
|
// packages/pi-rig/src/commands.ts
|
|
230
424
|
function runRecordFromPayload(payload) {
|
|
@@ -364,7 +558,8 @@ function createPiRigExtensionState(input = {}) {
|
|
|
364
558
|
const context = createRigContextFromEnv(input.env ?? process.env);
|
|
365
559
|
return {
|
|
366
560
|
...context,
|
|
367
|
-
client: new RigBridgeClient({ context, fetchImpl: input.fetchImpl })
|
|
561
|
+
client: new RigBridgeClient({ context, fetchImpl: input.fetchImpl }),
|
|
562
|
+
...input.webSocketFactory ? { webSocketFactory: input.webSocketFactory } : {}
|
|
368
563
|
};
|
|
369
564
|
}
|
|
370
565
|
function notify(ctx, message, level = "info") {
|
|
@@ -374,6 +569,10 @@ function notify(ctx, message, level = "info") {
|
|
|
374
569
|
notifyFn.call(ui, message, level);
|
|
375
570
|
}
|
|
376
571
|
}
|
|
572
|
+
function canNotify(ctx) {
|
|
573
|
+
const ui = ctx && typeof ctx === "object" ? ctx.ui : null;
|
|
574
|
+
return Boolean(ui && typeof ui === "object" && typeof ui.notify === "function");
|
|
575
|
+
}
|
|
377
576
|
function setWidget(ctx, id, lines) {
|
|
378
577
|
const ui = ctx && typeof ctx === "object" ? ctx.ui : null;
|
|
379
578
|
const setWidgetFn = ui && typeof ui === "object" ? ui.setWidget : null;
|
|
@@ -403,6 +602,25 @@ function setFooter(ctx, line) {
|
|
|
403
602
|
invalidate() {}
|
|
404
603
|
}));
|
|
405
604
|
}
|
|
605
|
+
function createBridgeGate(state) {
|
|
606
|
+
let pending = null;
|
|
607
|
+
let warned = false;
|
|
608
|
+
return async (ctx) => {
|
|
609
|
+
if (!state.active || !state.serverUrl)
|
|
610
|
+
return { allowed: true, message: null, status: "indeterminate" };
|
|
611
|
+
pending ??= state.client.checkProtocolCompatibility();
|
|
612
|
+
const check = await pending;
|
|
613
|
+
if (check.status !== "mismatch")
|
|
614
|
+
return { allowed: true, message: null, status: check.status };
|
|
615
|
+
const message = check.message ?? "Rig protocol mismatch \u2014 the Rig bridge is disabled.";
|
|
616
|
+
if (!warned && canNotify(ctx)) {
|
|
617
|
+
warned = true;
|
|
618
|
+
notify(ctx, message, "error");
|
|
619
|
+
setStatus(ctx, "rig", "Rig bridge disabled (protocol mismatch)");
|
|
620
|
+
}
|
|
621
|
+
return { allowed: false, message, status: "mismatch" };
|
|
622
|
+
};
|
|
623
|
+
}
|
|
406
624
|
function steeringText(message) {
|
|
407
625
|
const text = typeof message.message === "string" ? message.message.trim() : "";
|
|
408
626
|
if (!text)
|
|
@@ -411,19 +629,48 @@ function steeringText(message) {
|
|
|
411
629
|
return `[Rig steering from ${actor}]
|
|
412
630
|
${text}`;
|
|
413
631
|
}
|
|
414
|
-
|
|
632
|
+
function unrefTimer(timer) {
|
|
633
|
+
if (typeof timer.unref === "function") {
|
|
634
|
+
timer.unref();
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
var STEERING_DEDUPE_LIMIT = 500;
|
|
638
|
+
function rememberDeliveredSteeringId(deliveredIds, id) {
|
|
639
|
+
deliveredIds.add(id);
|
|
640
|
+
if (deliveredIds.size > STEERING_DEDUPE_LIMIT) {
|
|
641
|
+
const oldest = deliveredIds.values().next().value;
|
|
642
|
+
if (typeof oldest === "string")
|
|
643
|
+
deliveredIds.delete(oldest);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
async function deliverSteeringMessage(pi, deliveredIds, message) {
|
|
647
|
+
if (typeof pi.sendUserMessage !== "function")
|
|
648
|
+
return false;
|
|
649
|
+
const id = typeof message.id === "string" && message.id.trim() ? message.id : null;
|
|
650
|
+
if (id && deliveredIds.has(id))
|
|
651
|
+
return false;
|
|
652
|
+
const text = steeringText(message);
|
|
653
|
+
if (!text)
|
|
654
|
+
return false;
|
|
655
|
+
if (id)
|
|
656
|
+
rememberDeliveredSteeringId(deliveredIds, id);
|
|
657
|
+
await pi.sendUserMessage(text, { deliverAs: "steer", triggerTurn: true });
|
|
658
|
+
return true;
|
|
659
|
+
}
|
|
660
|
+
async function consumeQueuedSteering(pi, state, ctx, gate, deliveredIds) {
|
|
415
661
|
if (state.operatorSession || !state.active || !state.runId || typeof pi.sendUserMessage !== "function")
|
|
416
662
|
return;
|
|
663
|
+
if (!(await gate(ctx)).allowed)
|
|
664
|
+
return;
|
|
417
665
|
try {
|
|
418
666
|
const messages = await state.client.consumeSteering(state.runId);
|
|
667
|
+
let deliveredCount = 0;
|
|
419
668
|
for (const message of messages) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
continue;
|
|
423
|
-
await pi.sendUserMessage(text, { deliverAs: "steer", triggerTurn: true });
|
|
669
|
+
if (await deliverSteeringMessage(pi, deliveredIds, message))
|
|
670
|
+
deliveredCount += 1;
|
|
424
671
|
}
|
|
425
|
-
if (
|
|
426
|
-
notify(ctx, `Delivered ${
|
|
672
|
+
if (deliveredCount > 0) {
|
|
673
|
+
notify(ctx, `Delivered ${deliveredCount} Rig steering message${deliveredCount === 1 ? "" : "s"}.`);
|
|
427
674
|
}
|
|
428
675
|
} catch (error) {
|
|
429
676
|
notify(ctx, `Rig steering sync failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
@@ -435,12 +682,14 @@ function inputText(event) {
|
|
|
435
682
|
const text = event.text;
|
|
436
683
|
return typeof text === "string" && text.trim() ? text.trim() : null;
|
|
437
684
|
}
|
|
438
|
-
async function handleOperatorInput(event, state, ctx) {
|
|
685
|
+
async function handleOperatorInput(event, state, ctx, gate) {
|
|
439
686
|
if (!state.operatorSession || !state.active || !state.runId)
|
|
440
687
|
return;
|
|
441
688
|
const text = inputText(event);
|
|
442
689
|
if (!text || text.startsWith("/"))
|
|
443
690
|
return;
|
|
691
|
+
if (!(await gate(ctx)).allowed)
|
|
692
|
+
return;
|
|
444
693
|
try {
|
|
445
694
|
await state.client.steer(text, state.runId);
|
|
446
695
|
notify(ctx, "Rig steering message queued.");
|
|
@@ -463,15 +712,18 @@ function runLocation(run) {
|
|
|
463
712
|
function runPayload(payload) {
|
|
464
713
|
return payload.run && typeof payload.run === "object" && !Array.isArray(payload.run) ? payload.run : payload;
|
|
465
714
|
}
|
|
466
|
-
|
|
715
|
+
var OPERATOR_WIDGET_WS_FALLBACK_MS = 1e4;
|
|
716
|
+
function startOperatorRunWidget(state, ctx, live) {
|
|
467
717
|
if (!state.operatorSession || !state.active || !state.runId)
|
|
468
718
|
return;
|
|
469
719
|
let inFlight = false;
|
|
470
720
|
let frame = 0;
|
|
721
|
+
let lastRefreshAt = 0;
|
|
471
722
|
const refresh = async () => {
|
|
472
723
|
if (inFlight)
|
|
473
724
|
return;
|
|
474
725
|
inFlight = true;
|
|
726
|
+
lastRefreshAt = Date.now();
|
|
475
727
|
const spinner = SPINNER_FRAMES[frame++ % SPINNER_FRAMES.length] ?? "\u2022";
|
|
476
728
|
try {
|
|
477
729
|
const [runPayloadRecord, timeline] = await Promise.all([
|
|
@@ -502,32 +754,127 @@ function startOperatorRunWidget(state, ctx) {
|
|
|
502
754
|
}
|
|
503
755
|
};
|
|
504
756
|
refresh();
|
|
505
|
-
const timer = setInterval(() =>
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
757
|
+
const timer = setInterval(() => {
|
|
758
|
+
const triggered = live?.consumePushTrigger() ?? false;
|
|
759
|
+
if ((live?.isConnected() ?? false) && !triggered && Date.now() - lastRefreshAt < OPERATOR_WIDGET_WS_FALLBACK_MS) {
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
refresh();
|
|
763
|
+
}, 1000);
|
|
764
|
+
unrefTimer(timer);
|
|
765
|
+
}
|
|
766
|
+
function operatorInboxNotification(event) {
|
|
767
|
+
const type = typeof event.type === "string" ? event.type : null;
|
|
768
|
+
if (type !== "rig.approval.requested" && type !== "rig.user-input.requested")
|
|
769
|
+
return null;
|
|
770
|
+
const payload = event.payload && typeof event.payload === "object" && !Array.isArray(event.payload) ? event.payload : {};
|
|
771
|
+
const runId = typeof payload.runId === "string" && payload.runId.trim() ? payload.runId : typeof event.aggregateId === "string" && event.aggregateId.trim() ? event.aggregateId : "unknown";
|
|
772
|
+
const requestId = typeof payload.requestId === "string" && payload.requestId.trim() ? payload.requestId : `${type}:${runId}`;
|
|
773
|
+
const waitingOn = type === "rig.approval.requested" ? "an approval" : "user input";
|
|
774
|
+
return {
|
|
775
|
+
key: requestId,
|
|
776
|
+
text: `Run ${runId} is waiting on ${waitingOn} \u2014 resolve with /rig inbox or \`rig inbox\`.`
|
|
777
|
+
};
|
|
509
778
|
}
|
|
510
|
-
function
|
|
779
|
+
function startOperatorBridge(state, ctx) {
|
|
780
|
+
if (!state.operatorSession || !state.active)
|
|
781
|
+
return;
|
|
782
|
+
const notifiedRequests = new Set;
|
|
783
|
+
let pushTrigger = false;
|
|
784
|
+
const socket = new RigBridgeSocket({
|
|
785
|
+
context: state,
|
|
786
|
+
webSocketFactory: state.webSocketFactory,
|
|
787
|
+
handlers: {
|
|
788
|
+
onRigEvent: (event) => {
|
|
789
|
+
pushTrigger = true;
|
|
790
|
+
const notification = operatorInboxNotification(event);
|
|
791
|
+
if (!notification || notifiedRequests.has(notification.key))
|
|
792
|
+
return;
|
|
793
|
+
notifiedRequests.add(notification.key);
|
|
794
|
+
if (notifiedRequests.size > 1000) {
|
|
795
|
+
const oldest = notifiedRequests.values().next().value;
|
|
796
|
+
if (typeof oldest === "string")
|
|
797
|
+
notifiedRequests.delete(oldest);
|
|
798
|
+
}
|
|
799
|
+
notify(ctx, notification.text);
|
|
800
|
+
},
|
|
801
|
+
onSnapshotInvalidated: () => {
|
|
802
|
+
pushTrigger = true;
|
|
803
|
+
},
|
|
804
|
+
onDisconnect: () => {
|
|
805
|
+
pushTrigger = true;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
if (!socket.start())
|
|
810
|
+
return;
|
|
811
|
+
return {
|
|
812
|
+
isConnected: () => socket.connected,
|
|
813
|
+
consumePushTrigger: () => {
|
|
814
|
+
const triggered = pushTrigger;
|
|
815
|
+
pushTrigger = false;
|
|
816
|
+
return triggered;
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
function startSteeringBridge(pi, state, ctx, gate, deliveredIds) {
|
|
511
821
|
if (state.operatorSession || !state.active || !state.runId || typeof pi.sendUserMessage !== "function")
|
|
512
822
|
return;
|
|
823
|
+
const runId = state.runId;
|
|
824
|
+
const socket = new RigBridgeSocket({
|
|
825
|
+
context: state,
|
|
826
|
+
webSocketFactory: state.webSocketFactory,
|
|
827
|
+
handlers: {
|
|
828
|
+
onSteeringMessage: (message) => {
|
|
829
|
+
(async () => {
|
|
830
|
+
try {
|
|
831
|
+
if (!await deliverSteeringMessage(pi, deliveredIds, message))
|
|
832
|
+
return;
|
|
833
|
+
const id = typeof message.id === "string" && message.id.trim() ? message.id : null;
|
|
834
|
+
if (id)
|
|
835
|
+
socket.ackSteering(runId, [id]);
|
|
836
|
+
notify(ctx, "Delivered 1 Rig steering message.");
|
|
837
|
+
} catch (error) {
|
|
838
|
+
notify(ctx, `Rig steering sync failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
839
|
+
}
|
|
840
|
+
})();
|
|
841
|
+
},
|
|
842
|
+
onConnect: () => {
|
|
843
|
+
consumeQueuedSteering(pi, state, ctx, gate, deliveredIds);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
(async () => {
|
|
848
|
+
const gateResult = await gate(ctx);
|
|
849
|
+
if (!gateResult.allowed)
|
|
850
|
+
return;
|
|
851
|
+
if (gateResult.status === "compatible") {
|
|
852
|
+
socket.start();
|
|
853
|
+
}
|
|
854
|
+
})();
|
|
513
855
|
const intervalMs = state.steeringPollMs ?? 1000;
|
|
514
856
|
if (intervalMs <= 0)
|
|
515
857
|
return;
|
|
858
|
+
const WS_CONNECTED_SWEEP_MS = 1e4;
|
|
516
859
|
let inFlight = false;
|
|
860
|
+
let lastSweepAt = 0;
|
|
517
861
|
const timer = setInterval(() => {
|
|
518
862
|
if (inFlight)
|
|
519
863
|
return;
|
|
864
|
+
if (socket.connected && Date.now() - lastSweepAt < WS_CONNECTED_SWEEP_MS)
|
|
865
|
+
return;
|
|
520
866
|
inFlight = true;
|
|
521
|
-
|
|
867
|
+
lastSweepAt = Date.now();
|
|
868
|
+
consumeQueuedSteering(pi, state, ctx, gate, deliveredIds).finally(() => {
|
|
522
869
|
inFlight = false;
|
|
523
870
|
});
|
|
524
871
|
}, intervalMs);
|
|
525
|
-
|
|
526
|
-
timer.unref();
|
|
527
|
-
}
|
|
872
|
+
unrefTimer(timer);
|
|
528
873
|
}
|
|
529
874
|
function createPiRigExtension(pi, options = {}) {
|
|
530
875
|
const state = options.state ?? createPiRigExtensionState();
|
|
876
|
+
const gate = createBridgeGate(state);
|
|
877
|
+
const deliveredSteeringIds = new Set;
|
|
531
878
|
const commands = createRigSlashCommands({
|
|
532
879
|
context: state,
|
|
533
880
|
client: state.client,
|
|
@@ -537,6 +884,11 @@ function createPiRigExtension(pi, options = {}) {
|
|
|
537
884
|
pi.registerCommand?.(name, {
|
|
538
885
|
description: command.description,
|
|
539
886
|
handler: async (args, ctx) => {
|
|
887
|
+
const gateResult = await gate(ctx);
|
|
888
|
+
if (!gateResult.allowed) {
|
|
889
|
+
notify(ctx, gateResult.message ?? "Rig bridge disabled (protocol mismatch).", "error");
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
540
892
|
const nextCommands = createRigSlashCommands({
|
|
541
893
|
context: state,
|
|
542
894
|
client: state.client,
|
|
@@ -548,20 +900,33 @@ function createPiRigExtension(pi, options = {}) {
|
|
|
548
900
|
}
|
|
549
901
|
if (state.active && state.runId) {
|
|
550
902
|
for (const tool of createRigTools({ context: state, client: state.client })) {
|
|
551
|
-
pi.registerTool?.(
|
|
903
|
+
pi.registerTool?.({
|
|
904
|
+
...tool,
|
|
905
|
+
execute: async (toolCallId, params) => {
|
|
906
|
+
const gateResult = await gate(globalThis);
|
|
907
|
+
if (!gateResult.allowed) {
|
|
908
|
+
return { content: [{ type: "text", text: gateResult.message ?? "Rig bridge disabled (protocol mismatch)." }], isError: true };
|
|
909
|
+
}
|
|
910
|
+
return tool.execute(toolCallId, params);
|
|
911
|
+
}
|
|
912
|
+
});
|
|
552
913
|
}
|
|
553
|
-
|
|
914
|
+
startSteeringBridge(pi, state, globalThis, gate, deliveredSteeringIds);
|
|
554
915
|
}
|
|
555
|
-
pi.on?.("input", async (event, ctx) => handleOperatorInput(event, state, ctx));
|
|
916
|
+
pi.on?.("input", async (event, ctx) => handleOperatorInput(event, state, ctx, gate));
|
|
556
917
|
pi.on?.("session_start", async (_event, ctx) => {
|
|
557
918
|
if (!state.active || !state.runId)
|
|
558
919
|
return;
|
|
920
|
+
const gateResult = await gate(ctx);
|
|
921
|
+
if (!gateResult.allowed)
|
|
922
|
+
return;
|
|
559
923
|
setStatus(ctx, "rig", `Rig ${state.runId}`);
|
|
560
|
-
|
|
561
|
-
|
|
924
|
+
const live = gateResult.status === "compatible" ? startOperatorBridge(state, ctx) : undefined;
|
|
925
|
+
startOperatorRunWidget(state, ctx, live);
|
|
926
|
+
await consumeQueuedSteering(pi, state, ctx, gate, deliveredSteeringIds);
|
|
562
927
|
});
|
|
563
928
|
pi.on?.("turn_end", async (_event, ctx) => {
|
|
564
|
-
await consumeQueuedSteering(pi, state, ctx);
|
|
929
|
+
await consumeQueuedSteering(pi, state, ctx, gate, deliveredSteeringIds);
|
|
565
930
|
});
|
|
566
931
|
}
|
|
567
932
|
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.65",
|
|
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.65"
|
|
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
|
}
|