@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.
@@ -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 authToken = cleanString(server?.authToken);
70
+ const authToken2 = cleanString(server?.authToken);
64
71
  return {
65
72
  projectRoot,
66
73
  ...host && port ? { serverUrl: `http://${host}:${port}` } : {},
67
- ...authToken ? { authToken } : {}
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
- return { projectRoot, ...baseUrl ? { serverUrl: baseUrl } : {} };
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 && !headers.has("authorization")) {
135
- headers.set("authorization", `Bearer ${this.context.authToken}`);
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 response = await this.fetchImpl(joinUrl(requireServerUrl(this.context), pathname), { ...init, headers, signal });
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) {
@@ -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 authToken = cleanString(server?.authToken);
72
+ const authToken2 = cleanString(server?.authToken);
66
73
  return {
67
74
  projectRoot,
68
75
  ...host && port ? { serverUrl: `http://${host}:${port}` } : {},
69
- ...authToken ? { authToken } : {}
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
- return { projectRoot, ...baseUrl ? { serverUrl: baseUrl } : {} };
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 && !headers.has("authorization")) {
137
- headers.set("authorization", `Bearer ${this.context.authToken}`);
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 response = await this.fetchImpl(joinUrl(requireServerUrl(this.context), pathname), { ...init, headers, signal });
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 && typeof ctx === "object" ? ctx.ui : null;
991
- const notifyFn = ui && typeof ui === "object" ? ui.notify : null;
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 && typeof ctx === "object" ? ctx.ui : null;
998
- return Boolean(ui && typeof ui === "object" && typeof ui.notify === "function");
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 && typeof ctx === "object" ? ctx.ui : null;
1002
- const setWidgetFn = ui && typeof ui === "object" ? ui.setWidget : null;
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 && typeof ctx === "object" ? ctx.ui : null;
1009
- const setStatusFn = ui && typeof ui === "object" ? ui.setStatus : null;
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
- setStatus(ctx, "rig", `drone ${shortId} \xB7 server unreachable: ${error instanceof Error ? error.message : String(error)}`);
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
- notify(ctx, `Live drone transcript unavailable: ${error instanceof Error ? error.message : String(error)}`, "error");
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
- setStatus(ctx, "rig-worker", "drone link live");
1489
+ if (!disposed)
1490
+ setStatus(ctx, "rig-worker", "drone link live");
1423
1491
  },
1424
1492
  onDisconnect: () => {
1425
- setStatus(ctx, "rig-worker", "drone link down (reconnecting\u2026)");
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
- const timer = setInterval(() => {
1514
- if (attempts >= 60) {
1515
- clearInterval(timer);
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
- notify(ctx, `Rig steering sync failed: ${error instanceof Error ? error.message : String(error)}`, "error");
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
- startOperatorRunStatusLine(state, ctx, live);
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.82",
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.82"
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",