@nordbyte/nordrelay 0.8.0 → 0.8.2

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 (173) hide show
  1. package/.env.example +9 -0
  2. package/README.md +81 -1197
  3. package/dist/{access-control.js → access/access-control.js} +1 -1
  4. package/dist/{audit-log.js → access/audit-log.js} +2 -2
  5. package/dist/{session-locks.js → access/session-locks.js} +1 -1
  6. package/dist/{user-management.js → access/user-management.js} +1 -1
  7. package/dist/{claude-code-cli.js → agents/claude-code/claude-code-cli.js} +2 -2
  8. package/dist/{claude-code-session.js → agents/claude-code/claude-code-session.js} +1 -1
  9. package/dist/{codex-cli.js → agents/codex/codex-cli.js} +14 -5
  10. package/dist/{codex-session.js → agents/codex/codex-session.js} +2 -4
  11. package/dist/{hermes-cli.js → agents/hermes/hermes-cli.js} +2 -2
  12. package/dist/{hermes-launch.js → agents/hermes/hermes-launch.js} +1 -1
  13. package/dist/{hermes-session.js → agents/hermes/hermes-session.js} +1 -1
  14. package/dist/{openclaw-cli.js → agents/openclaw/openclaw-cli.js} +2 -2
  15. package/dist/{openclaw-launch.js → agents/openclaw/openclaw-launch.js} +1 -1
  16. package/dist/{openclaw-session.js → agents/openclaw/openclaw-session.js} +1 -1
  17. package/dist/{pi-cli.js → agents/pi/pi-cli.js} +2 -2
  18. package/dist/{pi-launch.js → agents/pi/pi-launch.js} +1 -1
  19. package/dist/{pi-session.js → agents/pi/pi-session.js} +1 -1
  20. package/dist/{adapter-conformance.js → agents/shared/adapter-conformance.js} +2 -2
  21. package/dist/{agent-activity.js → agents/shared/agent-activity.js} +5 -5
  22. package/dist/agents/shared/agent-auth-commands.js +30 -0
  23. package/dist/{agent-factory.js → agents/shared/agent-factory.js} +5 -5
  24. package/dist/{agent-feature-matrix.js → agents/shared/agent-feature-matrix.js} +2 -2
  25. package/dist/{agent-updates.js → agents/shared/agent-updates.js} +7 -7
  26. package/dist/{discord-artifacts.js → channels/discord/discord-artifacts.js} +4 -4
  27. package/dist/{discord-bot.js → channels/discord/discord-bot.js} +164 -424
  28. package/dist/{discord-channel-runtime.js → channels/discord/discord-channel-runtime.js} +2 -2
  29. package/dist/{discord-command-surface.js → channels/discord/discord-command-surface.js} +3 -3
  30. package/dist/{bot-rendering.js → channels/shared/bot-rendering.js} +6 -6
  31. package/dist/{channel-actions.js → channels/shared/channel-actions.js} +4 -4
  32. package/dist/channels/shared/channel-bridge-controller.js +69 -0
  33. package/dist/channels/shared/channel-cli-artifacts.js +51 -0
  34. package/dist/{channel-command-service.js → channels/shared/channel-command-service.js} +51 -28
  35. package/dist/channels/shared/channel-external-mirror-controller.js +193 -0
  36. package/dist/channels/shared/channel-external-monitor.js +52 -0
  37. package/dist/{channel-mirror-registry.js → channels/shared/channel-mirror-registry.js} +14 -6
  38. package/dist/{channel-peer-prompt.js → channels/shared/channel-peer-prompt.js} +3 -3
  39. package/dist/{channel-turn-service.js → channels/shared/channel-turn-service.js} +2 -2
  40. package/dist/{context-key.js → channels/shared/context-key.js} +1 -1
  41. package/dist/{session-format.js → channels/shared/session-format.js} +2 -2
  42. package/dist/{slack-artifacts.js → channels/slack/slack-artifacts.js} +4 -4
  43. package/dist/{slack-bot.js → channels/slack/slack-bot.js} +159 -294
  44. package/dist/{slack-channel-runtime.js → channels/slack/slack-channel-runtime.js} +2 -2
  45. package/dist/{slack-command-surface.js → channels/slack/slack-command-surface.js} +2 -2
  46. package/dist/{slack-diagnostics.js → channels/slack/slack-diagnostics.js} +2 -2
  47. package/dist/{bot-ui.js → channels/telegram/bot-ui.js} +1 -1
  48. package/dist/{bot.js → channels/telegram/bot.js} +178 -427
  49. package/dist/{telegram-access-commands.js → channels/telegram/telegram-access-commands.js} +3 -3
  50. package/dist/{telegram-access-middleware.js → channels/telegram/telegram-access-middleware.js} +4 -4
  51. package/dist/{telegram-agent-commands.js → channels/telegram/telegram-agent-commands.js} +9 -9
  52. package/dist/{telegram-artifact-commands.js → channels/telegram/telegram-artifact-commands.js} +4 -4
  53. package/dist/{telegram-channel-runtime.js → channels/telegram/telegram-channel-runtime.js} +2 -2
  54. package/dist/{telegram-command-menu.js → channels/telegram/telegram-command-menu.js} +1 -1
  55. package/dist/{telegram-diagnostics-command.js → channels/telegram/telegram-diagnostics-command.js} +7 -7
  56. package/dist/{telegram-general-commands.js → channels/telegram/telegram-general-commands.js} +4 -4
  57. package/dist/{telegram-operational-commands.js → channels/telegram/telegram-operational-commands.js} +5 -5
  58. package/dist/{telegram-output.js → channels/telegram/telegram-output.js} +2 -2
  59. package/dist/{telegram-preference-commands.js → channels/telegram/telegram-preference-commands.js} +3 -3
  60. package/dist/{telegram-queue-commands.js → channels/telegram/telegram-queue-commands.js} +6 -6
  61. package/dist/{telegram-support-command.js → channels/telegram/telegram-support-command.js} +4 -4
  62. package/dist/{telegram-update-commands.js → channels/telegram/telegram-update-commands.js} +5 -5
  63. package/dist/{config-metadata.js → core/config-metadata.js} +8 -0
  64. package/dist/{config.js → core/config.js} +11 -3
  65. package/dist/index.js +27 -23
  66. package/dist/{peer-client.js → peers/peer-client.js} +57 -1
  67. package/dist/peers/peer-discovery-jobs.js +206 -0
  68. package/dist/peers/peer-discovery.js +223 -0
  69. package/dist/peers/peer-health-monitor.js +49 -0
  70. package/dist/{peer-identity.js → peers/peer-identity.js} +50 -1
  71. package/dist/{peer-runtime-service.js → peers/peer-runtime-service.js} +29 -7
  72. package/dist/{peer-server.js → peers/peer-server.js} +23 -6
  73. package/dist/{peer-store.js → peers/peer-store.js} +84 -11
  74. package/dist/{peer-types.js → peers/peer-types.js} +9 -0
  75. package/dist/peers/peer-web-proxy-contract.js +127 -0
  76. package/dist/{metrics.js → runtime/metrics.js} +5 -3
  77. package/dist/{relay-artifact-service.js → runtime/relay-artifact-service.js} +1 -1
  78. package/dist/runtime/relay-auth-service.js +63 -0
  79. package/dist/runtime/relay-dashboard-service.js +139 -0
  80. package/dist/{relay-external-activity-monitor.js → runtime/relay-external-activity-monitor.js} +140 -53
  81. package/dist/runtime/relay-runtime-active-sessions.js +387 -0
  82. package/dist/runtime/relay-runtime-dashboard.js +201 -0
  83. package/dist/runtime/relay-runtime-prompt-queue-artifacts.js +307 -0
  84. package/dist/runtime/relay-runtime-sessions.js +623 -0
  85. package/dist/runtime/relay-runtime-types.js +1 -0
  86. package/dist/runtime/relay-runtime-updates-jobs.js +360 -0
  87. package/dist/runtime/relay-runtime.js +451 -0
  88. package/dist/runtime/runtime-cache.js +117 -0
  89. package/dist/{session-registry.js → state/session-registry.js} +3 -3
  90. package/dist/{operations.js → support/operations.js} +7 -7
  91. package/dist/{support-bundle.js → support/support-bundle.js} +1 -1
  92. package/dist/{web-api-contract.js → web/web-api-contract.js} +17 -3
  93. package/dist/web/web-api-types.js +1 -0
  94. package/dist/{web-dashboard-access-routes.js → web/web-dashboard-access-routes.js} +2 -2
  95. package/dist/{web-dashboard-assets.js → web/web-dashboard-assets.js} +24 -2
  96. package/dist/{web-dashboard-http.js → web/web-dashboard-http.js} +41 -5
  97. package/dist/{web-dashboard-pages.js → web/web-dashboard-pages.js} +37 -10
  98. package/dist/{web-dashboard-peer-routes.js → web/web-dashboard-peer-routes.js} +102 -7
  99. package/dist/web/web-dashboard-security.js +14 -0
  100. package/dist/{web-dashboard-session-routes.js → web/web-dashboard-session-routes.js} +12 -1
  101. package/dist/{web-dashboard.js → web/web-dashboard.js} +132 -48
  102. package/dist/web/web-performance.js +60 -0
  103. package/dist/web/web-rate-limit.js +19 -0
  104. package/dist/{web-state.js → web/web-state.js} +74 -5
  105. package/dist/webui-assets/dashboard.css +171 -10
  106. package/dist/webui-assets/dashboard.js +515 -48
  107. package/dist/webui-assets/favicon.ico +0 -0
  108. package/dist/webui-assets/favicon.png +0 -0
  109. package/dist/webui-assets/logo.png +0 -0
  110. package/package.json +4 -3
  111. package/plugins/nordrelay/scripts/nordrelay.mjs +17 -5
  112. package/{launchd/start.sh → scripts/launchd-start.sh} +1 -1
  113. package/dist/relay-runtime.js +0 -1916
  114. package/dist/runtime-cache.js +0 -57
  115. /package/dist/{user-management-crypto.js → access/user-management-crypto.js} +0 -0
  116. /package/dist/{user-management-normalize.js → access/user-management-normalize.js} +0 -0
  117. /package/dist/{user-management-types.js → access/user-management-types.js} +0 -0
  118. /package/dist/{claude-code-auth.js → agents/claude-code/claude-code-auth.js} +0 -0
  119. /package/dist/{claude-code-launch.js → agents/claude-code/claude-code-launch.js} +0 -0
  120. /package/dist/{claude-code-state.js → agents/claude-code/claude-code-state.js} +0 -0
  121. /package/dist/{codex-auth.js → agents/codex/codex-auth.js} +0 -0
  122. /package/dist/{codex-config.js → agents/codex/codex-config.js} +0 -0
  123. /package/dist/{codex-launch.js → agents/codex/codex-launch.js} +0 -0
  124. /package/dist/{codex-state.js → agents/codex/codex-state.js} +0 -0
  125. /package/dist/{hermes-api.js → agents/hermes/hermes-api.js} +0 -0
  126. /package/dist/{hermes-auth.js → agents/hermes/hermes-auth.js} +0 -0
  127. /package/dist/{hermes-state.js → agents/hermes/hermes-state.js} +0 -0
  128. /package/dist/{openclaw-auth.js → agents/openclaw/openclaw-auth.js} +0 -0
  129. /package/dist/{openclaw-gateway.js → agents/openclaw/openclaw-gateway.js} +0 -0
  130. /package/dist/{openclaw-state.js → agents/openclaw/openclaw-state.js} +0 -0
  131. /package/dist/{pi-auth.js → agents/pi/pi-auth.js} +0 -0
  132. /package/dist/{pi-rpc.js → agents/pi/pi-rpc.js} +0 -0
  133. /package/dist/{pi-state.js → agents/pi/pi-state.js} +0 -0
  134. /package/dist/{agent-adapter.js → agents/shared/agent-adapter.js} +0 -0
  135. /package/dist/{agent.js → agents/shared/agent.js} +0 -0
  136. /package/dist/{artifacts.js → artifacts/artifacts.js} +0 -0
  137. /package/dist/{attachments.js → artifacts/attachments.js} +0 -0
  138. /package/dist/{voice.js → artifacts/voice.js} +0 -0
  139. /package/dist/{discord-rate-limit.js → channels/discord/discord-rate-limit.js} +0 -0
  140. /package/dist/{channel-adapter.js → channels/shared/channel-adapter.js} +0 -0
  141. /package/dist/{relay-runtime-types.js → channels/shared/channel-bridge-state.js} +0 -0
  142. /package/dist/{channel-command-catalog.js → channels/shared/channel-command-catalog.js} +0 -0
  143. /package/dist/{channel-command-core.js → channels/shared/channel-command-core.js} +0 -0
  144. /package/dist/{channel-prompt-engine.js → channels/shared/channel-prompt-engine.js} +0 -0
  145. /package/dist/{channel-runtime.js → channels/shared/channel-runtime.js} +0 -0
  146. /package/dist/{channel-turn-lifecycle.js → channels/shared/channel-turn-lifecycle.js} +0 -0
  147. /package/dist/{slack-rate-limit.js → channels/slack/slack-rate-limit.js} +0 -0
  148. /package/dist/{telegram-command-types.js → channels/telegram/telegram-command-types.js} +0 -0
  149. /package/dist/{telegram-rate-limit.js → channels/telegram/telegram-rate-limit.js} +0 -0
  150. /package/dist/{activity-events.js → core/activity-events.js} +0 -0
  151. /package/dist/{error-messages.js → core/error-messages.js} +0 -0
  152. /package/dist/{format.js → core/format.js} +0 -0
  153. /package/dist/{logger.js → core/logger.js} +0 -0
  154. /package/dist/{redaction.js → core/redaction.js} +0 -0
  155. /package/dist/{settings-service.js → core/settings-service.js} +0 -0
  156. /package/dist/{settings-wizard-test.js → core/settings-wizard-test.js} +0 -0
  157. /package/dist/{workspace-policy.js → core/workspace-policy.js} +0 -0
  158. /package/dist/{peer-auth.js → peers/peer-auth.js} +0 -0
  159. /package/dist/{peer-context.js → peers/peer-context.js} +0 -0
  160. /package/dist/{peer-readiness.js → peers/peer-readiness.js} +0 -0
  161. /package/dist/{relay-queue-service.js → runtime/relay-queue-service.js} +0 -0
  162. /package/dist/{web-api-types.js → runtime/relay-runtime-delegate.js} +0 -0
  163. /package/dist/{relay-runtime-helpers.js → runtime/relay-runtime-helpers.js} +0 -0
  164. /package/dist/{remote-prompt.js → runtime/remote-prompt.js} +0 -0
  165. /package/dist/{bot-preferences.js → state/bot-preferences.js} +0 -0
  166. /package/dist/{job-store.js → state/job-store.js} +0 -0
  167. /package/dist/{persistence.js → state/persistence.js} +0 -0
  168. /package/dist/{prompt-store.js → state/prompt-store.js} +0 -0
  169. /package/dist/{state-backend.js → state/state-backend.js} +0 -0
  170. /package/dist/{zip-writer.js → support/zip-writer.js} +0 -0
  171. /package/dist/{web-dashboard-artifact-routes.js → web/web-dashboard-artifact-routes.js} +0 -0
  172. /package/dist/{web-dashboard-runtime-routes.js → web/web-dashboard-runtime-routes.js} +0 -0
  173. /package/dist/{web-dashboard-ui.js → web/web-dashboard-ui.js} +0 -0
@@ -1,1916 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { ensureOutDir } from "./artifacts.js";
3
- import { buildFileInstructions, outboxPath, stageFile, } from "./attachments.js";
4
- import { CODEX_AGENT_CAPABILITIES, agentLabel, agentReasoningLabel, agentReasoningOptions, isAgentId, } from "./agent.js";
5
- import { getAgentDiagnostics, getExternalSnapshotForSession, } from "./agent-activity.js";
6
- import { listAgentAdapterDescriptors } from "./agent-adapter.js";
7
- import { AgentUpdateManager } from "./agent-updates.js";
8
- import { createAgentSessionService, enabledAgents } from "./agent-factory.js";
9
- import { AuditLogStore } from "./audit-log.js";
10
- import { BotPreferencesStore } from "./bot-preferences.js";
11
- import { ChannelTurnService } from "./channel-turn-service.js";
12
- import { activeSessionSourceForContextKey, ChannelMirrorRegistry } from "./channel-mirror-registry.js";
13
- import { checkAuthStatus, startLogin as startCodexLogin, startLogout as startCodexLogout } from "./codex-auth.js";
14
- import { checkClaudeCodeAuthStatus, startClaudeCodeLogin, startClaudeCodeLogout } from "./claude-code-auth.js";
15
- import { listThreads as listCodexThreads } from "./codex-state.js";
16
- import { friendlyErrorText } from "./error-messages.js";
17
- import { checkHermesAuthStatus, startHermesLogin, startHermesLogout } from "./hermes-auth.js";
18
- import { checkOpenClawAuthStatus } from "./openclaw-auth.js";
19
- import { clearLogFile, getAgentUpdateLogPath, getConnectorHealth, getConnectorLogPath, getPackageVersion, getUpdateLogPath, getVersionChecks, readConnectorState, readFormattedLogTail, spawnConnectorRestart, spawnSelfUpdate } from "./operations.js";
20
- import { checkPiAuthStatus } from "./pi-auth.js";
21
- import { PromptStore, toPromptEnvelope } from "./prompt-store.js";
22
- import { UnifiedJobStore } from "./job-store.js";
23
- import { buildRuntimeMetrics } from "./metrics.js";
24
- import { RelayArtifactService } from "./relay-artifact-service.js";
25
- import { RelayExternalActivityMonitor } from "./relay-external-activity-monitor.js";
26
- import { RelayQueueService } from "./relay-queue-service.js";
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";
29
- import { renderSessionInfoPlain, renderSessionUsageRows } from "./session-format.js";
30
- import { SessionLockStore } from "./session-locks.js";
31
- import { SessionRegistry } from "./session-registry.js";
32
- import { collectSlackDiagnostics } from "./slack-diagnostics.js";
33
- import { getSlackRateLimitMetrics } from "./slack-rate-limit.js";
34
- import { createSupportBundle } from "./support-bundle.js";
35
- import { transcribeAudio } from "./voice.js";
36
- import { WebActivityStore, WebChatStore, } from "./web-state.js";
37
- import { evaluateWorkspacePolicy, filterAllowedWorkspaces } from "./workspace-policy.js";
38
- export const WEB_CONTEXT_KEY = "web:dashboard";
39
- const ACTIVE_CODEX_DISCOVERY_LIMIT = 200;
40
- const ACTIVE_ACTIVITY_TTL_MS = 6 * 60 * 60 * 1000;
41
- const MAX_WEB_SESSION_PAGE_SIZE = 50;
42
- const MAX_CHAT_HISTORY = 250;
43
- export class RelayRuntime {
44
- config;
45
- contextKey;
46
- registry;
47
- promptStore;
48
- chatStore;
49
- activityStore;
50
- auditStore;
51
- lockStore;
52
- agentUpdates;
53
- queueService;
54
- jobStore;
55
- artifactService;
56
- mirrorRegistry;
57
- externalActivityMonitor;
58
- cache = new RuntimeSnapshotCache();
59
- turnService;
60
- subscribers = new Set();
61
- agentUpdateActors = new Map();
62
- agentUpdateStates = new Map();
63
- externalMonitor;
64
- activeSessionsBroadcastTimer = null;
65
- activeSessionsLastBroadcastAt = 0;
66
- draining = false;
67
- currentTurnId = null;
68
- accumulatedText = "";
69
- currentTurnStartedAt = 0;
70
- currentProgress = null;
71
- constructor(config, options = {}) {
72
- this.config = config;
73
- this.contextKey = options.contextKey ?? WEB_CONTEXT_KEY;
74
- this.registry = new SessionRegistry(config, {
75
- fileName: options.registryFileName ?? "web-contexts.json",
76
- sqliteKey: options.registrySqliteKey ?? "web-contexts",
77
- });
78
- this.promptStore = new PromptStore(config.workspace, config.stateBackend);
79
- this.chatStore = new WebChatStore(config.workspace, config.stateBackend, MAX_CHAT_HISTORY);
80
- this.activityStore = new WebActivityStore(config.workspace, config.stateBackend, config.auditMaxEvents);
81
- this.auditStore = new AuditLogStore(config.workspace, config.stateBackend, config.auditMaxEvents);
82
- this.lockStore = new SessionLockStore(config.workspace, config.stateBackend);
83
- this.queueService = new RelayQueueService(this.promptStore, this.contextKey);
84
- this.jobStore = new UnifiedJobStore(config.workspace, config.stateBackend, config.unifiedJobMaxItems);
85
- this.artifactService = new RelayArtifactService(config);
86
- this.mirrorRegistry = new ChannelMirrorRegistry(config, this.promptStore);
87
- this.agentUpdates = new AgentUpdateManager({
88
- onUpdate: (job) => {
89
- this.broadcast({ type: "agent_update", job });
90
- this.recordAgentUpdateLifecycle(job);
91
- },
92
- });
93
- this.externalActivityMonitor = new RelayExternalActivityMonitor({
94
- config,
95
- getSession: () => this.getSession(true),
96
- publicInfo: (session) => this.publicInfo(session),
97
- queueLength: () => this.queueService.length(),
98
- chatStore: this.chatStore,
99
- chatHistory: () => this.chatHistory(),
100
- persistWorkspaceArtifactsForTurn: (workspace, turnId, startedAt) => this.artifactService.persistWorkspaceArtifactsForTurn(workspace, turnId, startedAt),
101
- drainQueue: () => this.drainQueue(),
102
- appendActivity: (input) => this.appendActivity(input),
103
- broadcast: (event) => this.broadcast(event),
104
- broadcastStatus: (message, level) => this.broadcastStatus(message, level),
105
- });
106
- if (config.codexExternalBusyCheckMs > 0) {
107
- this.externalMonitor = setInterval(() => {
108
- void this.externalActivityMonitor.monitorSafe();
109
- }, config.codexExternalBusyCheckMs);
110
- this.externalMonitor.unref?.();
111
- }
112
- this.turnService = new ChannelTurnService({
113
- source: "web",
114
- contextKey: this.contextKey,
115
- chatStore: this.chatStore,
116
- artifactService: this.artifactService,
117
- checkAuth: (info) => this.checkAgentAuth(info),
118
- ensureActiveThread: (session) => this.ensureActiveThread(session),
119
- updateSession: (session) => this.updateSession(session),
120
- appendActivity: (input) => this.appendActivity(input),
121
- appendAudit: (input) => this.appendAudit(input),
122
- broadcast: (event) => this.broadcast(event),
123
- chatHistory: () => this.chatHistory(),
124
- setLastPrompt: (envelope) => this.queueService.setLastPrompt(envelope),
125
- getCurrentProgress: () => this.currentProgress,
126
- setCurrentProgress: (progress) => {
127
- this.currentProgress = progress;
128
- },
129
- setCurrentTurn: (id, startedAt, accumulatedText) => {
130
- this.currentTurnId = id;
131
- if (startedAt !== undefined)
132
- this.currentTurnStartedAt = startedAt;
133
- if (accumulatedText !== undefined)
134
- this.accumulatedText = accumulatedText;
135
- },
136
- getCurrentTurnStartedAt: () => this.currentTurnStartedAt,
137
- getAccumulatedText: () => this.accumulatedText,
138
- setAccumulatedText: (text) => {
139
- this.accumulatedText = text;
140
- },
141
- });
142
- }
143
- subscribe(callback) {
144
- this.subscribers.add(callback);
145
- void this.snapshot().then((data) => callback({ type: "snapshot", data })).catch(() => { });
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(() => { });
148
- callback({ type: "activity_update", events: this.activity({ limit: 50 }) });
149
- return () => this.subscribers.delete(callback);
150
- }
151
- async snapshot() {
152
- const session = await this.getSession(true);
153
- const info = this.publicInfo(session);
154
- return {
155
- session: info,
156
- sessionText: renderSessionInfoPlain(info),
157
- queue: this.queue(),
158
- queuePaused: this.queuePaused(),
159
- processing: session.isProcessing(),
160
- enabledAgents: enabledAgents(this.config),
161
- workspaces: filterAllowedWorkspaces(session.listWorkspaces(), this.config),
162
- };
163
- }
164
- async status() {
165
- const cliOptions = this.cliPathOptions();
166
- const [health, versionChecks, snapshot] = await Promise.all([
167
- getConnectorHealth(cliOptions),
168
- getVersionChecks(cliOptions),
169
- this.snapshot(),
170
- ]);
171
- return {
172
- health,
173
- versionChecks,
174
- snapshot,
175
- };
176
- }
177
- async bootstrapStatus() {
178
- return {
179
- health: {
180
- version: await getPackageVersion(),
181
- state: await readConnectorState(),
182
- },
183
- snapshot: await this.snapshot(),
184
- };
185
- }
186
- async version() {
187
- return this.cached("version", async () => {
188
- const cliOptions = this.cliPathOptions();
189
- const [health, state, versionChecks] = await Promise.all([
190
- getConnectorHealth(cliOptions),
191
- readConnectorState(),
192
- getVersionChecks(cliOptions),
193
- ]);
194
- return {
195
- health,
196
- state,
197
- versionChecks,
198
- };
199
- });
200
- }
201
- updateConnector(actor) {
202
- this.cache.invalidate("version");
203
- const update = spawnSelfUpdate();
204
- this.broadcastStatus(`Update started with ${update.method}. Log: ${update.logPath}`, "warn");
205
- this.appendActivity({
206
- source: "web",
207
- status: "info",
208
- type: "update_started",
209
- threadId: null,
210
- workspace: this.config.workspace,
211
- actor,
212
- detail: `${update.method}: ${update.summary}`,
213
- });
214
- this.appendAudit({
215
- action: "command",
216
- status: "ok",
217
- contextKey: this.contextKey,
218
- actor,
219
- description: "update",
220
- detail: update.summary,
221
- });
222
- return update;
223
- }
224
- agentUpdateJobs() {
225
- return this.agentUpdates.list();
226
- }
227
- startAgentUpdate(agentId, operation = "update", actor) {
228
- this.cache.invalidate("adapterHealth");
229
- this.cache.invalidate("version");
230
- const job = this.agentUpdates.start(agentId, {
231
- piCliPath: this.config.piCliPath,
232
- hermesCliPath: this.config.hermesCliPath,
233
- openClawCliPath: this.config.openClawCliPath,
234
- claudeCodeCliPath: this.config.claudeCodeCliPath,
235
- }, operation);
236
- if (actor) {
237
- this.agentUpdateActors.set(job.id, actor);
238
- }
239
- this.agentUpdateStates.set(job.id, { status: job.status, needsInput: job.needsInput });
240
- this.broadcastStatus(`${job.agentLabel} ${operation} started. Log: ${job.logPath}`, "warn");
241
- this.appendActivity({
242
- source: "web",
243
- status: "info",
244
- type: operation === "install" ? "agent_install_started" : "agent_update_started",
245
- agentId,
246
- threadId: null,
247
- workspace: this.config.workspace,
248
- actor,
249
- detail: `${job.method}: ${job.summary}`,
250
- });
251
- this.appendAudit({
252
- action: "command",
253
- status: "ok",
254
- contextKey: this.contextKey,
255
- agentId,
256
- actor,
257
- description: `${operation} ${agentId}`,
258
- detail: job.summary,
259
- });
260
- return job;
261
- }
262
- agentUpdateLog(id) {
263
- return this.agentUpdates.readLog(id);
264
- }
265
- deleteAgentUpdateLog(id, actor) {
266
- const job = this.agentUpdates.deleteLog(id);
267
- this.appendActivity({
268
- source: "web",
269
- status: "info",
270
- type: "agent_update_log_deleted",
271
- agentId: job.agentId,
272
- threadId: null,
273
- workspace: this.config.workspace,
274
- actor,
275
- detail: job.logPath,
276
- });
277
- this.appendAudit({
278
- action: "command",
279
- status: "ok",
280
- contextKey: this.contextKey,
281
- agentId: job.agentId,
282
- actor,
283
- description: `delete update log ${id}`,
284
- detail: job.logPath,
285
- });
286
- return job;
287
- }
288
- sendAgentUpdateInput(id, input, actor) {
289
- const job = this.agentUpdates.sendInput(id, input);
290
- this.appendActivity({
291
- source: "web",
292
- status: "info",
293
- type: "agent_update_input_sent",
294
- agentId: job.agentId,
295
- threadId: null,
296
- workspace: this.config.workspace,
297
- actor: actor ?? this.agentUpdateActors.get(id),
298
- detail: `Input sent to ${job.agentLabel} ${job.operation}.`,
299
- });
300
- return job;
301
- }
302
- cancelAgentUpdate(id, actor) {
303
- const job = this.agentUpdates.cancel(id);
304
- this.appendActivity({
305
- source: "web",
306
- status: "aborted",
307
- type: "agent_update_cancel_requested",
308
- agentId: job.agentId,
309
- threadId: null,
310
- workspace: this.config.workspace,
311
- actor: actor ?? this.agentUpdateActors.get(id),
312
- detail: `${job.agentLabel} ${job.operation} cancellation requested.`,
313
- });
314
- return job;
315
- }
316
- async diagnostics() {
317
- return this.cached("diagnostics", async () => {
318
- const cliOptions = this.cliPathOptions();
319
- const [health, versionChecks, snapshot, session] = await Promise.all([
320
- getConnectorHealth(cliOptions),
321
- getVersionChecks(cliOptions),
322
- this.snapshot(),
323
- this.getSession(true),
324
- ]);
325
- return {
326
- health,
327
- versionChecks,
328
- snapshot,
329
- runtime: {
330
- stateBackend: this.config.stateBackend,
331
- sourceWorkspace: this.config.workspace,
332
- queuePaused: this.queueService.isPaused(),
333
- externalMirror: this.externalActivityMonitor.snapshot(),
334
- agentDiagnostics: getAgentDiagnostics(session, this.config),
335
- slackDiagnostics: await collectSlackDiagnostics({
336
- config: this.config,
337
- timeoutMs: 2_500,
338
- rateLimit: getSlackRateLimitMetrics(),
339
- }),
340
- },
341
- };
342
- });
343
- }
344
- async adapterHealth() {
345
- return this.cached("adapterHealth", async () => {
346
- const cliOptions = this.cliPathOptions();
347
- const [health, versions] = await Promise.all([
348
- getConnectorHealth(cliOptions),
349
- getVersionChecks(cliOptions),
350
- ]);
351
- return Promise.all(listAgentAdapterDescriptors().map(async (descriptor) => {
352
- const enabled = enabledAgents(this.config).includes(descriptor.id);
353
- const auth = descriptor.capabilities.auth && enabled
354
- ? await this.authStatus(descriptor.id).catch((error) => ({
355
- agentId: descriptor.id,
356
- agentLabel: descriptor.label,
357
- supported: descriptor.capabilities.auth,
358
- authenticated: false,
359
- detail: friendlyErrorText(error),
360
- loginSupported: descriptor.capabilities.login,
361
- logoutSupported: descriptor.capabilities.logout,
362
- }))
363
- : null;
364
- const cli = cliHealthForAgent(descriptor.id, health);
365
- const version = versionCheckForAgent(descriptor.id, versions);
366
- return {
367
- id: descriptor.id,
368
- label: descriptor.label,
369
- enabled,
370
- status: descriptor.status === "available" ? (enabled ? "enabled" : "disabled") : "planned",
371
- auth: {
372
- supported: descriptor.capabilities.auth,
373
- authenticated: auth ? auth.authenticated : null,
374
- method: auth?.method,
375
- detail: auth?.detail,
376
- },
377
- cli,
378
- version: {
379
- installed: version.installedLabel,
380
- latest: version.latestVersion,
381
- status: version.status,
382
- detail: version.detail,
383
- },
384
- capabilities: descriptor.capabilities,
385
- notes: descriptor.notes,
386
- };
387
- }));
388
- });
389
- }
390
- permissions() {
391
- return {
392
- mode: "users",
393
- message: "Access is managed by NordRelay users, groups, Telegram identities, Telegram chat access records, Discord identities, and Discord channel access records.",
394
- };
395
- }
396
- tasks() {
397
- return {
398
- current: this.currentProgress ? { ...this.currentProgress, tools: [...this.currentProgress.tools] } : null,
399
- external: this.externalActivityMonitor.task(),
400
- queue: this.queue(),
401
- queuePaused: this.queuePaused(),
402
- recent: this.activity({ limit: 20 }),
403
- };
404
- }
405
- async jobs() {
406
- const jobs = [];
407
- const current = this.currentProgress;
408
- if (current) {
409
- jobs.push(taskToUnifiedJob("web:current", "web-turn", "Current WebUI turn", current, {
410
- canCancel: current.status === "running",
411
- canRetry: false,
412
- canReadLog: false,
413
- }));
414
- }
415
- const external = this.externalActivityMonitor.task();
416
- if (external) {
417
- jobs.push(taskToUnifiedJob(`external:${external.agentId ?? "agent"}:${external.threadId ?? "pending"}`, "external-turn", "External CLI turn", external, {
418
- canCancel: false,
419
- canRetry: false,
420
- canReadLog: false,
421
- }));
422
- }
423
- for (const item of this.queueService.rawList()) {
424
- const createdAt = new Date(item.createdAt).toISOString();
425
- jobs.push({
426
- id: `queue:${item.id}`,
427
- kind: "queued-prompt",
428
- title: `Queued prompt ${item.id}`,
429
- status: "queued",
430
- source: "web",
431
- threadId: null,
432
- workspace: this.config.workspace,
433
- owner: item.activityActor,
434
- startedAt: createdAt,
435
- updatedAt: createdAt,
436
- summary: item.description,
437
- queueId: item.id,
438
- logTail: item.lastError,
439
- canCancel: true,
440
- canRetry: true,
441
- canReadLog: true,
442
- });
443
- }
444
- for (const job of this.agentUpdates.list()) {
445
- jobs.push({
446
- id: `agent-update:${job.id}`,
447
- kind: "agent-update",
448
- title: `${job.agentLabel} ${job.operation}`,
449
- status: agentUpdateStatusToUnified(job.status),
450
- source: "web",
451
- agentId: job.agentId,
452
- agentLabel: job.agentLabel,
453
- threadId: null,
454
- workspace: this.config.workspace,
455
- owner: this.agentUpdateActors.get(job.id),
456
- startedAt: job.startedAt,
457
- updatedAt: job.updatedAt,
458
- finishedAt: job.finishedAt,
459
- summary: job.error || job.summary,
460
- logPath: job.logPath,
461
- logTail: job.outputTail,
462
- updateJobId: job.id,
463
- canCancel: job.status === "running",
464
- canRetry: job.status !== "running",
465
- canReadLog: true,
466
- });
467
- }
468
- for (const event of this.activity({ limit: 100 })) {
469
- if (event.type === "diagnostics_bundle_exported") {
470
- jobs.push(activityToUnifiedJob(event, "support-bundle", "Diagnostics support bundle", {
471
- canCancel: false,
472
- canRetry: true,
473
- canReadLog: Boolean(event.detail),
474
- }));
475
- }
476
- else if (event.type === "update_started") {
477
- jobs.push(activityToUnifiedJob(event, "connector-update", "NordRelay update", {
478
- canCancel: false,
479
- canRetry: true,
480
- canReadLog: Boolean(event.detail),
481
- }));
482
- }
483
- else if (event.category === "prompt" && event.type.startsWith("prompt_")) {
484
- jobs.push(promptActivityToUnifiedJob(event));
485
- }
486
- }
487
- const liveJobs = dedupeJobs(jobs);
488
- const storedJobs = this.jobStore.upsertMany(liveJobs);
489
- return {
490
- jobs: dedupeJobs([...liveJobs, ...storedJobs]).sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt)),
491
- updatedAt: new Date().toISOString(),
492
- };
493
- }
494
- async jobLog(id) {
495
- if (id.startsWith("agent-update:")) {
496
- const updateId = id.slice("agent-update:".length);
497
- const log = this.agentUpdates.readLog(updateId);
498
- return { job: (await this.jobs()).jobs.find((job) => job.id === id) ?? null, plain: log.plain };
499
- }
500
- if (id.startsWith("queue:")) {
501
- const queueId = id.slice("queue:".length);
502
- const item = this.queueService.rawList().find((candidate) => candidate.id === queueId);
503
- return {
504
- job: (await this.jobs()).jobs.find((job) => job.id === id) ?? null,
505
- plain: item ? [
506
- `Queued prompt: ${item.id}`,
507
- `Created: ${new Date(item.createdAt).toISOString()}`,
508
- `Attempts: ${item.attempts ?? 0}`,
509
- `Description: ${item.description}`,
510
- item.lastError ? `Last error: ${item.lastError}` : "",
511
- ].filter(Boolean).join("\n") : "Queued prompt not found.",
512
- };
513
- }
514
- const job = (await this.jobs()).jobs.find((candidate) => candidate.id === id) ?? null;
515
- return { job, plain: job?.logTail || job?.logPath || job?.summary || this.jobStore.get(id)?.summary || "No log available for this job." };
516
- }
517
- async jobAction(id, action, actor) {
518
- if (id === "web:current" && action === "cancel") {
519
- await this.abort(actor);
520
- return this.jobs();
521
- }
522
- if (id.startsWith("queue:")) {
523
- const queueId = id.slice("queue:".length);
524
- this.queueService.apply(action === "cancel" ? "cancel" : "run", queueId);
525
- this.jobStore.patch(id, {
526
- status: action === "cancel" ? "aborted" : "queued",
527
- summary: action === "cancel" ? `Cancelled queued prompt ${queueId}.` : `Queued prompt ${queueId} moved to the front.`,
528
- canCancel: action !== "cancel",
529
- canRetry: action === "cancel",
530
- finishedAt: action === "cancel" ? new Date().toISOString() : undefined,
531
- });
532
- this.broadcast({ type: "queue_update", queue: this.queue(), paused: this.queuePaused() });
533
- this.appendActivity({
534
- source: "web",
535
- status: action === "cancel" ? "aborted" : "queued",
536
- type: action === "cancel" ? "job_cancelled" : "job_retried",
537
- threadId: null,
538
- workspace: this.config.workspace,
539
- actor,
540
- detail: `queue:${queueId}`,
541
- });
542
- if (action === "retry") {
543
- void this.drainQueue();
544
- }
545
- return this.jobs();
546
- }
547
- if (id.startsWith("agent-update:")) {
548
- const updateId = id.slice("agent-update:".length);
549
- const current = this.agentUpdates.get(updateId);
550
- if (!current) {
551
- throw new Error("Unknown agent update job.");
552
- }
553
- if (action === "cancel") {
554
- this.cancelAgentUpdate(updateId, actor);
555
- }
556
- else {
557
- this.startAgentUpdate(current.agentId, current.operation, actor);
558
- }
559
- return this.jobs();
560
- }
561
- if (id.startsWith("support-bundle:") && action === "retry") {
562
- await this.supportBundle(actor);
563
- return this.jobs();
564
- }
565
- if (id.startsWith("connector-update:") && action === "retry") {
566
- this.updateConnector(actor);
567
- return this.jobs();
568
- }
569
- throw new Error(`Unsupported job action: ${action} ${id}`);
570
- }
571
- async activeSessions() {
572
- const sessions = new Map();
573
- const knownContexts = this.listKnownContextMetadata();
574
- const preferences = new BotPreferencesStore(this.config.workspace, this.config.stateBackend);
575
- const addActiveSession = (session) => {
576
- const key = this.activeSessionKey(session);
577
- const existing = sessions.get(key);
578
- sessions.set(key, this.preferredActiveSession(existing, session));
579
- };
580
- if (this.currentProgress?.status === "running") {
581
- addActiveSession({
582
- ...this.currentProgress,
583
- contextKey: this.contextKey,
584
- sourceContextKey: this.contextKey,
585
- source: "web",
586
- status: "running",
587
- queueLength: this.queueService.length(),
588
- queuePaused: this.queueService.isPaused(),
589
- });
590
- }
591
- for (const active of this.discoverRunningConnectorSessions()) {
592
- addActiveSession(active);
593
- }
594
- for (const active of this.discoverActiveCodexSessions(knownContexts, preferences)) {
595
- addActiveSession(active);
596
- }
597
- for (const meta of knownContexts) {
598
- if (meta.contextKey === this.contextKey && this.currentProgress?.status === "running") {
599
- continue;
600
- }
601
- const active = this.externalActiveSession(meta, knownContexts, preferences);
602
- if (active) {
603
- addActiveSession(active);
604
- }
605
- }
606
- return {
607
- sessions: [...sessions.values()].sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt)),
608
- updatedAt: new Date().toISOString(),
609
- };
610
- }
611
- async metrics() {
612
- const [active, jobs] = await Promise.all([
613
- this.activeSessions(),
614
- this.jobs(),
615
- ]);
616
- return buildRuntimeMetrics({
617
- queueLength: this.queueService.length(),
618
- queuePaused: this.queueService.isPaused(),
619
- activeTurnCount: active.sessions.length,
620
- jobs: jobs.jobs,
621
- activity: this.activity({ limit: 500 }),
622
- });
623
- }
624
- audit(options = 50) {
625
- return this.auditStore.list(options);
626
- }
627
- async supportBundle(actor) {
628
- const bundle = await createSupportBundle({
629
- config: this.config,
630
- diagnostics: await this.diagnostics(),
631
- adapterHealth: await this.adapterHealth(),
632
- auditEvents: this.auditStore.list(100),
633
- agentUpdateJobs: this.agentUpdates.list(),
634
- source: "web",
635
- });
636
- this.appendActivity({
637
- source: "web",
638
- status: "info",
639
- type: "diagnostics_bundle_exported",
640
- threadId: null,
641
- workspace: this.config.workspace,
642
- actor,
643
- detail: bundle.path,
644
- });
645
- this.appendAudit({
646
- action: "command",
647
- status: "ok",
648
- contextKey: this.contextKey,
649
- actor,
650
- description: "export diagnostics bundle",
651
- detail: bundle.path,
652
- });
653
- return bundle;
654
- }
655
- locks() {
656
- return this.lockStore.list();
657
- }
658
- lockWebSession(ownerName = "Web dashboard", actor) {
659
- const label = ownerName || actor?.label || "Web dashboard";
660
- const lock = this.lockStore.set(this.contextKey, {
661
- userId: actor?.id ?? "web",
662
- label,
663
- channel: "web",
664
- }, this.config.sessionLockTtlMs);
665
- this.appendActivity({
666
- source: "web",
667
- status: "info",
668
- type: "lock_created",
669
- threadId: null,
670
- workspace: this.config.workspace,
671
- actor,
672
- detail: `locked by ${label}`,
673
- });
674
- this.appendAudit({
675
- action: "lock_updated",
676
- status: "ok",
677
- contextKey: this.contextKey,
678
- actor,
679
- description: "lock",
680
- detail: `locked by ${label}`,
681
- });
682
- return lock;
683
- }
684
- unlockWebSession(actor) {
685
- const removed = this.lockStore.clear(this.contextKey);
686
- this.appendActivity({
687
- source: "web",
688
- status: "info",
689
- type: "lock_removed",
690
- threadId: null,
691
- workspace: this.config.workspace,
692
- actor,
693
- detail: removed ? "unlocked" : "no lock",
694
- });
695
- this.appendAudit({
696
- action: "lock_updated",
697
- status: "ok",
698
- contextKey: this.contextKey,
699
- actor,
700
- description: "unlock",
701
- detail: removed ? "unlocked" : "no lock",
702
- });
703
- return { removed, locks: this.locks() };
704
- }
705
- async controlOptions(agentId) {
706
- const { session, dispose } = await this.getControlSession(agentId);
707
- try {
708
- const info = this.publicInfo(session);
709
- const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
710
- if (capabilities.modelSelection) {
711
- await session.refreshModels().catch((error) => {
712
- console.warn(`Failed to refresh ${agentLabel(info.agentId)} models: ${error instanceof Error ? error.message : String(error)}`);
713
- });
714
- }
715
- return {
716
- models: capabilities.modelSelection ? session.listModels() : [],
717
- reasoningLabel: agentReasoningLabel(info.agentId),
718
- reasoningOptions: agentReasoningOptions(info.agentId),
719
- launchProfiles: capabilities.launchProfiles ? session.listLaunchProfiles() : [],
720
- workspaces: filterAllowedWorkspaces(session.listWorkspaces(), this.config),
721
- capabilities,
722
- };
723
- }
724
- finally {
725
- if (dispose) {
726
- session.dispose();
727
- }
728
- }
729
- }
730
- async authStatus(agentId) {
731
- const { session, dispose } = await this.getControlSession(agentId);
732
- try {
733
- const info = this.publicInfo(session);
734
- const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
735
- if (!capabilities.auth) {
736
- return {
737
- agentId: info.agentId,
738
- agentLabel: info.agentLabel,
739
- supported: false,
740
- authenticated: null,
741
- detail: `${info.agentLabel} authentication is managed outside NordRelay.`,
742
- loginSupported: false,
743
- logoutSupported: false,
744
- hostLoginCommand: hostLoginCommand(info, this.config),
745
- hostLogoutCommand: hostLogoutCommand(info, this.config),
746
- };
747
- }
748
- const status = await this.checkAgentAuth(info);
749
- return {
750
- agentId: info.agentId,
751
- agentLabel: info.agentLabel,
752
- supported: true,
753
- authenticated: status.authenticated,
754
- method: status.method,
755
- detail: status.detail,
756
- loginSupported: capabilities.login,
757
- logoutSupported: capabilities.logout,
758
- hostLoginCommand: hostLoginCommand(info, this.config),
759
- hostLogoutCommand: hostLogoutCommand(info, this.config),
760
- };
761
- }
762
- finally {
763
- if (dispose) {
764
- session.dispose();
765
- }
766
- }
767
- }
768
- async login(agentId, actor) {
769
- const { session, dispose } = await this.getControlSession(agentId);
770
- try {
771
- const info = this.publicInfo(session);
772
- const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
773
- if (!capabilities.login) {
774
- return {
775
- ...(await this.authStatus(info.agentId)),
776
- result: {
777
- success: false,
778
- message: `${info.agentLabel} login is not managed by NordRelay. Run ${hostLoginCommand(info, this.config)} on the host.`,
779
- },
780
- };
781
- }
782
- if (!this.config.enableTelegramLogin) {
783
- return {
784
- ...(await this.authStatus(info.agentId)),
785
- result: {
786
- success: false,
787
- message: `Remote login is disabled. Run ${hostLoginCommand(info, this.config)} on the host.`,
788
- },
789
- };
790
- }
791
- const result = await this.startAgentLogin(info);
792
- this.appendActivity({
793
- source: "web",
794
- status: result.success ? "info" : "failed",
795
- type: result.success ? "login_started" : "login_failed",
796
- threadId: info.threadId,
797
- workspace: info.workspace,
798
- agentId: info.agentId,
799
- actor,
800
- detail: result.message,
801
- });
802
- this.appendAudit({
803
- action: "command",
804
- status: result.success ? "ok" : "failed",
805
- contextKey: this.contextKey,
806
- agentId: info.agentId,
807
- threadId: info.threadId,
808
- workspace: info.workspace,
809
- actor,
810
- description: "login",
811
- detail: result.message,
812
- });
813
- return { ...(await this.authStatus(info.agentId)), result };
814
- }
815
- finally {
816
- if (dispose) {
817
- session.dispose();
818
- }
819
- }
820
- }
821
- async logout(agentId, actor) {
822
- const { session, dispose } = await this.getControlSession(agentId);
823
- try {
824
- const info = this.publicInfo(session);
825
- const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
826
- if (!capabilities.logout) {
827
- return {
828
- ...(await this.authStatus(info.agentId)),
829
- result: {
830
- success: false,
831
- message: `${info.agentLabel} logout is not managed by NordRelay. Run ${hostLogoutCommand(info, this.config)} on the host.`,
832
- },
833
- };
834
- }
835
- if (!this.config.enableTelegramLogin) {
836
- return {
837
- ...(await this.authStatus(info.agentId)),
838
- result: {
839
- success: false,
840
- message: `Remote auth management is disabled. Run ${hostLogoutCommand(info, this.config)} on the host.`,
841
- },
842
- };
843
- }
844
- const current = await this.checkAgentAuth(info);
845
- if (current.method === "api-key") {
846
- return {
847
- ...(await this.authStatus(info.agentId)),
848
- result: {
849
- success: false,
850
- message: "Cannot logout while API-key authentication is configured. Remove the API key from .env to use CLI auth.",
851
- },
852
- };
853
- }
854
- const result = await this.startAgentLogout(info);
855
- this.appendActivity({
856
- source: "web",
857
- status: result.success ? "info" : "failed",
858
- type: result.success ? "logout_completed" : "logout_failed",
859
- threadId: info.threadId,
860
- workspace: info.workspace,
861
- agentId: info.agentId,
862
- actor,
863
- detail: result.message,
864
- });
865
- this.appendAudit({
866
- action: "command",
867
- status: result.success ? "ok" : "failed",
868
- contextKey: this.contextKey,
869
- agentId: info.agentId,
870
- threadId: info.threadId,
871
- workspace: info.workspace,
872
- actor,
873
- description: "logout",
874
- detail: result.message,
875
- });
876
- return { ...(await this.authStatus(info.agentId)), result };
877
- }
878
- finally {
879
- if (dispose) {
880
- session.dispose();
881
- }
882
- }
883
- }
884
- async chatHistory(limit = 200) {
885
- const session = await this.getSession(true);
886
- return this.chatStore.list(this.publicInfo(session).threadId, limit);
887
- }
888
- async sessionDetail(threadId) {
889
- const session = await this.getSession(true);
890
- const record = session.getSessionRecord(threadId);
891
- const active = this.publicInfo(session);
892
- return {
893
- record,
894
- active,
895
- usageRows: active.threadId === threadId ? renderSessionUsageRows(active) : [],
896
- messages: this.chatStore.list(threadId, 100),
897
- activity: this.activity({ limit: 100 }).filter((event) => event.threadId === threadId),
898
- };
899
- }
900
- async clearChatHistory(actor) {
901
- const session = await this.getSession(true);
902
- const info = this.publicInfo(session);
903
- const removed = this.chatStore.clear(info.threadId);
904
- const messages = await this.chatHistory();
905
- this.broadcast({ type: "chat_history", messages });
906
- this.appendActivity({
907
- source: "web",
908
- status: "info",
909
- type: "chat_history_cleared",
910
- threadId: info.threadId,
911
- workspace: info.workspace,
912
- agentId: info.agentId,
913
- actor,
914
- detail: `${removed} messages removed.`,
915
- });
916
- return { removed, messages };
917
- }
918
- activity(options = {}) {
919
- const currentInfo = this.registry.get(this.contextKey)?.getInfo();
920
- return this.activityStore.list(options).map((event) => this.enrichActivityEvent(event, currentInfo));
921
- }
922
- async retry(actor) {
923
- const cached = this.queueService.getLastPrompt();
924
- if (!cached) {
925
- throw new Error("Nothing to retry. Send a message first.");
926
- }
927
- this.appendAudit({
928
- action: "command",
929
- status: "ok",
930
- contextKey: this.contextKey,
931
- actor,
932
- description: "retry",
933
- detail: cached.description,
934
- });
935
- return this.sendEnvelope({ ...cached, activityActor: cached.activityActor ?? actor }, actor);
936
- }
937
- async sync(actor) {
938
- const session = await this.getSession(true);
939
- const info = this.publicInfo(session);
940
- if (!(info.capabilities ?? CODEX_AGENT_CAPABILITIES).externalActivity) {
941
- throw new Error(`${info.agentLabel} has no external state watcher to sync.`);
942
- }
943
- const result = session.syncFromAgentState({ reattach: true });
944
- if (result.changed) {
945
- this.updateSession(session);
946
- }
947
- this.appendActivity({
948
- source: "web",
949
- status: "info",
950
- type: "session_sync",
951
- threadId: result.info.threadId,
952
- workspace: result.info.workspace,
953
- agentId: result.info.agentId,
954
- actor,
955
- detail: result.changedFields.length > 0 ? result.changedFields.join(", ") : "already in sync",
956
- });
957
- this.appendAudit({
958
- action: "command",
959
- status: "ok",
960
- contextKey: this.contextKey,
961
- agentId: result.info.agentId,
962
- threadId: result.info.threadId,
963
- workspace: result.info.workspace,
964
- actor,
965
- description: "sync",
966
- detail: result.changedFields.join(", ") || "none",
967
- });
968
- return result;
969
- }
970
- async listSessions(limit = 80, query = "", agentId) {
971
- const { session, dispose } = await this.getControlSession(agentId);
972
- try {
973
- return this.filteredSessions(session, query, Math.max(1, limit * 3)).slice(0, limit);
974
- }
975
- finally {
976
- if (dispose) {
977
- session.dispose();
978
- }
979
- }
980
- }
981
- async listSessionsPage(page = 1, pageSize = MAX_WEB_SESSION_PAGE_SIZE, query = "", agentId) {
982
- const { session, dispose } = await this.getControlSession(agentId);
983
- try {
984
- const effectivePage = Math.max(1, Math.floor(page));
985
- const effectivePageSize = Math.min(MAX_WEB_SESSION_PAGE_SIZE, Math.max(1, Math.floor(pageSize)));
986
- const offset = (effectivePage - 1) * effectivePageSize;
987
- const requested = Math.min(5_000, Math.max(100, (offset + effectivePageSize + 1) * 3));
988
- const records = this.filteredSessions(session, query, requested);
989
- return {
990
- sessions: records.slice(offset, offset + effectivePageSize),
991
- pagination: {
992
- page: effectivePage,
993
- pageSize: effectivePageSize,
994
- hasPrevious: effectivePage > 1,
995
- hasNext: records.length > offset + effectivePageSize,
996
- },
997
- };
998
- }
999
- finally {
1000
- if (dispose) {
1001
- session.dispose();
1002
- }
1003
- }
1004
- }
1005
- filteredSessions(session, query, limit) {
1006
- const normalized = query.trim().toLowerCase();
1007
- return session.listAllSessions(limit)
1008
- .filter((record) => evaluateWorkspacePolicy(record.cwd, this.config).allowed)
1009
- .filter((record) => {
1010
- if (!normalized) {
1011
- return true;
1012
- }
1013
- return [
1014
- record.id,
1015
- record.title,
1016
- record.cwd,
1017
- record.model,
1018
- record.reasoningEffort,
1019
- record.firstUserMessage,
1020
- ].some((value) => value?.toLowerCase().includes(normalized));
1021
- });
1022
- }
1023
- async listModels() {
1024
- const session = await this.getSession(true);
1025
- const info = this.publicInfo(session);
1026
- await session.refreshModels({ force: true }).catch((error) => {
1027
- console.warn(`Failed to refresh ${agentLabel(info.agentId)} models: ${error instanceof Error ? error.message : String(error)}`);
1028
- });
1029
- return session.listModels();
1030
- }
1031
- async setAgent(agentId, actor) {
1032
- if (!enabledAgents(this.config).includes(agentId)) {
1033
- throw new Error(`Agent is not enabled: ${agentId}`);
1034
- }
1035
- const session = await this.registry.switchAgent(this.contextKey, agentId);
1036
- this.updateSession(session);
1037
- const info = this.publicInfo(session);
1038
- this.appendActivity({
1039
- source: "web",
1040
- status: "info",
1041
- type: "agent_switch",
1042
- threadId: info.threadId,
1043
- workspace: info.workspace,
1044
- agentId: info.agentId,
1045
- actor,
1046
- detail: `Dashboard switched agent to ${info.agentLabel}.`,
1047
- });
1048
- return this.publicInfo(session);
1049
- }
1050
- async newSession(options = {}, actor) {
1051
- const session = options.agentId ? await this.registry.switchAgent(this.contextKey, options.agentId) : await this.getSession(true);
1052
- this.ensureIdle(session);
1053
- if (options.reasoningEffort) {
1054
- const reasoningOptions = agentReasoningOptions(session.getInfo().agentId);
1055
- if (!reasoningOptions.includes(options.reasoningEffort)) {
1056
- throw new Error(`Invalid ${agentReasoningLabel(session.getInfo().agentId)} value: ${options.reasoningEffort}`);
1057
- }
1058
- session.setReasoningEffort(options.reasoningEffort);
1059
- }
1060
- if (options.launchProfileId && (session.getInfo().capabilities ?? CODEX_AGENT_CAPABILITIES).launchProfiles) {
1061
- session.setLaunchProfile(options.launchProfileId);
1062
- }
1063
- if (typeof options.fastMode === "boolean" && (session.getInfo().capabilities ?? CODEX_AGENT_CAPABILITIES).fastMode) {
1064
- session.setFastMode(options.fastMode);
1065
- }
1066
- const info = await session.newThread(options.workspace, options.model);
1067
- this.updateSession(session);
1068
- this.appendActivity({
1069
- source: "web",
1070
- status: "info",
1071
- type: "session_new",
1072
- threadId: info.threadId,
1073
- workspace: info.workspace,
1074
- agentId: info.agentId,
1075
- actor,
1076
- detail: "New dashboard session created.",
1077
- });
1078
- return this.publicInfo(session);
1079
- }
1080
- async switchSession(threadId, actor) {
1081
- const session = await this.getSession(true);
1082
- this.ensureIdle(session);
1083
- const info = await session.switchSession(threadId);
1084
- this.updateSession(session);
1085
- this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
1086
- this.appendActivity({
1087
- source: "web",
1088
- status: "info",
1089
- type: "session_switch",
1090
- threadId: info.threadId,
1091
- workspace: info.workspace,
1092
- agentId: info.agentId,
1093
- actor,
1094
- detail: "Dashboard switched session.",
1095
- });
1096
- return this.publicInfo(session);
1097
- }
1098
- async attachSession(threadId, actor) {
1099
- return this.switchSession(threadId, actor);
1100
- }
1101
- async setModel(model, actor) {
1102
- const session = await this.getSession(true);
1103
- this.ensureIdle(session);
1104
- await session.setModelForCurrentSession(model);
1105
- this.updateSession(session);
1106
- const info = this.publicInfo(session);
1107
- this.appendActivity({ source: "web", status: "info", type: "model_changed", threadId: info.threadId, workspace: info.workspace, agentId: info.agentId, actor, detail: model });
1108
- return info;
1109
- }
1110
- async setReasoningEffort(effort, actor) {
1111
- const session = await this.getSession(true);
1112
- this.ensureIdle(session);
1113
- const options = agentReasoningOptions(session.getInfo().agentId);
1114
- if (!options.includes(effort)) {
1115
- throw new Error(`Invalid ${agentReasoningLabel(session.getInfo().agentId)} value: ${effort}`);
1116
- }
1117
- await session.setReasoningEffortForCurrentSession(effort);
1118
- this.updateSession(session);
1119
- const info = this.publicInfo(session);
1120
- this.appendActivity({ source: "web", status: "info", type: "reasoning_changed", threadId: info.threadId, workspace: info.workspace, agentId: info.agentId, actor, detail: effort });
1121
- return info;
1122
- }
1123
- async setFastMode(enabled, actor) {
1124
- const session = await this.getSession(true);
1125
- this.ensureIdle(session);
1126
- if (!(session.getInfo().capabilities ?? CODEX_AGENT_CAPABILITIES).fastMode) {
1127
- throw new Error(`Fast mode is not supported for ${agentLabel(session.getInfo().agentId)}.`);
1128
- }
1129
- session.setFastMode(enabled);
1130
- this.updateSession(session);
1131
- const info = this.publicInfo(session);
1132
- this.appendActivity({ source: "web", status: "info", type: "fast_mode_changed", threadId: info.threadId, workspace: info.workspace, agentId: info.agentId, actor, detail: enabled ? "on" : "off" });
1133
- return info;
1134
- }
1135
- async setLaunchProfile(profileId, actor) {
1136
- const session = await this.getSession(true);
1137
- this.ensureIdle(session);
1138
- session.setLaunchProfile(profileId);
1139
- this.updateSession(session);
1140
- const info = this.publicInfo(session);
1141
- this.appendActivity({ source: "web", status: "info", type: "launch_profile_changed", threadId: info.threadId, workspace: info.workspace, agentId: info.agentId, actor, detail: info.launchProfileLabel ?? profileId });
1142
- return info;
1143
- }
1144
- async handback(actor) {
1145
- const session = await this.getSession(true);
1146
- this.ensureIdle(session);
1147
- const result = session.handback();
1148
- this.updateSession(session);
1149
- const info = this.publicInfo(session);
1150
- this.appendActivity({ source: "web", status: "info", type: "handback", threadId: result.threadId, workspace: result.workspace, agentId: info.agentId, actor, detail: result.command ?? "Thread handed back." });
1151
- return result;
1152
- }
1153
- async abort(actor) {
1154
- const session = await this.getSession(true);
1155
- const snapshot = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
1156
- if (snapshot?.activity.active && !session.isProcessing()) {
1157
- this.broadcast({
1158
- type: "status",
1159
- level: "warn",
1160
- message: `Cannot abort the external ${snapshot.agentLabel} CLI task from NordRelay. Stop it in the terminal where it is running.`,
1161
- at: new Date().toISOString(),
1162
- });
1163
- const info = this.publicInfo(session);
1164
- this.appendActivity({ source: "web", status: "aborted", type: "prompt_abort_rejected", threadId: info.threadId, workspace: info.workspace, agentId: info.agentId, actor, detail: `External ${snapshot.agentLabel} CLI task is active.` });
1165
- return;
1166
- }
1167
- await session.abort();
1168
- const info = this.publicInfo(session);
1169
- this.appendActivity({ source: "web", status: "aborted", type: "prompt_aborted", threadId: info.threadId, workspace: info.workspace, agentId: info.agentId, actor, detail: "Current operation aborted." });
1170
- this.broadcast({ type: "status", level: "warn", message: "Current operation aborted.", at: new Date().toISOString() });
1171
- }
1172
- async sendPrompt(text, actor) {
1173
- const trimmed = text.trim();
1174
- if (!trimmed) {
1175
- throw new Error("Prompt is empty.");
1176
- }
1177
- return this.sendEnvelope({ ...toPromptEnvelope(trimmed), activityActor: actor }, actor);
1178
- }
1179
- async sendUploadPrompt(options, actor) {
1180
- const text = options.text?.trim() ?? "";
1181
- const files = options.files.filter((file) => file.data.byteLength > 0);
1182
- if (!text && files.length === 0) {
1183
- throw new Error("Prompt is empty.");
1184
- }
1185
- const session = await this.getSession(false);
1186
- const workspace = session.getInfo().workspace;
1187
- const turnId = randomUUID().slice(0, 12);
1188
- const outDir = outboxPath(workspace, turnId);
1189
- await ensureOutDir(outDir);
1190
- const stagedFiles = [];
1191
- const imagePaths = [];
1192
- const transcriptParts = [];
1193
- for (const [index, file] of files.entries()) {
1194
- const mimeType = normalizeMimeType(file.mimeType, file.name);
1195
- const staged = await stageFile(file.data, file.name || `upload-${index + 1}`, mimeType, {
1196
- workspace,
1197
- turnId,
1198
- maxFileSize: this.config.maxFileSize,
1199
- });
1200
- stagedFiles.push(staged);
1201
- if (mimeType.startsWith("image/")) {
1202
- imagePaths.push(staged.localPath);
1203
- }
1204
- if (mimeType.startsWith("audio/")) {
1205
- const result = await transcribeAudio(staged.localPath, {
1206
- preferredBackend: this.config.voicePreferredBackend === "auto"
1207
- ? undefined
1208
- : this.config.voicePreferredBackend,
1209
- language: this.config.voiceDefaultLanguage,
1210
- });
1211
- const transcript = result.text.trim();
1212
- if (transcript) {
1213
- transcriptParts.push(`Audio transcript (${staged.safeName}, via ${result.backend}):\n${transcript}`);
1214
- this.appendActivity({
1215
- source: "web",
1216
- status: "completed",
1217
- type: "voice_transcribed",
1218
- threadId: session.getInfo().threadId,
1219
- workspace,
1220
- agentId: session.getInfo().agentId,
1221
- actor,
1222
- detail: `${staged.safeName} via ${result.backend}`,
1223
- durationMs: result.durationMs,
1224
- });
1225
- }
1226
- }
1227
- }
1228
- if (stagedFiles.length > 0) {
1229
- this.appendActivity({
1230
- source: "web",
1231
- status: "info",
1232
- type: "attachment_staged",
1233
- threadId: session.getInfo().threadId,
1234
- workspace,
1235
- agentId: session.getInfo().agentId,
1236
- actor,
1237
- detail: `${stagedFiles.length} file(s): ${stagedFiles.map((file) => file.safeName).join(", ")}`,
1238
- });
1239
- }
1240
- const audioOnly = stagedFiles.length > 0 && stagedFiles.every((file) => file.mimeType.startsWith("audio/"));
1241
- if (this.config.voiceTranscribeOnly && audioOnly && !text) {
1242
- return {
1243
- queued: false,
1244
- transcript: transcriptParts.join("\n\n"),
1245
- transcribeOnly: true,
1246
- files: uploadFileDtos(stagedFiles),
1247
- };
1248
- }
1249
- const promptInput = {};
1250
- const textParts = [text, ...transcriptParts].filter(Boolean);
1251
- if (textParts.length > 0) {
1252
- promptInput.text = textParts.join("\n\n");
1253
- }
1254
- if (imagePaths.length > 0) {
1255
- promptInput.imagePaths = imagePaths;
1256
- }
1257
- if (stagedFiles.length > 0) {
1258
- promptInput.stagedFileInstructions = buildFileInstructions(stagedFiles, outDir);
1259
- }
1260
- const result = await this.sendEnvelope({ ...toPromptEnvelope(promptInput, outDir), activityActor: actor }, actor);
1261
- return {
1262
- ...result,
1263
- transcript: transcriptParts.join("\n\n") || undefined,
1264
- files: uploadFileDtos(stagedFiles),
1265
- };
1266
- }
1267
- async sendEnvelope(envelope, actor) {
1268
- const activityActor = envelope.activityActor ?? actor;
1269
- const session = await this.getSession(false);
1270
- const external = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
1271
- if (session.isProcessing() || external?.activity.active) {
1272
- const queued = this.queueService.enqueue(envelope);
1273
- const info = this.publicInfo(session);
1274
- this.appendActivity({
1275
- source: "web",
1276
- status: "queued",
1277
- type: "prompt_queued",
1278
- threadId: info.threadId,
1279
- workspace: info.workspace,
1280
- agentId: info.agentId,
1281
- actor: activityActor,
1282
- prompt: envelope.description,
1283
- detail: external?.activity.active
1284
- ? `Queued because ${external.agentLabel} CLI is still processing another task.`
1285
- : `Queued at position ${this.queueService.length()}.`,
1286
- });
1287
- this.appendAudit({
1288
- action: "prompt_queued",
1289
- status: "ok",
1290
- contextKey: this.contextKey,
1291
- agentId: info.agentId,
1292
- threadId: info.threadId,
1293
- workspace: info.workspace,
1294
- promptId: queued.id,
1295
- actor: activityActor,
1296
- description: envelope.description,
1297
- });
1298
- if (external?.activity.active) {
1299
- this.broadcastStatus(`Waiting for ${external.agentLabel} CLI task... ${this.queueService.length()} queued.`, "info");
1300
- }
1301
- this.broadcastQueue();
1302
- return { queued: true, queueId: queued.id };
1303
- }
1304
- void this.runPrompt(session, { ...envelope, activityActor }).catch((error) => {
1305
- this.broadcast({ type: "turn_error", id: this.currentTurnId ?? "turn", error: friendlyErrorText(error), at: new Date().toISOString() });
1306
- });
1307
- return { queued: false };
1308
- }
1309
- queue() {
1310
- return this.queueService.list();
1311
- }
1312
- queuePaused() {
1313
- return this.queueService.isPaused();
1314
- }
1315
- queueAction(action, id, actor) {
1316
- const before = this.queueService.rawList();
1317
- const affected = id ? before.find((item) => item.id === id) : undefined;
1318
- this.queueService.apply(action, id);
1319
- if (id && action === "run") {
1320
- void this.drainQueue().catch((error) => this.broadcastStatus(friendlyErrorText(error), "error"));
1321
- }
1322
- this.appendActivity({
1323
- source: "web",
1324
- status: "info",
1325
- type: `queue_${action}`,
1326
- threadId: null,
1327
- workspace: this.config.workspace,
1328
- actor,
1329
- prompt: affected?.description,
1330
- detail: id ? `${action}: ${id}` : `${action}: ${before.length} queued`,
1331
- });
1332
- this.appendAudit({
1333
- action: "queue_updated",
1334
- status: "ok",
1335
- contextKey: this.contextKey,
1336
- actor,
1337
- description: id ? `${action}: ${id}` : action,
1338
- });
1339
- this.broadcastQueue();
1340
- return this.queue();
1341
- }
1342
- async artifacts() {
1343
- const session = await this.getSession(true);
1344
- return this.artifactService.list(session.getInfo().workspace, 20);
1345
- }
1346
- async artifact(turnId) {
1347
- const session = await this.getSession(true);
1348
- return this.artifactService.get(session.getInfo().workspace, turnId);
1349
- }
1350
- async deleteArtifact(turnId, actor) {
1351
- const session = await this.getSession(true);
1352
- const info = this.publicInfo(session);
1353
- const removed = await this.artifactService.delete(info.workspace, turnId);
1354
- this.appendActivity({
1355
- source: "web",
1356
- status: removed ? "info" : "failed",
1357
- type: "artifact_deleted",
1358
- threadId: info.threadId,
1359
- workspace: info.workspace,
1360
- agentId: info.agentId,
1361
- actor,
1362
- detail: turnId,
1363
- });
1364
- return removed;
1365
- }
1366
- async createArtifactZip(turnId, actor) {
1367
- const session = await this.getSession(true);
1368
- const info = this.publicInfo(session);
1369
- const zip = await this.artifactService.createZip(info.workspace, turnId);
1370
- if (zip) {
1371
- this.appendActivity({
1372
- source: "web",
1373
- status: "info",
1374
- type: "artifact_zip_created",
1375
- threadId: info.threadId,
1376
- workspace: info.workspace,
1377
- agentId: info.agentId,
1378
- actor,
1379
- detail: zip.name,
1380
- });
1381
- }
1382
- return zip;
1383
- }
1384
- async artifactPreview(turnId, relativePath) {
1385
- const session = await this.getSession(true);
1386
- return this.artifactService.preview(session.getInfo().workspace, turnId, relativePath);
1387
- }
1388
- async logs(target = "connector", lines = 100) {
1389
- if (target === "update") {
1390
- return readFormattedLogTail(lines, getUpdateLogPath());
1391
- }
1392
- if (target === "agent-updates") {
1393
- return readFormattedLogTail(lines, getAgentUpdateLogPath());
1394
- }
1395
- return readFormattedLogTail(lines);
1396
- }
1397
- clearLogs(target = "connector", actor) {
1398
- const result = clearLogFile(target === "update" ? getUpdateLogPath() : target === "agent-updates" ? getAgentUpdateLogPath() : getConnectorLogPath());
1399
- this.appendActivity({
1400
- source: "web",
1401
- status: "info",
1402
- type: "logs_cleared",
1403
- threadId: null,
1404
- workspace: this.config.workspace,
1405
- actor,
1406
- detail: `Cleared ${target} log.`,
1407
- });
1408
- this.appendAudit({
1409
- action: "command",
1410
- status: "ok",
1411
- contextKey: this.contextKey,
1412
- actor,
1413
- description: `clear ${target} log`,
1414
- detail: result.filePath,
1415
- });
1416
- return { ok: true, filePath: result.filePath, clearedAt: result.clearedAt.toISOString() };
1417
- }
1418
- restartConnector(actor) {
1419
- spawnConnectorRestart();
1420
- this.broadcastStatus("Restart requested. The dashboard may disconnect briefly.", "warn");
1421
- this.appendActivity({
1422
- source: "web",
1423
- status: "info",
1424
- type: "restart_requested",
1425
- threadId: null,
1426
- workspace: this.config.workspace,
1427
- actor,
1428
- detail: "Dashboard requested a connector restart.",
1429
- });
1430
- this.appendAudit({
1431
- action: "command",
1432
- status: "ok",
1433
- contextKey: this.contextKey,
1434
- actor,
1435
- description: "restart connector",
1436
- });
1437
- return { ok: true, message: "Restart requested." };
1438
- }
1439
- dispose() {
1440
- if (this.externalMonitor) {
1441
- clearInterval(this.externalMonitor);
1442
- }
1443
- this.agentUpdates.cancelAll();
1444
- this.registry.disposeAll();
1445
- this.subscribers.clear();
1446
- }
1447
- async getSession(deferThreadStart) {
1448
- return this.registry.getOrCreate(this.contextKey, { deferThreadStart });
1449
- }
1450
- async cached(key, producer) {
1451
- return (await this.cache.get(key, this.config.dashboardCacheTtlMs, producer)).value;
1452
- }
1453
- listKnownContextMetadata() {
1454
- const contexts = new Map();
1455
- const add = (meta) => {
1456
- if (meta?.contextKey) {
1457
- contexts.set(meta.contextKey, meta);
1458
- }
1459
- };
1460
- for (const meta of this.registry.listContexts()) {
1461
- add(meta);
1462
- }
1463
- const sharedRegistry = new SessionRegistry(this.config);
1464
- try {
1465
- for (const meta of sharedRegistry.listContexts()) {
1466
- add(meta);
1467
- }
1468
- }
1469
- finally {
1470
- sharedRegistry.disposeAll();
1471
- }
1472
- const current = this.registry.get(this.contextKey)?.getInfo();
1473
- if (current) {
1474
- add({
1475
- contextKey: this.contextKey,
1476
- agentId: current.agentId,
1477
- threadId: current.threadId,
1478
- workspace: current.workspace,
1479
- model: current.model,
1480
- reasoningEffort: current.reasoningEffort,
1481
- launchProfileId: current.nextLaunchProfileId ?? current.launchProfileId,
1482
- sessionPath: current.sessionPath,
1483
- updatedAt: Date.now(),
1484
- });
1485
- }
1486
- return [...contexts.values()];
1487
- }
1488
- discoverRunningConnectorSessions() {
1489
- const active = [];
1490
- const terminal = new Set();
1491
- const now = Date.now();
1492
- for (const event of this.activityStore.list({ limit: 500 })) {
1493
- if (!event.threadId || !event.agentId || !event.contextKey) {
1494
- continue;
1495
- }
1496
- const key = `${event.source}:${event.contextKey}:${event.agentId}:${event.threadId}`;
1497
- if (isPromptTerminalActivity(event)) {
1498
- terminal.add(key);
1499
- continue;
1500
- }
1501
- if (event.type !== "prompt_started" || event.status !== "running" || event.source === "cli") {
1502
- continue;
1503
- }
1504
- if (terminal.has(key)) {
1505
- continue;
1506
- }
1507
- const startedMs = Date.parse(event.timestamp);
1508
- if (!Number.isFinite(startedMs) || now - startedMs > ACTIVE_ACTIVITY_TTL_MS) {
1509
- continue;
1510
- }
1511
- active.push({
1512
- id: `${event.contextKey}:${event.id}`,
1513
- contextKey: event.contextKey,
1514
- sourceContextKey: event.contextKey,
1515
- source: event.source,
1516
- status: "running",
1517
- agentId: event.agentId,
1518
- agentLabel: event.agentId ? agentLabel(event.agentId) : undefined,
1519
- threadId: event.threadId,
1520
- workspace: event.workspace,
1521
- prompt: event.prompt,
1522
- startedAt: event.timestamp,
1523
- updatedAt: event.timestamp,
1524
- durationMs: Math.max(0, now - startedMs),
1525
- queueLength: this.promptStore.list(event.contextKey).length,
1526
- queuePaused: this.promptStore.isPaused(event.contextKey),
1527
- detail: event.actor?.label ? `Started by ${event.actor.label}` : undefined,
1528
- });
1529
- }
1530
- return active;
1531
- }
1532
- discoverActiveCodexSessions(knownContexts, preferences) {
1533
- if (!this.config.codexEnabled || !enabledAgents(this.config).includes("codex")) {
1534
- return [];
1535
- }
1536
- const capabilities = this.capabilitiesForAgent("codex");
1537
- if (!capabilities.externalActivity) {
1538
- return [];
1539
- }
1540
- const active = [];
1541
- const nowMs = Date.now();
1542
- const staleAfterMs = this.config.codexExternalBusyStaleMs;
1543
- for (const thread of listCodexThreads(ACTIVE_CODEX_DISCOVERY_LIMIT)) {
1544
- if (staleAfterMs > 0 && nowMs - thread.updatedAt.getTime() > staleAfterMs) {
1545
- continue;
1546
- }
1547
- const meta = {
1548
- contextKey: `cli:codex:${thread.id}`,
1549
- agentId: "codex",
1550
- threadId: thread.id,
1551
- workspace: thread.cwd,
1552
- model: thread.model ?? undefined,
1553
- reasoningEffort: thread.reasoningEffort ?? undefined,
1554
- updatedAt: thread.updatedAt.getTime(),
1555
- };
1556
- const session = this.externalActiveSession(meta, knownContexts, preferences);
1557
- if (session) {
1558
- active.push(session);
1559
- }
1560
- }
1561
- return active;
1562
- }
1563
- externalActiveSession(meta, knownContexts, preferences) {
1564
- if (!meta.threadId) {
1565
- return null;
1566
- }
1567
- const agentId = isAgentId(meta.agentId) ? meta.agentId : this.config.defaultAgent;
1568
- if (!enabledAgents(this.config).includes(agentId)) {
1569
- return null;
1570
- }
1571
- const capabilities = this.capabilitiesForAgent(agentId);
1572
- if (!capabilities.externalActivity) {
1573
- return null;
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
- }
1581
- const snapshot = getExternalSnapshotForSession(this.sessionStubForMetadata(meta, agentId, capabilities), this.config, {
1582
- maxEvents: 8,
1583
- });
1584
- if (!snapshot?.activity.active) {
1585
- return null;
1586
- }
1587
- const startedAt = snapshot.activity.startedAt?.toISOString() ?? new Date().toISOString();
1588
- const updatedAt = snapshot.activity.updatedAt?.toISOString() ?? new Date().toISOString();
1589
- const startedMs = Date.parse(startedAt);
1590
- const sourceContextKey = `cli:${snapshot.agentId}:${snapshot.threadId}`;
1591
- const mirrorChannels = this.mirrorRegistry.activeMirrorsForThread(snapshot.agentId, snapshot.threadId, knownContexts, preferences);
1592
- const queueLength = this.mirrorRegistry.queueLengthForExternalSource(sourceContextKey, mirrorChannels);
1593
- const mirrorDetail = mirrorChannels.length > 0
1594
- ? `Mirroring: ${mirrorChannels.map((mirror) => `${mirror.source} ${mirror.mode}`).join(", ")}`
1595
- : "Mirroring: none";
1596
- return {
1597
- id: `${sourceContextKey}:${snapshot.activity.turnId ?? snapshot.threadId}`,
1598
- contextKey: sourceContextKey,
1599
- sourceContextKey,
1600
- source: "cli",
1601
- status: "external",
1602
- agentId: snapshot.agentId,
1603
- agentLabel: snapshot.agentLabel,
1604
- threadId: snapshot.threadId,
1605
- workspace: meta.workspace,
1606
- prompt: snapshot.latestUserMessage ?? undefined,
1607
- currentTool: snapshot.latestToolName ?? undefined,
1608
- lastTool: snapshot.latestToolName ?? undefined,
1609
- startedAt,
1610
- updatedAt,
1611
- durationMs: Number.isFinite(startedMs) ? Math.max(0, Date.now() - startedMs) : 0,
1612
- queueLength,
1613
- queuePaused: this.mirrorRegistry.queuePausedForExternalSource(sourceContextKey, mirrorChannels),
1614
- mirrorChannels,
1615
- detail: `${mirrorDetail} | ${snapshot.sourceLabel}: ${snapshot.sourcePath}`,
1616
- };
1617
- }
1618
- sessionStubForMetadata(meta, agentId, capabilities) {
1619
- const info = {
1620
- agentId,
1621
- agentLabel: agentLabel(agentId),
1622
- threadId: meta.threadId,
1623
- workspace: meta.workspace,
1624
- model: meta.model,
1625
- reasoningEffort: meta.reasoningEffort,
1626
- launchProfileId: meta.launchProfileId ?? this.config.defaultLaunchProfileId,
1627
- launchProfileLabel: meta.launchProfileId ?? this.config.defaultLaunchProfileId,
1628
- launchProfileBehavior: "-",
1629
- sandboxMode: "-",
1630
- approvalPolicy: "-",
1631
- fastMode: false,
1632
- unsafeLaunch: false,
1633
- sessionPath: meta.sessionPath,
1634
- capabilities,
1635
- };
1636
- return {
1637
- getInfo: () => info,
1638
- getActiveThreadId: () => meta.threadId,
1639
- };
1640
- }
1641
- capabilitiesForAgent(agentId) {
1642
- return listAgentAdapterDescriptors().find((descriptor) => descriptor.id === agentId)?.capabilities ?? CODEX_AGENT_CAPABILITIES;
1643
- }
1644
- activeSessionKey(session) {
1645
- return session.threadId ? `${session.agentId ?? "unknown"}:${session.threadId}` : session.id;
1646
- }
1647
- preferredActiveSession(existing, candidate) {
1648
- if (!existing) {
1649
- return candidate;
1650
- }
1651
- const existingPriority = activeSessionPriority(existing);
1652
- const candidatePriority = activeSessionPriority(candidate);
1653
- if (candidatePriority !== existingPriority) {
1654
- return candidatePriority > existingPriority ? candidate : existing;
1655
- }
1656
- return Date.parse(candidate.updatedAt) >= Date.parse(existing.updatedAt) ? candidate : existing;
1657
- }
1658
- async getControlSession(agentId) {
1659
- const active = await this.getSession(true);
1660
- const activeInfo = this.publicInfo(active);
1661
- if (!agentId || agentId === activeInfo.agentId) {
1662
- return { session: active, dispose: false };
1663
- }
1664
- if (!enabledAgents(this.config).includes(agentId)) {
1665
- throw new Error(`Agent is not enabled: ${agentId}`);
1666
- }
1667
- const session = await createAgentSessionService(this.config, agentId, {
1668
- deferThreadStart: true,
1669
- workspace: activeInfo.workspace,
1670
- });
1671
- return { session, dispose: true };
1672
- }
1673
- cliPathOptions() {
1674
- return {
1675
- piCliPath: this.config.piCliPath,
1676
- hermesCliPath: this.config.hermesCliPath,
1677
- openClawCliPath: this.config.openClawCliPath,
1678
- claudeCodeCliPath: this.config.claudeCodeCliPath,
1679
- };
1680
- }
1681
- async ensureActiveThread(session) {
1682
- if (!session.hasActiveThread()) {
1683
- await session.newThread();
1684
- this.updateSession(session);
1685
- }
1686
- }
1687
- async checkAgentAuth(info) {
1688
- if (info.agentId === "pi") {
1689
- return checkPiAuthStatus(info.model);
1690
- }
1691
- if (info.agentId === "hermes") {
1692
- return checkHermesAuthStatus({
1693
- baseUrl: this.config.hermesApiBaseUrl,
1694
- apiKey: this.config.hermesApiKey,
1695
- });
1696
- }
1697
- if (info.agentId === "openclaw") {
1698
- return checkOpenClawAuthStatus({
1699
- gatewayUrl: this.config.openClawGatewayUrl,
1700
- token: this.config.openClawGatewayToken,
1701
- password: this.config.openClawGatewayPassword,
1702
- });
1703
- }
1704
- if (info.agentId === "claude-code") {
1705
- return checkClaudeCodeAuthStatus(this.config.claudeCodeCliPath);
1706
- }
1707
- return checkAuthStatus(this.config.codexApiKey);
1708
- }
1709
- async startAgentLogin(info) {
1710
- if (info.agentId === "hermes") {
1711
- return startHermesLogin(this.config.hermesCliPath);
1712
- }
1713
- if (info.agentId === "claude-code") {
1714
- return startClaudeCodeLogin(this.config.claudeCodeCliPath);
1715
- }
1716
- if (info.agentId === "codex") {
1717
- return startCodexLogin();
1718
- }
1719
- return {
1720
- success: false,
1721
- message: `${info.agentLabel} login is not managed by NordRelay. Run the agent login flow on the host.`,
1722
- };
1723
- }
1724
- async startAgentLogout(info) {
1725
- if (info.agentId === "hermes") {
1726
- return startHermesLogout(this.config.hermesCliPath);
1727
- }
1728
- if (info.agentId === "claude-code") {
1729
- return startClaudeCodeLogout(this.config.claudeCodeCliPath);
1730
- }
1731
- if (info.agentId === "codex") {
1732
- return startCodexLogout();
1733
- }
1734
- return {
1735
- success: false,
1736
- message: `${info.agentLabel} logout is not managed by NordRelay. Run the agent logout flow on the host.`,
1737
- };
1738
- }
1739
- ensureIdle(session) {
1740
- if (session.isProcessing()) {
1741
- throw new Error("The active session is still processing a turn.");
1742
- }
1743
- }
1744
- async runPrompt(session, envelope) {
1745
- const workspacePolicy = evaluateWorkspacePolicy(session.getInfo().workspace, this.config);
1746
- if (!workspacePolicy.allowed) {
1747
- throw new Error(workspacePolicy.warning ?? "Current workspace is blocked by policy.");
1748
- }
1749
- try {
1750
- await this.turnService.run(session, envelope);
1751
- }
1752
- finally {
1753
- await this.drainQueue();
1754
- }
1755
- }
1756
- async drainQueue() {
1757
- if (this.draining || this.queueService.isPaused()) {
1758
- return;
1759
- }
1760
- this.draining = true;
1761
- try {
1762
- const session = await this.getSession(false);
1763
- while (!session.isProcessing()) {
1764
- const external = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
1765
- if (external?.activity.active) {
1766
- this.broadcastStatus(`Waiting for ${external.agentLabel} CLI task... ${this.queueService.length()} queued.`, "info");
1767
- return;
1768
- }
1769
- const next = this.queueService.dequeue();
1770
- this.broadcastQueue();
1771
- if (!next) {
1772
- return;
1773
- }
1774
- await this.runPrompt(session, next);
1775
- }
1776
- }
1777
- finally {
1778
- this.draining = false;
1779
- }
1780
- }
1781
- updateSession(session) {
1782
- this.registry.updateMetadata(this.contextKey, session);
1783
- this.broadcast({ type: "session_update", session: this.publicInfo(session) });
1784
- }
1785
- recordActivity(input) {
1786
- return this.appendActivity(input);
1787
- }
1788
- recordAgentUpdateLifecycle(job) {
1789
- const previous = this.agentUpdateStates.get(job.id);
1790
- const actor = this.agentUpdateActors.get(job.id);
1791
- if (job.needsInput && !previous?.needsInput) {
1792
- this.appendActivity({
1793
- source: "web",
1794
- status: "info",
1795
- type: "agent_update_input_required",
1796
- agentId: job.agentId,
1797
- threadId: null,
1798
- workspace: this.config.workspace,
1799
- actor,
1800
- detail: `${job.agentLabel} ${job.operation} may require input.`,
1801
- });
1802
- }
1803
- if (job.status !== "running" && previous?.status === "running") {
1804
- this.appendActivity({
1805
- source: "web",
1806
- status: job.status === "completed" ? "completed" : job.status === "cancelled" ? "aborted" : "failed",
1807
- type: job.operation === "install" ? `agent_install_${job.status}` : `agent_update_${job.status}`,
1808
- agentId: job.agentId,
1809
- threadId: null,
1810
- workspace: this.config.workspace,
1811
- actor,
1812
- detail: job.error ?? `${job.agentLabel} ${job.operation} ${job.status}.`,
1813
- durationMs: Math.max(0, Date.parse(job.finishedAt ?? job.updatedAt) - Date.parse(job.startedAt)),
1814
- });
1815
- this.agentUpdateActors.delete(job.id);
1816
- }
1817
- this.agentUpdateStates.set(job.id, { status: job.status, needsInput: job.needsInput });
1818
- }
1819
- appendActivity(input) {
1820
- const event = this.activityStore.append(this.enrichActivityInput(input));
1821
- this.broadcast({ type: "activity_update", events: this.activity({ limit: 50 }) });
1822
- return event;
1823
- }
1824
- enrichActivityInput(input) {
1825
- return this.enrichActivityFields(input);
1826
- }
1827
- enrichActivityEvent(event, info) {
1828
- return this.enrichActivityFields(event, info);
1829
- }
1830
- enrichActivityFields(event, info) {
1831
- if (!info) {
1832
- return !event.threadId && !event.workspace ? { ...event, workspace: this.config.workspace } : event;
1833
- }
1834
- if (event.threadId && info.threadId && event.threadId === info.threadId) {
1835
- return { ...event, workspace: event.workspace ?? info.workspace, agentId: event.agentId ?? info.agentId };
1836
- }
1837
- if (!event.threadId && !event.workspace) {
1838
- return { ...event, workspace: this.config.workspace };
1839
- }
1840
- return event;
1841
- }
1842
- appendAudit(input) {
1843
- return this.auditStore.append({ ...input, channelId: "web" });
1844
- }
1845
- updateCurrentProgress(patch = {}) {
1846
- if (!this.currentProgress) {
1847
- return;
1848
- }
1849
- if ("currentTool" in patch) {
1850
- this.currentProgress.currentTool = patch.currentTool;
1851
- const { currentTool: _currentTool, ...rest } = patch;
1852
- Object.assign(this.currentProgress, rest);
1853
- }
1854
- else {
1855
- Object.assign(this.currentProgress, patch);
1856
- }
1857
- this.currentProgress.durationMs = Date.now() - this.currentTurnStartedAt;
1858
- this.currentProgress.updatedAt = new Date().toISOString();
1859
- }
1860
- addCurrentTool(toolName) {
1861
- if (!this.currentProgress) {
1862
- return;
1863
- }
1864
- const existing = this.currentProgress.tools.find((tool) => tool.name === toolName);
1865
- if (existing) {
1866
- existing.count += 1;
1867
- }
1868
- else {
1869
- this.currentProgress.tools.push({ name: toolName, count: 1 });
1870
- }
1871
- this.updateCurrentProgress({ currentTool: toolName, lastTool: toolName });
1872
- }
1873
- broadcastQueue() {
1874
- this.broadcast({ type: "queue_update", queue: this.queue(), paused: this.queuePaused() });
1875
- }
1876
- broadcastStatus(message, level = "info") {
1877
- this.broadcast({ type: "status", message, level, at: new Date().toISOString() });
1878
- }
1879
- broadcast(event) {
1880
- for (const subscriber of this.subscribers) {
1881
- try {
1882
- subscriber(event);
1883
- }
1884
- catch {
1885
- this.subscribers.delete(subscriber);
1886
- }
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?.();
1905
- }
1906
- publicInfo(session) {
1907
- const info = session.getInfo();
1908
- const agentId = info.agentId ?? "codex";
1909
- return {
1910
- ...info,
1911
- agentId,
1912
- agentLabel: info.agentLabel ?? agentLabel(agentId),
1913
- capabilities: info.capabilities ?? CODEX_AGENT_CAPABILITIES,
1914
- };
1915
- }
1916
- }