@nordbyte/nordrelay 0.6.0 → 0.7.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 (42) hide show
  1. package/.env.example +17 -0
  2. package/README.md +67 -6
  3. package/dist/access-control.js +6 -1
  4. package/dist/activity-events.js +2 -2
  5. package/dist/bot-preferences.js +1 -0
  6. package/dist/bot.js +77 -6
  7. package/dist/channel-adapter.js +11 -5
  8. package/dist/channel-command-catalog.js +88 -0
  9. package/dist/channel-command-service.js +214 -1
  10. package/dist/channel-mirror-registry.js +77 -0
  11. package/dist/channel-peer-prompt.js +95 -0
  12. package/dist/channel-runtime.js +12 -5
  13. package/dist/codex-state.js +114 -78
  14. package/dist/config-metadata.js +15 -0
  15. package/dist/config.js +31 -6
  16. package/dist/context-key.js +10 -0
  17. package/dist/discord-bot.js +85 -26
  18. package/dist/discord-command-surface.js +11 -73
  19. package/dist/index.js +20 -0
  20. package/dist/metrics.js +46 -0
  21. package/dist/peer-auth.js +85 -0
  22. package/dist/peer-client.js +256 -0
  23. package/dist/peer-context.js +21 -0
  24. package/dist/peer-identity.js +127 -0
  25. package/dist/peer-runtime-service.js +636 -0
  26. package/dist/peer-server.js +220 -0
  27. package/dist/peer-store.js +294 -0
  28. package/dist/peer-types.js +52 -0
  29. package/dist/relay-runtime-helpers.js +208 -0
  30. package/dist/relay-runtime.js +72 -274
  31. package/dist/remote-prompt.js +98 -0
  32. package/dist/telegram-command-menu.js +3 -53
  33. package/dist/telegram-general-commands.js +14 -0
  34. package/dist/telegram-preference-commands.js +23 -127
  35. package/dist/web-api-contract.js +8 -0
  36. package/dist/web-dashboard-pages.js +12 -0
  37. package/dist/web-dashboard-peer-routes.js +204 -0
  38. package/dist/web-dashboard-ui.js +1 -0
  39. package/dist/web-dashboard.js +12 -0
  40. package/dist/webui-assets/dashboard.js +427 -14
  41. package/package.json +3 -2
  42. package/plugins/nordrelay/scripts/nordrelay.mjs +373 -7
@@ -1,5 +1,4 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import path from "node:path";
3
2
  import { ensureOutDir } from "./artifacts.js";
4
3
  import { buildFileInstructions, outboxPath, stageFile, } from "./attachments.js";
5
4
  import { CODEX_AGENT_CAPABILITIES, agentLabel, agentReasoningLabel, agentReasoningOptions, isAgentId, } from "./agent.js";
@@ -10,6 +9,7 @@ import { createAgentSessionService, enabledAgents } from "./agent-factory.js";
10
9
  import { AuditLogStore } from "./audit-log.js";
11
10
  import { BotPreferencesStore } from "./bot-preferences.js";
12
11
  import { ChannelTurnService } from "./channel-turn-service.js";
12
+ import { activeSessionSourceForContextKey, ChannelMirrorRegistry } from "./channel-mirror-registry.js";
13
13
  import { checkAuthStatus, startLogin as startCodexLogin, startLogout as startCodexLogout } from "./codex-auth.js";
14
14
  import { checkClaudeCodeAuthStatus, startClaudeCodeLogin, startClaudeCodeLogout } from "./claude-code-auth.js";
15
15
  import { listThreads as listCodexThreads } from "./codex-state.js";
@@ -25,21 +25,22 @@ import { RelayArtifactService } from "./relay-artifact-service.js";
25
25
  import { RelayExternalActivityMonitor } from "./relay-external-activity-monitor.js";
26
26
  import { RelayQueueService } from "./relay-queue-service.js";
27
27
  import { RuntimeSnapshotCache } from "./runtime-cache.js";
28
+ import { activeSessionPriority, activityToUnifiedJob, agentUpdateStatusToUnified, cliHealthForAgent, dedupeJobs, hostLoginCommand, hostLogoutCommand, isPromptTerminalActivity, normalizeMimeType, promptActivityToUnifiedJob, shouldRefreshActiveSessions, taskToUnifiedJob, uploadFileDtos, versionCheckForAgent, } from "./relay-runtime-helpers.js";
28
29
  import { renderSessionInfoPlain, renderSessionUsageRows } from "./session-format.js";
29
30
  import { SessionLockStore } from "./session-locks.js";
30
31
  import { SessionRegistry } from "./session-registry.js";
31
32
  import { createSupportBundle } from "./support-bundle.js";
32
33
  import { transcribeAudio } from "./voice.js";
33
34
  import { WebActivityStore, WebChatStore, } from "./web-state.js";
34
- import { channelIdForContextKey } from "./context-key.js";
35
35
  import { evaluateWorkspacePolicy, filterAllowedWorkspaces } from "./workspace-policy.js";
36
- const WEB_CONTEXT_KEY = "web:dashboard";
36
+ export const WEB_CONTEXT_KEY = "web:dashboard";
37
37
  const ACTIVE_CODEX_DISCOVERY_LIMIT = 200;
38
38
  const ACTIVE_ACTIVITY_TTL_MS = 6 * 60 * 60 * 1000;
39
39
  const MAX_WEB_SESSION_PAGE_SIZE = 50;
40
40
  const MAX_CHAT_HISTORY = 250;
41
41
  export class RelayRuntime {
42
42
  config;
43
+ contextKey;
43
44
  registry;
44
45
  promptStore;
45
46
  chatStore;
@@ -50,6 +51,7 @@ export class RelayRuntime {
50
51
  queueService;
51
52
  jobStore;
52
53
  artifactService;
54
+ mirrorRegistry;
53
55
  externalActivityMonitor;
54
56
  cache = new RuntimeSnapshotCache();
55
57
  turnService;
@@ -57,25 +59,29 @@ export class RelayRuntime {
57
59
  agentUpdateActors = new Map();
58
60
  agentUpdateStates = new Map();
59
61
  externalMonitor;
62
+ activeSessionsBroadcastTimer = null;
63
+ activeSessionsLastBroadcastAt = 0;
60
64
  draining = false;
61
65
  currentTurnId = null;
62
66
  accumulatedText = "";
63
67
  currentTurnStartedAt = 0;
64
68
  currentProgress = null;
65
- constructor(config) {
69
+ constructor(config, options = {}) {
66
70
  this.config = config;
71
+ this.contextKey = options.contextKey ?? WEB_CONTEXT_KEY;
67
72
  this.registry = new SessionRegistry(config, {
68
- fileName: "web-contexts.json",
69
- sqliteKey: "web-contexts",
73
+ fileName: options.registryFileName ?? "web-contexts.json",
74
+ sqliteKey: options.registrySqliteKey ?? "web-contexts",
70
75
  });
71
76
  this.promptStore = new PromptStore(config.workspace, config.stateBackend);
72
77
  this.chatStore = new WebChatStore(config.workspace, config.stateBackend, MAX_CHAT_HISTORY);
73
78
  this.activityStore = new WebActivityStore(config.workspace, config.stateBackend, config.auditMaxEvents);
74
79
  this.auditStore = new AuditLogStore(config.workspace, config.stateBackend, config.auditMaxEvents);
75
80
  this.lockStore = new SessionLockStore(config.workspace, config.stateBackend);
76
- this.queueService = new RelayQueueService(this.promptStore, WEB_CONTEXT_KEY);
81
+ this.queueService = new RelayQueueService(this.promptStore, this.contextKey);
77
82
  this.jobStore = new UnifiedJobStore(config.workspace, config.stateBackend, config.unifiedJobMaxItems);
78
83
  this.artifactService = new RelayArtifactService(config);
84
+ this.mirrorRegistry = new ChannelMirrorRegistry(config, this.promptStore);
79
85
  this.agentUpdates = new AgentUpdateManager({
80
86
  onUpdate: (job) => {
81
87
  this.broadcast({ type: "agent_update", job });
@@ -103,7 +109,7 @@ export class RelayRuntime {
103
109
  }
104
110
  this.turnService = new ChannelTurnService({
105
111
  source: "web",
106
- contextKey: WEB_CONTEXT_KEY,
112
+ contextKey: this.contextKey,
107
113
  chatStore: this.chatStore,
108
114
  artifactService: this.artifactService,
109
115
  checkAuth: (info) => this.checkAgentAuth(info),
@@ -136,6 +142,7 @@ export class RelayRuntime {
136
142
  this.subscribers.add(callback);
137
143
  void this.snapshot().then((data) => callback({ type: "snapshot", data })).catch(() => { });
138
144
  void this.chatHistory().then((messages) => callback({ type: "chat_history", messages })).catch(() => { });
145
+ void this.activeSessions().then((active) => callback({ type: "active_sessions_update", active })).catch(() => { });
139
146
  callback({ type: "activity_update", events: this.activity({ limit: 50 }) });
140
147
  return () => this.subscribers.delete(callback);
141
148
  }
@@ -205,7 +212,7 @@ export class RelayRuntime {
205
212
  this.appendAudit({
206
213
  action: "command",
207
214
  status: "ok",
208
- contextKey: WEB_CONTEXT_KEY,
215
+ contextKey: this.contextKey,
209
216
  actor,
210
217
  description: "update",
211
218
  detail: update.summary,
@@ -242,7 +249,7 @@ export class RelayRuntime {
242
249
  this.appendAudit({
243
250
  action: "command",
244
251
  status: "ok",
245
- contextKey: WEB_CONTEXT_KEY,
252
+ contextKey: this.contextKey,
246
253
  agentId,
247
254
  actor,
248
255
  description: `${operation} ${agentId}`,
@@ -268,7 +275,7 @@ export class RelayRuntime {
268
275
  this.appendAudit({
269
276
  action: "command",
270
277
  status: "ok",
271
- contextKey: WEB_CONTEXT_KEY,
278
+ contextKey: this.contextKey,
272
279
  agentId: job.agentId,
273
280
  actor,
274
281
  description: `delete update log ${id}`,
@@ -566,8 +573,8 @@ export class RelayRuntime {
566
573
  if (this.currentProgress?.status === "running") {
567
574
  addActiveSession({
568
575
  ...this.currentProgress,
569
- contextKey: WEB_CONTEXT_KEY,
570
- sourceContextKey: WEB_CONTEXT_KEY,
576
+ contextKey: this.contextKey,
577
+ sourceContextKey: this.contextKey,
571
578
  source: "web",
572
579
  status: "running",
573
580
  queueLength: this.queueService.length(),
@@ -581,7 +588,7 @@ export class RelayRuntime {
581
588
  addActiveSession(active);
582
589
  }
583
590
  for (const meta of knownContexts) {
584
- if (meta.contextKey === WEB_CONTEXT_KEY && this.currentProgress?.status === "running") {
591
+ if (meta.contextKey === this.contextKey && this.currentProgress?.status === "running") {
585
592
  continue;
586
593
  }
587
594
  const active = this.externalActiveSession(meta, knownContexts, preferences);
@@ -631,7 +638,7 @@ export class RelayRuntime {
631
638
  this.appendAudit({
632
639
  action: "command",
633
640
  status: "ok",
634
- contextKey: WEB_CONTEXT_KEY,
641
+ contextKey: this.contextKey,
635
642
  actor,
636
643
  description: "export diagnostics bundle",
637
644
  detail: bundle.path,
@@ -643,7 +650,7 @@ export class RelayRuntime {
643
650
  }
644
651
  lockWebSession(ownerName = "Web dashboard", actor) {
645
652
  const label = ownerName || actor?.label || "Web dashboard";
646
- const lock = this.lockStore.set(WEB_CONTEXT_KEY, {
653
+ const lock = this.lockStore.set(this.contextKey, {
647
654
  userId: actor?.id ?? "web",
648
655
  label,
649
656
  channel: "web",
@@ -660,7 +667,7 @@ export class RelayRuntime {
660
667
  this.appendAudit({
661
668
  action: "lock_updated",
662
669
  status: "ok",
663
- contextKey: WEB_CONTEXT_KEY,
670
+ contextKey: this.contextKey,
664
671
  actor,
665
672
  description: "lock",
666
673
  detail: `locked by ${label}`,
@@ -668,7 +675,7 @@ export class RelayRuntime {
668
675
  return lock;
669
676
  }
670
677
  unlockWebSession(actor) {
671
- const removed = this.lockStore.clear(WEB_CONTEXT_KEY);
678
+ const removed = this.lockStore.clear(this.contextKey);
672
679
  this.appendActivity({
673
680
  source: "web",
674
681
  status: "info",
@@ -681,7 +688,7 @@ export class RelayRuntime {
681
688
  this.appendAudit({
682
689
  action: "lock_updated",
683
690
  status: "ok",
684
- contextKey: WEB_CONTEXT_KEY,
691
+ contextKey: this.contextKey,
685
692
  actor,
686
693
  description: "unlock",
687
694
  detail: removed ? "unlocked" : "no lock",
@@ -788,7 +795,7 @@ export class RelayRuntime {
788
795
  this.appendAudit({
789
796
  action: "command",
790
797
  status: result.success ? "ok" : "failed",
791
- contextKey: WEB_CONTEXT_KEY,
798
+ contextKey: this.contextKey,
792
799
  agentId: info.agentId,
793
800
  threadId: info.threadId,
794
801
  workspace: info.workspace,
@@ -851,7 +858,7 @@ export class RelayRuntime {
851
858
  this.appendAudit({
852
859
  action: "command",
853
860
  status: result.success ? "ok" : "failed",
854
- contextKey: WEB_CONTEXT_KEY,
861
+ contextKey: this.contextKey,
855
862
  agentId: info.agentId,
856
863
  threadId: info.threadId,
857
864
  workspace: info.workspace,
@@ -902,7 +909,7 @@ export class RelayRuntime {
902
909
  return { removed, messages };
903
910
  }
904
911
  activity(options = {}) {
905
- const currentInfo = this.registry.get(WEB_CONTEXT_KEY)?.getInfo();
912
+ const currentInfo = this.registry.get(this.contextKey)?.getInfo();
906
913
  return this.activityStore.list(options).map((event) => this.enrichActivityEvent(event, currentInfo));
907
914
  }
908
915
  async retry(actor) {
@@ -913,7 +920,7 @@ export class RelayRuntime {
913
920
  this.appendAudit({
914
921
  action: "command",
915
922
  status: "ok",
916
- contextKey: WEB_CONTEXT_KEY,
923
+ contextKey: this.contextKey,
917
924
  actor,
918
925
  description: "retry",
919
926
  detail: cached.description,
@@ -943,7 +950,7 @@ export class RelayRuntime {
943
950
  this.appendAudit({
944
951
  action: "command",
945
952
  status: "ok",
946
- contextKey: WEB_CONTEXT_KEY,
953
+ contextKey: this.contextKey,
947
954
  agentId: result.info.agentId,
948
955
  threadId: result.info.threadId,
949
956
  workspace: result.info.workspace,
@@ -1018,7 +1025,7 @@ export class RelayRuntime {
1018
1025
  if (!enabledAgents(this.config).includes(agentId)) {
1019
1026
  throw new Error(`Agent is not enabled: ${agentId}`);
1020
1027
  }
1021
- const session = await this.registry.switchAgent(WEB_CONTEXT_KEY, agentId);
1028
+ const session = await this.registry.switchAgent(this.contextKey, agentId);
1022
1029
  this.updateSession(session);
1023
1030
  const info = this.publicInfo(session);
1024
1031
  this.appendActivity({
@@ -1034,7 +1041,7 @@ export class RelayRuntime {
1034
1041
  return this.publicInfo(session);
1035
1042
  }
1036
1043
  async newSession(options = {}, actor) {
1037
- const session = options.agentId ? await this.registry.switchAgent(WEB_CONTEXT_KEY, options.agentId) : await this.getSession(true);
1044
+ const session = options.agentId ? await this.registry.switchAgent(this.contextKey, options.agentId) : await this.getSession(true);
1038
1045
  this.ensureIdle(session);
1039
1046
  if (options.reasoningEffort) {
1040
1047
  const reasoningOptions = agentReasoningOptions(session.getInfo().agentId);
@@ -1273,7 +1280,7 @@ export class RelayRuntime {
1273
1280
  this.appendAudit({
1274
1281
  action: "prompt_queued",
1275
1282
  status: "ok",
1276
- contextKey: WEB_CONTEXT_KEY,
1283
+ contextKey: this.contextKey,
1277
1284
  agentId: info.agentId,
1278
1285
  threadId: info.threadId,
1279
1286
  workspace: info.workspace,
@@ -1318,7 +1325,7 @@ export class RelayRuntime {
1318
1325
  this.appendAudit({
1319
1326
  action: "queue_updated",
1320
1327
  status: "ok",
1321
- contextKey: WEB_CONTEXT_KEY,
1328
+ contextKey: this.contextKey,
1322
1329
  actor,
1323
1330
  description: id ? `${action}: ${id}` : action,
1324
1331
  });
@@ -1394,7 +1401,7 @@ export class RelayRuntime {
1394
1401
  this.appendAudit({
1395
1402
  action: "command",
1396
1403
  status: "ok",
1397
- contextKey: WEB_CONTEXT_KEY,
1404
+ contextKey: this.contextKey,
1398
1405
  actor,
1399
1406
  description: `clear ${target} log`,
1400
1407
  detail: result.filePath,
@@ -1416,7 +1423,7 @@ export class RelayRuntime {
1416
1423
  this.appendAudit({
1417
1424
  action: "command",
1418
1425
  status: "ok",
1419
- contextKey: WEB_CONTEXT_KEY,
1426
+ contextKey: this.contextKey,
1420
1427
  actor,
1421
1428
  description: "restart connector",
1422
1429
  });
@@ -1431,7 +1438,7 @@ export class RelayRuntime {
1431
1438
  this.subscribers.clear();
1432
1439
  }
1433
1440
  async getSession(deferThreadStart) {
1434
- return this.registry.getOrCreate(WEB_CONTEXT_KEY, { deferThreadStart });
1441
+ return this.registry.getOrCreate(this.contextKey, { deferThreadStart });
1435
1442
  }
1436
1443
  async cached(key, producer) {
1437
1444
  return (await this.cache.get(key, this.config.dashboardCacheTtlMs, producer)).value;
@@ -1455,10 +1462,10 @@ export class RelayRuntime {
1455
1462
  finally {
1456
1463
  sharedRegistry.disposeAll();
1457
1464
  }
1458
- const current = this.registry.get(WEB_CONTEXT_KEY)?.getInfo();
1465
+ const current = this.registry.get(this.contextKey)?.getInfo();
1459
1466
  if (current) {
1460
1467
  add({
1461
- contextKey: WEB_CONTEXT_KEY,
1468
+ contextKey: this.contextKey,
1462
1469
  agentId: current.agentId,
1463
1470
  threadId: current.threadId,
1464
1471
  workspace: current.workspace,
@@ -1524,7 +1531,12 @@ export class RelayRuntime {
1524
1531
  return [];
1525
1532
  }
1526
1533
  const active = [];
1534
+ const nowMs = Date.now();
1535
+ const staleAfterMs = this.config.codexExternalBusyStaleMs;
1527
1536
  for (const thread of listCodexThreads(ACTIVE_CODEX_DISCOVERY_LIMIT)) {
1537
+ if (staleAfterMs > 0 && nowMs - thread.updatedAt.getTime() > staleAfterMs) {
1538
+ continue;
1539
+ }
1528
1540
  const meta = {
1529
1541
  contextKey: `cli:codex:${thread.id}`,
1530
1542
  agentId: "codex",
@@ -1553,6 +1565,12 @@ export class RelayRuntime {
1553
1565
  if (!capabilities.externalActivity) {
1554
1566
  return null;
1555
1567
  }
1568
+ if (agentId === "codex" &&
1569
+ meta.updatedAt &&
1570
+ this.config.codexExternalBusyStaleMs > 0 &&
1571
+ Date.now() - meta.updatedAt > this.config.codexExternalBusyStaleMs) {
1572
+ return null;
1573
+ }
1556
1574
  const snapshot = getExternalSnapshotForSession(this.sessionStubForMetadata(meta, agentId, capabilities), this.config, {
1557
1575
  maxEvents: 8,
1558
1576
  });
@@ -1563,8 +1581,8 @@ export class RelayRuntime {
1563
1581
  const updatedAt = snapshot.activity.updatedAt?.toISOString() ?? new Date().toISOString();
1564
1582
  const startedMs = Date.parse(startedAt);
1565
1583
  const sourceContextKey = `cli:${snapshot.agentId}:${snapshot.threadId}`;
1566
- const mirrorChannels = this.activeMirrorChannels(snapshot.agentId, snapshot.threadId, knownContexts, preferences);
1567
- const queueLength = mirrorChannels.reduce((sum, mirror) => sum + mirror.queueLength, this.promptStore.list(sourceContextKey).length);
1584
+ const mirrorChannels = this.mirrorRegistry.activeMirrorsForThread(snapshot.agentId, snapshot.threadId, knownContexts, preferences);
1585
+ const queueLength = this.mirrorRegistry.queueLengthForExternalSource(sourceContextKey, mirrorChannels);
1568
1586
  const mirrorDetail = mirrorChannels.length > 0
1569
1587
  ? `Mirroring: ${mirrorChannels.map((mirror) => `${mirror.source} ${mirror.mode}`).join(", ")}`
1570
1588
  : "Mirroring: none";
@@ -1585,42 +1603,11 @@ export class RelayRuntime {
1585
1603
  updatedAt,
1586
1604
  durationMs: Number.isFinite(startedMs) ? Math.max(0, Date.now() - startedMs) : 0,
1587
1605
  queueLength,
1588
- queuePaused: mirrorChannels.some((mirror) => mirror.queuePaused) || this.promptStore.isPaused(sourceContextKey),
1606
+ queuePaused: this.mirrorRegistry.queuePausedForExternalSource(sourceContextKey, mirrorChannels),
1589
1607
  mirrorChannels,
1590
1608
  detail: `${mirrorDetail} | ${snapshot.sourceLabel}: ${snapshot.sourcePath}`,
1591
1609
  };
1592
1610
  }
1593
- activeMirrorChannels(agentId, threadId, knownContexts, preferences) {
1594
- const mirrors = [];
1595
- const seen = new Set();
1596
- for (const meta of knownContexts) {
1597
- const metaAgentId = meta.agentId ?? this.config.defaultAgent ?? "codex";
1598
- if (meta.threadId !== threadId || metaAgentId !== agentId) {
1599
- continue;
1600
- }
1601
- const source = activeSessionSourceForContext(meta.contextKey);
1602
- if (source !== "telegram" && source !== "discord") {
1603
- continue;
1604
- }
1605
- const mode = this.effectiveMirrorMode(meta.contextKey, source, preferences);
1606
- if (mode === "off" || seen.has(meta.contextKey)) {
1607
- continue;
1608
- }
1609
- seen.add(meta.contextKey);
1610
- mirrors.push({
1611
- source,
1612
- contextKey: meta.contextKey,
1613
- mode,
1614
- queueLength: this.promptStore.list(meta.contextKey).length,
1615
- queuePaused: this.promptStore.isPaused(meta.contextKey),
1616
- });
1617
- }
1618
- return mirrors;
1619
- }
1620
- effectiveMirrorMode(contextKey, source, preferences) {
1621
- const configured = source === "telegram" ? this.config.telegramMirrorMode : this.config.discordMirrorMode;
1622
- return preferences.get(contextKey).mirrorMode ?? configured;
1623
- }
1624
1611
  sessionStubForMetadata(meta, agentId, capabilities) {
1625
1612
  const info = {
1626
1613
  agentId,
@@ -1785,7 +1772,7 @@ export class RelayRuntime {
1785
1772
  }
1786
1773
  }
1787
1774
  updateSession(session) {
1788
- this.registry.updateMetadata(WEB_CONTEXT_KEY, session);
1775
+ this.registry.updateMetadata(this.contextKey, session);
1789
1776
  this.broadcast({ type: "session_update", session: this.publicInfo(session) });
1790
1777
  }
1791
1778
  recordActivity(input) {
@@ -1891,6 +1878,23 @@ export class RelayRuntime {
1891
1878
  this.subscribers.delete(subscriber);
1892
1879
  }
1893
1880
  }
1881
+ if (shouldRefreshActiveSessions(event)) {
1882
+ this.scheduleActiveSessionsBroadcast();
1883
+ }
1884
+ }
1885
+ scheduleActiveSessionsBroadcast() {
1886
+ if (this.activeSessionsBroadcastTimer) {
1887
+ return;
1888
+ }
1889
+ const delayMs = Math.max(0, 1_000 - (Date.now() - this.activeSessionsLastBroadcastAt));
1890
+ this.activeSessionsBroadcastTimer = setTimeout(() => {
1891
+ this.activeSessionsBroadcastTimer = null;
1892
+ this.activeSessionsLastBroadcastAt = Date.now();
1893
+ void this.activeSessions()
1894
+ .then((active) => this.broadcast({ type: "active_sessions_update", active }))
1895
+ .catch(() => { });
1896
+ }, delayMs);
1897
+ this.activeSessionsBroadcastTimer.unref?.();
1894
1898
  }
1895
1899
  publicInfo(session) {
1896
1900
  const info = session.getInfo();
@@ -1903,209 +1907,3 @@ export class RelayRuntime {
1903
1907
  };
1904
1908
  }
1905
1909
  }
1906
- function cliHealthForAgent(agentId, health) {
1907
- if (agentId === "pi") {
1908
- return { path: health.piCliPath, label: health.piCli, version: health.piCliVersion };
1909
- }
1910
- if (agentId === "hermes") {
1911
- return { path: health.hermesCliPath, label: health.hermesCli, version: health.hermesCliVersion };
1912
- }
1913
- if (agentId === "openclaw") {
1914
- return { path: health.openClawCliPath, label: health.openClawCli, version: health.openClawCliVersion };
1915
- }
1916
- if (agentId === "claude-code") {
1917
- return { path: health.claudeCodeCliPath, label: health.claudeCodeCli, version: health.claudeCodeCliVersion };
1918
- }
1919
- return { path: health.codexCliPath, label: health.codexCli, version: health.codexCliVersion };
1920
- }
1921
- function versionCheckForAgent(agentId, versions) {
1922
- if (agentId === "pi")
1923
- return versions.pi;
1924
- if (agentId === "hermes")
1925
- return versions.hermes;
1926
- if (agentId === "openclaw")
1927
- return versions.openclaw;
1928
- if (agentId === "claude-code")
1929
- return versions.claudeCode;
1930
- return versions.codex;
1931
- }
1932
- function hostLoginCommand(info, config) {
1933
- if (info.agentId === "hermes") {
1934
- return `${config.hermesCliPath ?? "hermes"} login --no-browser`;
1935
- }
1936
- if (info.agentId === "claude-code") {
1937
- return `${config.claudeCodeCliPath ?? "claude"} auth login`;
1938
- }
1939
- if (info.agentId === "pi") {
1940
- return `${config.piCliPath ?? "pi"} auth login`;
1941
- }
1942
- if (info.agentId === "openclaw") {
1943
- return `${config.openClawCliPath ?? "openclaw"} login`;
1944
- }
1945
- return "codex login --device-auth";
1946
- }
1947
- function hostLogoutCommand(info, config) {
1948
- if (info.agentId === "hermes") {
1949
- return `${config.hermesCliPath ?? "hermes"} logout`;
1950
- }
1951
- if (info.agentId === "claude-code") {
1952
- return `${config.claudeCodeCliPath ?? "claude"} auth logout`;
1953
- }
1954
- if (info.agentId === "pi") {
1955
- return `${config.piCliPath ?? "pi"} auth logout`;
1956
- }
1957
- if (info.agentId === "openclaw") {
1958
- return `${config.openClawCliPath ?? "openclaw"} logout`;
1959
- }
1960
- return "codex logout";
1961
- }
1962
- function activeSessionSourceForContext(contextKey) {
1963
- const channelId = channelIdForContextKey(contextKey);
1964
- if (channelId === "telegram") {
1965
- return "telegram";
1966
- }
1967
- if (channelId === "discord") {
1968
- return "discord";
1969
- }
1970
- if (channelId === "web") {
1971
- return "web";
1972
- }
1973
- return "cli";
1974
- }
1975
- function activeSessionPriority(session) {
1976
- if (session.status === "running") {
1977
- return 3;
1978
- }
1979
- return session.contextKey.startsWith("cli:") ? 1 : 2;
1980
- }
1981
- function isPromptTerminalActivity(event) {
1982
- return event.status === "completed" ||
1983
- event.status === "failed" ||
1984
- event.status === "aborted" ||
1985
- event.type === "prompt_completed" ||
1986
- event.type === "prompt_failed" ||
1987
- event.type === "prompt_aborted";
1988
- }
1989
- function taskToUnifiedJob(id, kind, title, task, options) {
1990
- return {
1991
- id,
1992
- kind,
1993
- title,
1994
- status: task.status,
1995
- source: task.source,
1996
- agentId: task.agentId,
1997
- agentLabel: task.agentLabel,
1998
- threadId: task.threadId,
1999
- workspace: task.workspace,
2000
- startedAt: task.startedAt,
2001
- updatedAt: task.updatedAt,
2002
- durationMs: task.durationMs,
2003
- summary: task.prompt || task.detail,
2004
- logTail: task.currentTool || task.lastTool ? `Current tool: ${task.currentTool ?? "-"}\nLast tool: ${task.lastTool ?? "-"}` : undefined,
2005
- ...options,
2006
- };
2007
- }
2008
- function activityToUnifiedJob(event, kind, title, options) {
2009
- return {
2010
- id: `${kind}:${event.id}`,
2011
- kind,
2012
- title,
2013
- status: event.status,
2014
- source: event.source,
2015
- agentId: event.agentId,
2016
- threadId: event.threadId,
2017
- workspace: event.workspace,
2018
- owner: event.actor,
2019
- startedAt: event.timestamp,
2020
- updatedAt: event.timestamp,
2021
- finishedAt: event.timestamp,
2022
- durationMs: event.durationMs,
2023
- summary: event.prompt || event.detail,
2024
- logPath: event.detail,
2025
- logTail: event.detail,
2026
- ...options,
2027
- };
2028
- }
2029
- function promptActivityToUnifiedJob(event) {
2030
- const status = event.status === "info" ? "completed" : event.status;
2031
- const sourceLabel = event.source === "web"
2032
- ? "WebUI"
2033
- : event.source === "telegram"
2034
- ? "Telegram"
2035
- : event.source === "discord"
2036
- ? "Discord"
2037
- : "CLI";
2038
- const promptKey = event.threadId ?? event.contextKey ?? event.id;
2039
- return {
2040
- id: `prompt:${event.source}:${promptKey}:${event.id}`,
2041
- kind: event.source === "cli" ? "external-turn" : "web-turn",
2042
- title: `${sourceLabel} prompt`,
2043
- status,
2044
- source: event.source,
2045
- agentId: event.agentId,
2046
- threadId: event.threadId,
2047
- workspace: event.workspace,
2048
- owner: event.actor,
2049
- startedAt: event.timestamp,
2050
- updatedAt: event.timestamp,
2051
- finishedAt: status === "running" || status === "queued" ? undefined : event.timestamp,
2052
- durationMs: event.durationMs,
2053
- summary: event.prompt || event.detail,
2054
- logTail: event.detail,
2055
- canCancel: status === "running" && event.source === "web",
2056
- canRetry: status !== "running",
2057
- canReadLog: Boolean(event.detail || event.prompt),
2058
- };
2059
- }
2060
- function agentUpdateStatusToUnified(status) {
2061
- if (status === "cancelled")
2062
- return "aborted";
2063
- if (status === "running")
2064
- return "running";
2065
- if (status === "completed")
2066
- return "completed";
2067
- return "failed";
2068
- }
2069
- function dedupeJobs(jobs) {
2070
- const seen = new Set();
2071
- return jobs.filter((job) => {
2072
- if (seen.has(job.id)) {
2073
- return false;
2074
- }
2075
- seen.add(job.id);
2076
- return true;
2077
- });
2078
- }
2079
- function normalizeMimeType(value, name) {
2080
- const configured = value?.trim();
2081
- if (configured) {
2082
- return configured;
2083
- }
2084
- const extension = path.extname(name).toLowerCase();
2085
- if ([".jpg", ".jpeg"].includes(extension))
2086
- return "image/jpeg";
2087
- if (extension === ".png")
2088
- return "image/png";
2089
- if (extension === ".gif")
2090
- return "image/gif";
2091
- if (extension === ".webp")
2092
- return "image/webp";
2093
- if (extension === ".mp3")
2094
- return "audio/mpeg";
2095
- if (extension === ".wav")
2096
- return "audio/wav";
2097
- if (extension === ".ogg" || extension === ".oga")
2098
- return "audio/ogg";
2099
- if (extension === ".m4a")
2100
- return "audio/mp4";
2101
- if (extension === ".webm")
2102
- return "audio/webm";
2103
- return "application/octet-stream";
2104
- }
2105
- function uploadFileDtos(files) {
2106
- return files.map((file) => ({
2107
- name: file.safeName,
2108
- mimeType: file.mimeType,
2109
- sizeBytes: file.sizeBytes,
2110
- }));
2111
- }