@nordbyte/nordrelay 0.6.0 → 0.8.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 (62) hide show
  1. package/.env.example +52 -0
  2. package/README.md +171 -50
  3. package/dist/access-control.js +6 -1
  4. package/dist/activity-events.js +2 -2
  5. package/dist/adapter-conformance.js +61 -0
  6. package/dist/bot-preferences.js +1 -0
  7. package/dist/bot.js +95 -37
  8. package/dist/channel-adapter.js +44 -11
  9. package/dist/channel-command-catalog.js +94 -0
  10. package/dist/channel-command-core.js +60 -0
  11. package/dist/channel-command-service.js +230 -1
  12. package/dist/channel-mirror-registry.js +84 -0
  13. package/dist/channel-peer-prompt.js +95 -0
  14. package/dist/channel-prompt-engine.js +177 -0
  15. package/dist/channel-runtime.js +12 -5
  16. package/dist/channel-turn-lifecycle.js +73 -0
  17. package/dist/codex-state.js +114 -78
  18. package/dist/config-metadata.js +82 -8
  19. package/dist/config.js +79 -7
  20. package/dist/context-key.js +42 -0
  21. package/dist/discord-bot.js +173 -342
  22. package/dist/discord-command-surface.js +11 -73
  23. package/dist/index.js +29 -0
  24. package/dist/metrics.js +48 -0
  25. package/dist/peer-auth.js +85 -0
  26. package/dist/peer-client.js +288 -0
  27. package/dist/peer-context.js +21 -0
  28. package/dist/peer-identity.js +127 -0
  29. package/dist/peer-readiness.js +77 -0
  30. package/dist/peer-runtime-service.js +658 -0
  31. package/dist/peer-server.js +220 -0
  32. package/dist/peer-store.js +307 -0
  33. package/dist/peer-types.js +52 -0
  34. package/dist/relay-runtime-helpers.js +210 -0
  35. package/dist/relay-runtime.js +79 -274
  36. package/dist/remote-prompt.js +98 -0
  37. package/dist/settings-wizard-test.js +216 -0
  38. package/dist/slack-artifacts.js +165 -0
  39. package/dist/slack-bot.js +1461 -0
  40. package/dist/slack-channel-runtime.js +147 -0
  41. package/dist/slack-command-surface.js +46 -0
  42. package/dist/slack-diagnostics.js +116 -0
  43. package/dist/slack-rate-limit.js +139 -0
  44. package/dist/telegram-command-menu.js +3 -53
  45. package/dist/telegram-general-commands.js +14 -0
  46. package/dist/telegram-preference-commands.js +23 -127
  47. package/dist/user-management-crypto.js +38 -0
  48. package/dist/user-management-normalize.js +188 -0
  49. package/dist/user-management-types.js +1 -0
  50. package/dist/user-management.js +193 -196
  51. package/dist/web-api-contract.js +16 -0
  52. package/dist/web-dashboard-access-routes.js +62 -0
  53. package/dist/web-dashboard-assets.js +1 -0
  54. package/dist/web-dashboard-pages.js +26 -4
  55. package/dist/web-dashboard-peer-routes.js +225 -0
  56. package/dist/web-dashboard-ui.js +1 -0
  57. package/dist/web-dashboard.js +46 -0
  58. package/dist/web-state.js +2 -2
  59. package/dist/webui-assets/dashboard.css +193 -0
  60. package/dist/webui-assets/dashboard.js +870 -57
  61. package/package.json +5 -2
  62. package/plugins/nordrelay/scripts/nordrelay.mjs +468 -11
@@ -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,24 @@ 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";
32
+ import { collectSlackDiagnostics } from "./slack-diagnostics.js";
33
+ import { getSlackRateLimitMetrics } from "./slack-rate-limit.js";
31
34
  import { createSupportBundle } from "./support-bundle.js";
32
35
  import { transcribeAudio } from "./voice.js";
33
36
  import { WebActivityStore, WebChatStore, } from "./web-state.js";
34
- import { channelIdForContextKey } from "./context-key.js";
35
37
  import { evaluateWorkspacePolicy, filterAllowedWorkspaces } from "./workspace-policy.js";
36
- const WEB_CONTEXT_KEY = "web:dashboard";
38
+ export const WEB_CONTEXT_KEY = "web:dashboard";
37
39
  const ACTIVE_CODEX_DISCOVERY_LIMIT = 200;
38
40
  const ACTIVE_ACTIVITY_TTL_MS = 6 * 60 * 60 * 1000;
39
41
  const MAX_WEB_SESSION_PAGE_SIZE = 50;
40
42
  const MAX_CHAT_HISTORY = 250;
41
43
  export class RelayRuntime {
42
44
  config;
45
+ contextKey;
43
46
  registry;
44
47
  promptStore;
45
48
  chatStore;
@@ -50,6 +53,7 @@ export class RelayRuntime {
50
53
  queueService;
51
54
  jobStore;
52
55
  artifactService;
56
+ mirrorRegistry;
53
57
  externalActivityMonitor;
54
58
  cache = new RuntimeSnapshotCache();
55
59
  turnService;
@@ -57,25 +61,29 @@ export class RelayRuntime {
57
61
  agentUpdateActors = new Map();
58
62
  agentUpdateStates = new Map();
59
63
  externalMonitor;
64
+ activeSessionsBroadcastTimer = null;
65
+ activeSessionsLastBroadcastAt = 0;
60
66
  draining = false;
61
67
  currentTurnId = null;
62
68
  accumulatedText = "";
63
69
  currentTurnStartedAt = 0;
64
70
  currentProgress = null;
65
- constructor(config) {
71
+ constructor(config, options = {}) {
66
72
  this.config = config;
73
+ this.contextKey = options.contextKey ?? WEB_CONTEXT_KEY;
67
74
  this.registry = new SessionRegistry(config, {
68
- fileName: "web-contexts.json",
69
- sqliteKey: "web-contexts",
75
+ fileName: options.registryFileName ?? "web-contexts.json",
76
+ sqliteKey: options.registrySqliteKey ?? "web-contexts",
70
77
  });
71
78
  this.promptStore = new PromptStore(config.workspace, config.stateBackend);
72
79
  this.chatStore = new WebChatStore(config.workspace, config.stateBackend, MAX_CHAT_HISTORY);
73
80
  this.activityStore = new WebActivityStore(config.workspace, config.stateBackend, config.auditMaxEvents);
74
81
  this.auditStore = new AuditLogStore(config.workspace, config.stateBackend, config.auditMaxEvents);
75
82
  this.lockStore = new SessionLockStore(config.workspace, config.stateBackend);
76
- this.queueService = new RelayQueueService(this.promptStore, WEB_CONTEXT_KEY);
83
+ this.queueService = new RelayQueueService(this.promptStore, this.contextKey);
77
84
  this.jobStore = new UnifiedJobStore(config.workspace, config.stateBackend, config.unifiedJobMaxItems);
78
85
  this.artifactService = new RelayArtifactService(config);
86
+ this.mirrorRegistry = new ChannelMirrorRegistry(config, this.promptStore);
79
87
  this.agentUpdates = new AgentUpdateManager({
80
88
  onUpdate: (job) => {
81
89
  this.broadcast({ type: "agent_update", job });
@@ -103,7 +111,7 @@ export class RelayRuntime {
103
111
  }
104
112
  this.turnService = new ChannelTurnService({
105
113
  source: "web",
106
- contextKey: WEB_CONTEXT_KEY,
114
+ contextKey: this.contextKey,
107
115
  chatStore: this.chatStore,
108
116
  artifactService: this.artifactService,
109
117
  checkAuth: (info) => this.checkAgentAuth(info),
@@ -136,6 +144,7 @@ export class RelayRuntime {
136
144
  this.subscribers.add(callback);
137
145
  void this.snapshot().then((data) => callback({ type: "snapshot", data })).catch(() => { });
138
146
  void this.chatHistory().then((messages) => callback({ type: "chat_history", messages })).catch(() => { });
147
+ void this.activeSessions().then((active) => callback({ type: "active_sessions_update", active })).catch(() => { });
139
148
  callback({ type: "activity_update", events: this.activity({ limit: 50 }) });
140
149
  return () => this.subscribers.delete(callback);
141
150
  }
@@ -205,7 +214,7 @@ export class RelayRuntime {
205
214
  this.appendAudit({
206
215
  action: "command",
207
216
  status: "ok",
208
- contextKey: WEB_CONTEXT_KEY,
217
+ contextKey: this.contextKey,
209
218
  actor,
210
219
  description: "update",
211
220
  detail: update.summary,
@@ -242,7 +251,7 @@ export class RelayRuntime {
242
251
  this.appendAudit({
243
252
  action: "command",
244
253
  status: "ok",
245
- contextKey: WEB_CONTEXT_KEY,
254
+ contextKey: this.contextKey,
246
255
  agentId,
247
256
  actor,
248
257
  description: `${operation} ${agentId}`,
@@ -268,7 +277,7 @@ export class RelayRuntime {
268
277
  this.appendAudit({
269
278
  action: "command",
270
279
  status: "ok",
271
- contextKey: WEB_CONTEXT_KEY,
280
+ contextKey: this.contextKey,
272
281
  agentId: job.agentId,
273
282
  actor,
274
283
  description: `delete update log ${id}`,
@@ -323,6 +332,11 @@ export class RelayRuntime {
323
332
  queuePaused: this.queueService.isPaused(),
324
333
  externalMirror: this.externalActivityMonitor.snapshot(),
325
334
  agentDiagnostics: getAgentDiagnostics(session, this.config),
335
+ slackDiagnostics: await collectSlackDiagnostics({
336
+ config: this.config,
337
+ timeoutMs: 2_500,
338
+ rateLimit: getSlackRateLimitMetrics(),
339
+ }),
326
340
  },
327
341
  };
328
342
  });
@@ -566,8 +580,8 @@ export class RelayRuntime {
566
580
  if (this.currentProgress?.status === "running") {
567
581
  addActiveSession({
568
582
  ...this.currentProgress,
569
- contextKey: WEB_CONTEXT_KEY,
570
- sourceContextKey: WEB_CONTEXT_KEY,
583
+ contextKey: this.contextKey,
584
+ sourceContextKey: this.contextKey,
571
585
  source: "web",
572
586
  status: "running",
573
587
  queueLength: this.queueService.length(),
@@ -581,7 +595,7 @@ export class RelayRuntime {
581
595
  addActiveSession(active);
582
596
  }
583
597
  for (const meta of knownContexts) {
584
- if (meta.contextKey === WEB_CONTEXT_KEY && this.currentProgress?.status === "running") {
598
+ if (meta.contextKey === this.contextKey && this.currentProgress?.status === "running") {
585
599
  continue;
586
600
  }
587
601
  const active = this.externalActiveSession(meta, knownContexts, preferences);
@@ -631,7 +645,7 @@ export class RelayRuntime {
631
645
  this.appendAudit({
632
646
  action: "command",
633
647
  status: "ok",
634
- contextKey: WEB_CONTEXT_KEY,
648
+ contextKey: this.contextKey,
635
649
  actor,
636
650
  description: "export diagnostics bundle",
637
651
  detail: bundle.path,
@@ -643,7 +657,7 @@ export class RelayRuntime {
643
657
  }
644
658
  lockWebSession(ownerName = "Web dashboard", actor) {
645
659
  const label = ownerName || actor?.label || "Web dashboard";
646
- const lock = this.lockStore.set(WEB_CONTEXT_KEY, {
660
+ const lock = this.lockStore.set(this.contextKey, {
647
661
  userId: actor?.id ?? "web",
648
662
  label,
649
663
  channel: "web",
@@ -660,7 +674,7 @@ export class RelayRuntime {
660
674
  this.appendAudit({
661
675
  action: "lock_updated",
662
676
  status: "ok",
663
- contextKey: WEB_CONTEXT_KEY,
677
+ contextKey: this.contextKey,
664
678
  actor,
665
679
  description: "lock",
666
680
  detail: `locked by ${label}`,
@@ -668,7 +682,7 @@ export class RelayRuntime {
668
682
  return lock;
669
683
  }
670
684
  unlockWebSession(actor) {
671
- const removed = this.lockStore.clear(WEB_CONTEXT_KEY);
685
+ const removed = this.lockStore.clear(this.contextKey);
672
686
  this.appendActivity({
673
687
  source: "web",
674
688
  status: "info",
@@ -681,7 +695,7 @@ export class RelayRuntime {
681
695
  this.appendAudit({
682
696
  action: "lock_updated",
683
697
  status: "ok",
684
- contextKey: WEB_CONTEXT_KEY,
698
+ contextKey: this.contextKey,
685
699
  actor,
686
700
  description: "unlock",
687
701
  detail: removed ? "unlocked" : "no lock",
@@ -788,7 +802,7 @@ export class RelayRuntime {
788
802
  this.appendAudit({
789
803
  action: "command",
790
804
  status: result.success ? "ok" : "failed",
791
- contextKey: WEB_CONTEXT_KEY,
805
+ contextKey: this.contextKey,
792
806
  agentId: info.agentId,
793
807
  threadId: info.threadId,
794
808
  workspace: info.workspace,
@@ -851,7 +865,7 @@ export class RelayRuntime {
851
865
  this.appendAudit({
852
866
  action: "command",
853
867
  status: result.success ? "ok" : "failed",
854
- contextKey: WEB_CONTEXT_KEY,
868
+ contextKey: this.contextKey,
855
869
  agentId: info.agentId,
856
870
  threadId: info.threadId,
857
871
  workspace: info.workspace,
@@ -902,7 +916,7 @@ export class RelayRuntime {
902
916
  return { removed, messages };
903
917
  }
904
918
  activity(options = {}) {
905
- const currentInfo = this.registry.get(WEB_CONTEXT_KEY)?.getInfo();
919
+ const currentInfo = this.registry.get(this.contextKey)?.getInfo();
906
920
  return this.activityStore.list(options).map((event) => this.enrichActivityEvent(event, currentInfo));
907
921
  }
908
922
  async retry(actor) {
@@ -913,7 +927,7 @@ export class RelayRuntime {
913
927
  this.appendAudit({
914
928
  action: "command",
915
929
  status: "ok",
916
- contextKey: WEB_CONTEXT_KEY,
930
+ contextKey: this.contextKey,
917
931
  actor,
918
932
  description: "retry",
919
933
  detail: cached.description,
@@ -943,7 +957,7 @@ export class RelayRuntime {
943
957
  this.appendAudit({
944
958
  action: "command",
945
959
  status: "ok",
946
- contextKey: WEB_CONTEXT_KEY,
960
+ contextKey: this.contextKey,
947
961
  agentId: result.info.agentId,
948
962
  threadId: result.info.threadId,
949
963
  workspace: result.info.workspace,
@@ -1018,7 +1032,7 @@ export class RelayRuntime {
1018
1032
  if (!enabledAgents(this.config).includes(agentId)) {
1019
1033
  throw new Error(`Agent is not enabled: ${agentId}`);
1020
1034
  }
1021
- const session = await this.registry.switchAgent(WEB_CONTEXT_KEY, agentId);
1035
+ const session = await this.registry.switchAgent(this.contextKey, agentId);
1022
1036
  this.updateSession(session);
1023
1037
  const info = this.publicInfo(session);
1024
1038
  this.appendActivity({
@@ -1034,7 +1048,7 @@ export class RelayRuntime {
1034
1048
  return this.publicInfo(session);
1035
1049
  }
1036
1050
  async newSession(options = {}, actor) {
1037
- const session = options.agentId ? await this.registry.switchAgent(WEB_CONTEXT_KEY, options.agentId) : await this.getSession(true);
1051
+ const session = options.agentId ? await this.registry.switchAgent(this.contextKey, options.agentId) : await this.getSession(true);
1038
1052
  this.ensureIdle(session);
1039
1053
  if (options.reasoningEffort) {
1040
1054
  const reasoningOptions = agentReasoningOptions(session.getInfo().agentId);
@@ -1273,7 +1287,7 @@ export class RelayRuntime {
1273
1287
  this.appendAudit({
1274
1288
  action: "prompt_queued",
1275
1289
  status: "ok",
1276
- contextKey: WEB_CONTEXT_KEY,
1290
+ contextKey: this.contextKey,
1277
1291
  agentId: info.agentId,
1278
1292
  threadId: info.threadId,
1279
1293
  workspace: info.workspace,
@@ -1318,7 +1332,7 @@ export class RelayRuntime {
1318
1332
  this.appendAudit({
1319
1333
  action: "queue_updated",
1320
1334
  status: "ok",
1321
- contextKey: WEB_CONTEXT_KEY,
1335
+ contextKey: this.contextKey,
1322
1336
  actor,
1323
1337
  description: id ? `${action}: ${id}` : action,
1324
1338
  });
@@ -1394,7 +1408,7 @@ export class RelayRuntime {
1394
1408
  this.appendAudit({
1395
1409
  action: "command",
1396
1410
  status: "ok",
1397
- contextKey: WEB_CONTEXT_KEY,
1411
+ contextKey: this.contextKey,
1398
1412
  actor,
1399
1413
  description: `clear ${target} log`,
1400
1414
  detail: result.filePath,
@@ -1416,7 +1430,7 @@ export class RelayRuntime {
1416
1430
  this.appendAudit({
1417
1431
  action: "command",
1418
1432
  status: "ok",
1419
- contextKey: WEB_CONTEXT_KEY,
1433
+ contextKey: this.contextKey,
1420
1434
  actor,
1421
1435
  description: "restart connector",
1422
1436
  });
@@ -1431,7 +1445,7 @@ export class RelayRuntime {
1431
1445
  this.subscribers.clear();
1432
1446
  }
1433
1447
  async getSession(deferThreadStart) {
1434
- return this.registry.getOrCreate(WEB_CONTEXT_KEY, { deferThreadStart });
1448
+ return this.registry.getOrCreate(this.contextKey, { deferThreadStart });
1435
1449
  }
1436
1450
  async cached(key, producer) {
1437
1451
  return (await this.cache.get(key, this.config.dashboardCacheTtlMs, producer)).value;
@@ -1455,10 +1469,10 @@ export class RelayRuntime {
1455
1469
  finally {
1456
1470
  sharedRegistry.disposeAll();
1457
1471
  }
1458
- const current = this.registry.get(WEB_CONTEXT_KEY)?.getInfo();
1472
+ const current = this.registry.get(this.contextKey)?.getInfo();
1459
1473
  if (current) {
1460
1474
  add({
1461
- contextKey: WEB_CONTEXT_KEY,
1475
+ contextKey: this.contextKey,
1462
1476
  agentId: current.agentId,
1463
1477
  threadId: current.threadId,
1464
1478
  workspace: current.workspace,
@@ -1524,7 +1538,12 @@ export class RelayRuntime {
1524
1538
  return [];
1525
1539
  }
1526
1540
  const active = [];
1541
+ const nowMs = Date.now();
1542
+ const staleAfterMs = this.config.codexExternalBusyStaleMs;
1527
1543
  for (const thread of listCodexThreads(ACTIVE_CODEX_DISCOVERY_LIMIT)) {
1544
+ if (staleAfterMs > 0 && nowMs - thread.updatedAt.getTime() > staleAfterMs) {
1545
+ continue;
1546
+ }
1528
1547
  const meta = {
1529
1548
  contextKey: `cli:codex:${thread.id}`,
1530
1549
  agentId: "codex",
@@ -1553,6 +1572,12 @@ export class RelayRuntime {
1553
1572
  if (!capabilities.externalActivity) {
1554
1573
  return null;
1555
1574
  }
1575
+ if (agentId === "codex" &&
1576
+ meta.updatedAt &&
1577
+ this.config.codexExternalBusyStaleMs > 0 &&
1578
+ Date.now() - meta.updatedAt > this.config.codexExternalBusyStaleMs) {
1579
+ return null;
1580
+ }
1556
1581
  const snapshot = getExternalSnapshotForSession(this.sessionStubForMetadata(meta, agentId, capabilities), this.config, {
1557
1582
  maxEvents: 8,
1558
1583
  });
@@ -1563,8 +1588,8 @@ export class RelayRuntime {
1563
1588
  const updatedAt = snapshot.activity.updatedAt?.toISOString() ?? new Date().toISOString();
1564
1589
  const startedMs = Date.parse(startedAt);
1565
1590
  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);
1591
+ const mirrorChannels = this.mirrorRegistry.activeMirrorsForThread(snapshot.agentId, snapshot.threadId, knownContexts, preferences);
1592
+ const queueLength = this.mirrorRegistry.queueLengthForExternalSource(sourceContextKey, mirrorChannels);
1568
1593
  const mirrorDetail = mirrorChannels.length > 0
1569
1594
  ? `Mirroring: ${mirrorChannels.map((mirror) => `${mirror.source} ${mirror.mode}`).join(", ")}`
1570
1595
  : "Mirroring: none";
@@ -1585,42 +1610,11 @@ export class RelayRuntime {
1585
1610
  updatedAt,
1586
1611
  durationMs: Number.isFinite(startedMs) ? Math.max(0, Date.now() - startedMs) : 0,
1587
1612
  queueLength,
1588
- queuePaused: mirrorChannels.some((mirror) => mirror.queuePaused) || this.promptStore.isPaused(sourceContextKey),
1613
+ queuePaused: this.mirrorRegistry.queuePausedForExternalSource(sourceContextKey, mirrorChannels),
1589
1614
  mirrorChannels,
1590
1615
  detail: `${mirrorDetail} | ${snapshot.sourceLabel}: ${snapshot.sourcePath}`,
1591
1616
  };
1592
1617
  }
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
1618
  sessionStubForMetadata(meta, agentId, capabilities) {
1625
1619
  const info = {
1626
1620
  agentId,
@@ -1785,7 +1779,7 @@ export class RelayRuntime {
1785
1779
  }
1786
1780
  }
1787
1781
  updateSession(session) {
1788
- this.registry.updateMetadata(WEB_CONTEXT_KEY, session);
1782
+ this.registry.updateMetadata(this.contextKey, session);
1789
1783
  this.broadcast({ type: "session_update", session: this.publicInfo(session) });
1790
1784
  }
1791
1785
  recordActivity(input) {
@@ -1891,6 +1885,23 @@ export class RelayRuntime {
1891
1885
  this.subscribers.delete(subscriber);
1892
1886
  }
1893
1887
  }
1888
+ if (shouldRefreshActiveSessions(event)) {
1889
+ this.scheduleActiveSessionsBroadcast();
1890
+ }
1891
+ }
1892
+ scheduleActiveSessionsBroadcast() {
1893
+ if (this.activeSessionsBroadcastTimer) {
1894
+ return;
1895
+ }
1896
+ const delayMs = Math.max(0, 1_000 - (Date.now() - this.activeSessionsLastBroadcastAt));
1897
+ this.activeSessionsBroadcastTimer = setTimeout(() => {
1898
+ this.activeSessionsBroadcastTimer = null;
1899
+ this.activeSessionsLastBroadcastAt = Date.now();
1900
+ void this.activeSessions()
1901
+ .then((active) => this.broadcast({ type: "active_sessions_update", active }))
1902
+ .catch(() => { });
1903
+ }, delayMs);
1904
+ this.activeSessionsBroadcastTimer.unref?.();
1894
1905
  }
1895
1906
  publicInfo(session) {
1896
1907
  const info = session.getInfo();
@@ -1903,209 +1914,3 @@ export class RelayRuntime {
1903
1914
  };
1904
1915
  }
1905
1916
  }
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
- }