@h-rig/pi-rig 0.0.6-alpha.82 → 0.0.6-alpha.83
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 +41 -9
- package/dist/src/index.d.ts +1 -3
- package/dist/src/index.js +172 -47
- package/package.json +2 -2
package/dist/src/client.js
CHANGED
|
@@ -45,6 +45,13 @@ function resolveGlobalConnectionsPath(env) {
|
|
|
45
45
|
return resolve(stateDir, "connections.json");
|
|
46
46
|
return resolve(homedir(), ".rig", "connections.json");
|
|
47
47
|
}
|
|
48
|
+
function inferRemoteProjectRoot(repoConnection, authState) {
|
|
49
|
+
const repoSlug = cleanString(repoConnection?.project) ?? cleanString(authState?.selectedRepo);
|
|
50
|
+
const checkoutBaseDir = cleanString(authState?.checkoutBaseDir);
|
|
51
|
+
if (!repoSlug || !checkoutBaseDir || !repoSlug.includes("/"))
|
|
52
|
+
return null;
|
|
53
|
+
return resolve(checkoutBaseDir, repoSlug);
|
|
54
|
+
}
|
|
48
55
|
function discoverRigContext(env) {
|
|
49
56
|
const cwd = cleanString(env.PWD);
|
|
50
57
|
if (!cwd)
|
|
@@ -60,11 +67,11 @@ function discoverRigContext(env) {
|
|
|
60
67
|
const server = readJson(resolve(projectRoot, ".rig", "state", "rig-server.json"));
|
|
61
68
|
const host = cleanString(server?.host);
|
|
62
69
|
const port = typeof server?.port === "number" ? server.port : null;
|
|
63
|
-
const
|
|
70
|
+
const authToken2 = cleanString(server?.authToken);
|
|
64
71
|
return {
|
|
65
72
|
projectRoot,
|
|
66
73
|
...host && port ? { serverUrl: `http://${host}:${port}` } : {},
|
|
67
|
-
...
|
|
74
|
+
...authToken2 ? { authToken: authToken2 } : {}
|
|
68
75
|
};
|
|
69
76
|
}
|
|
70
77
|
const global = readJson(resolveGlobalConnectionsPath(env));
|
|
@@ -72,15 +79,22 @@ function discoverRigContext(env) {
|
|
|
72
79
|
const selectedConnection = connections[selected];
|
|
73
80
|
const record = selectedConnection && typeof selectedConnection === "object" && !Array.isArray(selectedConnection) ? selectedConnection : null;
|
|
74
81
|
const baseUrl = record?.kind === "remote" ? cleanString(record.baseUrl) : null;
|
|
75
|
-
|
|
82
|
+
const authState = readJson(resolve(projectRoot, ".rig", "state", "github-auth.json"));
|
|
83
|
+
const authToken = cleanString(authState?.apiSessionToken) ?? cleanString(authState?.sessionToken);
|
|
84
|
+
const serverProjectRoot = cleanString(repoConnection?.serverProjectRoot) ?? inferRemoteProjectRoot(repoConnection, authState);
|
|
85
|
+
return {
|
|
86
|
+
projectRoot: serverProjectRoot ?? projectRoot,
|
|
87
|
+
...baseUrl ? { serverUrl: baseUrl } : {},
|
|
88
|
+
...authToken ? { authToken } : {}
|
|
89
|
+
};
|
|
76
90
|
}
|
|
77
91
|
function createRigContextFromEnv(env = process.env) {
|
|
78
92
|
const runId = env.RIG_RUN_ID ?? env.RIG_SERVER_RUN_ID;
|
|
79
93
|
const taskId = env.RIG_TASK_ID;
|
|
80
94
|
const discovered = discoverRigContext(env);
|
|
81
|
-
const serverUrl = env.RIG_SERVER_URL ?? env.RIG_SERVER_BASE_URL ?? discovered.serverUrl;
|
|
82
|
-
const projectRoot = env.RIG_PROJECT_ROOT ?? env.PROJECT_RIG_ROOT ?? discovered.projectRoot;
|
|
83
|
-
const authToken = env.RIG_AUTH_TOKEN ?? env.RIG_SERVER_AUTH_TOKEN ?? discovered.authToken;
|
|
95
|
+
const serverUrl = cleanString(env.RIG_SERVER_URL) ?? cleanString(env.RIG_SERVER_BASE_URL) ?? discovered.serverUrl;
|
|
96
|
+
const projectRoot = cleanString(env.RIG_PROJECT_ROOT) ?? cleanString(env.PROJECT_RIG_ROOT) ?? discovered.projectRoot;
|
|
97
|
+
const authToken = cleanString(env.RIG_AUTH_TOKEN) ?? cleanString(env.RIG_SERVER_AUTH_TOKEN) ?? discovered.authToken;
|
|
84
98
|
const steeringPollMs = cleanNonNegativeInteger(env.RIG_STEERING_POLL_MS);
|
|
85
99
|
const operatorSession = env.RIG_PI_OPERATOR_SESSION === "1" || env.RIG_PI_OPERATOR_SESSION === "true";
|
|
86
100
|
const active = Boolean(runId || taskId || serverUrl || projectRoot);
|
|
@@ -121,6 +135,16 @@ function requireServerUrl(context) {
|
|
|
121
135
|
}
|
|
122
136
|
var BRIDGE_REQUEST_TIMEOUT_MS = 30000;
|
|
123
137
|
var PROTOCOL_CHECK_TIMEOUT_MS = 1e4;
|
|
138
|
+
function mergeCookie(existing, name, value) {
|
|
139
|
+
const encoded = `${name}=${encodeURIComponent(value)}`;
|
|
140
|
+
if (!existing?.trim())
|
|
141
|
+
return encoded;
|
|
142
|
+
const parts = existing.split(";").map((part) => part.trim()).filter((part) => part && !part.startsWith(`${name}=`));
|
|
143
|
+
return [...parts, encoded].join("; ");
|
|
144
|
+
}
|
|
145
|
+
function queryAuthFallbackEnabled(env = process.env) {
|
|
146
|
+
return env.RIG_ENABLE_QUERY_AUTH_FALLBACK === "1" || env.RIG_QUERY_AUTH_FALLBACK === "1";
|
|
147
|
+
}
|
|
124
148
|
|
|
125
149
|
class RigBridgeClient {
|
|
126
150
|
context;
|
|
@@ -131,14 +155,22 @@ class RigBridgeClient {
|
|
|
131
155
|
}
|
|
132
156
|
async request(pathname, init, timeoutMs = BRIDGE_REQUEST_TIMEOUT_MS) {
|
|
133
157
|
const headers = new Headers(init?.headers);
|
|
134
|
-
if (this.context.authToken
|
|
135
|
-
|
|
158
|
+
if (this.context.authToken) {
|
|
159
|
+
const bearer = `Bearer ${this.context.authToken}`;
|
|
160
|
+
if (!headers.has("authorization"))
|
|
161
|
+
headers.set("authorization", bearer);
|
|
162
|
+
if (!headers.has("x-auth"))
|
|
163
|
+
headers.set("x-auth", bearer);
|
|
164
|
+
headers.set("cookie", mergeCookie(headers.get("cookie"), "rig_auth", this.context.authToken));
|
|
136
165
|
}
|
|
137
166
|
if (this.context.projectRoot && !headers.has("x-rig-project-root")) {
|
|
138
167
|
headers.set("x-rig-project-root", this.context.projectRoot);
|
|
139
168
|
}
|
|
140
169
|
const signal = init?.signal ?? (timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined);
|
|
141
|
-
const
|
|
170
|
+
const requestUrl = new URL(joinUrl(requireServerUrl(this.context), pathname));
|
|
171
|
+
if (this.context.authToken && queryAuthFallbackEnabled())
|
|
172
|
+
requestUrl.searchParams.set("rt", this.context.authToken);
|
|
173
|
+
const response = await this.fetchImpl(requestUrl.toString(), { ...init, headers, signal });
|
|
142
174
|
return readJsonResponse(response);
|
|
143
175
|
}
|
|
144
176
|
async status(timeoutMs) {
|
package/dist/src/index.d.ts
CHANGED
|
@@ -46,13 +46,11 @@ export type PiRigBridgeGate = {
|
|
|
46
46
|
readonly status: RigProtocolCheck["status"];
|
|
47
47
|
};
|
|
48
48
|
export type PiRigBridgeGateCheck = (ctx: unknown) => Promise<PiRigBridgeGate>;
|
|
49
|
-
/** Live refresh control handed to the operator widget by the WS bridge:
|
|
50
|
-
* while the socket is up, pushes (rig.event / snapshotInvalidated) drive the
|
|
51
|
-
* widget instead of the 1s status poll. */
|
|
52
49
|
export type OperatorLiveRefresh = {
|
|
53
50
|
isConnected(): boolean;
|
|
54
51
|
/** Returns true (and resets) when a push arrived since the last check. */
|
|
55
52
|
consumePushTrigger(): boolean;
|
|
53
|
+
dispose(): void;
|
|
56
54
|
};
|
|
57
55
|
export default function createPiRigExtension(pi: MinimalPiApi, options?: {
|
|
58
56
|
state?: PiRigExtensionState;
|
package/dist/src/index.js
CHANGED
|
@@ -47,6 +47,13 @@ function resolveGlobalConnectionsPath(env) {
|
|
|
47
47
|
return resolve(stateDir, "connections.json");
|
|
48
48
|
return resolve(homedir(), ".rig", "connections.json");
|
|
49
49
|
}
|
|
50
|
+
function inferRemoteProjectRoot(repoConnection, authState) {
|
|
51
|
+
const repoSlug = cleanString(repoConnection?.project) ?? cleanString(authState?.selectedRepo);
|
|
52
|
+
const checkoutBaseDir = cleanString(authState?.checkoutBaseDir);
|
|
53
|
+
if (!repoSlug || !checkoutBaseDir || !repoSlug.includes("/"))
|
|
54
|
+
return null;
|
|
55
|
+
return resolve(checkoutBaseDir, repoSlug);
|
|
56
|
+
}
|
|
50
57
|
function discoverRigContext(env) {
|
|
51
58
|
const cwd = cleanString(env.PWD);
|
|
52
59
|
if (!cwd)
|
|
@@ -62,11 +69,11 @@ function discoverRigContext(env) {
|
|
|
62
69
|
const server = readJson(resolve(projectRoot, ".rig", "state", "rig-server.json"));
|
|
63
70
|
const host = cleanString(server?.host);
|
|
64
71
|
const port = typeof server?.port === "number" ? server.port : null;
|
|
65
|
-
const
|
|
72
|
+
const authToken2 = cleanString(server?.authToken);
|
|
66
73
|
return {
|
|
67
74
|
projectRoot,
|
|
68
75
|
...host && port ? { serverUrl: `http://${host}:${port}` } : {},
|
|
69
|
-
...
|
|
76
|
+
...authToken2 ? { authToken: authToken2 } : {}
|
|
70
77
|
};
|
|
71
78
|
}
|
|
72
79
|
const global = readJson(resolveGlobalConnectionsPath(env));
|
|
@@ -74,15 +81,22 @@ function discoverRigContext(env) {
|
|
|
74
81
|
const selectedConnection = connections[selected];
|
|
75
82
|
const record = selectedConnection && typeof selectedConnection === "object" && !Array.isArray(selectedConnection) ? selectedConnection : null;
|
|
76
83
|
const baseUrl = record?.kind === "remote" ? cleanString(record.baseUrl) : null;
|
|
77
|
-
|
|
84
|
+
const authState = readJson(resolve(projectRoot, ".rig", "state", "github-auth.json"));
|
|
85
|
+
const authToken = cleanString(authState?.apiSessionToken) ?? cleanString(authState?.sessionToken);
|
|
86
|
+
const serverProjectRoot = cleanString(repoConnection?.serverProjectRoot) ?? inferRemoteProjectRoot(repoConnection, authState);
|
|
87
|
+
return {
|
|
88
|
+
projectRoot: serverProjectRoot ?? projectRoot,
|
|
89
|
+
...baseUrl ? { serverUrl: baseUrl } : {},
|
|
90
|
+
...authToken ? { authToken } : {}
|
|
91
|
+
};
|
|
78
92
|
}
|
|
79
93
|
function createRigContextFromEnv(env = process.env) {
|
|
80
94
|
const runId = env.RIG_RUN_ID ?? env.RIG_SERVER_RUN_ID;
|
|
81
95
|
const taskId = env.RIG_TASK_ID;
|
|
82
96
|
const discovered = discoverRigContext(env);
|
|
83
|
-
const serverUrl = env.RIG_SERVER_URL ?? env.RIG_SERVER_BASE_URL ?? discovered.serverUrl;
|
|
84
|
-
const projectRoot = env.RIG_PROJECT_ROOT ?? env.PROJECT_RIG_ROOT ?? discovered.projectRoot;
|
|
85
|
-
const authToken = env.RIG_AUTH_TOKEN ?? env.RIG_SERVER_AUTH_TOKEN ?? discovered.authToken;
|
|
97
|
+
const serverUrl = cleanString(env.RIG_SERVER_URL) ?? cleanString(env.RIG_SERVER_BASE_URL) ?? discovered.serverUrl;
|
|
98
|
+
const projectRoot = cleanString(env.RIG_PROJECT_ROOT) ?? cleanString(env.PROJECT_RIG_ROOT) ?? discovered.projectRoot;
|
|
99
|
+
const authToken = cleanString(env.RIG_AUTH_TOKEN) ?? cleanString(env.RIG_SERVER_AUTH_TOKEN) ?? discovered.authToken;
|
|
86
100
|
const steeringPollMs = cleanNonNegativeInteger(env.RIG_STEERING_POLL_MS);
|
|
87
101
|
const operatorSession = env.RIG_PI_OPERATOR_SESSION === "1" || env.RIG_PI_OPERATOR_SESSION === "true";
|
|
88
102
|
const active = Boolean(runId || taskId || serverUrl || projectRoot);
|
|
@@ -123,6 +137,16 @@ function requireServerUrl(context) {
|
|
|
123
137
|
}
|
|
124
138
|
var BRIDGE_REQUEST_TIMEOUT_MS = 30000;
|
|
125
139
|
var PROTOCOL_CHECK_TIMEOUT_MS = 1e4;
|
|
140
|
+
function mergeCookie(existing, name, value) {
|
|
141
|
+
const encoded = `${name}=${encodeURIComponent(value)}`;
|
|
142
|
+
if (!existing?.trim())
|
|
143
|
+
return encoded;
|
|
144
|
+
const parts = existing.split(";").map((part) => part.trim()).filter((part) => part && !part.startsWith(`${name}=`));
|
|
145
|
+
return [...parts, encoded].join("; ");
|
|
146
|
+
}
|
|
147
|
+
function queryAuthFallbackEnabled(env = process.env) {
|
|
148
|
+
return env.RIG_ENABLE_QUERY_AUTH_FALLBACK === "1" || env.RIG_QUERY_AUTH_FALLBACK === "1";
|
|
149
|
+
}
|
|
126
150
|
|
|
127
151
|
class RigBridgeClient {
|
|
128
152
|
context;
|
|
@@ -133,14 +157,22 @@ class RigBridgeClient {
|
|
|
133
157
|
}
|
|
134
158
|
async request(pathname, init, timeoutMs = BRIDGE_REQUEST_TIMEOUT_MS) {
|
|
135
159
|
const headers = new Headers(init?.headers);
|
|
136
|
-
if (this.context.authToken
|
|
137
|
-
|
|
160
|
+
if (this.context.authToken) {
|
|
161
|
+
const bearer = `Bearer ${this.context.authToken}`;
|
|
162
|
+
if (!headers.has("authorization"))
|
|
163
|
+
headers.set("authorization", bearer);
|
|
164
|
+
if (!headers.has("x-auth"))
|
|
165
|
+
headers.set("x-auth", bearer);
|
|
166
|
+
headers.set("cookie", mergeCookie(headers.get("cookie"), "rig_auth", this.context.authToken));
|
|
138
167
|
}
|
|
139
168
|
if (this.context.projectRoot && !headers.has("x-rig-project-root")) {
|
|
140
169
|
headers.set("x-rig-project-root", this.context.projectRoot);
|
|
141
170
|
}
|
|
142
171
|
const signal = init?.signal ?? (timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined);
|
|
143
|
-
const
|
|
172
|
+
const requestUrl = new URL(joinUrl(requireServerUrl(this.context), pathname));
|
|
173
|
+
if (this.context.authToken && queryAuthFallbackEnabled())
|
|
174
|
+
requestUrl.searchParams.set("rt", this.context.authToken);
|
|
175
|
+
const response = await this.fetchImpl(requestUrl.toString(), { ...init, headers, signal });
|
|
144
176
|
return readJsonResponse(response);
|
|
145
177
|
}
|
|
146
178
|
async status(timeoutMs) {
|
|
@@ -986,35 +1018,52 @@ function createPiRigExtensionState(input = {}) {
|
|
|
986
1018
|
...input.webSocketFactory ? { webSocketFactory: input.webSocketFactory } : {}
|
|
987
1019
|
};
|
|
988
1020
|
}
|
|
1021
|
+
function isStalePiContextError(error) {
|
|
1022
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1023
|
+
return /ctx is stale|stale after session replacement|session replacement or reload/i.test(message);
|
|
1024
|
+
}
|
|
1025
|
+
function safeUiCall(action) {
|
|
1026
|
+
try {
|
|
1027
|
+
action();
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
if (!isStalePiContextError(error)) {
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
function uiOf(ctx) {
|
|
1035
|
+
try {
|
|
1036
|
+
const ui = ctx && typeof ctx === "object" ? ctx.ui : null;
|
|
1037
|
+
return ui && typeof ui === "object" ? ui : null;
|
|
1038
|
+
} catch {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
989
1042
|
function notify(ctx, message, level = "info") {
|
|
990
|
-
const ui = ctx
|
|
991
|
-
const notifyFn = ui
|
|
1043
|
+
const ui = uiOf(ctx);
|
|
1044
|
+
const notifyFn = ui?.notify;
|
|
992
1045
|
if (typeof notifyFn === "function") {
|
|
993
|
-
notifyFn.call(ui, message, level);
|
|
1046
|
+
safeUiCall(() => notifyFn.call(ui, message, level));
|
|
994
1047
|
}
|
|
995
1048
|
}
|
|
996
1049
|
function canNotify(ctx) {
|
|
997
|
-
const ui = ctx
|
|
998
|
-
return Boolean(ui && typeof ui
|
|
1050
|
+
const ui = uiOf(ctx);
|
|
1051
|
+
return Boolean(ui && typeof ui.notify === "function");
|
|
999
1052
|
}
|
|
1000
1053
|
function setWidget(ctx, id, lines) {
|
|
1001
|
-
const ui = ctx
|
|
1002
|
-
const setWidgetFn = ui
|
|
1054
|
+
const ui = uiOf(ctx);
|
|
1055
|
+
const setWidgetFn = ui?.setWidget;
|
|
1003
1056
|
if (typeof setWidgetFn === "function") {
|
|
1004
|
-
setWidgetFn.call(ui, id, lines);
|
|
1057
|
+
safeUiCall(() => setWidgetFn.call(ui, id, lines));
|
|
1005
1058
|
}
|
|
1006
1059
|
}
|
|
1007
1060
|
function setStatus(ctx, id, text) {
|
|
1008
|
-
const ui = ctx
|
|
1009
|
-
const setStatusFn = ui
|
|
1061
|
+
const ui = uiOf(ctx);
|
|
1062
|
+
const setStatusFn = ui?.setStatus;
|
|
1010
1063
|
if (typeof setStatusFn === "function") {
|
|
1011
|
-
setStatusFn.call(ui, id, text);
|
|
1064
|
+
safeUiCall(() => setStatusFn.call(ui, id, text));
|
|
1012
1065
|
}
|
|
1013
1066
|
}
|
|
1014
|
-
function uiOf(ctx) {
|
|
1015
|
-
const ui = ctx && typeof ctx === "object" ? ctx.ui : null;
|
|
1016
|
-
return ui && typeof ui === "object" ? ui : null;
|
|
1017
|
-
}
|
|
1018
1067
|
function setTitle(ctx, title) {
|
|
1019
1068
|
const ui = uiOf(ctx);
|
|
1020
1069
|
const setTitleFn = ui?.setTitle;
|
|
@@ -1181,24 +1230,31 @@ function startOperatorRunStatusLine(state, ctx, live) {
|
|
|
1181
1230
|
return;
|
|
1182
1231
|
const shortId = state.runId.slice(0, 8);
|
|
1183
1232
|
let inFlight = false;
|
|
1233
|
+
let disposed = false;
|
|
1184
1234
|
let lastRefreshAt = 0;
|
|
1185
1235
|
const refresh = async () => {
|
|
1186
|
-
if (inFlight)
|
|
1236
|
+
if (disposed || inFlight)
|
|
1187
1237
|
return;
|
|
1188
1238
|
inFlight = true;
|
|
1189
1239
|
lastRefreshAt = Date.now();
|
|
1190
1240
|
try {
|
|
1191
1241
|
const run = runPayload(await state.client.attach(state.runId));
|
|
1242
|
+
if (disposed)
|
|
1243
|
+
return;
|
|
1192
1244
|
const status = String(run.status ?? "unknown");
|
|
1193
1245
|
setStatus(ctx, "rig", `drone ${shortId} \xB7 ${status} \xB7 ${shortPath(runLocation(run))}`);
|
|
1194
1246
|
} catch (error) {
|
|
1195
|
-
|
|
1247
|
+
if (!disposed) {
|
|
1248
|
+
setStatus(ctx, "rig", `drone ${shortId} \xB7 server unreachable: ${error instanceof Error ? error.message : String(error)}`);
|
|
1249
|
+
}
|
|
1196
1250
|
} finally {
|
|
1197
1251
|
inFlight = false;
|
|
1198
1252
|
}
|
|
1199
1253
|
};
|
|
1200
1254
|
refresh();
|
|
1201
1255
|
const timer = setInterval(() => {
|
|
1256
|
+
if (disposed)
|
|
1257
|
+
return;
|
|
1202
1258
|
const triggered = live?.consumePushTrigger() ?? false;
|
|
1203
1259
|
if ((live?.isConnected() ?? false) && !triggered && Date.now() - lastRefreshAt < OPERATOR_WIDGET_WS_FALLBACK_MS) {
|
|
1204
1260
|
return;
|
|
@@ -1206,6 +1262,10 @@ function startOperatorRunStatusLine(state, ctx, live) {
|
|
|
1206
1262
|
refresh();
|
|
1207
1263
|
}, 5000);
|
|
1208
1264
|
unrefTimer(timer);
|
|
1265
|
+
return () => {
|
|
1266
|
+
disposed = true;
|
|
1267
|
+
clearInterval(timer);
|
|
1268
|
+
};
|
|
1209
1269
|
}
|
|
1210
1270
|
function operatorInboxNotification(event) {
|
|
1211
1271
|
const type = typeof event.type === "string" ? event.type : null;
|
|
@@ -1258,7 +1318,8 @@ function startOperatorBridge(state, ctx) {
|
|
|
1258
1318
|
const triggered = pushTrigger;
|
|
1259
1319
|
pushTrigger = false;
|
|
1260
1320
|
return triggered;
|
|
1261
|
-
}
|
|
1321
|
+
},
|
|
1322
|
+
dispose: () => socket.close()
|
|
1262
1323
|
};
|
|
1263
1324
|
}
|
|
1264
1325
|
function workerStatusLine(status) {
|
|
@@ -1380,24 +1441,30 @@ function registerOperatorConsoleCommands(pi, state, tryRegister) {
|
|
|
1380
1441
|
function startWorkerSessionMirror(pi, state, ctx) {
|
|
1381
1442
|
if (!state.operatorSession || !state.active || !state.runId)
|
|
1382
1443
|
return;
|
|
1444
|
+
let disposed = false;
|
|
1383
1445
|
let mirror = null;
|
|
1384
1446
|
const pendingEvents = [];
|
|
1385
1447
|
createLiveMirror({ pi }).then((created) => {
|
|
1448
|
+
if (disposed)
|
|
1449
|
+
return;
|
|
1386
1450
|
mirror = created;
|
|
1387
1451
|
const ui = uiOf(ctx);
|
|
1388
1452
|
if (ui && typeof ui.setWidget === "function") {
|
|
1389
|
-
ui.setWidget("rig-tui-capture", (tui) => created.captureTui(tui));
|
|
1453
|
+
safeUiCall(() => ui.setWidget("rig-tui-capture", (tui) => created.captureTui(tui)));
|
|
1390
1454
|
}
|
|
1391
1455
|
for (const event of pendingEvents.splice(0))
|
|
1392
1456
|
created.handleWorkerEvent(event);
|
|
1393
1457
|
}).catch((error) => {
|
|
1394
|
-
|
|
1458
|
+
if (!disposed)
|
|
1459
|
+
notify(ctx, `Live drone transcript unavailable: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
1395
1460
|
});
|
|
1396
1461
|
const socket = new RigWorkerEventsSocket({
|
|
1397
1462
|
context: state,
|
|
1398
1463
|
webSocketFactory: state.webSocketFactory,
|
|
1399
1464
|
handlers: {
|
|
1400
1465
|
onFrame: (frame) => {
|
|
1466
|
+
if (disposed)
|
|
1467
|
+
return;
|
|
1401
1468
|
if (frame.type === "status.update") {
|
|
1402
1469
|
const status = frame.status && typeof frame.status === "object" && !Array.isArray(frame.status) ? frame.status : null;
|
|
1403
1470
|
if (status)
|
|
@@ -1419,14 +1486,21 @@ function startWorkerSessionMirror(pi, state, ctx) {
|
|
|
1419
1486
|
pendingEvents.push(event);
|
|
1420
1487
|
},
|
|
1421
1488
|
onConnect: () => {
|
|
1422
|
-
|
|
1489
|
+
if (!disposed)
|
|
1490
|
+
setStatus(ctx, "rig-worker", "drone link live");
|
|
1423
1491
|
},
|
|
1424
1492
|
onDisconnect: () => {
|
|
1425
|
-
|
|
1493
|
+
if (!disposed)
|
|
1494
|
+
setStatus(ctx, "rig-worker", "drone link down (reconnecting\u2026)");
|
|
1426
1495
|
}
|
|
1427
1496
|
}
|
|
1428
1497
|
});
|
|
1429
1498
|
socket.start();
|
|
1499
|
+
return () => {
|
|
1500
|
+
disposed = true;
|
|
1501
|
+
pendingEvents.splice(0);
|
|
1502
|
+
socket.close();
|
|
1503
|
+
};
|
|
1430
1504
|
}
|
|
1431
1505
|
async function forwardWorkerUiRequest(state, ctx, event) {
|
|
1432
1506
|
const request = event.request && typeof event.request === "object" && !Array.isArray(event.request) ? event.request : event;
|
|
@@ -1496,8 +1570,10 @@ function startWorkerCommandRegistration(pi, state, ctx) {
|
|
|
1496
1570
|
const registeredNames = new Set;
|
|
1497
1571
|
let attempts = 0;
|
|
1498
1572
|
let inFlight = false;
|
|
1573
|
+
let disposed = false;
|
|
1574
|
+
let timer = null;
|
|
1499
1575
|
const attempt = async () => {
|
|
1500
|
-
if (inFlight)
|
|
1576
|
+
if (disposed || inFlight)
|
|
1501
1577
|
return false;
|
|
1502
1578
|
inFlight = true;
|
|
1503
1579
|
attempts += 1;
|
|
@@ -1508,64 +1584,84 @@ function startWorkerCommandRegistration(pi, state, ctx) {
|
|
|
1508
1584
|
}
|
|
1509
1585
|
};
|
|
1510
1586
|
attempt().then((ready) => {
|
|
1511
|
-
if (ready)
|
|
1587
|
+
if (disposed || ready)
|
|
1512
1588
|
return;
|
|
1513
|
-
|
|
1514
|
-
if (attempts >= 60) {
|
|
1515
|
-
|
|
1589
|
+
timer = setInterval(() => {
|
|
1590
|
+
if (disposed || attempts >= 60) {
|
|
1591
|
+
if (timer)
|
|
1592
|
+
clearInterval(timer);
|
|
1593
|
+
timer = null;
|
|
1516
1594
|
return;
|
|
1517
1595
|
}
|
|
1518
1596
|
attempt().then((nextReady) => {
|
|
1519
|
-
if (nextReady)
|
|
1597
|
+
if (nextReady && timer) {
|
|
1520
1598
|
clearInterval(timer);
|
|
1599
|
+
timer = null;
|
|
1600
|
+
}
|
|
1521
1601
|
});
|
|
1522
1602
|
}, 2000);
|
|
1523
1603
|
unrefTimer(timer);
|
|
1524
1604
|
});
|
|
1605
|
+
return () => {
|
|
1606
|
+
disposed = true;
|
|
1607
|
+
if (timer)
|
|
1608
|
+
clearInterval(timer);
|
|
1609
|
+
timer = null;
|
|
1610
|
+
};
|
|
1525
1611
|
}
|
|
1526
1612
|
function startSteeringBridge(pi, state, ctx, gate, deliveredIds) {
|
|
1527
1613
|
if (state.operatorSession || !state.active || !state.runId || typeof pi.sendUserMessage !== "function")
|
|
1528
1614
|
return;
|
|
1529
1615
|
const runId = state.runId;
|
|
1616
|
+
let disposed = false;
|
|
1530
1617
|
const socket = new RigBridgeSocket({
|
|
1531
1618
|
context: state,
|
|
1532
1619
|
webSocketFactory: state.webSocketFactory,
|
|
1533
1620
|
handlers: {
|
|
1534
1621
|
onSteeringMessage: (message) => {
|
|
1622
|
+
if (disposed)
|
|
1623
|
+
return;
|
|
1535
1624
|
(async () => {
|
|
1536
1625
|
try {
|
|
1537
|
-
if (!await deliverSteeringMessage(pi, deliveredIds, message))
|
|
1626
|
+
if (disposed || !await deliverSteeringMessage(pi, deliveredIds, message))
|
|
1538
1627
|
return;
|
|
1539
1628
|
const id = typeof message.id === "string" && message.id.trim() ? message.id : null;
|
|
1540
1629
|
if (id)
|
|
1541
1630
|
socket.ackSteering(runId, [id]);
|
|
1542
1631
|
notify(ctx, "Delivered 1 Rig steering message.");
|
|
1543
1632
|
} catch (error) {
|
|
1544
|
-
|
|
1633
|
+
if (!disposed)
|
|
1634
|
+
notify(ctx, `Rig steering sync failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
1545
1635
|
}
|
|
1546
1636
|
})();
|
|
1547
1637
|
},
|
|
1548
1638
|
onConnect: () => {
|
|
1639
|
+
if (disposed)
|
|
1640
|
+
return;
|
|
1549
1641
|
consumeQueuedSteering(pi, state, ctx, gate, deliveredIds);
|
|
1550
1642
|
}
|
|
1551
1643
|
}
|
|
1552
1644
|
});
|
|
1553
1645
|
(async () => {
|
|
1554
1646
|
const gateResult = await gate(ctx);
|
|
1555
|
-
if (!gateResult.allowed)
|
|
1647
|
+
if (disposed || !gateResult.allowed)
|
|
1556
1648
|
return;
|
|
1557
1649
|
if (gateResult.status === "compatible") {
|
|
1558
1650
|
socket.start();
|
|
1559
1651
|
}
|
|
1560
1652
|
})();
|
|
1561
1653
|
const intervalMs = state.steeringPollMs ?? 1000;
|
|
1562
|
-
if (intervalMs <= 0)
|
|
1563
|
-
return
|
|
1654
|
+
if (intervalMs <= 0) {
|
|
1655
|
+
return () => {
|
|
1656
|
+
disposed = true;
|
|
1657
|
+
socket.close();
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1564
1660
|
const WS_CONNECTED_SWEEP_MS = 1e4;
|
|
1565
1661
|
let inFlight = false;
|
|
1566
1662
|
let lastSweepAt = 0;
|
|
1567
1663
|
const timer = setInterval(() => {
|
|
1568
|
-
if (inFlight)
|
|
1664
|
+
if (disposed || inFlight)
|
|
1569
1665
|
return;
|
|
1570
1666
|
if (socket.connected && Date.now() - lastSweepAt < WS_CONNECTED_SWEEP_MS)
|
|
1571
1667
|
return;
|
|
@@ -1576,11 +1672,35 @@ function startSteeringBridge(pi, state, ctx, gate, deliveredIds) {
|
|
|
1576
1672
|
});
|
|
1577
1673
|
}, intervalMs);
|
|
1578
1674
|
unrefTimer(timer);
|
|
1675
|
+
return () => {
|
|
1676
|
+
disposed = true;
|
|
1677
|
+
clearInterval(timer);
|
|
1678
|
+
socket.close();
|
|
1679
|
+
};
|
|
1579
1680
|
}
|
|
1580
1681
|
function createPiRigExtension(pi, options = {}) {
|
|
1581
1682
|
const state = options.state ?? createPiRigExtensionState();
|
|
1582
1683
|
const gate = createBridgeGate(state);
|
|
1583
1684
|
const deliveredSteeringIds = new Set;
|
|
1685
|
+
const sessionDisposables = new Set;
|
|
1686
|
+
const runtimeDisposables = new Set;
|
|
1687
|
+
const addDisposable = (target, disposable) => {
|
|
1688
|
+
if (disposable)
|
|
1689
|
+
target.add(disposable);
|
|
1690
|
+
};
|
|
1691
|
+
const disposeSet = (target) => {
|
|
1692
|
+
for (const dispose of target) {
|
|
1693
|
+
try {
|
|
1694
|
+
dispose();
|
|
1695
|
+
} catch {}
|
|
1696
|
+
}
|
|
1697
|
+
target.clear();
|
|
1698
|
+
};
|
|
1699
|
+
const disposeSessionResources = () => disposeSet(sessionDisposables);
|
|
1700
|
+
const disposeRuntimeResources = () => {
|
|
1701
|
+
disposeSet(sessionDisposables);
|
|
1702
|
+
disposeSet(runtimeDisposables);
|
|
1703
|
+
};
|
|
1584
1704
|
const commands = createRigSlashCommands({
|
|
1585
1705
|
context: state,
|
|
1586
1706
|
client: state.client,
|
|
@@ -1630,10 +1750,14 @@ function createPiRigExtension(pi, options = {}) {
|
|
|
1630
1750
|
}
|
|
1631
1751
|
}));
|
|
1632
1752
|
}
|
|
1633
|
-
startSteeringBridge(pi, state, globalThis, gate, deliveredSteeringIds);
|
|
1753
|
+
addDisposable(runtimeDisposables, startSteeringBridge(pi, state, globalThis, gate, deliveredSteeringIds));
|
|
1634
1754
|
}
|
|
1635
1755
|
pi.on?.("input", async (event, ctx) => handleOperatorInput(event, state, ctx, gate));
|
|
1756
|
+
pi.on?.("session_shutdown", () => {
|
|
1757
|
+
disposeRuntimeResources();
|
|
1758
|
+
});
|
|
1636
1759
|
pi.on?.("session_start", async (_event, ctx) => {
|
|
1760
|
+
disposeSessionResources();
|
|
1637
1761
|
if (!state.active || !state.runId)
|
|
1638
1762
|
return;
|
|
1639
1763
|
const shortId = state.runId.slice(0, 8);
|
|
@@ -1650,10 +1774,11 @@ function createPiRigExtension(pi, options = {}) {
|
|
|
1650
1774
|
}
|
|
1651
1775
|
setStatus(ctx, "rig", `drone ${shortId} \xB7 connecting\u2026`);
|
|
1652
1776
|
const live = gateResult.status === "compatible" ? startOperatorBridge(state, ctx) : undefined;
|
|
1653
|
-
|
|
1777
|
+
addDisposable(sessionDisposables, live?.dispose);
|
|
1778
|
+
addDisposable(sessionDisposables, startOperatorRunStatusLine(state, ctx, live));
|
|
1654
1779
|
if (state.operatorSession && gateResult.status === "compatible") {
|
|
1655
|
-
startWorkerSessionMirror(pi, state, ctx);
|
|
1656
|
-
startWorkerCommandRegistration(pi, state, ctx);
|
|
1780
|
+
addDisposable(sessionDisposables, startWorkerSessionMirror(pi, state, ctx));
|
|
1781
|
+
addDisposable(sessionDisposables, startWorkerCommandRegistration(pi, state, ctx));
|
|
1657
1782
|
}
|
|
1658
1783
|
await consumeQueuedSteering(pi, state, ctx, gate, deliveredSteeringIds);
|
|
1659
1784
|
});
|
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.83",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Rig package",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
]
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.
|
|
41
|
+
"@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.83"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@earendil-works/pi-coding-agent": ">=0.79.0",
|