@h-rig/pi-rig 0.0.6-alpha.82 → 0.0.6-alpha.84
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/commands.js +11 -2
- package/dist/src/index.d.ts +1 -3
- package/dist/src/index.js +185 -51
- 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/commands.js
CHANGED
|
@@ -53,8 +53,17 @@ function createRigSlashCommands(input) {
|
|
|
53
53
|
notify("Usage: /rig steer <message>", "error");
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
|
-
await input.client.steer(message);
|
|
57
|
-
|
|
56
|
+
const result = await input.client.steer(message);
|
|
57
|
+
const accepted = result.ok !== false && result.queued !== false;
|
|
58
|
+
if (!accepted) {
|
|
59
|
+
const reason = typeof result.error === "string" && result.error.trim() ? `: ${result.error.trim()}` : "";
|
|
60
|
+
notify(`Rig did not accept the steering message${reason}.`, "error");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const entry = result.message && typeof result.message === "object" && !Array.isArray(result.message) ? result.message : null;
|
|
64
|
+
const runId = String(entry?.runId ?? second ?? input.context.runId ?? "run");
|
|
65
|
+
const preview = message.length > 80 ? `${message.slice(0, 79)}\u2026` : message;
|
|
66
|
+
notify(`Steering queued for ${runId} \u2192 "${preview}" \u2014 the worker applies it at its next checkpoint.`, "info");
|
|
58
67
|
return;
|
|
59
68
|
}
|
|
60
69
|
if (first === "stop") {
|
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) {
|
|
@@ -667,8 +699,17 @@ function createRigSlashCommands(input) {
|
|
|
667
699
|
notify("Usage: /rig steer <message>", "error");
|
|
668
700
|
return;
|
|
669
701
|
}
|
|
670
|
-
await input.client.steer(message);
|
|
671
|
-
|
|
702
|
+
const result = await input.client.steer(message);
|
|
703
|
+
const accepted = result.ok !== false && result.queued !== false;
|
|
704
|
+
if (!accepted) {
|
|
705
|
+
const reason = typeof result.error === "string" && result.error.trim() ? `: ${result.error.trim()}` : "";
|
|
706
|
+
notify(`Rig did not accept the steering message${reason}.`, "error");
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
const entry = result.message && typeof result.message === "object" && !Array.isArray(result.message) ? result.message : null;
|
|
710
|
+
const runId = String(entry?.runId ?? second ?? input.context.runId ?? "run");
|
|
711
|
+
const preview = message.length > 80 ? `${message.slice(0, 79)}\u2026` : message;
|
|
712
|
+
notify(`Steering queued for ${runId} \u2192 "${preview}" \u2014 the worker applies it at its next checkpoint.`, "info");
|
|
672
713
|
return;
|
|
673
714
|
}
|
|
674
715
|
if (first === "stop") {
|
|
@@ -986,35 +1027,52 @@ function createPiRigExtensionState(input = {}) {
|
|
|
986
1027
|
...input.webSocketFactory ? { webSocketFactory: input.webSocketFactory } : {}
|
|
987
1028
|
};
|
|
988
1029
|
}
|
|
1030
|
+
function isStalePiContextError(error) {
|
|
1031
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1032
|
+
return /ctx is stale|stale after session replacement|session replacement or reload/i.test(message);
|
|
1033
|
+
}
|
|
1034
|
+
function safeUiCall(action) {
|
|
1035
|
+
try {
|
|
1036
|
+
action();
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
if (!isStalePiContextError(error)) {
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
function uiOf(ctx) {
|
|
1044
|
+
try {
|
|
1045
|
+
const ui = ctx && typeof ctx === "object" ? ctx.ui : null;
|
|
1046
|
+
return ui && typeof ui === "object" ? ui : null;
|
|
1047
|
+
} catch {
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
989
1051
|
function notify(ctx, message, level = "info") {
|
|
990
|
-
const ui = ctx
|
|
991
|
-
const notifyFn = ui
|
|
1052
|
+
const ui = uiOf(ctx);
|
|
1053
|
+
const notifyFn = ui?.notify;
|
|
992
1054
|
if (typeof notifyFn === "function") {
|
|
993
|
-
notifyFn.call(ui, message, level);
|
|
1055
|
+
safeUiCall(() => notifyFn.call(ui, message, level));
|
|
994
1056
|
}
|
|
995
1057
|
}
|
|
996
1058
|
function canNotify(ctx) {
|
|
997
|
-
const ui = ctx
|
|
998
|
-
return Boolean(ui && typeof ui
|
|
1059
|
+
const ui = uiOf(ctx);
|
|
1060
|
+
return Boolean(ui && typeof ui.notify === "function");
|
|
999
1061
|
}
|
|
1000
1062
|
function setWidget(ctx, id, lines) {
|
|
1001
|
-
const ui = ctx
|
|
1002
|
-
const setWidgetFn = ui
|
|
1063
|
+
const ui = uiOf(ctx);
|
|
1064
|
+
const setWidgetFn = ui?.setWidget;
|
|
1003
1065
|
if (typeof setWidgetFn === "function") {
|
|
1004
|
-
setWidgetFn.call(ui, id, lines);
|
|
1066
|
+
safeUiCall(() => setWidgetFn.call(ui, id, lines));
|
|
1005
1067
|
}
|
|
1006
1068
|
}
|
|
1007
1069
|
function setStatus(ctx, id, text) {
|
|
1008
|
-
const ui = ctx
|
|
1009
|
-
const setStatusFn = ui
|
|
1070
|
+
const ui = uiOf(ctx);
|
|
1071
|
+
const setStatusFn = ui?.setStatus;
|
|
1010
1072
|
if (typeof setStatusFn === "function") {
|
|
1011
|
-
setStatusFn.call(ui, id, text);
|
|
1073
|
+
safeUiCall(() => setStatusFn.call(ui, id, text));
|
|
1012
1074
|
}
|
|
1013
1075
|
}
|
|
1014
|
-
function uiOf(ctx) {
|
|
1015
|
-
const ui = ctx && typeof ctx === "object" ? ctx.ui : null;
|
|
1016
|
-
return ui && typeof ui === "object" ? ui : null;
|
|
1017
|
-
}
|
|
1018
1076
|
function setTitle(ctx, title) {
|
|
1019
1077
|
const ui = uiOf(ctx);
|
|
1020
1078
|
const setTitleFn = ui?.setTitle;
|
|
@@ -1181,24 +1239,31 @@ function startOperatorRunStatusLine(state, ctx, live) {
|
|
|
1181
1239
|
return;
|
|
1182
1240
|
const shortId = state.runId.slice(0, 8);
|
|
1183
1241
|
let inFlight = false;
|
|
1242
|
+
let disposed = false;
|
|
1184
1243
|
let lastRefreshAt = 0;
|
|
1185
1244
|
const refresh = async () => {
|
|
1186
|
-
if (inFlight)
|
|
1245
|
+
if (disposed || inFlight)
|
|
1187
1246
|
return;
|
|
1188
1247
|
inFlight = true;
|
|
1189
1248
|
lastRefreshAt = Date.now();
|
|
1190
1249
|
try {
|
|
1191
1250
|
const run = runPayload(await state.client.attach(state.runId));
|
|
1251
|
+
if (disposed)
|
|
1252
|
+
return;
|
|
1192
1253
|
const status = String(run.status ?? "unknown");
|
|
1193
1254
|
setStatus(ctx, "rig", `drone ${shortId} \xB7 ${status} \xB7 ${shortPath(runLocation(run))}`);
|
|
1194
1255
|
} catch (error) {
|
|
1195
|
-
|
|
1256
|
+
if (!disposed) {
|
|
1257
|
+
setStatus(ctx, "rig", `drone ${shortId} \xB7 server unreachable: ${error instanceof Error ? error.message : String(error)}`);
|
|
1258
|
+
}
|
|
1196
1259
|
} finally {
|
|
1197
1260
|
inFlight = false;
|
|
1198
1261
|
}
|
|
1199
1262
|
};
|
|
1200
1263
|
refresh();
|
|
1201
1264
|
const timer = setInterval(() => {
|
|
1265
|
+
if (disposed)
|
|
1266
|
+
return;
|
|
1202
1267
|
const triggered = live?.consumePushTrigger() ?? false;
|
|
1203
1268
|
if ((live?.isConnected() ?? false) && !triggered && Date.now() - lastRefreshAt < OPERATOR_WIDGET_WS_FALLBACK_MS) {
|
|
1204
1269
|
return;
|
|
@@ -1206,6 +1271,10 @@ function startOperatorRunStatusLine(state, ctx, live) {
|
|
|
1206
1271
|
refresh();
|
|
1207
1272
|
}, 5000);
|
|
1208
1273
|
unrefTimer(timer);
|
|
1274
|
+
return () => {
|
|
1275
|
+
disposed = true;
|
|
1276
|
+
clearInterval(timer);
|
|
1277
|
+
};
|
|
1209
1278
|
}
|
|
1210
1279
|
function operatorInboxNotification(event) {
|
|
1211
1280
|
const type = typeof event.type === "string" ? event.type : null;
|
|
@@ -1258,7 +1327,8 @@ function startOperatorBridge(state, ctx) {
|
|
|
1258
1327
|
const triggered = pushTrigger;
|
|
1259
1328
|
pushTrigger = false;
|
|
1260
1329
|
return triggered;
|
|
1261
|
-
}
|
|
1330
|
+
},
|
|
1331
|
+
dispose: () => socket.close()
|
|
1262
1332
|
};
|
|
1263
1333
|
}
|
|
1264
1334
|
function workerStatusLine(status) {
|
|
@@ -1380,24 +1450,30 @@ function registerOperatorConsoleCommands(pi, state, tryRegister) {
|
|
|
1380
1450
|
function startWorkerSessionMirror(pi, state, ctx) {
|
|
1381
1451
|
if (!state.operatorSession || !state.active || !state.runId)
|
|
1382
1452
|
return;
|
|
1453
|
+
let disposed = false;
|
|
1383
1454
|
let mirror = null;
|
|
1384
1455
|
const pendingEvents = [];
|
|
1385
1456
|
createLiveMirror({ pi }).then((created) => {
|
|
1457
|
+
if (disposed)
|
|
1458
|
+
return;
|
|
1386
1459
|
mirror = created;
|
|
1387
1460
|
const ui = uiOf(ctx);
|
|
1388
1461
|
if (ui && typeof ui.setWidget === "function") {
|
|
1389
|
-
ui.setWidget("rig-tui-capture", (tui) => created.captureTui(tui));
|
|
1462
|
+
safeUiCall(() => ui.setWidget("rig-tui-capture", (tui) => created.captureTui(tui)));
|
|
1390
1463
|
}
|
|
1391
1464
|
for (const event of pendingEvents.splice(0))
|
|
1392
1465
|
created.handleWorkerEvent(event);
|
|
1393
1466
|
}).catch((error) => {
|
|
1394
|
-
|
|
1467
|
+
if (!disposed)
|
|
1468
|
+
notify(ctx, `Live drone transcript unavailable: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
1395
1469
|
});
|
|
1396
1470
|
const socket = new RigWorkerEventsSocket({
|
|
1397
1471
|
context: state,
|
|
1398
1472
|
webSocketFactory: state.webSocketFactory,
|
|
1399
1473
|
handlers: {
|
|
1400
1474
|
onFrame: (frame) => {
|
|
1475
|
+
if (disposed)
|
|
1476
|
+
return;
|
|
1401
1477
|
if (frame.type === "status.update") {
|
|
1402
1478
|
const status = frame.status && typeof frame.status === "object" && !Array.isArray(frame.status) ? frame.status : null;
|
|
1403
1479
|
if (status)
|
|
@@ -1419,14 +1495,21 @@ function startWorkerSessionMirror(pi, state, ctx) {
|
|
|
1419
1495
|
pendingEvents.push(event);
|
|
1420
1496
|
},
|
|
1421
1497
|
onConnect: () => {
|
|
1422
|
-
|
|
1498
|
+
if (!disposed)
|
|
1499
|
+
setStatus(ctx, "rig-worker", "drone link live");
|
|
1423
1500
|
},
|
|
1424
1501
|
onDisconnect: () => {
|
|
1425
|
-
|
|
1502
|
+
if (!disposed)
|
|
1503
|
+
setStatus(ctx, "rig-worker", "drone link down (reconnecting\u2026)");
|
|
1426
1504
|
}
|
|
1427
1505
|
}
|
|
1428
1506
|
});
|
|
1429
1507
|
socket.start();
|
|
1508
|
+
return () => {
|
|
1509
|
+
disposed = true;
|
|
1510
|
+
pendingEvents.splice(0);
|
|
1511
|
+
socket.close();
|
|
1512
|
+
};
|
|
1430
1513
|
}
|
|
1431
1514
|
async function forwardWorkerUiRequest(state, ctx, event) {
|
|
1432
1515
|
const request = event.request && typeof event.request === "object" && !Array.isArray(event.request) ? event.request : event;
|
|
@@ -1496,8 +1579,10 @@ function startWorkerCommandRegistration(pi, state, ctx) {
|
|
|
1496
1579
|
const registeredNames = new Set;
|
|
1497
1580
|
let attempts = 0;
|
|
1498
1581
|
let inFlight = false;
|
|
1582
|
+
let disposed = false;
|
|
1583
|
+
let timer = null;
|
|
1499
1584
|
const attempt = async () => {
|
|
1500
|
-
if (inFlight)
|
|
1585
|
+
if (disposed || inFlight)
|
|
1501
1586
|
return false;
|
|
1502
1587
|
inFlight = true;
|
|
1503
1588
|
attempts += 1;
|
|
@@ -1508,64 +1593,84 @@ function startWorkerCommandRegistration(pi, state, ctx) {
|
|
|
1508
1593
|
}
|
|
1509
1594
|
};
|
|
1510
1595
|
attempt().then((ready) => {
|
|
1511
|
-
if (ready)
|
|
1596
|
+
if (disposed || ready)
|
|
1512
1597
|
return;
|
|
1513
|
-
|
|
1514
|
-
if (attempts >= 60) {
|
|
1515
|
-
|
|
1598
|
+
timer = setInterval(() => {
|
|
1599
|
+
if (disposed || attempts >= 60) {
|
|
1600
|
+
if (timer)
|
|
1601
|
+
clearInterval(timer);
|
|
1602
|
+
timer = null;
|
|
1516
1603
|
return;
|
|
1517
1604
|
}
|
|
1518
1605
|
attempt().then((nextReady) => {
|
|
1519
|
-
if (nextReady)
|
|
1606
|
+
if (nextReady && timer) {
|
|
1520
1607
|
clearInterval(timer);
|
|
1608
|
+
timer = null;
|
|
1609
|
+
}
|
|
1521
1610
|
});
|
|
1522
1611
|
}, 2000);
|
|
1523
1612
|
unrefTimer(timer);
|
|
1524
1613
|
});
|
|
1614
|
+
return () => {
|
|
1615
|
+
disposed = true;
|
|
1616
|
+
if (timer)
|
|
1617
|
+
clearInterval(timer);
|
|
1618
|
+
timer = null;
|
|
1619
|
+
};
|
|
1525
1620
|
}
|
|
1526
1621
|
function startSteeringBridge(pi, state, ctx, gate, deliveredIds) {
|
|
1527
1622
|
if (state.operatorSession || !state.active || !state.runId || typeof pi.sendUserMessage !== "function")
|
|
1528
1623
|
return;
|
|
1529
1624
|
const runId = state.runId;
|
|
1625
|
+
let disposed = false;
|
|
1530
1626
|
const socket = new RigBridgeSocket({
|
|
1531
1627
|
context: state,
|
|
1532
1628
|
webSocketFactory: state.webSocketFactory,
|
|
1533
1629
|
handlers: {
|
|
1534
1630
|
onSteeringMessage: (message) => {
|
|
1631
|
+
if (disposed)
|
|
1632
|
+
return;
|
|
1535
1633
|
(async () => {
|
|
1536
1634
|
try {
|
|
1537
|
-
if (!await deliverSteeringMessage(pi, deliveredIds, message))
|
|
1635
|
+
if (disposed || !await deliverSteeringMessage(pi, deliveredIds, message))
|
|
1538
1636
|
return;
|
|
1539
1637
|
const id = typeof message.id === "string" && message.id.trim() ? message.id : null;
|
|
1540
1638
|
if (id)
|
|
1541
1639
|
socket.ackSteering(runId, [id]);
|
|
1542
1640
|
notify(ctx, "Delivered 1 Rig steering message.");
|
|
1543
1641
|
} catch (error) {
|
|
1544
|
-
|
|
1642
|
+
if (!disposed)
|
|
1643
|
+
notify(ctx, `Rig steering sync failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
1545
1644
|
}
|
|
1546
1645
|
})();
|
|
1547
1646
|
},
|
|
1548
1647
|
onConnect: () => {
|
|
1648
|
+
if (disposed)
|
|
1649
|
+
return;
|
|
1549
1650
|
consumeQueuedSteering(pi, state, ctx, gate, deliveredIds);
|
|
1550
1651
|
}
|
|
1551
1652
|
}
|
|
1552
1653
|
});
|
|
1553
1654
|
(async () => {
|
|
1554
1655
|
const gateResult = await gate(ctx);
|
|
1555
|
-
if (!gateResult.allowed)
|
|
1656
|
+
if (disposed || !gateResult.allowed)
|
|
1556
1657
|
return;
|
|
1557
1658
|
if (gateResult.status === "compatible") {
|
|
1558
1659
|
socket.start();
|
|
1559
1660
|
}
|
|
1560
1661
|
})();
|
|
1561
1662
|
const intervalMs = state.steeringPollMs ?? 1000;
|
|
1562
|
-
if (intervalMs <= 0)
|
|
1563
|
-
return
|
|
1663
|
+
if (intervalMs <= 0) {
|
|
1664
|
+
return () => {
|
|
1665
|
+
disposed = true;
|
|
1666
|
+
socket.close();
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1564
1669
|
const WS_CONNECTED_SWEEP_MS = 1e4;
|
|
1565
1670
|
let inFlight = false;
|
|
1566
1671
|
let lastSweepAt = 0;
|
|
1567
1672
|
const timer = setInterval(() => {
|
|
1568
|
-
if (inFlight)
|
|
1673
|
+
if (disposed || inFlight)
|
|
1569
1674
|
return;
|
|
1570
1675
|
if (socket.connected && Date.now() - lastSweepAt < WS_CONNECTED_SWEEP_MS)
|
|
1571
1676
|
return;
|
|
@@ -1576,11 +1681,35 @@ function startSteeringBridge(pi, state, ctx, gate, deliveredIds) {
|
|
|
1576
1681
|
});
|
|
1577
1682
|
}, intervalMs);
|
|
1578
1683
|
unrefTimer(timer);
|
|
1684
|
+
return () => {
|
|
1685
|
+
disposed = true;
|
|
1686
|
+
clearInterval(timer);
|
|
1687
|
+
socket.close();
|
|
1688
|
+
};
|
|
1579
1689
|
}
|
|
1580
1690
|
function createPiRigExtension(pi, options = {}) {
|
|
1581
1691
|
const state = options.state ?? createPiRigExtensionState();
|
|
1582
1692
|
const gate = createBridgeGate(state);
|
|
1583
1693
|
const deliveredSteeringIds = new Set;
|
|
1694
|
+
const sessionDisposables = new Set;
|
|
1695
|
+
const runtimeDisposables = new Set;
|
|
1696
|
+
const addDisposable = (target, disposable) => {
|
|
1697
|
+
if (disposable)
|
|
1698
|
+
target.add(disposable);
|
|
1699
|
+
};
|
|
1700
|
+
const disposeSet = (target) => {
|
|
1701
|
+
for (const dispose of target) {
|
|
1702
|
+
try {
|
|
1703
|
+
dispose();
|
|
1704
|
+
} catch {}
|
|
1705
|
+
}
|
|
1706
|
+
target.clear();
|
|
1707
|
+
};
|
|
1708
|
+
const disposeSessionResources = () => disposeSet(sessionDisposables);
|
|
1709
|
+
const disposeRuntimeResources = () => {
|
|
1710
|
+
disposeSet(sessionDisposables);
|
|
1711
|
+
disposeSet(runtimeDisposables);
|
|
1712
|
+
};
|
|
1584
1713
|
const commands = createRigSlashCommands({
|
|
1585
1714
|
context: state,
|
|
1586
1715
|
client: state.client,
|
|
@@ -1630,10 +1759,14 @@ function createPiRigExtension(pi, options = {}) {
|
|
|
1630
1759
|
}
|
|
1631
1760
|
}));
|
|
1632
1761
|
}
|
|
1633
|
-
startSteeringBridge(pi, state, globalThis, gate, deliveredSteeringIds);
|
|
1762
|
+
addDisposable(runtimeDisposables, startSteeringBridge(pi, state, globalThis, gate, deliveredSteeringIds));
|
|
1634
1763
|
}
|
|
1635
1764
|
pi.on?.("input", async (event, ctx) => handleOperatorInput(event, state, ctx, gate));
|
|
1765
|
+
pi.on?.("session_shutdown", () => {
|
|
1766
|
+
disposeRuntimeResources();
|
|
1767
|
+
});
|
|
1636
1768
|
pi.on?.("session_start", async (_event, ctx) => {
|
|
1769
|
+
disposeSessionResources();
|
|
1637
1770
|
if (!state.active || !state.runId)
|
|
1638
1771
|
return;
|
|
1639
1772
|
const shortId = state.runId.slice(0, 8);
|
|
@@ -1649,11 +1782,12 @@ function createPiRigExtension(pi, options = {}) {
|
|
|
1649
1782
|
return;
|
|
1650
1783
|
}
|
|
1651
1784
|
setStatus(ctx, "rig", `drone ${shortId} \xB7 connecting\u2026`);
|
|
1652
|
-
const live = gateResult.
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1785
|
+
const live = gateResult.allowed ? startOperatorBridge(state, ctx) : undefined;
|
|
1786
|
+
addDisposable(sessionDisposables, live?.dispose);
|
|
1787
|
+
addDisposable(sessionDisposables, startOperatorRunStatusLine(state, ctx, live));
|
|
1788
|
+
if (state.operatorSession && gateResult.allowed) {
|
|
1789
|
+
addDisposable(sessionDisposables, startWorkerSessionMirror(pi, state, ctx));
|
|
1790
|
+
addDisposable(sessionDisposables, startWorkerCommandRegistration(pi, state, ctx));
|
|
1657
1791
|
}
|
|
1658
1792
|
await consumeQueuedSteering(pi, state, ctx, gate, deliveredSteeringIds);
|
|
1659
1793
|
});
|
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.84",
|
|
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.84"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@earendil-works/pi-coding-agent": ">=0.79.0",
|