@ganglion/xacpx-relay 0.11.3 → 0.12.0

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.
Files changed (77) hide show
  1. package/dist/cli.js +550 -23
  2. package/dist/gateway/instance-gateway.d.ts +7 -1
  3. package/dist/gateway/web-gateway.d.ts +56 -1
  4. package/dist/gateway/web-inbound.d.ts +10 -8
  5. package/dist/relay-web/assets/{DashboardView-Dtb2Glmt.css → DashboardView-BNwcKQ6u.css} +1 -1
  6. package/dist/relay-web/assets/DashboardView-CM38P675.js +436 -0
  7. package/dist/relay-web/assets/{InviteView-DtidlMrD.js → InviteView-C3kg9Vwr.js} +1 -1
  8. package/dist/relay-web/assets/{LoginView-CNkNRebg.js → LoginView-DoM2bMxD.js} +1 -1
  9. package/dist/relay-web/assets/{SettingsView-DCYGtpUv.js → SettingsView-D2ejxeDR.js} +1 -1
  10. package/dist/relay-web/assets/{abnfDiagram-VRR7QNED-Bd29dhCa.js → abnfDiagram-VRR7QNED-deRHrXse.js} +1 -1
  11. package/dist/relay-web/assets/{arc-CrQfTVtY.js → arc-DN18fNDk.js} +1 -1
  12. package/dist/relay-web/assets/{architectureDiagram-ZJ3FMSHR-CLNuP_U4.js → architectureDiagram-ZJ3FMSHR-BkPUXZS-.js} +1 -1
  13. package/dist/relay-web/assets/{blockDiagram-677ZJIJ3-CGqit6bA.js → blockDiagram-677ZJIJ3-B5TMW0sV.js} +1 -1
  14. package/dist/relay-web/assets/{c4Diagram-LMCZKHZV-DD7o_s-W.js → c4Diagram-LMCZKHZV-DaqXZK5X.js} +1 -1
  15. package/dist/relay-web/assets/channel-DUBJdIk3.js +1 -0
  16. package/dist/relay-web/assets/{chunk-2Q5K7J3B-DHLIOuAq.js → chunk-2Q5K7J3B-CulRrf5Q.js} +1 -1
  17. package/dist/relay-web/assets/{chunk-32BRIVSS-BENzymJv.js → chunk-32BRIVSS-Bs0K3Abr.js} +1 -1
  18. package/dist/relay-web/assets/{chunk-5VM5RSS4-wPvLHeXr.js → chunk-5VM5RSS4-DLC3RAol.js} +1 -1
  19. package/dist/relay-web/assets/{chunk-EX3LRPZG-CE48BnPr.js → chunk-EX3LRPZG-BQztEinp.js} +1 -1
  20. package/dist/relay-web/assets/{chunk-JWPE2WC7-xqfxYXvP.js → chunk-JWPE2WC7-BeYzb5GM.js} +1 -1
  21. package/dist/relay-web/assets/{chunk-MOJQB5TN-B7xWKcW9.js → chunk-MOJQB5TN-wdl6yjvQ.js} +1 -1
  22. package/dist/relay-web/assets/{chunk-RYQCIY6F-DDBvROhN.js → chunk-RYQCIY6F-CfiIg3aE.js} +1 -1
  23. package/dist/relay-web/assets/{chunk-V7JOEXUC-Bmzqw51I.js → chunk-V7JOEXUC-DDuTsOmE.js} +1 -1
  24. package/dist/relay-web/assets/{chunk-VR4S4FIN-BbZAqPQo.js → chunk-VR4S4FIN-Blf49feW.js} +1 -1
  25. package/dist/relay-web/assets/{chunk-XXDRQBXY-DKrO8Usy.js → chunk-XXDRQBXY-BWoej7D5.js} +1 -1
  26. package/dist/relay-web/assets/classDiagram-OUVF2IWQ-C07wXHb7.js +1 -0
  27. package/dist/relay-web/assets/classDiagram-v2-EOCWNBFH-C07wXHb7.js +1 -0
  28. package/dist/relay-web/assets/{cose-bilkent-JH36ORCC-yBWTTk4O.js → cose-bilkent-JH36ORCC-Dw3-NQmG.js} +1 -1
  29. package/dist/relay-web/assets/{cynefin-VYW2F7L2-B-tHJ3Js.js → cynefin-VYW2F7L2-CCqas_tu.js} +1 -1
  30. package/dist/relay-web/assets/{cynefinDiagram-TSTJHNR4-DNw-0MKO.js → cynefinDiagram-TSTJHNR4-CUxgvNWK.js} +1 -1
  31. package/dist/relay-web/assets/{dagre-VKFMJZFB-DbOX-V0S.js → dagre-VKFMJZFB-DTufXiab.js} +1 -1
  32. package/dist/relay-web/assets/{diagram-FQU43EPY-NHSV-gLW.js → diagram-FQU43EPY-B4BMTPl8.js} +1 -1
  33. package/dist/relay-web/assets/{diagram-G47NLZAW-B5zpBk_f.js → diagram-G47NLZAW-BGE6VzZs.js} +1 -1
  34. package/dist/relay-web/assets/{diagram-NH7WQ7WH-Coan_Ptw.js → diagram-NH7WQ7WH-DAxE22CH.js} +1 -1
  35. package/dist/relay-web/assets/{diagram-OA4YK3LP-ZYEaYDqj.js → diagram-OA4YK3LP-CyfVqStt.js} +1 -1
  36. package/dist/relay-web/assets/{diagram-WEI45ONY-DDvCyduS.js → diagram-WEI45ONY-DxzIDJtH.js} +1 -1
  37. package/dist/relay-web/assets/{ebnfDiagram-CCIWWBDH-BJ__ou6u.js → ebnfDiagram-CCIWWBDH-Dwe8jOTg.js} +1 -1
  38. package/dist/relay-web/assets/{erDiagram-Q63AITRT-BmCBLrxn.js → erDiagram-Q63AITRT-BjkjUdTK.js} +1 -1
  39. package/dist/relay-web/assets/{flowDiagram-23GEKE2U-C7gtTBpm.js → flowDiagram-23GEKE2U-DLZaZxoR.js} +1 -1
  40. package/dist/relay-web/assets/{ganttDiagram-NO4QXBWP-DOCLZLky.js → ganttDiagram-NO4QXBWP-Cugmec_D.js} +1 -1
  41. package/dist/relay-web/assets/{ghostty-web-DJNNWGKw.js → ghostty-web-Btdylsvz.js} +1 -1
  42. package/dist/relay-web/assets/{gitGraphDiagram-IHSO6WYX-CmHqnB47.js → gitGraphDiagram-IHSO6WYX-CvtXdEWO.js} +1 -1
  43. package/dist/relay-web/assets/{index-B29mFOXU.js → index-D52ntTFm.js} +18 -18
  44. package/dist/relay-web/assets/{infoDiagram-FWYZ7A6U-BqeSGvj4.js → infoDiagram-FWYZ7A6U-CyisQOMU.js} +1 -1
  45. package/dist/relay-web/assets/{ishikawaDiagram-FXEZZL3T-D1teo9mW.js → ishikawaDiagram-FXEZZL3T-AdWvcsaG.js} +1 -1
  46. package/dist/relay-web/assets/{journeyDiagram-5HDEW3XC-C0PwKyKj.js → journeyDiagram-5HDEW3XC-BPvobm1L.js} +1 -1
  47. package/dist/relay-web/assets/{kanban-definition-HUTT4EX6-C1PBukK2.js → kanban-definition-HUTT4EX6-I5jq5AhB.js} +1 -1
  48. package/dist/relay-web/assets/{linear-CmAS5Fla.js → linear-z4GCDjJV.js} +1 -1
  49. package/dist/relay-web/assets/{mermaid.core-B17Q-J1t.js → mermaid.core-BdsVh7Pj.js} +4 -4
  50. package/dist/relay-web/assets/{mindmap-definition-LN4V7U3C-DYxYq6Un.js → mindmap-definition-LN4V7U3C-DzGr1rLu.js} +1 -1
  51. package/dist/relay-web/assets/{pegDiagram-2B236MQR-BlCDSiXA.js → pegDiagram-2B236MQR-Bf60O0vf.js} +1 -1
  52. package/dist/relay-web/assets/{pieDiagram-ENE6RG2P-BCINN16i.js → pieDiagram-ENE6RG2P-3JHpstBi.js} +1 -1
  53. package/dist/relay-web/assets/{quadrantDiagram-ABIIQ3AL-C9b5gZyw.js → quadrantDiagram-ABIIQ3AL-Bn8A4z_W.js} +1 -1
  54. package/dist/relay-web/assets/{railroadDiagram-RFXS5EU6-BxIP6BRa.js → railroadDiagram-RFXS5EU6-Cm9CSksD.js} +1 -1
  55. package/dist/relay-web/assets/{requirementDiagram-TGXJPOKE-D-SMDKif.js → requirementDiagram-TGXJPOKE-Rz0DyU_-.js} +1 -1
  56. package/dist/relay-web/assets/{sankeyDiagram-HTMAVEWB-rTjFCUNr.js → sankeyDiagram-HTMAVEWB-Cgb-wJlV.js} +1 -1
  57. package/dist/relay-web/assets/{sequenceDiagram-DBY2YBRQ-BsqP7y8O.js → sequenceDiagram-DBY2YBRQ-C7FgOmV3.js} +1 -1
  58. package/dist/relay-web/assets/{sizeCapture-X5ZJPWSS-CW369UlW.js → sizeCapture-X5ZJPWSS-CP1rDXXR.js} +1 -1
  59. package/dist/relay-web/assets/{stateDiagram-2N3HPSRC-B9arh27E.js → stateDiagram-2N3HPSRC-Di5pHgGQ.js} +1 -1
  60. package/dist/relay-web/assets/stateDiagram-v2-6OUMAXLB-BPeNqnaa.js +1 -0
  61. package/dist/relay-web/assets/{swimlanes-5IMT3BWC-ClJcyhhP.js → swimlanes-5IMT3BWC-BJoJKh0U.js} +2 -2
  62. package/dist/relay-web/assets/swimlanesDiagram-G3AALYLV-Cm9UEn_b.js +8 -0
  63. package/dist/relay-web/assets/{theme-1IR1O1bT.js → theme-31dEzsN7.js} +1 -1
  64. package/dist/relay-web/assets/{timeline-definition-FHXFAJF6-DAJSZFfu.js → timeline-definition-FHXFAJF6-CxE_BEJ7.js} +1 -1
  65. package/dist/relay-web/assets/{vennDiagram-L72KCM5P-Dpab5g2u.js → vennDiagram-L72KCM5P-sguHqvWG.js} +1 -1
  66. package/dist/relay-web/assets/{wardleyDiagram-EHGQE667-CqO8MTvZ.js → wardleyDiagram-EHGQE667-CEuHfxX6.js} +1 -1
  67. package/dist/relay-web/assets/{xychartDiagram-FW5EYKEG-CDpFaAWf.js → xychartDiagram-FW5EYKEG-CLljoDKz.js} +1 -1
  68. package/dist/relay-web/index.html +1 -1
  69. package/dist/relay-web/sw.js +1 -1
  70. package/dist/stores/instances.d.ts +10 -1
  71. package/package.json +2 -2
  72. package/dist/relay-web/assets/DashboardView-BENAQLdv.js +0 -436
  73. package/dist/relay-web/assets/channel-BDCHXdrq.js +0 -1
  74. package/dist/relay-web/assets/classDiagram-OUVF2IWQ-w2002tim.js +0 -1
  75. package/dist/relay-web/assets/classDiagram-v2-EOCWNBFH-w2002tim.js +0 -1
  76. package/dist/relay-web/assets/stateDiagram-v2-6OUMAXLB-nhU-bjvr.js +0 -1
  77. package/dist/relay-web/assets/swimlanesDiagram-G3AALYLV-CHH67zRv.js +0 -8
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // packages/relay/src/cli.ts
6
- import { randomUUID as randomUUID4 } from "node:crypto";
6
+ import { randomUUID as randomUUID5 } from "node:crypto";
7
7
  import { existsSync } from "node:fs";
8
8
  import { homedir } from "node:os";
9
9
  import { dirname as dirname3, join as join2, resolve } from "node:path";
@@ -140,6 +140,7 @@ function initSchema(db) {
140
140
  name TEXT NOT NULL,
141
141
  credential_hash TEXT NOT NULL,
142
142
  core_version TEXT,
143
+ capabilities_json TEXT,
143
144
  last_seen_at TEXT,
144
145
  created_at TEXT NOT NULL
145
146
  );
@@ -181,6 +182,10 @@ function initSchema(db) {
181
182
  if (!messageCols.some((c) => c.name === "prompt_request_id")) {
182
183
  db.exec("ALTER TABLE messages ADD COLUMN prompt_request_id TEXT");
183
184
  }
185
+ const instanceCols = db.all("PRAGMA table_info(instances)");
186
+ if (!instanceCols.some((c) => c.name === "capabilities_json")) {
187
+ db.exec("ALTER TABLE instances ADD COLUMN capabilities_json TEXT");
188
+ }
184
189
  db.exec(`
185
190
  CREATE INDEX IF NOT EXISTS idx_messages_origin_queue ON messages (instance_id, session_alias, origin_queue_item_id);
186
191
  CREATE INDEX IF NOT EXISTS idx_messages_prompt_request ON messages (prompt_request_id);
@@ -452,16 +457,31 @@ class InstanceStore {
452
457
  return null;
453
458
  return toInstanceRow(row);
454
459
  }
455
- touch(instanceId, coreVersion) {
460
+ touch(instanceId, coreVersion, capabilities) {
461
+ const nowIso = this.now().toISOString();
462
+ if (capabilities !== undefined && coreVersion !== undefined) {
463
+ this.db.run("UPDATE instances SET last_seen_at = ?, core_version = ?, capabilities_json = ? WHERE id = ?", [nowIso, coreVersion, JSON.stringify(capabilities), instanceId]);
464
+ return;
465
+ }
466
+ if (capabilities !== undefined) {
467
+ this.db.run("UPDATE instances SET last_seen_at = ?, capabilities_json = ? WHERE id = ?", [nowIso, JSON.stringify(capabilities), instanceId]);
468
+ return;
469
+ }
456
470
  if (coreVersion !== undefined) {
457
471
  this.db.run("UPDATE instances SET last_seen_at = ?, core_version = ? WHERE id = ?", [
458
- this.now().toISOString(),
472
+ nowIso,
459
473
  coreVersion,
460
474
  instanceId
461
475
  ]);
462
476
  return;
463
477
  }
464
- this.db.run("UPDATE instances SET last_seen_at = ? WHERE id = ?", [this.now().toISOString(), instanceId]);
478
+ this.db.run("UPDATE instances SET last_seen_at = ? WHERE id = ?", [nowIso, instanceId]);
479
+ }
480
+ setCapabilities(instanceId, capabilities) {
481
+ this.db.run("UPDATE instances SET capabilities_json = ? WHERE id = ?", [
482
+ JSON.stringify(capabilities),
483
+ instanceId
484
+ ]);
465
485
  }
466
486
  listByAccount(accountId) {
467
487
  return this.db.all("SELECT * FROM instances WHERE account_id = ? ORDER BY created_at", [accountId]).map(toInstanceRow);
@@ -489,12 +509,23 @@ class InstanceStore {
489
509
  return row?.n ?? 0;
490
510
  }
491
511
  }
512
+ function parseCapabilitiesJson(raw) {
513
+ if (raw == null || raw === "")
514
+ return [];
515
+ try {
516
+ const parsed = JSON.parse(raw);
517
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : [];
518
+ } catch {
519
+ return [];
520
+ }
521
+ }
492
522
  function toInstanceRow(row) {
493
523
  return {
494
524
  id: row.id,
495
525
  accountId: row.account_id,
496
526
  name: row.name,
497
527
  coreVersion: row.core_version,
528
+ capabilities: parseCapabilitiesJson(row.capabilities_json),
498
529
  lastSeenAt: row.last_seen_at,
499
530
  createdAt: row.created_at
500
531
  };
@@ -650,9 +681,11 @@ class RecoveryReceiptStore {
650
681
  import {
651
682
  MSG,
652
683
  RELAY_PROTOCOL_VERSION,
684
+ TERMINAL_HUB_REQUEST_TIMEOUT_MS,
653
685
  decodeEnvelope,
654
686
  encodeEnvelope,
655
- errorPayload
687
+ errorPayload,
688
+ normalizeCapabilities
656
689
  } from "@ganglion/xacpx-relay-protocol";
657
690
 
658
691
  // packages/relay/src/logging.ts
@@ -736,6 +769,7 @@ function startHeartbeat(socket, intervalMs = HEARTBEAT_INTERVAL_MS, maxMissed =
736
769
  // packages/relay/src/gateway/instance-gateway.ts
737
770
  var DEFAULT_REQUEST_TIMEOUT_MS = 120000;
738
771
  var REQUEST_RESPONSE_RESERVE_MS = 15000;
772
+ var TERMINAL_REQUEST_TIMEOUT_MS = TERMINAL_HUB_REQUEST_TIMEOUT_MS;
739
773
 
740
774
  class InstanceGateway {
741
775
  deps;
@@ -873,7 +907,7 @@ class InstanceGateway {
873
907
  result = redeemed;
874
908
  }
875
909
  respond({ instanceId: result.instanceId, credential: result.credential });
876
- this.deps.instances.touch(result.instanceId);
910
+ this.deps.instances.touch(result.instanceId, payload?.coreVersion, normalizeCapabilities(payload?.capabilities));
877
911
  return { instanceId: result.instanceId, accountId: result.accountId };
878
912
  }
879
913
  if (envelope.type === MSG.instanceAuth) {
@@ -886,7 +920,7 @@ class InstanceGateway {
886
920
  return null;
887
921
  }
888
922
  respond({ ok: true });
889
- this.deps.instances.touch(instance.id, payload?.coreVersion);
923
+ this.deps.instances.touch(instance.id, payload?.coreVersion, normalizeCapabilities(payload?.capabilities));
890
924
  return { instanceId: instance.id, accountId: instance.accountId };
891
925
  }
892
926
  this.logger.info("relay.instance.handshake_failed", "handshake rejected", { reason: "unknown-message-type" });
@@ -912,13 +946,13 @@ class InstanceGateway {
912
946
  this.logger.debug("relay.instance.disconnect_failed", "closing revoked instance socket failed", { instanceId, error: String(err) });
913
947
  }
914
948
  }
915
- async sendRequest(instanceId, type, payload) {
949
+ async sendRequest(instanceId, type, payload, options) {
916
950
  const connection = this.connections.get(instanceId);
917
951
  if (!connection) {
918
952
  throw new Error("instance-offline");
919
953
  }
920
954
  const id = `relay-${++this.seq}`;
921
- const timeoutMs = this.deps.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
955
+ const timeoutMs = options?.timeoutMs ?? this.deps.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
922
956
  const requestBudgetMs = Math.max(timeoutMs - REQUEST_RESPONSE_RESERVE_MS, 1);
923
957
  const requestDeadlineAt = Date.now() + requestBudgetMs;
924
958
  return await new Promise((resolve, reject) => {
@@ -950,14 +984,24 @@ class InstanceGateway {
950
984
  }
951
985
 
952
986
  // packages/relay/src/gateway/web-gateway.ts
987
+ import { randomUUID as randomUUID3 } from "node:crypto";
953
988
  import { encodeEnvelope as encodeEnvelope2, webEventEnvelope } from "@ganglion/xacpx-relay-protocol";
954
989
  var WS_OPEN = 1;
955
990
  var BACKPRESSURE_MAX = 4 * 1024 * 1024;
991
+ function terminalKey(instanceId, terminalId) {
992
+ return `${instanceId}\x00${terminalId}`;
993
+ }
956
994
 
957
995
  class WebGateway {
958
996
  options;
959
997
  byAccount = new Map;
960
998
  subscriptions = new Map;
999
+ viewerBySocket = new Map;
1000
+ accountBySocket = new Map;
1001
+ attachmentsBySocket = new Map;
1002
+ socketByAttachment = new Map;
1003
+ bindingByAttachment = new Map;
1004
+ attachmentsByTerminal = new Map;
961
1005
  constructor(options = {}) {
962
1006
  this.options = options;
963
1007
  }
@@ -965,19 +1009,105 @@ class WebGateway {
965
1009
  const set = this.byAccount.get(accountId) ?? new Set;
966
1010
  set.add(socket);
967
1011
  this.byAccount.set(accountId, set);
1012
+ const viewerId = randomUUID3();
1013
+ this.viewerBySocket.set(socket, viewerId);
1014
+ this.accountBySocket.set(socket, accountId);
1015
+ this.attachmentsBySocket.set(socket, new Set);
968
1016
  this.options.logger?.debug("relay.web.connected", "web client connected", { accountId });
969
1017
  startHeartbeat(socket, this.options.heartbeatIntervalMs, undefined, this.options.logger);
970
1018
  socket.on("close", () => {
1019
+ this.clearSocketAttachments(socket);
971
1020
  set.delete(socket);
972
1021
  this.subscriptions.delete(socket);
1022
+ this.viewerBySocket.delete(socket);
1023
+ this.accountBySocket.delete(socket);
1024
+ this.attachmentsBySocket.delete(socket);
973
1025
  if (set.size === 0)
974
1026
  this.byAccount.delete(accountId);
975
1027
  this.options.logger?.debug("relay.web.disconnected", "web client disconnected", { accountId });
976
1028
  });
1029
+ return viewerId;
1030
+ }
1031
+ getViewerId(socket) {
1032
+ return this.viewerBySocket.get(socket);
977
1033
  }
978
1034
  setSubscription(socket, instanceIds) {
979
1035
  this.subscriptions.set(socket, new Set(instanceIds));
980
1036
  }
1037
+ bindAttachment(input) {
1038
+ const viewerId = this.viewerBySocket.get(input.socket);
1039
+ if (!viewerId) {
1040
+ throw new Error("socket has no viewerId; register() before bindAttachment");
1041
+ }
1042
+ this.unbindAttachment(input.attachmentId, { notifyDetach: false });
1043
+ const binding = {
1044
+ attachmentId: input.attachmentId,
1045
+ viewerId,
1046
+ terminalId: input.terminalId,
1047
+ instanceId: input.instanceId,
1048
+ socket: input.socket
1049
+ };
1050
+ this.bindingByAttachment.set(input.attachmentId, binding);
1051
+ this.socketByAttachment.set(input.attachmentId, input.socket);
1052
+ const sockSet = this.attachmentsBySocket.get(input.socket) ?? new Set;
1053
+ sockSet.add(input.attachmentId);
1054
+ this.attachmentsBySocket.set(input.socket, sockSet);
1055
+ const tKey = terminalKey(input.instanceId, input.terminalId);
1056
+ const termSet = this.attachmentsByTerminal.get(tKey) ?? new Set;
1057
+ termSet.add(input.attachmentId);
1058
+ this.attachmentsByTerminal.set(tKey, termSet);
1059
+ }
1060
+ unbindAttachment(attachmentId, opts = {}) {
1061
+ const binding = this.bindingByAttachment.get(attachmentId);
1062
+ if (!binding)
1063
+ return;
1064
+ this.bindingByAttachment.delete(attachmentId);
1065
+ this.socketByAttachment.delete(attachmentId);
1066
+ this.attachmentsBySocket.get(binding.socket)?.delete(attachmentId);
1067
+ const tKey = terminalKey(binding.instanceId, binding.terminalId);
1068
+ const termSet = this.attachmentsByTerminal.get(tKey);
1069
+ if (termSet) {
1070
+ termSet.delete(attachmentId);
1071
+ if (termSet.size === 0)
1072
+ this.attachmentsByTerminal.delete(tKey);
1073
+ }
1074
+ if (opts.notifyDetach !== false) {
1075
+ this.options.onAttachmentDetached?.({
1076
+ instanceId: binding.instanceId,
1077
+ attachmentId: binding.attachmentId,
1078
+ viewerId: binding.viewerId
1079
+ });
1080
+ }
1081
+ return binding;
1082
+ }
1083
+ getAttachmentBinding(attachmentId) {
1084
+ return this.bindingByAttachment.get(attachmentId);
1085
+ }
1086
+ socketOwnsAttachment(socket, attachmentId) {
1087
+ return this.socketByAttachment.get(attachmentId) === socket;
1088
+ }
1089
+ sendToAttachment(viewerId, attachmentId, event) {
1090
+ const binding = this.bindingByAttachment.get(attachmentId);
1091
+ if (!binding)
1092
+ return false;
1093
+ if (binding.viewerId !== viewerId)
1094
+ return false;
1095
+ if (this.socketByAttachment.get(attachmentId) !== binding.socket)
1096
+ return false;
1097
+ return this.send(binding.socket, event);
1098
+ }
1099
+ fanoutTerminalExit(instanceId, terminalId, event) {
1100
+ const ids = this.attachmentsByTerminal.get(terminalKey(instanceId, terminalId));
1101
+ if (!ids)
1102
+ return;
1103
+ for (const attachmentId of [...ids]) {
1104
+ const binding = this.bindingByAttachment.get(attachmentId);
1105
+ if (!binding)
1106
+ continue;
1107
+ this.send(binding.socket, event);
1108
+ this.unbindAttachment(attachmentId, { notifyDetach: false });
1109
+ }
1110
+ }
981
1111
  send(socket, event) {
982
1112
  return this.sendEncoded(socket, encodeEnvelope2(webEventEnvelope(event)));
983
1113
  }
@@ -989,6 +1119,7 @@ class WebGateway {
989
1119
  ...accountId ? { accountId } : {},
990
1120
  bufferedAmount: socket.bufferedAmount
991
1121
  });
1122
+ this.clearSocketAttachments(socket);
992
1123
  try {
993
1124
  socket.terminate?.();
994
1125
  } catch {}
@@ -996,11 +1127,17 @@ class WebGateway {
996
1127
  }
997
1128
  try {
998
1129
  socket.send(data);
999
- return true;
1000
1130
  } catch (err) {
1001
1131
  this.options.logger?.error("relay.web.broadcast_failed", "web event send failed", { error: String(err) });
1002
1132
  return false;
1003
1133
  }
1134
+ return true;
1135
+ }
1136
+ clearSocketAttachments(socket) {
1137
+ const ids = [...this.attachmentsBySocket.get(socket) ?? []];
1138
+ for (const attachmentId of ids) {
1139
+ this.unbindAttachment(attachmentId, { notifyDetach: true });
1140
+ }
1004
1141
  }
1005
1142
  broadcast(accountId, event) {
1006
1143
  const set = this.byAccount.get(accountId);
@@ -1020,8 +1157,38 @@ class WebGateway {
1020
1157
  }
1021
1158
 
1022
1159
  // packages/relay/src/gateway/web-inbound.ts
1023
- import { decodeEnvelope as decodeEnvelope2, MSG as MSG2, parseWebClientMessage } from "@ganglion/xacpx-relay-protocol";
1160
+ import {
1161
+ decodeEnvelope as decodeEnvelope2,
1162
+ isErrorPayload,
1163
+ MSG as MSG2,
1164
+ parseTerminalEventPayload,
1165
+ parseWebClientMessage
1166
+ } from "@ganglion/xacpx-relay-protocol";
1167
+ function fail(deps, socket, requestId, instanceId, code, message) {
1168
+ deps.webGateway.send(socket, {
1169
+ kind: "terminal-request-failed",
1170
+ requestId,
1171
+ instanceId,
1172
+ code,
1173
+ message
1174
+ });
1175
+ }
1176
+ function mapConnectorError(err) {
1177
+ if (err instanceof Error) {
1178
+ if (err.message === "instance-offline" || err.message === "instance-reconnected") {
1179
+ return { code: "instance-offline", message: err.message };
1180
+ }
1181
+ if (err.message === "timeout") {
1182
+ return { code: "terminal-timeout", message: "terminal request timed out" };
1183
+ }
1184
+ return { code: "terminal-protocol-error", message: err.message };
1185
+ }
1186
+ return { code: "terminal-protocol-error", message: String(err) };
1187
+ }
1024
1188
  function handleWebClientMessage(deps, accountId, socket, raw) {
1189
+ handleWebClientMessageAsync(deps, accountId, socket, raw);
1190
+ }
1191
+ async function handleWebClientMessageAsync(deps, accountId, socket, raw) {
1025
1192
  const decoded = decodeEnvelope2(raw);
1026
1193
  if (!decoded.ok)
1027
1194
  return;
@@ -1033,26 +1200,373 @@ function handleWebClientMessage(deps, accountId, socket, raw) {
1033
1200
  const instanceIds = [...new Set(msg.instanceIds)].filter((id) => ownedIds.has(id));
1034
1201
  deps.webGateway.setSubscription(socket, instanceIds);
1035
1202
  for (const instanceId of instanceIds) {
1036
- deps.webGateway.send(socket, { kind: "state-snapshot", instanceId, ...deps.stateSnapshot(instanceId) });
1203
+ deps.webGateway.send(socket, {
1204
+ kind: "state-snapshot",
1205
+ instanceId,
1206
+ ...deps.stateSnapshot(instanceId)
1207
+ });
1037
1208
  }
1038
1209
  return;
1039
1210
  }
1040
1211
  if (!deps.instances.getOwned(msg.instanceId, accountId))
1041
1212
  return;
1042
- if (msg.kind === "terminal-input")
1043
- deps.gateway.sendEvent(msg.instanceId, MSG2.terminalInput, { terminalId: msg.terminalId, data: msg.data });
1044
- else if (msg.kind === "terminal-resize")
1045
- deps.gateway.sendEvent(msg.instanceId, MSG2.terminalResize, { terminalId: msg.terminalId, cols: msg.cols, rows: msg.rows });
1046
- else if (msg.kind === "terminal-close")
1213
+ if (msg.kind === "terminal-open") {
1214
+ await handleTerminalOpen(deps, accountId, socket, msg);
1215
+ return;
1216
+ }
1217
+ if (msg.kind === "terminal-stream-start") {
1218
+ const viewerId = deps.webGateway.getViewerId(socket);
1219
+ if (!viewerId || !deps.webGateway.socketOwnsAttachment(socket, msg.attachmentId))
1220
+ return;
1221
+ deps.gateway.sendEvent(msg.instanceId, MSG2.terminalStreamStart, {
1222
+ attachmentId: msg.attachmentId,
1223
+ viewerId
1224
+ });
1225
+ return;
1226
+ }
1227
+ if (msg.kind === "terminal-take-control") {
1228
+ await handleTakeControl(deps, socket, msg);
1229
+ return;
1230
+ }
1231
+ if (msg.kind === "terminal-resync") {
1232
+ await handleResync(deps, socket, msg);
1233
+ return;
1234
+ }
1235
+ if (msg.kind === "terminal-terminate") {
1236
+ await handleTerminate(deps, socket, msg);
1237
+ return;
1238
+ }
1239
+ if (msg.kind === "terminal-input" && "attachmentId" in msg) {
1240
+ const viewerId = deps.webGateway.getViewerId(socket);
1241
+ if (!viewerId || !deps.webGateway.socketOwnsAttachment(socket, msg.attachmentId))
1242
+ return;
1243
+ deps.gateway.sendEvent(msg.instanceId, MSG2.terminalInput, {
1244
+ attachmentId: msg.attachmentId,
1245
+ generation: msg.generation,
1246
+ viewerId,
1247
+ dataBase64: msg.dataBase64
1248
+ });
1249
+ return;
1250
+ }
1251
+ if (msg.kind === "terminal-resize" && "attachmentId" in msg) {
1252
+ const viewerId = deps.webGateway.getViewerId(socket);
1253
+ if (!viewerId || !deps.webGateway.socketOwnsAttachment(socket, msg.attachmentId))
1254
+ return;
1255
+ deps.gateway.sendEvent(msg.instanceId, MSG2.terminalResize, {
1256
+ attachmentId: msg.attachmentId,
1257
+ generation: msg.generation,
1258
+ viewerId,
1259
+ cols: msg.cols,
1260
+ rows: msg.rows
1261
+ });
1262
+ return;
1263
+ }
1264
+ if (msg.kind === "terminal-heartbeat") {
1265
+ const viewerId = deps.webGateway.getViewerId(socket);
1266
+ if (!viewerId || !deps.webGateway.socketOwnsAttachment(socket, msg.attachmentId))
1267
+ return;
1268
+ deps.gateway.sendEvent(msg.instanceId, MSG2.terminalHeartbeat, {
1269
+ attachmentId: msg.attachmentId,
1270
+ viewerId
1271
+ });
1272
+ return;
1273
+ }
1274
+ if (msg.kind === "terminal-detach") {
1275
+ if (!deps.webGateway.socketOwnsAttachment(socket, msg.attachmentId))
1276
+ return;
1277
+ const binding = deps.webGateway.unbindAttachment(msg.attachmentId);
1278
+ if (binding) {
1279
+ deps.gateway.sendEvent(msg.instanceId, MSG2.terminalDetach, {
1280
+ attachmentId: msg.attachmentId,
1281
+ viewerId: binding.viewerId
1282
+ });
1283
+ }
1284
+ return;
1285
+ }
1286
+ if (msg.kind === "terminal-input" && "terminalId" in msg) {
1287
+ deps.gateway.sendEvent(msg.instanceId, MSG2.terminalInput, {
1288
+ terminalId: msg.terminalId,
1289
+ data: msg.data
1290
+ });
1291
+ } else if (msg.kind === "terminal-resize" && "terminalId" in msg) {
1292
+ deps.gateway.sendEvent(msg.instanceId, MSG2.terminalResize, {
1293
+ terminalId: msg.terminalId,
1294
+ cols: msg.cols,
1295
+ rows: msg.rows
1296
+ });
1297
+ } else if (msg.kind === "terminal-close") {
1047
1298
  deps.gateway.sendEvent(msg.instanceId, MSG2.terminalClose, { terminalId: msg.terminalId });
1299
+ }
1300
+ }
1301
+ async function handleTerminalOpen(deps, accountId, socket, msg) {
1302
+ const viewerId = deps.webGateway.getViewerId(socket);
1303
+ if (!viewerId) {
1304
+ fail(deps, socket, msg.requestId, msg.instanceId, "terminal-protocol-error", "missing viewer identity");
1305
+ return;
1306
+ }
1307
+ if (!deps.gateway.isOnline(msg.instanceId)) {
1308
+ fail(deps, socket, msg.requestId, msg.instanceId, "instance-offline", "instance is offline");
1309
+ return;
1310
+ }
1311
+ let payload;
1312
+ try {
1313
+ payload = await deps.gateway.sendRequest(msg.instanceId, MSG2.terminalOpen, {
1314
+ chatKey: `relay:${accountId}`,
1315
+ sessionAlias: msg.sessionAlias,
1316
+ viewerId,
1317
+ cols: msg.cols,
1318
+ rows: msg.rows
1319
+ }, { timeoutMs: TERMINAL_REQUEST_TIMEOUT_MS });
1320
+ } catch (err) {
1321
+ const mapped = mapConnectorError(err);
1322
+ fail(deps, socket, msg.requestId, msg.instanceId, mapped.code, mapped.message);
1323
+ return;
1324
+ }
1325
+ if (isErrorPayload(payload)) {
1326
+ fail(deps, socket, msg.requestId, msg.instanceId, payload.error.code, payload.error.message);
1327
+ return;
1328
+ }
1329
+ const result = payload;
1330
+ if (!result || typeof result.terminalId !== "string" || typeof result.generation !== "string" || typeof result.attachmentId !== "string") {
1331
+ fail(deps, socket, msg.requestId, msg.instanceId, "terminal-protocol-error", "malformed open result");
1332
+ return;
1333
+ }
1334
+ if (deps.webGateway.getViewerId(socket) !== viewerId) {
1335
+ detachConnectorAttachment(deps, msg.instanceId, result.attachmentId, viewerId);
1336
+ fail(deps, socket, msg.requestId, msg.instanceId, "terminal-unavailable", "viewer disconnected");
1337
+ return;
1338
+ }
1339
+ try {
1340
+ deps.webGateway.bindAttachment({
1341
+ socket,
1342
+ attachmentId: result.attachmentId,
1343
+ terminalId: result.terminalId,
1344
+ instanceId: msg.instanceId
1345
+ });
1346
+ } catch {
1347
+ detachConnectorAttachment(deps, msg.instanceId, result.attachmentId, viewerId);
1348
+ fail(deps, socket, msg.requestId, msg.instanceId, "terminal-unavailable", "viewer disconnected");
1349
+ return;
1350
+ }
1351
+ const sent = deps.webGateway.send(socket, {
1352
+ kind: "terminal-opened",
1353
+ requestId: msg.requestId,
1354
+ instanceId: msg.instanceId,
1355
+ terminalId: result.terminalId,
1356
+ generation: result.generation,
1357
+ attachmentId: result.attachmentId,
1358
+ role: result.role,
1359
+ viewerCount: result.viewerCount
1360
+ });
1361
+ if (!sent) {
1362
+ deps.webGateway.unbindAttachment(result.attachmentId);
1363
+ detachConnectorAttachment(deps, msg.instanceId, result.attachmentId, viewerId);
1364
+ }
1365
+ }
1366
+ function detachConnectorAttachment(deps, instanceId, attachmentId, viewerId) {
1367
+ deps.gateway.sendEvent(instanceId, MSG2.terminalDetach, {
1368
+ attachmentId,
1369
+ viewerId
1370
+ });
1371
+ }
1372
+ async function handleTakeControl(deps, socket, msg) {
1373
+ const viewerId = deps.webGateway.getViewerId(socket);
1374
+ if (!viewerId || !deps.webGateway.socketOwnsAttachment(socket, msg.attachmentId)) {
1375
+ fail(deps, socket, msg.requestId, msg.instanceId, "terminal-attachment-not-found", "attachment not bound");
1376
+ return;
1377
+ }
1378
+ try {
1379
+ const payload = await deps.gateway.sendRequest(msg.instanceId, MSG2.terminalTakeControl, { attachmentId: msg.attachmentId, generation: msg.generation, viewerId }, { timeoutMs: TERMINAL_REQUEST_TIMEOUT_MS });
1380
+ if (isErrorPayload(payload)) {
1381
+ fail(deps, socket, msg.requestId, msg.instanceId, payload.error.code, payload.error.message);
1382
+ return;
1383
+ }
1384
+ const result = payload;
1385
+ deps.webGateway.send(socket, {
1386
+ kind: "terminal-opened",
1387
+ requestId: msg.requestId,
1388
+ instanceId: msg.instanceId,
1389
+ terminalId: result.terminalId,
1390
+ generation: result.generation,
1391
+ attachmentId: result.attachmentId,
1392
+ role: result.role,
1393
+ viewerCount: result.viewerCount
1394
+ });
1395
+ } catch (err) {
1396
+ const mapped = mapConnectorError(err);
1397
+ fail(deps, socket, msg.requestId, msg.instanceId, mapped.code, mapped.message);
1398
+ }
1399
+ }
1400
+ async function handleResync(deps, socket, msg) {
1401
+ const viewerId = deps.webGateway.getViewerId(socket);
1402
+ if (!viewerId || !deps.webGateway.socketOwnsAttachment(socket, msg.attachmentId)) {
1403
+ fail(deps, socket, msg.requestId, msg.instanceId, "terminal-attachment-not-found", "attachment not bound");
1404
+ return;
1405
+ }
1406
+ try {
1407
+ const payload = await deps.gateway.sendRequest(msg.instanceId, MSG2.terminalResync, { attachmentId: msg.attachmentId, generation: msg.generation, viewerId }, { timeoutMs: TERMINAL_REQUEST_TIMEOUT_MS });
1408
+ if (isErrorPayload(payload)) {
1409
+ fail(deps, socket, msg.requestId, msg.instanceId, payload.error.code, payload.error.message);
1410
+ return;
1411
+ }
1412
+ deps.webGateway.send(socket, {
1413
+ kind: "terminal-request-failed",
1414
+ requestId: msg.requestId,
1415
+ instanceId: msg.instanceId,
1416
+ code: "ok",
1417
+ message: "resync-accepted"
1418
+ });
1419
+ } catch (err) {
1420
+ const mapped = mapConnectorError(err);
1421
+ fail(deps, socket, msg.requestId, msg.instanceId, mapped.code, mapped.message);
1422
+ }
1423
+ }
1424
+ async function handleTerminate(deps, socket, msg) {
1425
+ try {
1426
+ const payload = await deps.gateway.sendRequest(msg.instanceId, MSG2.terminalTerminate, { terminalId: msg.terminalId, generation: msg.generation }, { timeoutMs: TERMINAL_REQUEST_TIMEOUT_MS });
1427
+ if (isErrorPayload(payload)) {
1428
+ fail(deps, socket, msg.requestId, msg.instanceId, payload.error.code, payload.error.message);
1429
+ return;
1430
+ }
1431
+ const result = payload;
1432
+ deps.webGateway.send(socket, {
1433
+ kind: "terminal-exit",
1434
+ instanceId: msg.instanceId,
1435
+ terminalId: msg.terminalId,
1436
+ generation: msg.generation,
1437
+ reason: result.status === "cleanup-pending" ? "cleanup-pending" : "terminated"
1438
+ });
1439
+ deps.webGateway.send(socket, {
1440
+ kind: "terminal-request-failed",
1441
+ requestId: msg.requestId,
1442
+ instanceId: msg.instanceId,
1443
+ code: result.status,
1444
+ message: result.status
1445
+ });
1446
+ } catch (err) {
1447
+ const mapped = mapConnectorError(err);
1448
+ fail(deps, socket, msg.requestId, msg.instanceId, mapped.code, mapped.message);
1449
+ }
1450
+ }
1451
+ function handleConnectorTerminalEvent(webGateway, instanceId, envelopeType, payload) {
1452
+ if (envelopeType === MSG2.terminalViewerEvent) {
1453
+ const parsed = parseTerminalEventPayload(MSG2.terminalViewerEvent, payload);
1454
+ if (!parsed)
1455
+ return true;
1456
+ const p = parsed;
1457
+ if (!p?.viewerId || !p?.attachmentId || !p?.event)
1458
+ return true;
1459
+ const inner = p.event;
1460
+ let event = null;
1461
+ switch (inner.kind) {
1462
+ case "terminal-rebase-start":
1463
+ event = {
1464
+ kind: "terminal-rebase-start",
1465
+ instanceId,
1466
+ attachmentId: p.attachmentId,
1467
+ generation: inner.generation,
1468
+ epoch: inner.epoch,
1469
+ nextSequence: inner.nextSequence,
1470
+ cols: inner.cols,
1471
+ rows: inner.rows,
1472
+ alternate: inner.alternate,
1473
+ totalBytes: inner.totalBytes,
1474
+ chunkCount: inner.chunkCount
1475
+ };
1476
+ break;
1477
+ case "terminal-rebase-chunk":
1478
+ event = {
1479
+ kind: "terminal-rebase-chunk",
1480
+ instanceId,
1481
+ attachmentId: p.attachmentId,
1482
+ generation: inner.generation,
1483
+ epoch: inner.epoch,
1484
+ index: inner.index,
1485
+ dataBase64: inner.dataBase64
1486
+ };
1487
+ break;
1488
+ case "terminal-rebase-end":
1489
+ event = {
1490
+ kind: "terminal-rebase-end",
1491
+ instanceId,
1492
+ attachmentId: p.attachmentId,
1493
+ generation: inner.generation,
1494
+ epoch: inner.epoch
1495
+ };
1496
+ break;
1497
+ case "terminal-bytes":
1498
+ event = {
1499
+ kind: "terminal-bytes",
1500
+ instanceId,
1501
+ attachmentId: p.attachmentId,
1502
+ generation: inner.generation,
1503
+ epoch: inner.epoch,
1504
+ sequence: inner.sequence,
1505
+ dataBase64: inner.dataBase64
1506
+ };
1507
+ break;
1508
+ case "terminal-role-changed":
1509
+ event = {
1510
+ kind: "terminal-role-changed",
1511
+ instanceId,
1512
+ attachmentId: p.attachmentId,
1513
+ terminalId: inner.terminalId,
1514
+ role: inner.role,
1515
+ viewerCount: inner.viewerCount
1516
+ };
1517
+ break;
1518
+ case "terminal-request-failed":
1519
+ event = {
1520
+ kind: "terminal-request-failed",
1521
+ requestId: inner.requestId ?? "",
1522
+ instanceId,
1523
+ code: inner.code,
1524
+ message: inner.message
1525
+ };
1526
+ break;
1527
+ case "terminal-recovery-failed":
1528
+ event = {
1529
+ kind: "terminal-recovery-failed",
1530
+ instanceId,
1531
+ attachmentId: p.attachmentId,
1532
+ generation: inner.generation,
1533
+ code: inner.code,
1534
+ message: inner.message
1535
+ };
1536
+ break;
1537
+ default:
1538
+ return true;
1539
+ }
1540
+ if (event)
1541
+ webGateway.sendToAttachment(p.viewerId, p.attachmentId, event);
1542
+ return true;
1543
+ }
1544
+ if (envelopeType === MSG2.terminalResourceExit) {
1545
+ const parsed = parseTerminalEventPayload(MSG2.terminalResourceExit, payload);
1546
+ if (!parsed)
1547
+ return true;
1548
+ const p = parsed;
1549
+ if (!p?.terminalId || !p?.generation)
1550
+ return true;
1551
+ webGateway.fanoutTerminalExit(instanceId, p.terminalId, {
1552
+ kind: "terminal-exit",
1553
+ instanceId,
1554
+ terminalId: p.terminalId,
1555
+ generation: p.generation,
1556
+ reason: p.reason,
1557
+ ...p.code !== undefined ? { code: p.code } : {}
1558
+ });
1559
+ return true;
1560
+ }
1561
+ return false;
1048
1562
  }
1049
1563
 
1050
1564
  // packages/relay/src/http/app.ts
1051
1565
  import { Hono } from "hono";
1052
1566
  import { deleteCookie, getCookie, setCookie } from "hono/cookie";
1053
1567
  import { serveStatic } from "@hono/node-server/serve-static";
1054
- import { randomUUID as randomUUID3 } from "node:crypto";
1055
- import { isErrorPayload, MSG as MSG3, parseControlPayload } from "@ganglion/xacpx-relay-protocol";
1568
+ import { randomUUID as randomUUID4 } from "node:crypto";
1569
+ import { isErrorPayload as isErrorPayload2, MSG as MSG3, parseControlPayload } from "@ganglion/xacpx-relay-protocol";
1056
1570
 
1057
1571
  // packages/relay/src/http/client-ip.ts
1058
1572
  import { getConnInfo } from "@hono/node-server/conninfo";
@@ -1583,7 +2097,7 @@ function createApp(deps) {
1583
2097
  ...previewUrl ? { previewUrl } : {}
1584
2098
  };
1585
2099
  });
1586
- const promptRequestId = randomUUID3();
2100
+ const promptRequestId = randomUUID4();
1587
2101
  persistedPromptId = deps.messages.append(instance.id, p.sessionAlias, "in", p.text, undefined, attachments, promptRequestId);
1588
2102
  if (body.type === MSG3.prompt && typeof payload === "object" && payload !== null) {
1589
2103
  payload.promptRequestId = promptRequestId;
@@ -1599,7 +2113,7 @@ function createApp(deps) {
1599
2113
  queueItemId: result.queueItemId
1600
2114
  });
1601
2115
  }
1602
- if (removeInput && !isErrorPayload(result)) {
2116
+ if (removeInput && !isErrorPayload2(result)) {
1603
2117
  deps.messages.deleteBySession(instance.id, removeInput.alias);
1604
2118
  }
1605
2119
  if (body.type === MSG3.commandExecute) {
@@ -1733,7 +2247,16 @@ async function createRelayRuntime(dbPath, options = {}) {
1733
2247
  const accounts = new AccountStore(db);
1734
2248
  const instances = new InstanceStore(db);
1735
2249
  const messages = new MessageStore(db);
1736
- const webGateway = new WebGateway({ logger });
2250
+ let gatewayRef = null;
2251
+ const webGateway = new WebGateway({
2252
+ logger,
2253
+ onAttachmentDetached: (info) => {
2254
+ gatewayRef?.sendEvent(info.instanceId, MSG4.terminalDetach, {
2255
+ attachmentId: info.attachmentId,
2256
+ viewerId: info.viewerId
2257
+ });
2258
+ }
2259
+ });
1737
2260
  const turnBuffers = new Map;
1738
2261
  const key = (instanceId, alias) => `${instanceId}\x00${alias}`;
1739
2262
  const recoveredFingerprints = new Set;
@@ -1832,6 +2355,9 @@ async function createRelayRuntime(dbPath, options = {}) {
1832
2355
  webGateway.broadcast(accountId, { kind: "instance-status", instanceId, online });
1833
2356
  },
1834
2357
  onEvent: (instanceId, accountId, envelope) => {
2358
+ if (handleConnectorTerminalEvent(webGateway, instanceId, envelope.type, envelope.payload)) {
2359
+ return;
2360
+ }
1835
2361
  const recentRows = (sessionAlias) => messages.listBySession(accountId, instanceId, sessionAlias, { limit: 5 }).messages;
1836
2362
  const hasTrailingPrompt = (sessionAlias, prompt) => {
1837
2363
  const rows = recentRows(sessionAlias);
@@ -2086,6 +2612,7 @@ async function createRelayRuntime(dbPath, options = {}) {
2086
2612
  }
2087
2613
  }
2088
2614
  });
2615
+ gatewayRef = gateway;
2089
2616
  const app = createApp({
2090
2617
  accounts,
2091
2618
  instances,
@@ -2329,7 +2856,7 @@ async function runRelayCli(args, io) {
2329
2856
  const label = flag(args, "--label");
2330
2857
  const runtime = await createRelayRuntime(dbPath);
2331
2858
  try {
2332
- const username = "u-" + randomUUID4();
2859
+ const username = "u-" + randomUUID5();
2333
2860
  const acc = runtime.accounts.createAccount(username);
2334
2861
  const { token } = runtime.accounts.createLoginToken(acc.id, label);
2335
2862
  io.print(`access token: ${token}`);