@nordbyte/nordrelay 0.5.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +80 -11
- package/README.md +154 -22
- package/dist/access-control.js +7 -1
- package/dist/activity-events.js +44 -0
- package/dist/audit-log.js +40 -2
- package/dist/bot-preferences.js +1 -0
- package/dist/bot-rendering.js +10 -7
- package/dist/bot.js +535 -11
- package/dist/channel-actions.js +7 -2
- package/dist/channel-adapter.js +40 -7
- package/dist/channel-command-catalog.js +88 -0
- package/dist/channel-command-service.js +369 -0
- package/dist/channel-mirror-registry.js +77 -0
- package/dist/channel-peer-prompt.js +95 -0
- package/dist/channel-runtime.js +12 -5
- package/dist/channel-turn-service.js +237 -0
- package/dist/codex-state.js +114 -78
- package/dist/config-metadata.js +93 -13
- package/dist/config.js +103 -8
- package/dist/context-key.js +87 -5
- package/dist/discord-artifacts.js +165 -0
- package/dist/discord-bot.js +2073 -0
- package/dist/discord-channel-runtime.js +133 -0
- package/dist/discord-command-surface.js +57 -0
- package/dist/discord-rate-limit.js +141 -0
- package/dist/index.js +36 -5
- package/dist/job-store.js +127 -0
- package/dist/metrics.js +87 -0
- package/dist/peer-auth.js +85 -0
- package/dist/peer-client.js +256 -0
- package/dist/peer-context.js +21 -0
- package/dist/peer-identity.js +127 -0
- package/dist/peer-runtime-service.js +636 -0
- package/dist/peer-server.js +220 -0
- package/dist/peer-store.js +294 -0
- package/dist/peer-types.js +52 -0
- package/dist/relay-external-activity-monitor.js +47 -6
- package/dist/relay-runtime-helpers.js +208 -0
- package/dist/relay-runtime.js +897 -394
- package/dist/remote-prompt.js +98 -0
- package/dist/runtime-cache.js +57 -0
- package/dist/session-locks.js +10 -7
- package/dist/support-bundle.js +1 -0
- package/dist/telegram-access-commands.js +15 -2
- package/dist/telegram-access-middleware.js +16 -3
- package/dist/telegram-agent-commands.js +25 -0
- package/dist/telegram-artifact-commands.js +46 -0
- package/dist/telegram-command-menu.js +3 -53
- package/dist/telegram-diagnostics-command.js +5 -50
- package/dist/telegram-general-commands.js +16 -6
- package/dist/telegram-operational-commands.js +14 -6
- package/dist/telegram-preference-commands.js +23 -127
- package/dist/telegram-queue-commands.js +74 -4
- package/dist/telegram-support-command.js +7 -0
- package/dist/telegram-update-commands.js +27 -0
- package/dist/user-management.js +208 -0
- package/dist/web-api-contract.js +17 -0
- package/dist/web-dashboard-access-routes.js +74 -1
- package/dist/web-dashboard-artifact-routes.js +3 -3
- package/dist/web-dashboard-assets.js +2 -0
- package/dist/web-dashboard-pages.js +109 -13
- package/dist/web-dashboard-peer-routes.js +204 -0
- package/dist/web-dashboard-runtime-routes.js +53 -8
- package/dist/web-dashboard-session-routes.js +27 -20
- package/dist/web-dashboard-ui.js +2 -0
- package/dist/web-dashboard.js +160 -6
- package/dist/web-state.js +33 -2
- package/dist/webui-assets/dashboard.css +75 -1
- package/dist/webui-assets/dashboard.js +779 -55
- package/package.json +5 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +578 -19
package/dist/relay-runtime.js
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
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
|
-
import { CODEX_AGENT_CAPABILITIES, agentLabel, agentReasoningLabel, agentReasoningOptions, } from "./agent.js";
|
|
4
|
+
import { CODEX_AGENT_CAPABILITIES, agentLabel, agentReasoningLabel, agentReasoningOptions, isAgentId, } from "./agent.js";
|
|
6
5
|
import { getAgentDiagnostics, getExternalSnapshotForSession, } from "./agent-activity.js";
|
|
7
6
|
import { listAgentAdapterDescriptors } from "./agent-adapter.js";
|
|
8
7
|
import { AgentUpdateManager } from "./agent-updates.js";
|
|
9
8
|
import { createAgentSessionService, enabledAgents } from "./agent-factory.js";
|
|
10
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";
|
|
11
13
|
import { checkAuthStatus, startLogin as startCodexLogin, startLogout as startCodexLogout } from "./codex-auth.js";
|
|
12
14
|
import { checkClaudeCodeAuthStatus, startClaudeCodeLogin, startClaudeCodeLogout } from "./claude-code-auth.js";
|
|
15
|
+
import { listThreads as listCodexThreads } from "./codex-state.js";
|
|
13
16
|
import { friendlyErrorText } from "./error-messages.js";
|
|
14
17
|
import { checkHermesAuthStatus, startHermesLogin, startHermesLogout } from "./hermes-auth.js";
|
|
15
18
|
import { checkOpenClawAuthStatus } from "./openclaw-auth.js";
|
|
16
19
|
import { clearLogFile, getAgentUpdateLogPath, getConnectorHealth, getConnectorLogPath, getPackageVersion, getUpdateLogPath, getVersionChecks, readConnectorState, readFormattedLogTail, spawnConnectorRestart, spawnSelfUpdate } from "./operations.js";
|
|
17
20
|
import { checkPiAuthStatus } from "./pi-auth.js";
|
|
18
21
|
import { PromptStore, toPromptEnvelope } from "./prompt-store.js";
|
|
22
|
+
import { UnifiedJobStore } from "./job-store.js";
|
|
23
|
+
import { buildRuntimeMetrics } from "./metrics.js";
|
|
19
24
|
import { RelayArtifactService } from "./relay-artifact-service.js";
|
|
20
25
|
import { RelayExternalActivityMonitor } from "./relay-external-activity-monitor.js";
|
|
21
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";
|
|
22
29
|
import { renderSessionInfoPlain, renderSessionUsageRows } from "./session-format.js";
|
|
23
30
|
import { SessionLockStore } from "./session-locks.js";
|
|
24
31
|
import { SessionRegistry } from "./session-registry.js";
|
|
@@ -26,11 +33,14 @@ import { createSupportBundle } from "./support-bundle.js";
|
|
|
26
33
|
import { transcribeAudio } from "./voice.js";
|
|
27
34
|
import { WebActivityStore, WebChatStore, } from "./web-state.js";
|
|
28
35
|
import { evaluateWorkspacePolicy, filterAllowedWorkspaces } from "./workspace-policy.js";
|
|
29
|
-
const WEB_CONTEXT_KEY = "web:dashboard";
|
|
36
|
+
export const WEB_CONTEXT_KEY = "web:dashboard";
|
|
37
|
+
const ACTIVE_CODEX_DISCOVERY_LIMIT = 200;
|
|
38
|
+
const ACTIVE_ACTIVITY_TTL_MS = 6 * 60 * 60 * 1000;
|
|
30
39
|
const MAX_WEB_SESSION_PAGE_SIZE = 50;
|
|
31
40
|
const MAX_CHAT_HISTORY = 250;
|
|
32
41
|
export class RelayRuntime {
|
|
33
42
|
config;
|
|
43
|
+
contextKey;
|
|
34
44
|
registry;
|
|
35
45
|
promptStore;
|
|
36
46
|
chatStore;
|
|
@@ -39,30 +49,44 @@ export class RelayRuntime {
|
|
|
39
49
|
lockStore;
|
|
40
50
|
agentUpdates;
|
|
41
51
|
queueService;
|
|
52
|
+
jobStore;
|
|
42
53
|
artifactService;
|
|
54
|
+
mirrorRegistry;
|
|
43
55
|
externalActivityMonitor;
|
|
56
|
+
cache = new RuntimeSnapshotCache();
|
|
57
|
+
turnService;
|
|
44
58
|
subscribers = new Set();
|
|
59
|
+
agentUpdateActors = new Map();
|
|
60
|
+
agentUpdateStates = new Map();
|
|
45
61
|
externalMonitor;
|
|
62
|
+
activeSessionsBroadcastTimer = null;
|
|
63
|
+
activeSessionsLastBroadcastAt = 0;
|
|
46
64
|
draining = false;
|
|
47
65
|
currentTurnId = null;
|
|
48
66
|
accumulatedText = "";
|
|
49
67
|
currentTurnStartedAt = 0;
|
|
50
68
|
currentProgress = null;
|
|
51
|
-
constructor(config) {
|
|
69
|
+
constructor(config, options = {}) {
|
|
52
70
|
this.config = config;
|
|
71
|
+
this.contextKey = options.contextKey ?? WEB_CONTEXT_KEY;
|
|
53
72
|
this.registry = new SessionRegistry(config, {
|
|
54
|
-
fileName: "web-contexts.json",
|
|
55
|
-
sqliteKey: "web-contexts",
|
|
73
|
+
fileName: options.registryFileName ?? "web-contexts.json",
|
|
74
|
+
sqliteKey: options.registrySqliteKey ?? "web-contexts",
|
|
56
75
|
});
|
|
57
76
|
this.promptStore = new PromptStore(config.workspace, config.stateBackend);
|
|
58
77
|
this.chatStore = new WebChatStore(config.workspace, config.stateBackend, MAX_CHAT_HISTORY);
|
|
59
78
|
this.activityStore = new WebActivityStore(config.workspace, config.stateBackend, config.auditMaxEvents);
|
|
60
79
|
this.auditStore = new AuditLogStore(config.workspace, config.stateBackend, config.auditMaxEvents);
|
|
61
80
|
this.lockStore = new SessionLockStore(config.workspace, config.stateBackend);
|
|
62
|
-
this.queueService = new RelayQueueService(this.promptStore,
|
|
81
|
+
this.queueService = new RelayQueueService(this.promptStore, this.contextKey);
|
|
82
|
+
this.jobStore = new UnifiedJobStore(config.workspace, config.stateBackend, config.unifiedJobMaxItems);
|
|
63
83
|
this.artifactService = new RelayArtifactService(config);
|
|
84
|
+
this.mirrorRegistry = new ChannelMirrorRegistry(config, this.promptStore);
|
|
64
85
|
this.agentUpdates = new AgentUpdateManager({
|
|
65
|
-
onUpdate: (job) =>
|
|
86
|
+
onUpdate: (job) => {
|
|
87
|
+
this.broadcast({ type: "agent_update", job });
|
|
88
|
+
this.recordAgentUpdateLifecycle(job);
|
|
89
|
+
},
|
|
66
90
|
});
|
|
67
91
|
this.externalActivityMonitor = new RelayExternalActivityMonitor({
|
|
68
92
|
config,
|
|
@@ -83,11 +107,42 @@ export class RelayRuntime {
|
|
|
83
107
|
}, config.codexExternalBusyCheckMs);
|
|
84
108
|
this.externalMonitor.unref?.();
|
|
85
109
|
}
|
|
110
|
+
this.turnService = new ChannelTurnService({
|
|
111
|
+
source: "web",
|
|
112
|
+
contextKey: this.contextKey,
|
|
113
|
+
chatStore: this.chatStore,
|
|
114
|
+
artifactService: this.artifactService,
|
|
115
|
+
checkAuth: (info) => this.checkAgentAuth(info),
|
|
116
|
+
ensureActiveThread: (session) => this.ensureActiveThread(session),
|
|
117
|
+
updateSession: (session) => this.updateSession(session),
|
|
118
|
+
appendActivity: (input) => this.appendActivity(input),
|
|
119
|
+
appendAudit: (input) => this.appendAudit(input),
|
|
120
|
+
broadcast: (event) => this.broadcast(event),
|
|
121
|
+
chatHistory: () => this.chatHistory(),
|
|
122
|
+
setLastPrompt: (envelope) => this.queueService.setLastPrompt(envelope),
|
|
123
|
+
getCurrentProgress: () => this.currentProgress,
|
|
124
|
+
setCurrentProgress: (progress) => {
|
|
125
|
+
this.currentProgress = progress;
|
|
126
|
+
},
|
|
127
|
+
setCurrentTurn: (id, startedAt, accumulatedText) => {
|
|
128
|
+
this.currentTurnId = id;
|
|
129
|
+
if (startedAt !== undefined)
|
|
130
|
+
this.currentTurnStartedAt = startedAt;
|
|
131
|
+
if (accumulatedText !== undefined)
|
|
132
|
+
this.accumulatedText = accumulatedText;
|
|
133
|
+
},
|
|
134
|
+
getCurrentTurnStartedAt: () => this.currentTurnStartedAt,
|
|
135
|
+
getAccumulatedText: () => this.accumulatedText,
|
|
136
|
+
setAccumulatedText: (text) => {
|
|
137
|
+
this.accumulatedText = text;
|
|
138
|
+
},
|
|
139
|
+
});
|
|
86
140
|
}
|
|
87
141
|
subscribe(callback) {
|
|
88
142
|
this.subscribers.add(callback);
|
|
89
143
|
void this.snapshot().then((data) => callback({ type: "snapshot", data })).catch(() => { });
|
|
90
144
|
void this.chatHistory().then((messages) => callback({ type: "chat_history", messages })).catch(() => { });
|
|
145
|
+
void this.activeSessions().then((active) => callback({ type: "active_sessions_update", active })).catch(() => { });
|
|
91
146
|
callback({ type: "activity_update", events: this.activity({ limit: 50 }) });
|
|
92
147
|
return () => this.subscribers.delete(callback);
|
|
93
148
|
}
|
|
@@ -127,19 +182,22 @@ export class RelayRuntime {
|
|
|
127
182
|
};
|
|
128
183
|
}
|
|
129
184
|
async version() {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
185
|
+
return this.cached("version", async () => {
|
|
186
|
+
const cliOptions = this.cliPathOptions();
|
|
187
|
+
const [health, state, versionChecks] = await Promise.all([
|
|
188
|
+
getConnectorHealth(cliOptions),
|
|
189
|
+
readConnectorState(),
|
|
190
|
+
getVersionChecks(cliOptions),
|
|
191
|
+
]);
|
|
192
|
+
return {
|
|
193
|
+
health,
|
|
194
|
+
state,
|
|
195
|
+
versionChecks,
|
|
196
|
+
};
|
|
197
|
+
});
|
|
141
198
|
}
|
|
142
|
-
updateConnector() {
|
|
199
|
+
updateConnector(actor) {
|
|
200
|
+
this.cache.invalidate("version");
|
|
143
201
|
const update = spawnSelfUpdate();
|
|
144
202
|
this.broadcastStatus(`Update started with ${update.method}. Log: ${update.logPath}`, "warn");
|
|
145
203
|
this.appendActivity({
|
|
@@ -148,12 +206,14 @@ export class RelayRuntime {
|
|
|
148
206
|
type: "update_started",
|
|
149
207
|
threadId: null,
|
|
150
208
|
workspace: this.config.workspace,
|
|
209
|
+
actor,
|
|
151
210
|
detail: `${update.method}: ${update.summary}`,
|
|
152
211
|
});
|
|
153
212
|
this.appendAudit({
|
|
154
213
|
action: "command",
|
|
155
214
|
status: "ok",
|
|
156
|
-
contextKey:
|
|
215
|
+
contextKey: this.contextKey,
|
|
216
|
+
actor,
|
|
157
217
|
description: "update",
|
|
158
218
|
detail: update.summary,
|
|
159
219
|
});
|
|
@@ -162,13 +222,19 @@ export class RelayRuntime {
|
|
|
162
222
|
agentUpdateJobs() {
|
|
163
223
|
return this.agentUpdates.list();
|
|
164
224
|
}
|
|
165
|
-
startAgentUpdate(agentId, operation = "update") {
|
|
225
|
+
startAgentUpdate(agentId, operation = "update", actor) {
|
|
226
|
+
this.cache.invalidate("adapterHealth");
|
|
227
|
+
this.cache.invalidate("version");
|
|
166
228
|
const job = this.agentUpdates.start(agentId, {
|
|
167
229
|
piCliPath: this.config.piCliPath,
|
|
168
230
|
hermesCliPath: this.config.hermesCliPath,
|
|
169
231
|
openClawCliPath: this.config.openClawCliPath,
|
|
170
232
|
claudeCodeCliPath: this.config.claudeCodeCliPath,
|
|
171
233
|
}, operation);
|
|
234
|
+
if (actor) {
|
|
235
|
+
this.agentUpdateActors.set(job.id, actor);
|
|
236
|
+
}
|
|
237
|
+
this.agentUpdateStates.set(job.id, { status: job.status, needsInput: job.needsInput });
|
|
172
238
|
this.broadcastStatus(`${job.agentLabel} ${operation} started. Log: ${job.logPath}`, "warn");
|
|
173
239
|
this.appendActivity({
|
|
174
240
|
source: "web",
|
|
@@ -177,13 +243,15 @@ export class RelayRuntime {
|
|
|
177
243
|
agentId,
|
|
178
244
|
threadId: null,
|
|
179
245
|
workspace: this.config.workspace,
|
|
246
|
+
actor,
|
|
180
247
|
detail: `${job.method}: ${job.summary}`,
|
|
181
248
|
});
|
|
182
249
|
this.appendAudit({
|
|
183
250
|
action: "command",
|
|
184
251
|
status: "ok",
|
|
185
|
-
contextKey:
|
|
252
|
+
contextKey: this.contextKey,
|
|
186
253
|
agentId,
|
|
254
|
+
actor,
|
|
187
255
|
description: `${operation} ${agentId}`,
|
|
188
256
|
detail: job.summary,
|
|
189
257
|
});
|
|
@@ -192,93 +260,130 @@ export class RelayRuntime {
|
|
|
192
260
|
agentUpdateLog(id) {
|
|
193
261
|
return this.agentUpdates.readLog(id);
|
|
194
262
|
}
|
|
195
|
-
deleteAgentUpdateLog(id) {
|
|
263
|
+
deleteAgentUpdateLog(id, actor) {
|
|
196
264
|
const job = this.agentUpdates.deleteLog(id);
|
|
265
|
+
this.appendActivity({
|
|
266
|
+
source: "web",
|
|
267
|
+
status: "info",
|
|
268
|
+
type: "agent_update_log_deleted",
|
|
269
|
+
agentId: job.agentId,
|
|
270
|
+
threadId: null,
|
|
271
|
+
workspace: this.config.workspace,
|
|
272
|
+
actor,
|
|
273
|
+
detail: job.logPath,
|
|
274
|
+
});
|
|
197
275
|
this.appendAudit({
|
|
198
276
|
action: "command",
|
|
199
277
|
status: "ok",
|
|
200
|
-
contextKey:
|
|
278
|
+
contextKey: this.contextKey,
|
|
201
279
|
agentId: job.agentId,
|
|
280
|
+
actor,
|
|
202
281
|
description: `delete update log ${id}`,
|
|
203
282
|
detail: job.logPath,
|
|
204
283
|
});
|
|
205
284
|
return job;
|
|
206
285
|
}
|
|
207
|
-
sendAgentUpdateInput(id, input) {
|
|
208
|
-
|
|
286
|
+
sendAgentUpdateInput(id, input, actor) {
|
|
287
|
+
const job = this.agentUpdates.sendInput(id, input);
|
|
288
|
+
this.appendActivity({
|
|
289
|
+
source: "web",
|
|
290
|
+
status: "info",
|
|
291
|
+
type: "agent_update_input_sent",
|
|
292
|
+
agentId: job.agentId,
|
|
293
|
+
threadId: null,
|
|
294
|
+
workspace: this.config.workspace,
|
|
295
|
+
actor: actor ?? this.agentUpdateActors.get(id),
|
|
296
|
+
detail: `Input sent to ${job.agentLabel} ${job.operation}.`,
|
|
297
|
+
});
|
|
298
|
+
return job;
|
|
209
299
|
}
|
|
210
|
-
cancelAgentUpdate(id) {
|
|
211
|
-
|
|
300
|
+
cancelAgentUpdate(id, actor) {
|
|
301
|
+
const job = this.agentUpdates.cancel(id);
|
|
302
|
+
this.appendActivity({
|
|
303
|
+
source: "web",
|
|
304
|
+
status: "aborted",
|
|
305
|
+
type: "agent_update_cancel_requested",
|
|
306
|
+
agentId: job.agentId,
|
|
307
|
+
threadId: null,
|
|
308
|
+
workspace: this.config.workspace,
|
|
309
|
+
actor: actor ?? this.agentUpdateActors.get(id),
|
|
310
|
+
detail: `${job.agentLabel} ${job.operation} cancellation requested.`,
|
|
311
|
+
});
|
|
312
|
+
return job;
|
|
212
313
|
}
|
|
213
314
|
async diagnostics() {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
health,
|
|
223
|
-
versionChecks,
|
|
224
|
-
snapshot,
|
|
225
|
-
runtime: {
|
|
226
|
-
stateBackend: this.config.stateBackend,
|
|
227
|
-
sourceWorkspace: this.config.workspace,
|
|
228
|
-
queuePaused: this.queueService.isPaused(),
|
|
229
|
-
externalMirror: this.externalActivityMonitor.snapshot(),
|
|
230
|
-
agentDiagnostics: getAgentDiagnostics(session, this.config),
|
|
231
|
-
},
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
async adapterHealth() {
|
|
235
|
-
const cliOptions = this.cliPathOptions();
|
|
236
|
-
const [health, versions] = await Promise.all([
|
|
237
|
-
getConnectorHealth(cliOptions),
|
|
238
|
-
getVersionChecks(cliOptions),
|
|
239
|
-
]);
|
|
240
|
-
return Promise.all(listAgentAdapterDescriptors().map(async (descriptor) => {
|
|
241
|
-
const enabled = enabledAgents(this.config).includes(descriptor.id);
|
|
242
|
-
const auth = descriptor.capabilities.auth && enabled
|
|
243
|
-
? await this.authStatus(descriptor.id).catch((error) => ({
|
|
244
|
-
agentId: descriptor.id,
|
|
245
|
-
agentLabel: descriptor.label,
|
|
246
|
-
supported: descriptor.capabilities.auth,
|
|
247
|
-
authenticated: false,
|
|
248
|
-
detail: friendlyErrorText(error),
|
|
249
|
-
loginSupported: descriptor.capabilities.login,
|
|
250
|
-
logoutSupported: descriptor.capabilities.logout,
|
|
251
|
-
}))
|
|
252
|
-
: null;
|
|
253
|
-
const cli = cliHealthForAgent(descriptor.id, health);
|
|
254
|
-
const version = versionCheckForAgent(descriptor.id, versions);
|
|
315
|
+
return this.cached("diagnostics", async () => {
|
|
316
|
+
const cliOptions = this.cliPathOptions();
|
|
317
|
+
const [health, versionChecks, snapshot, session] = await Promise.all([
|
|
318
|
+
getConnectorHealth(cliOptions),
|
|
319
|
+
getVersionChecks(cliOptions),
|
|
320
|
+
this.snapshot(),
|
|
321
|
+
this.getSession(true),
|
|
322
|
+
]);
|
|
255
323
|
return {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
},
|
|
266
|
-
cli,
|
|
267
|
-
version: {
|
|
268
|
-
installed: version.installedLabel,
|
|
269
|
-
latest: version.latestVersion,
|
|
270
|
-
status: version.status,
|
|
271
|
-
detail: version.detail,
|
|
324
|
+
health,
|
|
325
|
+
versionChecks,
|
|
326
|
+
snapshot,
|
|
327
|
+
runtime: {
|
|
328
|
+
stateBackend: this.config.stateBackend,
|
|
329
|
+
sourceWorkspace: this.config.workspace,
|
|
330
|
+
queuePaused: this.queueService.isPaused(),
|
|
331
|
+
externalMirror: this.externalActivityMonitor.snapshot(),
|
|
332
|
+
agentDiagnostics: getAgentDiagnostics(session, this.config),
|
|
272
333
|
},
|
|
273
|
-
capabilities: descriptor.capabilities,
|
|
274
|
-
notes: descriptor.notes,
|
|
275
334
|
};
|
|
276
|
-
})
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
async adapterHealth() {
|
|
338
|
+
return this.cached("adapterHealth", async () => {
|
|
339
|
+
const cliOptions = this.cliPathOptions();
|
|
340
|
+
const [health, versions] = await Promise.all([
|
|
341
|
+
getConnectorHealth(cliOptions),
|
|
342
|
+
getVersionChecks(cliOptions),
|
|
343
|
+
]);
|
|
344
|
+
return Promise.all(listAgentAdapterDescriptors().map(async (descriptor) => {
|
|
345
|
+
const enabled = enabledAgents(this.config).includes(descriptor.id);
|
|
346
|
+
const auth = descriptor.capabilities.auth && enabled
|
|
347
|
+
? await this.authStatus(descriptor.id).catch((error) => ({
|
|
348
|
+
agentId: descriptor.id,
|
|
349
|
+
agentLabel: descriptor.label,
|
|
350
|
+
supported: descriptor.capabilities.auth,
|
|
351
|
+
authenticated: false,
|
|
352
|
+
detail: friendlyErrorText(error),
|
|
353
|
+
loginSupported: descriptor.capabilities.login,
|
|
354
|
+
logoutSupported: descriptor.capabilities.logout,
|
|
355
|
+
}))
|
|
356
|
+
: null;
|
|
357
|
+
const cli = cliHealthForAgent(descriptor.id, health);
|
|
358
|
+
const version = versionCheckForAgent(descriptor.id, versions);
|
|
359
|
+
return {
|
|
360
|
+
id: descriptor.id,
|
|
361
|
+
label: descriptor.label,
|
|
362
|
+
enabled,
|
|
363
|
+
status: descriptor.status === "available" ? (enabled ? "enabled" : "disabled") : "planned",
|
|
364
|
+
auth: {
|
|
365
|
+
supported: descriptor.capabilities.auth,
|
|
366
|
+
authenticated: auth ? auth.authenticated : null,
|
|
367
|
+
method: auth?.method,
|
|
368
|
+
detail: auth?.detail,
|
|
369
|
+
},
|
|
370
|
+
cli,
|
|
371
|
+
version: {
|
|
372
|
+
installed: version.installedLabel,
|
|
373
|
+
latest: version.latestVersion,
|
|
374
|
+
status: version.status,
|
|
375
|
+
detail: version.detail,
|
|
376
|
+
},
|
|
377
|
+
capabilities: descriptor.capabilities,
|
|
378
|
+
notes: descriptor.notes,
|
|
379
|
+
};
|
|
380
|
+
}));
|
|
381
|
+
});
|
|
277
382
|
}
|
|
278
383
|
permissions() {
|
|
279
384
|
return {
|
|
280
385
|
mode: "users",
|
|
281
|
-
message: "Access is managed by NordRelay users, groups, Telegram identities,
|
|
386
|
+
message: "Access is managed by NordRelay users, groups, Telegram identities, Telegram chat access records, Discord identities, and Discord channel access records.",
|
|
282
387
|
};
|
|
283
388
|
}
|
|
284
389
|
tasks() {
|
|
@@ -290,10 +395,229 @@ export class RelayRuntime {
|
|
|
290
395
|
recent: this.activity({ limit: 20 }),
|
|
291
396
|
};
|
|
292
397
|
}
|
|
293
|
-
|
|
294
|
-
|
|
398
|
+
async jobs() {
|
|
399
|
+
const jobs = [];
|
|
400
|
+
const current = this.currentProgress;
|
|
401
|
+
if (current) {
|
|
402
|
+
jobs.push(taskToUnifiedJob("web:current", "web-turn", "Current WebUI turn", current, {
|
|
403
|
+
canCancel: current.status === "running",
|
|
404
|
+
canRetry: false,
|
|
405
|
+
canReadLog: false,
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
408
|
+
const external = this.externalActivityMonitor.task();
|
|
409
|
+
if (external) {
|
|
410
|
+
jobs.push(taskToUnifiedJob(`external:${external.agentId ?? "agent"}:${external.threadId ?? "pending"}`, "external-turn", "External CLI turn", external, {
|
|
411
|
+
canCancel: false,
|
|
412
|
+
canRetry: false,
|
|
413
|
+
canReadLog: false,
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
416
|
+
for (const item of this.queueService.rawList()) {
|
|
417
|
+
const createdAt = new Date(item.createdAt).toISOString();
|
|
418
|
+
jobs.push({
|
|
419
|
+
id: `queue:${item.id}`,
|
|
420
|
+
kind: "queued-prompt",
|
|
421
|
+
title: `Queued prompt ${item.id}`,
|
|
422
|
+
status: "queued",
|
|
423
|
+
source: "web",
|
|
424
|
+
threadId: null,
|
|
425
|
+
workspace: this.config.workspace,
|
|
426
|
+
owner: item.activityActor,
|
|
427
|
+
startedAt: createdAt,
|
|
428
|
+
updatedAt: createdAt,
|
|
429
|
+
summary: item.description,
|
|
430
|
+
queueId: item.id,
|
|
431
|
+
logTail: item.lastError,
|
|
432
|
+
canCancel: true,
|
|
433
|
+
canRetry: true,
|
|
434
|
+
canReadLog: true,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
for (const job of this.agentUpdates.list()) {
|
|
438
|
+
jobs.push({
|
|
439
|
+
id: `agent-update:${job.id}`,
|
|
440
|
+
kind: "agent-update",
|
|
441
|
+
title: `${job.agentLabel} ${job.operation}`,
|
|
442
|
+
status: agentUpdateStatusToUnified(job.status),
|
|
443
|
+
source: "web",
|
|
444
|
+
agentId: job.agentId,
|
|
445
|
+
agentLabel: job.agentLabel,
|
|
446
|
+
threadId: null,
|
|
447
|
+
workspace: this.config.workspace,
|
|
448
|
+
owner: this.agentUpdateActors.get(job.id),
|
|
449
|
+
startedAt: job.startedAt,
|
|
450
|
+
updatedAt: job.updatedAt,
|
|
451
|
+
finishedAt: job.finishedAt,
|
|
452
|
+
summary: job.error || job.summary,
|
|
453
|
+
logPath: job.logPath,
|
|
454
|
+
logTail: job.outputTail,
|
|
455
|
+
updateJobId: job.id,
|
|
456
|
+
canCancel: job.status === "running",
|
|
457
|
+
canRetry: job.status !== "running",
|
|
458
|
+
canReadLog: true,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
for (const event of this.activity({ limit: 100 })) {
|
|
462
|
+
if (event.type === "diagnostics_bundle_exported") {
|
|
463
|
+
jobs.push(activityToUnifiedJob(event, "support-bundle", "Diagnostics support bundle", {
|
|
464
|
+
canCancel: false,
|
|
465
|
+
canRetry: true,
|
|
466
|
+
canReadLog: Boolean(event.detail),
|
|
467
|
+
}));
|
|
468
|
+
}
|
|
469
|
+
else if (event.type === "update_started") {
|
|
470
|
+
jobs.push(activityToUnifiedJob(event, "connector-update", "NordRelay update", {
|
|
471
|
+
canCancel: false,
|
|
472
|
+
canRetry: true,
|
|
473
|
+
canReadLog: Boolean(event.detail),
|
|
474
|
+
}));
|
|
475
|
+
}
|
|
476
|
+
else if (event.category === "prompt" && event.type.startsWith("prompt_")) {
|
|
477
|
+
jobs.push(promptActivityToUnifiedJob(event));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const liveJobs = dedupeJobs(jobs);
|
|
481
|
+
const storedJobs = this.jobStore.upsertMany(liveJobs);
|
|
482
|
+
return {
|
|
483
|
+
jobs: dedupeJobs([...liveJobs, ...storedJobs]).sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt)),
|
|
484
|
+
updatedAt: new Date().toISOString(),
|
|
485
|
+
};
|
|
295
486
|
}
|
|
296
|
-
async
|
|
487
|
+
async jobLog(id) {
|
|
488
|
+
if (id.startsWith("agent-update:")) {
|
|
489
|
+
const updateId = id.slice("agent-update:".length);
|
|
490
|
+
const log = this.agentUpdates.readLog(updateId);
|
|
491
|
+
return { job: (await this.jobs()).jobs.find((job) => job.id === id) ?? null, plain: log.plain };
|
|
492
|
+
}
|
|
493
|
+
if (id.startsWith("queue:")) {
|
|
494
|
+
const queueId = id.slice("queue:".length);
|
|
495
|
+
const item = this.queueService.rawList().find((candidate) => candidate.id === queueId);
|
|
496
|
+
return {
|
|
497
|
+
job: (await this.jobs()).jobs.find((job) => job.id === id) ?? null,
|
|
498
|
+
plain: item ? [
|
|
499
|
+
`Queued prompt: ${item.id}`,
|
|
500
|
+
`Created: ${new Date(item.createdAt).toISOString()}`,
|
|
501
|
+
`Attempts: ${item.attempts ?? 0}`,
|
|
502
|
+
`Description: ${item.description}`,
|
|
503
|
+
item.lastError ? `Last error: ${item.lastError}` : "",
|
|
504
|
+
].filter(Boolean).join("\n") : "Queued prompt not found.",
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
const job = (await this.jobs()).jobs.find((candidate) => candidate.id === id) ?? null;
|
|
508
|
+
return { job, plain: job?.logTail || job?.logPath || job?.summary || this.jobStore.get(id)?.summary || "No log available for this job." };
|
|
509
|
+
}
|
|
510
|
+
async jobAction(id, action, actor) {
|
|
511
|
+
if (id === "web:current" && action === "cancel") {
|
|
512
|
+
await this.abort(actor);
|
|
513
|
+
return this.jobs();
|
|
514
|
+
}
|
|
515
|
+
if (id.startsWith("queue:")) {
|
|
516
|
+
const queueId = id.slice("queue:".length);
|
|
517
|
+
this.queueService.apply(action === "cancel" ? "cancel" : "run", queueId);
|
|
518
|
+
this.jobStore.patch(id, {
|
|
519
|
+
status: action === "cancel" ? "aborted" : "queued",
|
|
520
|
+
summary: action === "cancel" ? `Cancelled queued prompt ${queueId}.` : `Queued prompt ${queueId} moved to the front.`,
|
|
521
|
+
canCancel: action !== "cancel",
|
|
522
|
+
canRetry: action === "cancel",
|
|
523
|
+
finishedAt: action === "cancel" ? new Date().toISOString() : undefined,
|
|
524
|
+
});
|
|
525
|
+
this.broadcast({ type: "queue_update", queue: this.queue(), paused: this.queuePaused() });
|
|
526
|
+
this.appendActivity({
|
|
527
|
+
source: "web",
|
|
528
|
+
status: action === "cancel" ? "aborted" : "queued",
|
|
529
|
+
type: action === "cancel" ? "job_cancelled" : "job_retried",
|
|
530
|
+
threadId: null,
|
|
531
|
+
workspace: this.config.workspace,
|
|
532
|
+
actor,
|
|
533
|
+
detail: `queue:${queueId}`,
|
|
534
|
+
});
|
|
535
|
+
if (action === "retry") {
|
|
536
|
+
void this.drainQueue();
|
|
537
|
+
}
|
|
538
|
+
return this.jobs();
|
|
539
|
+
}
|
|
540
|
+
if (id.startsWith("agent-update:")) {
|
|
541
|
+
const updateId = id.slice("agent-update:".length);
|
|
542
|
+
const current = this.agentUpdates.get(updateId);
|
|
543
|
+
if (!current) {
|
|
544
|
+
throw new Error("Unknown agent update job.");
|
|
545
|
+
}
|
|
546
|
+
if (action === "cancel") {
|
|
547
|
+
this.cancelAgentUpdate(updateId, actor);
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
this.startAgentUpdate(current.agentId, current.operation, actor);
|
|
551
|
+
}
|
|
552
|
+
return this.jobs();
|
|
553
|
+
}
|
|
554
|
+
if (id.startsWith("support-bundle:") && action === "retry") {
|
|
555
|
+
await this.supportBundle(actor);
|
|
556
|
+
return this.jobs();
|
|
557
|
+
}
|
|
558
|
+
if (id.startsWith("connector-update:") && action === "retry") {
|
|
559
|
+
this.updateConnector(actor);
|
|
560
|
+
return this.jobs();
|
|
561
|
+
}
|
|
562
|
+
throw new Error(`Unsupported job action: ${action} ${id}`);
|
|
563
|
+
}
|
|
564
|
+
async activeSessions() {
|
|
565
|
+
const sessions = new Map();
|
|
566
|
+
const knownContexts = this.listKnownContextMetadata();
|
|
567
|
+
const preferences = new BotPreferencesStore(this.config.workspace, this.config.stateBackend);
|
|
568
|
+
const addActiveSession = (session) => {
|
|
569
|
+
const key = this.activeSessionKey(session);
|
|
570
|
+
const existing = sessions.get(key);
|
|
571
|
+
sessions.set(key, this.preferredActiveSession(existing, session));
|
|
572
|
+
};
|
|
573
|
+
if (this.currentProgress?.status === "running") {
|
|
574
|
+
addActiveSession({
|
|
575
|
+
...this.currentProgress,
|
|
576
|
+
contextKey: this.contextKey,
|
|
577
|
+
sourceContextKey: this.contextKey,
|
|
578
|
+
source: "web",
|
|
579
|
+
status: "running",
|
|
580
|
+
queueLength: this.queueService.length(),
|
|
581
|
+
queuePaused: this.queueService.isPaused(),
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
for (const active of this.discoverRunningConnectorSessions()) {
|
|
585
|
+
addActiveSession(active);
|
|
586
|
+
}
|
|
587
|
+
for (const active of this.discoverActiveCodexSessions(knownContexts, preferences)) {
|
|
588
|
+
addActiveSession(active);
|
|
589
|
+
}
|
|
590
|
+
for (const meta of knownContexts) {
|
|
591
|
+
if (meta.contextKey === this.contextKey && this.currentProgress?.status === "running") {
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
const active = this.externalActiveSession(meta, knownContexts, preferences);
|
|
595
|
+
if (active) {
|
|
596
|
+
addActiveSession(active);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return {
|
|
600
|
+
sessions: [...sessions.values()].sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt)),
|
|
601
|
+
updatedAt: new Date().toISOString(),
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
async metrics() {
|
|
605
|
+
const [active, jobs] = await Promise.all([
|
|
606
|
+
this.activeSessions(),
|
|
607
|
+
this.jobs(),
|
|
608
|
+
]);
|
|
609
|
+
return buildRuntimeMetrics({
|
|
610
|
+
queueLength: this.queueService.length(),
|
|
611
|
+
queuePaused: this.queueService.isPaused(),
|
|
612
|
+
activeTurnCount: active.sessions.length,
|
|
613
|
+
jobs: jobs.jobs,
|
|
614
|
+
activity: this.activity({ limit: 500 }),
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
audit(options = 50) {
|
|
618
|
+
return this.auditStore.list(options);
|
|
619
|
+
}
|
|
620
|
+
async supportBundle(actor) {
|
|
297
621
|
const bundle = await createSupportBundle({
|
|
298
622
|
config: this.config,
|
|
299
623
|
diagnostics: await this.diagnostics(),
|
|
@@ -308,12 +632,14 @@ export class RelayRuntime {
|
|
|
308
632
|
type: "diagnostics_bundle_exported",
|
|
309
633
|
threadId: null,
|
|
310
634
|
workspace: this.config.workspace,
|
|
635
|
+
actor,
|
|
311
636
|
detail: bundle.path,
|
|
312
637
|
});
|
|
313
638
|
this.appendAudit({
|
|
314
639
|
action: "command",
|
|
315
640
|
status: "ok",
|
|
316
|
-
contextKey:
|
|
641
|
+
contextKey: this.contextKey,
|
|
642
|
+
actor,
|
|
317
643
|
description: "export diagnostics bundle",
|
|
318
644
|
detail: bundle.path,
|
|
319
645
|
});
|
|
@@ -322,23 +648,48 @@ export class RelayRuntime {
|
|
|
322
648
|
locks() {
|
|
323
649
|
return this.lockStore.list();
|
|
324
650
|
}
|
|
325
|
-
lockWebSession(ownerName = "Web dashboard") {
|
|
326
|
-
const
|
|
651
|
+
lockWebSession(ownerName = "Web dashboard", actor) {
|
|
652
|
+
const label = ownerName || actor?.label || "Web dashboard";
|
|
653
|
+
const lock = this.lockStore.set(this.contextKey, {
|
|
654
|
+
userId: actor?.id ?? "web",
|
|
655
|
+
label,
|
|
656
|
+
channel: "web",
|
|
657
|
+
}, this.config.sessionLockTtlMs);
|
|
658
|
+
this.appendActivity({
|
|
659
|
+
source: "web",
|
|
660
|
+
status: "info",
|
|
661
|
+
type: "lock_created",
|
|
662
|
+
threadId: null,
|
|
663
|
+
workspace: this.config.workspace,
|
|
664
|
+
actor,
|
|
665
|
+
detail: `locked by ${label}`,
|
|
666
|
+
});
|
|
327
667
|
this.appendAudit({
|
|
328
668
|
action: "lock_updated",
|
|
329
669
|
status: "ok",
|
|
330
|
-
contextKey:
|
|
670
|
+
contextKey: this.contextKey,
|
|
671
|
+
actor,
|
|
331
672
|
description: "lock",
|
|
332
|
-
detail: `locked by ${
|
|
673
|
+
detail: `locked by ${label}`,
|
|
333
674
|
});
|
|
334
675
|
return lock;
|
|
335
676
|
}
|
|
336
|
-
unlockWebSession() {
|
|
337
|
-
const removed = this.lockStore.clear(
|
|
677
|
+
unlockWebSession(actor) {
|
|
678
|
+
const removed = this.lockStore.clear(this.contextKey);
|
|
679
|
+
this.appendActivity({
|
|
680
|
+
source: "web",
|
|
681
|
+
status: "info",
|
|
682
|
+
type: "lock_removed",
|
|
683
|
+
threadId: null,
|
|
684
|
+
workspace: this.config.workspace,
|
|
685
|
+
actor,
|
|
686
|
+
detail: removed ? "unlocked" : "no lock",
|
|
687
|
+
});
|
|
338
688
|
this.appendAudit({
|
|
339
689
|
action: "lock_updated",
|
|
340
690
|
status: "ok",
|
|
341
|
-
contextKey:
|
|
691
|
+
contextKey: this.contextKey,
|
|
692
|
+
actor,
|
|
342
693
|
description: "unlock",
|
|
343
694
|
detail: removed ? "unlocked" : "no lock",
|
|
344
695
|
});
|
|
@@ -407,7 +758,7 @@ export class RelayRuntime {
|
|
|
407
758
|
}
|
|
408
759
|
}
|
|
409
760
|
}
|
|
410
|
-
async login(agentId) {
|
|
761
|
+
async login(agentId, actor) {
|
|
411
762
|
const { session, dispose } = await this.getControlSession(agentId);
|
|
412
763
|
try {
|
|
413
764
|
const info = this.publicInfo(session);
|
|
@@ -431,13 +782,24 @@ export class RelayRuntime {
|
|
|
431
782
|
};
|
|
432
783
|
}
|
|
433
784
|
const result = await this.startAgentLogin(info);
|
|
785
|
+
this.appendActivity({
|
|
786
|
+
source: "web",
|
|
787
|
+
status: result.success ? "info" : "failed",
|
|
788
|
+
type: result.success ? "login_started" : "login_failed",
|
|
789
|
+
threadId: info.threadId,
|
|
790
|
+
workspace: info.workspace,
|
|
791
|
+
agentId: info.agentId,
|
|
792
|
+
actor,
|
|
793
|
+
detail: result.message,
|
|
794
|
+
});
|
|
434
795
|
this.appendAudit({
|
|
435
796
|
action: "command",
|
|
436
797
|
status: result.success ? "ok" : "failed",
|
|
437
|
-
contextKey:
|
|
798
|
+
contextKey: this.contextKey,
|
|
438
799
|
agentId: info.agentId,
|
|
439
800
|
threadId: info.threadId,
|
|
440
801
|
workspace: info.workspace,
|
|
802
|
+
actor,
|
|
441
803
|
description: "login",
|
|
442
804
|
detail: result.message,
|
|
443
805
|
});
|
|
@@ -449,7 +811,7 @@ export class RelayRuntime {
|
|
|
449
811
|
}
|
|
450
812
|
}
|
|
451
813
|
}
|
|
452
|
-
async logout(agentId) {
|
|
814
|
+
async logout(agentId, actor) {
|
|
453
815
|
const { session, dispose } = await this.getControlSession(agentId);
|
|
454
816
|
try {
|
|
455
817
|
const info = this.publicInfo(session);
|
|
@@ -483,13 +845,24 @@ export class RelayRuntime {
|
|
|
483
845
|
};
|
|
484
846
|
}
|
|
485
847
|
const result = await this.startAgentLogout(info);
|
|
848
|
+
this.appendActivity({
|
|
849
|
+
source: "web",
|
|
850
|
+
status: result.success ? "info" : "failed",
|
|
851
|
+
type: result.success ? "logout_completed" : "logout_failed",
|
|
852
|
+
threadId: info.threadId,
|
|
853
|
+
workspace: info.workspace,
|
|
854
|
+
agentId: info.agentId,
|
|
855
|
+
actor,
|
|
856
|
+
detail: result.message,
|
|
857
|
+
});
|
|
486
858
|
this.appendAudit({
|
|
487
859
|
action: "command",
|
|
488
860
|
status: result.success ? "ok" : "failed",
|
|
489
|
-
contextKey:
|
|
861
|
+
contextKey: this.contextKey,
|
|
490
862
|
agentId: info.agentId,
|
|
491
863
|
threadId: info.threadId,
|
|
492
864
|
workspace: info.workspace,
|
|
865
|
+
actor,
|
|
493
866
|
description: "logout",
|
|
494
867
|
detail: result.message,
|
|
495
868
|
});
|
|
@@ -517,18 +890,29 @@ export class RelayRuntime {
|
|
|
517
890
|
activity: this.activity({ limit: 100 }).filter((event) => event.threadId === threadId),
|
|
518
891
|
};
|
|
519
892
|
}
|
|
520
|
-
async clearChatHistory() {
|
|
893
|
+
async clearChatHistory(actor) {
|
|
521
894
|
const session = await this.getSession(true);
|
|
522
|
-
const
|
|
895
|
+
const info = this.publicInfo(session);
|
|
896
|
+
const removed = this.chatStore.clear(info.threadId);
|
|
523
897
|
const messages = await this.chatHistory();
|
|
524
898
|
this.broadcast({ type: "chat_history", messages });
|
|
899
|
+
this.appendActivity({
|
|
900
|
+
source: "web",
|
|
901
|
+
status: "info",
|
|
902
|
+
type: "chat_history_cleared",
|
|
903
|
+
threadId: info.threadId,
|
|
904
|
+
workspace: info.workspace,
|
|
905
|
+
agentId: info.agentId,
|
|
906
|
+
actor,
|
|
907
|
+
detail: `${removed} messages removed.`,
|
|
908
|
+
});
|
|
525
909
|
return { removed, messages };
|
|
526
910
|
}
|
|
527
911
|
activity(options = {}) {
|
|
528
|
-
const currentInfo = this.registry.get(
|
|
912
|
+
const currentInfo = this.registry.get(this.contextKey)?.getInfo();
|
|
529
913
|
return this.activityStore.list(options).map((event) => this.enrichActivityEvent(event, currentInfo));
|
|
530
914
|
}
|
|
531
|
-
async retry() {
|
|
915
|
+
async retry(actor) {
|
|
532
916
|
const cached = this.queueService.getLastPrompt();
|
|
533
917
|
if (!cached) {
|
|
534
918
|
throw new Error("Nothing to retry. Send a message first.");
|
|
@@ -536,13 +920,14 @@ export class RelayRuntime {
|
|
|
536
920
|
this.appendAudit({
|
|
537
921
|
action: "command",
|
|
538
922
|
status: "ok",
|
|
539
|
-
contextKey:
|
|
923
|
+
contextKey: this.contextKey,
|
|
924
|
+
actor,
|
|
540
925
|
description: "retry",
|
|
541
926
|
detail: cached.description,
|
|
542
927
|
});
|
|
543
|
-
return this.sendEnvelope(cached);
|
|
928
|
+
return this.sendEnvelope({ ...cached, activityActor: cached.activityActor ?? actor }, actor);
|
|
544
929
|
}
|
|
545
|
-
async sync() {
|
|
930
|
+
async sync(actor) {
|
|
546
931
|
const session = await this.getSession(true);
|
|
547
932
|
const info = this.publicInfo(session);
|
|
548
933
|
if (!(info.capabilities ?? CODEX_AGENT_CAPABILITIES).externalActivity) {
|
|
@@ -559,15 +944,17 @@ export class RelayRuntime {
|
|
|
559
944
|
threadId: result.info.threadId,
|
|
560
945
|
workspace: result.info.workspace,
|
|
561
946
|
agentId: result.info.agentId,
|
|
947
|
+
actor,
|
|
562
948
|
detail: result.changedFields.length > 0 ? result.changedFields.join(", ") : "already in sync",
|
|
563
949
|
});
|
|
564
950
|
this.appendAudit({
|
|
565
951
|
action: "command",
|
|
566
952
|
status: "ok",
|
|
567
|
-
contextKey:
|
|
953
|
+
contextKey: this.contextKey,
|
|
568
954
|
agentId: result.info.agentId,
|
|
569
955
|
threadId: result.info.threadId,
|
|
570
956
|
workspace: result.info.workspace,
|
|
957
|
+
actor,
|
|
571
958
|
description: "sync",
|
|
572
959
|
detail: result.changedFields.join(", ") || "none",
|
|
573
960
|
});
|
|
@@ -634,16 +1021,27 @@ export class RelayRuntime {
|
|
|
634
1021
|
});
|
|
635
1022
|
return session.listModels();
|
|
636
1023
|
}
|
|
637
|
-
async setAgent(agentId) {
|
|
1024
|
+
async setAgent(agentId, actor) {
|
|
638
1025
|
if (!enabledAgents(this.config).includes(agentId)) {
|
|
639
1026
|
throw new Error(`Agent is not enabled: ${agentId}`);
|
|
640
1027
|
}
|
|
641
|
-
const session = await this.registry.switchAgent(
|
|
1028
|
+
const session = await this.registry.switchAgent(this.contextKey, agentId);
|
|
642
1029
|
this.updateSession(session);
|
|
1030
|
+
const info = this.publicInfo(session);
|
|
1031
|
+
this.appendActivity({
|
|
1032
|
+
source: "web",
|
|
1033
|
+
status: "info",
|
|
1034
|
+
type: "agent_switch",
|
|
1035
|
+
threadId: info.threadId,
|
|
1036
|
+
workspace: info.workspace,
|
|
1037
|
+
agentId: info.agentId,
|
|
1038
|
+
actor,
|
|
1039
|
+
detail: `Dashboard switched agent to ${info.agentLabel}.`,
|
|
1040
|
+
});
|
|
643
1041
|
return this.publicInfo(session);
|
|
644
1042
|
}
|
|
645
|
-
async newSession(options = {}) {
|
|
646
|
-
const session = options.agentId ? await this.registry.switchAgent(
|
|
1043
|
+
async newSession(options = {}, actor) {
|
|
1044
|
+
const session = options.agentId ? await this.registry.switchAgent(this.contextKey, options.agentId) : await this.getSession(true);
|
|
647
1045
|
this.ensureIdle(session);
|
|
648
1046
|
if (options.reasoningEffort) {
|
|
649
1047
|
const reasoningOptions = agentReasoningOptions(session.getInfo().agentId);
|
|
@@ -667,11 +1065,12 @@ export class RelayRuntime {
|
|
|
667
1065
|
threadId: info.threadId,
|
|
668
1066
|
workspace: info.workspace,
|
|
669
1067
|
agentId: info.agentId,
|
|
1068
|
+
actor,
|
|
670
1069
|
detail: "New dashboard session created.",
|
|
671
1070
|
});
|
|
672
1071
|
return this.publicInfo(session);
|
|
673
1072
|
}
|
|
674
|
-
async switchSession(threadId) {
|
|
1073
|
+
async switchSession(threadId, actor) {
|
|
675
1074
|
const session = await this.getSession(true);
|
|
676
1075
|
this.ensureIdle(session);
|
|
677
1076
|
const info = await session.switchSession(threadId);
|
|
@@ -684,21 +1083,24 @@ export class RelayRuntime {
|
|
|
684
1083
|
threadId: info.threadId,
|
|
685
1084
|
workspace: info.workspace,
|
|
686
1085
|
agentId: info.agentId,
|
|
1086
|
+
actor,
|
|
687
1087
|
detail: "Dashboard switched session.",
|
|
688
1088
|
});
|
|
689
1089
|
return this.publicInfo(session);
|
|
690
1090
|
}
|
|
691
|
-
async attachSession(threadId) {
|
|
692
|
-
return this.switchSession(threadId);
|
|
1091
|
+
async attachSession(threadId, actor) {
|
|
1092
|
+
return this.switchSession(threadId, actor);
|
|
693
1093
|
}
|
|
694
|
-
async setModel(model) {
|
|
1094
|
+
async setModel(model, actor) {
|
|
695
1095
|
const session = await this.getSession(true);
|
|
696
1096
|
this.ensureIdle(session);
|
|
697
1097
|
await session.setModelForCurrentSession(model);
|
|
698
1098
|
this.updateSession(session);
|
|
699
|
-
|
|
1099
|
+
const info = this.publicInfo(session);
|
|
1100
|
+
this.appendActivity({ source: "web", status: "info", type: "model_changed", threadId: info.threadId, workspace: info.workspace, agentId: info.agentId, actor, detail: model });
|
|
1101
|
+
return info;
|
|
700
1102
|
}
|
|
701
|
-
async setReasoningEffort(effort) {
|
|
1103
|
+
async setReasoningEffort(effort, actor) {
|
|
702
1104
|
const session = await this.getSession(true);
|
|
703
1105
|
this.ensureIdle(session);
|
|
704
1106
|
const options = agentReasoningOptions(session.getInfo().agentId);
|
|
@@ -707,9 +1109,11 @@ export class RelayRuntime {
|
|
|
707
1109
|
}
|
|
708
1110
|
await session.setReasoningEffortForCurrentSession(effort);
|
|
709
1111
|
this.updateSession(session);
|
|
710
|
-
|
|
1112
|
+
const info = this.publicInfo(session);
|
|
1113
|
+
this.appendActivity({ source: "web", status: "info", type: "reasoning_changed", threadId: info.threadId, workspace: info.workspace, agentId: info.agentId, actor, detail: effort });
|
|
1114
|
+
return info;
|
|
711
1115
|
}
|
|
712
|
-
async setFastMode(enabled) {
|
|
1116
|
+
async setFastMode(enabled, actor) {
|
|
713
1117
|
const session = await this.getSession(true);
|
|
714
1118
|
this.ensureIdle(session);
|
|
715
1119
|
if (!(session.getInfo().capabilities ?? CODEX_AGENT_CAPABILITIES).fastMode) {
|
|
@@ -717,23 +1121,29 @@ export class RelayRuntime {
|
|
|
717
1121
|
}
|
|
718
1122
|
session.setFastMode(enabled);
|
|
719
1123
|
this.updateSession(session);
|
|
720
|
-
|
|
1124
|
+
const info = this.publicInfo(session);
|
|
1125
|
+
this.appendActivity({ source: "web", status: "info", type: "fast_mode_changed", threadId: info.threadId, workspace: info.workspace, agentId: info.agentId, actor, detail: enabled ? "on" : "off" });
|
|
1126
|
+
return info;
|
|
721
1127
|
}
|
|
722
|
-
async setLaunchProfile(profileId) {
|
|
1128
|
+
async setLaunchProfile(profileId, actor) {
|
|
723
1129
|
const session = await this.getSession(true);
|
|
724
1130
|
this.ensureIdle(session);
|
|
725
1131
|
session.setLaunchProfile(profileId);
|
|
726
1132
|
this.updateSession(session);
|
|
727
|
-
|
|
1133
|
+
const info = this.publicInfo(session);
|
|
1134
|
+
this.appendActivity({ source: "web", status: "info", type: "launch_profile_changed", threadId: info.threadId, workspace: info.workspace, agentId: info.agentId, actor, detail: info.launchProfileLabel ?? profileId });
|
|
1135
|
+
return info;
|
|
728
1136
|
}
|
|
729
|
-
async handback() {
|
|
1137
|
+
async handback(actor) {
|
|
730
1138
|
const session = await this.getSession(true);
|
|
731
1139
|
this.ensureIdle(session);
|
|
732
1140
|
const result = session.handback();
|
|
733
1141
|
this.updateSession(session);
|
|
1142
|
+
const info = this.publicInfo(session);
|
|
1143
|
+
this.appendActivity({ source: "web", status: "info", type: "handback", threadId: result.threadId, workspace: result.workspace, agentId: info.agentId, actor, detail: result.command ?? "Thread handed back." });
|
|
734
1144
|
return result;
|
|
735
1145
|
}
|
|
736
|
-
async abort() {
|
|
1146
|
+
async abort(actor) {
|
|
737
1147
|
const session = await this.getSession(true);
|
|
738
1148
|
const snapshot = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
|
|
739
1149
|
if (snapshot?.activity.active && !session.isProcessing()) {
|
|
@@ -743,19 +1153,23 @@ export class RelayRuntime {
|
|
|
743
1153
|
message: `Cannot abort the external ${snapshot.agentLabel} CLI task from NordRelay. Stop it in the terminal where it is running.`,
|
|
744
1154
|
at: new Date().toISOString(),
|
|
745
1155
|
});
|
|
1156
|
+
const info = this.publicInfo(session);
|
|
1157
|
+
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.` });
|
|
746
1158
|
return;
|
|
747
1159
|
}
|
|
748
1160
|
await session.abort();
|
|
1161
|
+
const info = this.publicInfo(session);
|
|
1162
|
+
this.appendActivity({ source: "web", status: "aborted", type: "prompt_aborted", threadId: info.threadId, workspace: info.workspace, agentId: info.agentId, actor, detail: "Current operation aborted." });
|
|
749
1163
|
this.broadcast({ type: "status", level: "warn", message: "Current operation aborted.", at: new Date().toISOString() });
|
|
750
1164
|
}
|
|
751
|
-
async sendPrompt(text) {
|
|
1165
|
+
async sendPrompt(text, actor) {
|
|
752
1166
|
const trimmed = text.trim();
|
|
753
1167
|
if (!trimmed) {
|
|
754
1168
|
throw new Error("Prompt is empty.");
|
|
755
1169
|
}
|
|
756
|
-
return this.sendEnvelope(toPromptEnvelope(trimmed));
|
|
1170
|
+
return this.sendEnvelope({ ...toPromptEnvelope(trimmed), activityActor: actor }, actor);
|
|
757
1171
|
}
|
|
758
|
-
async sendUploadPrompt(options) {
|
|
1172
|
+
async sendUploadPrompt(options, actor) {
|
|
759
1173
|
const text = options.text?.trim() ?? "";
|
|
760
1174
|
const files = options.files.filter((file) => file.data.byteLength > 0);
|
|
761
1175
|
if (!text && files.length === 0) {
|
|
@@ -790,9 +1204,32 @@ export class RelayRuntime {
|
|
|
790
1204
|
const transcript = result.text.trim();
|
|
791
1205
|
if (transcript) {
|
|
792
1206
|
transcriptParts.push(`Audio transcript (${staged.safeName}, via ${result.backend}):\n${transcript}`);
|
|
1207
|
+
this.appendActivity({
|
|
1208
|
+
source: "web",
|
|
1209
|
+
status: "completed",
|
|
1210
|
+
type: "voice_transcribed",
|
|
1211
|
+
threadId: session.getInfo().threadId,
|
|
1212
|
+
workspace,
|
|
1213
|
+
agentId: session.getInfo().agentId,
|
|
1214
|
+
actor,
|
|
1215
|
+
detail: `${staged.safeName} via ${result.backend}`,
|
|
1216
|
+
durationMs: result.durationMs,
|
|
1217
|
+
});
|
|
793
1218
|
}
|
|
794
1219
|
}
|
|
795
1220
|
}
|
|
1221
|
+
if (stagedFiles.length > 0) {
|
|
1222
|
+
this.appendActivity({
|
|
1223
|
+
source: "web",
|
|
1224
|
+
status: "info",
|
|
1225
|
+
type: "attachment_staged",
|
|
1226
|
+
threadId: session.getInfo().threadId,
|
|
1227
|
+
workspace,
|
|
1228
|
+
agentId: session.getInfo().agentId,
|
|
1229
|
+
actor,
|
|
1230
|
+
detail: `${stagedFiles.length} file(s): ${stagedFiles.map((file) => file.safeName).join(", ")}`,
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
796
1233
|
const audioOnly = stagedFiles.length > 0 && stagedFiles.every((file) => file.mimeType.startsWith("audio/"));
|
|
797
1234
|
if (this.config.voiceTranscribeOnly && audioOnly && !text) {
|
|
798
1235
|
return {
|
|
@@ -813,14 +1250,15 @@ export class RelayRuntime {
|
|
|
813
1250
|
if (stagedFiles.length > 0) {
|
|
814
1251
|
promptInput.stagedFileInstructions = buildFileInstructions(stagedFiles, outDir);
|
|
815
1252
|
}
|
|
816
|
-
const result = await this.sendEnvelope(toPromptEnvelope(promptInput, outDir));
|
|
1253
|
+
const result = await this.sendEnvelope({ ...toPromptEnvelope(promptInput, outDir), activityActor: actor }, actor);
|
|
817
1254
|
return {
|
|
818
1255
|
...result,
|
|
819
1256
|
transcript: transcriptParts.join("\n\n") || undefined,
|
|
820
1257
|
files: uploadFileDtos(stagedFiles),
|
|
821
1258
|
};
|
|
822
1259
|
}
|
|
823
|
-
async sendEnvelope(envelope) {
|
|
1260
|
+
async sendEnvelope(envelope, actor) {
|
|
1261
|
+
const activityActor = envelope.activityActor ?? actor;
|
|
824
1262
|
const session = await this.getSession(false);
|
|
825
1263
|
const external = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
|
|
826
1264
|
if (session.isProcessing() || external?.activity.active) {
|
|
@@ -833,6 +1271,7 @@ export class RelayRuntime {
|
|
|
833
1271
|
threadId: info.threadId,
|
|
834
1272
|
workspace: info.workspace,
|
|
835
1273
|
agentId: info.agentId,
|
|
1274
|
+
actor: activityActor,
|
|
836
1275
|
prompt: envelope.description,
|
|
837
1276
|
detail: external?.activity.active
|
|
838
1277
|
? `Queued because ${external.agentLabel} CLI is still processing another task.`
|
|
@@ -841,11 +1280,12 @@ export class RelayRuntime {
|
|
|
841
1280
|
this.appendAudit({
|
|
842
1281
|
action: "prompt_queued",
|
|
843
1282
|
status: "ok",
|
|
844
|
-
contextKey:
|
|
1283
|
+
contextKey: this.contextKey,
|
|
845
1284
|
agentId: info.agentId,
|
|
846
1285
|
threadId: info.threadId,
|
|
847
1286
|
workspace: info.workspace,
|
|
848
1287
|
promptId: queued.id,
|
|
1288
|
+
actor: activityActor,
|
|
849
1289
|
description: envelope.description,
|
|
850
1290
|
});
|
|
851
1291
|
if (external?.activity.active) {
|
|
@@ -854,7 +1294,7 @@ export class RelayRuntime {
|
|
|
854
1294
|
this.broadcastQueue();
|
|
855
1295
|
return { queued: true, queueId: queued.id };
|
|
856
1296
|
}
|
|
857
|
-
void this.runPrompt(session, envelope).catch((error) => {
|
|
1297
|
+
void this.runPrompt(session, { ...envelope, activityActor }).catch((error) => {
|
|
858
1298
|
this.broadcast({ type: "turn_error", id: this.currentTurnId ?? "turn", error: friendlyErrorText(error), at: new Date().toISOString() });
|
|
859
1299
|
});
|
|
860
1300
|
return { queued: false };
|
|
@@ -865,7 +1305,9 @@ export class RelayRuntime {
|
|
|
865
1305
|
queuePaused() {
|
|
866
1306
|
return this.queueService.isPaused();
|
|
867
1307
|
}
|
|
868
|
-
queueAction(action, id) {
|
|
1308
|
+
queueAction(action, id, actor) {
|
|
1309
|
+
const before = this.queueService.rawList();
|
|
1310
|
+
const affected = id ? before.find((item) => item.id === id) : undefined;
|
|
869
1311
|
this.queueService.apply(action, id);
|
|
870
1312
|
if (id && action === "run") {
|
|
871
1313
|
void this.drainQueue().catch((error) => this.broadcastStatus(friendlyErrorText(error), "error"));
|
|
@@ -873,15 +1315,18 @@ export class RelayRuntime {
|
|
|
873
1315
|
this.appendActivity({
|
|
874
1316
|
source: "web",
|
|
875
1317
|
status: "info",
|
|
876
|
-
type:
|
|
1318
|
+
type: `queue_${action}`,
|
|
877
1319
|
threadId: null,
|
|
878
1320
|
workspace: this.config.workspace,
|
|
879
|
-
|
|
1321
|
+
actor,
|
|
1322
|
+
prompt: affected?.description,
|
|
1323
|
+
detail: id ? `${action}: ${id}` : `${action}: ${before.length} queued`,
|
|
880
1324
|
});
|
|
881
1325
|
this.appendAudit({
|
|
882
1326
|
action: "queue_updated",
|
|
883
1327
|
status: "ok",
|
|
884
|
-
contextKey:
|
|
1328
|
+
contextKey: this.contextKey,
|
|
1329
|
+
actor,
|
|
885
1330
|
description: id ? `${action}: ${id}` : action,
|
|
886
1331
|
});
|
|
887
1332
|
this.broadcastQueue();
|
|
@@ -895,13 +1340,39 @@ export class RelayRuntime {
|
|
|
895
1340
|
const session = await this.getSession(true);
|
|
896
1341
|
return this.artifactService.get(session.getInfo().workspace, turnId);
|
|
897
1342
|
}
|
|
898
|
-
async deleteArtifact(turnId) {
|
|
1343
|
+
async deleteArtifact(turnId, actor) {
|
|
899
1344
|
const session = await this.getSession(true);
|
|
900
|
-
|
|
1345
|
+
const info = this.publicInfo(session);
|
|
1346
|
+
const removed = await this.artifactService.delete(info.workspace, turnId);
|
|
1347
|
+
this.appendActivity({
|
|
1348
|
+
source: "web",
|
|
1349
|
+
status: removed ? "info" : "failed",
|
|
1350
|
+
type: "artifact_deleted",
|
|
1351
|
+
threadId: info.threadId,
|
|
1352
|
+
workspace: info.workspace,
|
|
1353
|
+
agentId: info.agentId,
|
|
1354
|
+
actor,
|
|
1355
|
+
detail: turnId,
|
|
1356
|
+
});
|
|
1357
|
+
return removed;
|
|
901
1358
|
}
|
|
902
|
-
async createArtifactZip(turnId) {
|
|
1359
|
+
async createArtifactZip(turnId, actor) {
|
|
903
1360
|
const session = await this.getSession(true);
|
|
904
|
-
|
|
1361
|
+
const info = this.publicInfo(session);
|
|
1362
|
+
const zip = await this.artifactService.createZip(info.workspace, turnId);
|
|
1363
|
+
if (zip) {
|
|
1364
|
+
this.appendActivity({
|
|
1365
|
+
source: "web",
|
|
1366
|
+
status: "info",
|
|
1367
|
+
type: "artifact_zip_created",
|
|
1368
|
+
threadId: info.threadId,
|
|
1369
|
+
workspace: info.workspace,
|
|
1370
|
+
agentId: info.agentId,
|
|
1371
|
+
actor,
|
|
1372
|
+
detail: zip.name,
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
return zip;
|
|
905
1376
|
}
|
|
906
1377
|
async artifactPreview(turnId, relativePath) {
|
|
907
1378
|
const session = await this.getSession(true);
|
|
@@ -916,7 +1387,7 @@ export class RelayRuntime {
|
|
|
916
1387
|
}
|
|
917
1388
|
return readFormattedLogTail(lines);
|
|
918
1389
|
}
|
|
919
|
-
clearLogs(target = "connector") {
|
|
1390
|
+
clearLogs(target = "connector", actor) {
|
|
920
1391
|
const result = clearLogFile(target === "update" ? getUpdateLogPath() : target === "agent-updates" ? getAgentUpdateLogPath() : getConnectorLogPath());
|
|
921
1392
|
this.appendActivity({
|
|
922
1393
|
source: "web",
|
|
@@ -924,11 +1395,20 @@ export class RelayRuntime {
|
|
|
924
1395
|
type: "logs_cleared",
|
|
925
1396
|
threadId: null,
|
|
926
1397
|
workspace: this.config.workspace,
|
|
1398
|
+
actor,
|
|
927
1399
|
detail: `Cleared ${target} log.`,
|
|
928
1400
|
});
|
|
1401
|
+
this.appendAudit({
|
|
1402
|
+
action: "command",
|
|
1403
|
+
status: "ok",
|
|
1404
|
+
contextKey: this.contextKey,
|
|
1405
|
+
actor,
|
|
1406
|
+
description: `clear ${target} log`,
|
|
1407
|
+
detail: result.filePath,
|
|
1408
|
+
});
|
|
929
1409
|
return { ok: true, filePath: result.filePath, clearedAt: result.clearedAt.toISOString() };
|
|
930
1410
|
}
|
|
931
|
-
restartConnector() {
|
|
1411
|
+
restartConnector(actor) {
|
|
932
1412
|
spawnConnectorRestart();
|
|
933
1413
|
this.broadcastStatus("Restart requested. The dashboard may disconnect briefly.", "warn");
|
|
934
1414
|
this.appendActivity({
|
|
@@ -937,8 +1417,16 @@ export class RelayRuntime {
|
|
|
937
1417
|
type: "restart_requested",
|
|
938
1418
|
threadId: null,
|
|
939
1419
|
workspace: this.config.workspace,
|
|
1420
|
+
actor,
|
|
940
1421
|
detail: "Dashboard requested a connector restart.",
|
|
941
1422
|
});
|
|
1423
|
+
this.appendAudit({
|
|
1424
|
+
action: "command",
|
|
1425
|
+
status: "ok",
|
|
1426
|
+
contextKey: this.contextKey,
|
|
1427
|
+
actor,
|
|
1428
|
+
description: "restart connector",
|
|
1429
|
+
});
|
|
942
1430
|
return { ok: true, message: "Restart requested." };
|
|
943
1431
|
}
|
|
944
1432
|
dispose() {
|
|
@@ -950,7 +1438,215 @@ export class RelayRuntime {
|
|
|
950
1438
|
this.subscribers.clear();
|
|
951
1439
|
}
|
|
952
1440
|
async getSession(deferThreadStart) {
|
|
953
|
-
return this.registry.getOrCreate(
|
|
1441
|
+
return this.registry.getOrCreate(this.contextKey, { deferThreadStart });
|
|
1442
|
+
}
|
|
1443
|
+
async cached(key, producer) {
|
|
1444
|
+
return (await this.cache.get(key, this.config.dashboardCacheTtlMs, producer)).value;
|
|
1445
|
+
}
|
|
1446
|
+
listKnownContextMetadata() {
|
|
1447
|
+
const contexts = new Map();
|
|
1448
|
+
const add = (meta) => {
|
|
1449
|
+
if (meta?.contextKey) {
|
|
1450
|
+
contexts.set(meta.contextKey, meta);
|
|
1451
|
+
}
|
|
1452
|
+
};
|
|
1453
|
+
for (const meta of this.registry.listContexts()) {
|
|
1454
|
+
add(meta);
|
|
1455
|
+
}
|
|
1456
|
+
const sharedRegistry = new SessionRegistry(this.config);
|
|
1457
|
+
try {
|
|
1458
|
+
for (const meta of sharedRegistry.listContexts()) {
|
|
1459
|
+
add(meta);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
finally {
|
|
1463
|
+
sharedRegistry.disposeAll();
|
|
1464
|
+
}
|
|
1465
|
+
const current = this.registry.get(this.contextKey)?.getInfo();
|
|
1466
|
+
if (current) {
|
|
1467
|
+
add({
|
|
1468
|
+
contextKey: this.contextKey,
|
|
1469
|
+
agentId: current.agentId,
|
|
1470
|
+
threadId: current.threadId,
|
|
1471
|
+
workspace: current.workspace,
|
|
1472
|
+
model: current.model,
|
|
1473
|
+
reasoningEffort: current.reasoningEffort,
|
|
1474
|
+
launchProfileId: current.nextLaunchProfileId ?? current.launchProfileId,
|
|
1475
|
+
sessionPath: current.sessionPath,
|
|
1476
|
+
updatedAt: Date.now(),
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
return [...contexts.values()];
|
|
1480
|
+
}
|
|
1481
|
+
discoverRunningConnectorSessions() {
|
|
1482
|
+
const active = [];
|
|
1483
|
+
const terminal = new Set();
|
|
1484
|
+
const now = Date.now();
|
|
1485
|
+
for (const event of this.activityStore.list({ limit: 500 })) {
|
|
1486
|
+
if (!event.threadId || !event.agentId || !event.contextKey) {
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
const key = `${event.source}:${event.contextKey}:${event.agentId}:${event.threadId}`;
|
|
1490
|
+
if (isPromptTerminalActivity(event)) {
|
|
1491
|
+
terminal.add(key);
|
|
1492
|
+
continue;
|
|
1493
|
+
}
|
|
1494
|
+
if (event.type !== "prompt_started" || event.status !== "running" || event.source === "cli") {
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
if (terminal.has(key)) {
|
|
1498
|
+
continue;
|
|
1499
|
+
}
|
|
1500
|
+
const startedMs = Date.parse(event.timestamp);
|
|
1501
|
+
if (!Number.isFinite(startedMs) || now - startedMs > ACTIVE_ACTIVITY_TTL_MS) {
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
active.push({
|
|
1505
|
+
id: `${event.contextKey}:${event.id}`,
|
|
1506
|
+
contextKey: event.contextKey,
|
|
1507
|
+
sourceContextKey: event.contextKey,
|
|
1508
|
+
source: event.source,
|
|
1509
|
+
status: "running",
|
|
1510
|
+
agentId: event.agentId,
|
|
1511
|
+
agentLabel: event.agentId ? agentLabel(event.agentId) : undefined,
|
|
1512
|
+
threadId: event.threadId,
|
|
1513
|
+
workspace: event.workspace,
|
|
1514
|
+
prompt: event.prompt,
|
|
1515
|
+
startedAt: event.timestamp,
|
|
1516
|
+
updatedAt: event.timestamp,
|
|
1517
|
+
durationMs: Math.max(0, now - startedMs),
|
|
1518
|
+
queueLength: this.promptStore.list(event.contextKey).length,
|
|
1519
|
+
queuePaused: this.promptStore.isPaused(event.contextKey),
|
|
1520
|
+
detail: event.actor?.label ? `Started by ${event.actor.label}` : undefined,
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
return active;
|
|
1524
|
+
}
|
|
1525
|
+
discoverActiveCodexSessions(knownContexts, preferences) {
|
|
1526
|
+
if (!this.config.codexEnabled || !enabledAgents(this.config).includes("codex")) {
|
|
1527
|
+
return [];
|
|
1528
|
+
}
|
|
1529
|
+
const capabilities = this.capabilitiesForAgent("codex");
|
|
1530
|
+
if (!capabilities.externalActivity) {
|
|
1531
|
+
return [];
|
|
1532
|
+
}
|
|
1533
|
+
const active = [];
|
|
1534
|
+
const nowMs = Date.now();
|
|
1535
|
+
const staleAfterMs = this.config.codexExternalBusyStaleMs;
|
|
1536
|
+
for (const thread of listCodexThreads(ACTIVE_CODEX_DISCOVERY_LIMIT)) {
|
|
1537
|
+
if (staleAfterMs > 0 && nowMs - thread.updatedAt.getTime() > staleAfterMs) {
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
const meta = {
|
|
1541
|
+
contextKey: `cli:codex:${thread.id}`,
|
|
1542
|
+
agentId: "codex",
|
|
1543
|
+
threadId: thread.id,
|
|
1544
|
+
workspace: thread.cwd,
|
|
1545
|
+
model: thread.model ?? undefined,
|
|
1546
|
+
reasoningEffort: thread.reasoningEffort ?? undefined,
|
|
1547
|
+
updatedAt: thread.updatedAt.getTime(),
|
|
1548
|
+
};
|
|
1549
|
+
const session = this.externalActiveSession(meta, knownContexts, preferences);
|
|
1550
|
+
if (session) {
|
|
1551
|
+
active.push(session);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
return active;
|
|
1555
|
+
}
|
|
1556
|
+
externalActiveSession(meta, knownContexts, preferences) {
|
|
1557
|
+
if (!meta.threadId) {
|
|
1558
|
+
return null;
|
|
1559
|
+
}
|
|
1560
|
+
const agentId = isAgentId(meta.agentId) ? meta.agentId : this.config.defaultAgent;
|
|
1561
|
+
if (!enabledAgents(this.config).includes(agentId)) {
|
|
1562
|
+
return null;
|
|
1563
|
+
}
|
|
1564
|
+
const capabilities = this.capabilitiesForAgent(agentId);
|
|
1565
|
+
if (!capabilities.externalActivity) {
|
|
1566
|
+
return null;
|
|
1567
|
+
}
|
|
1568
|
+
if (agentId === "codex" &&
|
|
1569
|
+
meta.updatedAt &&
|
|
1570
|
+
this.config.codexExternalBusyStaleMs > 0 &&
|
|
1571
|
+
Date.now() - meta.updatedAt > this.config.codexExternalBusyStaleMs) {
|
|
1572
|
+
return null;
|
|
1573
|
+
}
|
|
1574
|
+
const snapshot = getExternalSnapshotForSession(this.sessionStubForMetadata(meta, agentId, capabilities), this.config, {
|
|
1575
|
+
maxEvents: 8,
|
|
1576
|
+
});
|
|
1577
|
+
if (!snapshot?.activity.active) {
|
|
1578
|
+
return null;
|
|
1579
|
+
}
|
|
1580
|
+
const startedAt = snapshot.activity.startedAt?.toISOString() ?? new Date().toISOString();
|
|
1581
|
+
const updatedAt = snapshot.activity.updatedAt?.toISOString() ?? new Date().toISOString();
|
|
1582
|
+
const startedMs = Date.parse(startedAt);
|
|
1583
|
+
const sourceContextKey = `cli:${snapshot.agentId}:${snapshot.threadId}`;
|
|
1584
|
+
const mirrorChannels = this.mirrorRegistry.activeMirrorsForThread(snapshot.agentId, snapshot.threadId, knownContexts, preferences);
|
|
1585
|
+
const queueLength = this.mirrorRegistry.queueLengthForExternalSource(sourceContextKey, mirrorChannels);
|
|
1586
|
+
const mirrorDetail = mirrorChannels.length > 0
|
|
1587
|
+
? `Mirroring: ${mirrorChannels.map((mirror) => `${mirror.source} ${mirror.mode}`).join(", ")}`
|
|
1588
|
+
: "Mirroring: none";
|
|
1589
|
+
return {
|
|
1590
|
+
id: `${sourceContextKey}:${snapshot.activity.turnId ?? snapshot.threadId}`,
|
|
1591
|
+
contextKey: sourceContextKey,
|
|
1592
|
+
sourceContextKey,
|
|
1593
|
+
source: "cli",
|
|
1594
|
+
status: "external",
|
|
1595
|
+
agentId: snapshot.agentId,
|
|
1596
|
+
agentLabel: snapshot.agentLabel,
|
|
1597
|
+
threadId: snapshot.threadId,
|
|
1598
|
+
workspace: meta.workspace,
|
|
1599
|
+
prompt: snapshot.latestUserMessage ?? undefined,
|
|
1600
|
+
currentTool: snapshot.latestToolName ?? undefined,
|
|
1601
|
+
lastTool: snapshot.latestToolName ?? undefined,
|
|
1602
|
+
startedAt,
|
|
1603
|
+
updatedAt,
|
|
1604
|
+
durationMs: Number.isFinite(startedMs) ? Math.max(0, Date.now() - startedMs) : 0,
|
|
1605
|
+
queueLength,
|
|
1606
|
+
queuePaused: this.mirrorRegistry.queuePausedForExternalSource(sourceContextKey, mirrorChannels),
|
|
1607
|
+
mirrorChannels,
|
|
1608
|
+
detail: `${mirrorDetail} | ${snapshot.sourceLabel}: ${snapshot.sourcePath}`,
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
sessionStubForMetadata(meta, agentId, capabilities) {
|
|
1612
|
+
const info = {
|
|
1613
|
+
agentId,
|
|
1614
|
+
agentLabel: agentLabel(agentId),
|
|
1615
|
+
threadId: meta.threadId,
|
|
1616
|
+
workspace: meta.workspace,
|
|
1617
|
+
model: meta.model,
|
|
1618
|
+
reasoningEffort: meta.reasoningEffort,
|
|
1619
|
+
launchProfileId: meta.launchProfileId ?? this.config.defaultLaunchProfileId,
|
|
1620
|
+
launchProfileLabel: meta.launchProfileId ?? this.config.defaultLaunchProfileId,
|
|
1621
|
+
launchProfileBehavior: "-",
|
|
1622
|
+
sandboxMode: "-",
|
|
1623
|
+
approvalPolicy: "-",
|
|
1624
|
+
fastMode: false,
|
|
1625
|
+
unsafeLaunch: false,
|
|
1626
|
+
sessionPath: meta.sessionPath,
|
|
1627
|
+
capabilities,
|
|
1628
|
+
};
|
|
1629
|
+
return {
|
|
1630
|
+
getInfo: () => info,
|
|
1631
|
+
getActiveThreadId: () => meta.threadId,
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
capabilitiesForAgent(agentId) {
|
|
1635
|
+
return listAgentAdapterDescriptors().find((descriptor) => descriptor.id === agentId)?.capabilities ?? CODEX_AGENT_CAPABILITIES;
|
|
1636
|
+
}
|
|
1637
|
+
activeSessionKey(session) {
|
|
1638
|
+
return session.threadId ? `${session.agentId ?? "unknown"}:${session.threadId}` : session.id;
|
|
1639
|
+
}
|
|
1640
|
+
preferredActiveSession(existing, candidate) {
|
|
1641
|
+
if (!existing) {
|
|
1642
|
+
return candidate;
|
|
1643
|
+
}
|
|
1644
|
+
const existingPriority = activeSessionPriority(existing);
|
|
1645
|
+
const candidatePriority = activeSessionPriority(candidate);
|
|
1646
|
+
if (candidatePriority !== existingPriority) {
|
|
1647
|
+
return candidatePriority > existingPriority ? candidate : existing;
|
|
1648
|
+
}
|
|
1649
|
+
return Date.parse(candidate.updatedAt) >= Date.parse(existing.updatedAt) ? candidate : existing;
|
|
954
1650
|
}
|
|
955
1651
|
async getControlSession(agentId) {
|
|
956
1652
|
const active = await this.getSession(true);
|
|
@@ -1039,169 +1735,14 @@ export class RelayRuntime {
|
|
|
1039
1735
|
}
|
|
1040
1736
|
}
|
|
1041
1737
|
async runPrompt(session, envelope) {
|
|
1042
|
-
await this.ensureActiveThread(session);
|
|
1043
|
-
const info = session.getInfo();
|
|
1044
|
-
if ((info.capabilities ?? CODEX_AGENT_CAPABILITIES).auth) {
|
|
1045
|
-
const auth = await this.checkAgentAuth(info);
|
|
1046
|
-
if (!auth.authenticated) {
|
|
1047
|
-
throw new Error(`${agentLabel(info.agentId)} is not authenticated: ${auth.detail}`);
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
1738
|
const workspacePolicy = evaluateWorkspacePolicy(session.getInfo().workspace, this.config);
|
|
1051
1739
|
if (!workspacePolicy.allowed) {
|
|
1052
1740
|
throw new Error(workspacePolicy.warning ?? "Current workspace is blocked by policy.");
|
|
1053
1741
|
}
|
|
1054
|
-
const turnId = randomUUID().slice(0, 12);
|
|
1055
|
-
this.currentTurnId = turnId;
|
|
1056
|
-
this.currentTurnStartedAt = Date.now();
|
|
1057
|
-
this.accumulatedText = "";
|
|
1058
|
-
this.currentProgress = {
|
|
1059
|
-
id: turnId,
|
|
1060
|
-
source: "web",
|
|
1061
|
-
status: "running",
|
|
1062
|
-
prompt: envelope.description,
|
|
1063
|
-
agentId: info.agentId,
|
|
1064
|
-
agentLabel: info.agentLabel,
|
|
1065
|
-
threadId: info.threadId,
|
|
1066
|
-
workspace: info.workspace,
|
|
1067
|
-
startedAt: new Date(this.currentTurnStartedAt).toISOString(),
|
|
1068
|
-
updatedAt: new Date(this.currentTurnStartedAt).toISOString(),
|
|
1069
|
-
durationMs: 0,
|
|
1070
|
-
outputChars: 0,
|
|
1071
|
-
tools: [],
|
|
1072
|
-
};
|
|
1073
|
-
this.queueService.setLastPrompt(envelope);
|
|
1074
|
-
const startedDate = new Date();
|
|
1075
|
-
const startedAt = startedDate.toISOString();
|
|
1076
|
-
this.chatStore.append({
|
|
1077
|
-
threadId: info.threadId ?? "pending",
|
|
1078
|
-
role: "user",
|
|
1079
|
-
text: envelope.description,
|
|
1080
|
-
source: "web",
|
|
1081
|
-
turnId,
|
|
1082
|
-
timestamp: startedAt,
|
|
1083
|
-
});
|
|
1084
|
-
this.appendActivity({
|
|
1085
|
-
source: "web",
|
|
1086
|
-
status: "running",
|
|
1087
|
-
type: "prompt_started",
|
|
1088
|
-
threadId: info.threadId,
|
|
1089
|
-
workspace: info.workspace,
|
|
1090
|
-
agentId: info.agentId,
|
|
1091
|
-
prompt: envelope.description,
|
|
1092
|
-
});
|
|
1093
|
-
this.appendAudit({
|
|
1094
|
-
action: "prompt_started",
|
|
1095
|
-
status: "ok",
|
|
1096
|
-
contextKey: WEB_CONTEXT_KEY,
|
|
1097
|
-
agentId: info.agentId,
|
|
1098
|
-
threadId: info.threadId,
|
|
1099
|
-
workspace: info.workspace,
|
|
1100
|
-
description: envelope.description,
|
|
1101
|
-
});
|
|
1102
|
-
this.broadcast({ type: "turn_start", id: turnId, prompt: envelope.description, at: startedAt, source: "web" });
|
|
1103
|
-
const callbacks = {
|
|
1104
|
-
onTextDelta: (delta) => {
|
|
1105
|
-
this.accumulatedText += delta;
|
|
1106
|
-
this.updateCurrentProgress({ outputChars: this.accumulatedText.length });
|
|
1107
|
-
this.broadcast({ type: "text_delta", id: turnId, delta });
|
|
1108
|
-
},
|
|
1109
|
-
onToolStart: (toolName, toolCallId) => {
|
|
1110
|
-
this.addCurrentTool(toolName);
|
|
1111
|
-
this.broadcast({ type: "tool_start", id: turnId, toolCallId, toolName });
|
|
1112
|
-
},
|
|
1113
|
-
onToolUpdate: (toolCallId, partialResult) => {
|
|
1114
|
-
this.updateCurrentProgress();
|
|
1115
|
-
this.broadcast({ type: "tool_update", id: turnId, toolCallId, partialResult });
|
|
1116
|
-
},
|
|
1117
|
-
onToolEnd: (toolCallId, isError) => {
|
|
1118
|
-
this.updateCurrentProgress({ currentTool: undefined });
|
|
1119
|
-
this.broadcast({ type: "tool_end", id: turnId, toolCallId, isError });
|
|
1120
|
-
},
|
|
1121
|
-
onTodoUpdate: (items) => {
|
|
1122
|
-
this.updateCurrentProgress({ detail: `Plan: ${items.filter((item) => item.completed).length}/${items.length} done` });
|
|
1123
|
-
this.broadcast({ type: "todo_update", id: turnId, items });
|
|
1124
|
-
},
|
|
1125
|
-
onTurnComplete: () => { },
|
|
1126
|
-
onAgentEnd: () => this.broadcast({ type: "turn_complete", id: turnId, at: new Date().toISOString() }),
|
|
1127
|
-
};
|
|
1128
1742
|
try {
|
|
1129
|
-
await
|
|
1130
|
-
this.updateSession(session);
|
|
1131
|
-
await this.artifactService.persistWorkspaceArtifactsForTurn(session.getInfo().workspace, turnId, startedDate);
|
|
1132
|
-
if (this.accumulatedText.trim()) {
|
|
1133
|
-
this.chatStore.append({
|
|
1134
|
-
threadId: info.threadId ?? "pending",
|
|
1135
|
-
role: "agent",
|
|
1136
|
-
text: this.accumulatedText,
|
|
1137
|
-
source: "web",
|
|
1138
|
-
turnId,
|
|
1139
|
-
});
|
|
1140
|
-
}
|
|
1141
|
-
this.appendActivity({
|
|
1142
|
-
source: "web",
|
|
1143
|
-
status: "completed",
|
|
1144
|
-
type: "prompt_completed",
|
|
1145
|
-
threadId: info.threadId,
|
|
1146
|
-
workspace: info.workspace,
|
|
1147
|
-
agentId: info.agentId,
|
|
1148
|
-
prompt: envelope.description,
|
|
1149
|
-
durationMs: Date.now() - this.currentTurnStartedAt,
|
|
1150
|
-
});
|
|
1151
|
-
this.appendAudit({
|
|
1152
|
-
action: "prompt_completed",
|
|
1153
|
-
status: "ok",
|
|
1154
|
-
contextKey: WEB_CONTEXT_KEY,
|
|
1155
|
-
agentId: info.agentId,
|
|
1156
|
-
threadId: info.threadId,
|
|
1157
|
-
workspace: info.workspace,
|
|
1158
|
-
description: envelope.description,
|
|
1159
|
-
});
|
|
1160
|
-
this.updateCurrentProgress({ status: "completed" });
|
|
1161
|
-
this.broadcast({ type: "turn_complete", id: turnId, at: new Date().toISOString() });
|
|
1162
|
-
this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
|
|
1163
|
-
}
|
|
1164
|
-
catch (error) {
|
|
1165
|
-
const errorText = friendlyErrorText(error);
|
|
1166
|
-
this.chatStore.append({
|
|
1167
|
-
threadId: info.threadId ?? "pending",
|
|
1168
|
-
role: "system",
|
|
1169
|
-
text: `Error: ${errorText}`,
|
|
1170
|
-
source: "web",
|
|
1171
|
-
turnId,
|
|
1172
|
-
});
|
|
1173
|
-
this.appendActivity({
|
|
1174
|
-
source: "web",
|
|
1175
|
-
status: "failed",
|
|
1176
|
-
type: "prompt_failed",
|
|
1177
|
-
threadId: info.threadId,
|
|
1178
|
-
workspace: info.workspace,
|
|
1179
|
-
agentId: info.agentId,
|
|
1180
|
-
prompt: envelope.description,
|
|
1181
|
-
detail: errorText,
|
|
1182
|
-
durationMs: Date.now() - this.currentTurnStartedAt,
|
|
1183
|
-
});
|
|
1184
|
-
this.appendAudit({
|
|
1185
|
-
action: "prompt_failed",
|
|
1186
|
-
status: "failed",
|
|
1187
|
-
contextKey: WEB_CONTEXT_KEY,
|
|
1188
|
-
agentId: info.agentId,
|
|
1189
|
-
threadId: info.threadId,
|
|
1190
|
-
workspace: info.workspace,
|
|
1191
|
-
description: envelope.description,
|
|
1192
|
-
detail: errorText,
|
|
1193
|
-
});
|
|
1194
|
-
this.updateCurrentProgress({ status: "failed", detail: errorText });
|
|
1195
|
-
this.broadcast({ type: "turn_error", id: turnId, error: errorText, at: new Date().toISOString() });
|
|
1196
|
-
this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
|
|
1197
|
-
throw error;
|
|
1743
|
+
await this.turnService.run(session, envelope);
|
|
1198
1744
|
}
|
|
1199
1745
|
finally {
|
|
1200
|
-
this.currentTurnId = null;
|
|
1201
|
-
if (this.currentProgress) {
|
|
1202
|
-
this.currentProgress.durationMs = Date.now() - this.currentTurnStartedAt;
|
|
1203
|
-
this.currentProgress.updatedAt = new Date().toISOString();
|
|
1204
|
-
}
|
|
1205
1746
|
await this.drainQueue();
|
|
1206
1747
|
}
|
|
1207
1748
|
}
|
|
@@ -1231,9 +1772,43 @@ export class RelayRuntime {
|
|
|
1231
1772
|
}
|
|
1232
1773
|
}
|
|
1233
1774
|
updateSession(session) {
|
|
1234
|
-
this.registry.updateMetadata(
|
|
1775
|
+
this.registry.updateMetadata(this.contextKey, session);
|
|
1235
1776
|
this.broadcast({ type: "session_update", session: this.publicInfo(session) });
|
|
1236
1777
|
}
|
|
1778
|
+
recordActivity(input) {
|
|
1779
|
+
return this.appendActivity(input);
|
|
1780
|
+
}
|
|
1781
|
+
recordAgentUpdateLifecycle(job) {
|
|
1782
|
+
const previous = this.agentUpdateStates.get(job.id);
|
|
1783
|
+
const actor = this.agentUpdateActors.get(job.id);
|
|
1784
|
+
if (job.needsInput && !previous?.needsInput) {
|
|
1785
|
+
this.appendActivity({
|
|
1786
|
+
source: "web",
|
|
1787
|
+
status: "info",
|
|
1788
|
+
type: "agent_update_input_required",
|
|
1789
|
+
agentId: job.agentId,
|
|
1790
|
+
threadId: null,
|
|
1791
|
+
workspace: this.config.workspace,
|
|
1792
|
+
actor,
|
|
1793
|
+
detail: `${job.agentLabel} ${job.operation} may require input.`,
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
if (job.status !== "running" && previous?.status === "running") {
|
|
1797
|
+
this.appendActivity({
|
|
1798
|
+
source: "web",
|
|
1799
|
+
status: job.status === "completed" ? "completed" : job.status === "cancelled" ? "aborted" : "failed",
|
|
1800
|
+
type: job.operation === "install" ? `agent_install_${job.status}` : `agent_update_${job.status}`,
|
|
1801
|
+
agentId: job.agentId,
|
|
1802
|
+
threadId: null,
|
|
1803
|
+
workspace: this.config.workspace,
|
|
1804
|
+
actor,
|
|
1805
|
+
detail: job.error ?? `${job.agentLabel} ${job.operation} ${job.status}.`,
|
|
1806
|
+
durationMs: Math.max(0, Date.parse(job.finishedAt ?? job.updatedAt) - Date.parse(job.startedAt)),
|
|
1807
|
+
});
|
|
1808
|
+
this.agentUpdateActors.delete(job.id);
|
|
1809
|
+
}
|
|
1810
|
+
this.agentUpdateStates.set(job.id, { status: job.status, needsInput: job.needsInput });
|
|
1811
|
+
}
|
|
1237
1812
|
appendActivity(input) {
|
|
1238
1813
|
const event = this.activityStore.append(this.enrichActivityInput(input));
|
|
1239
1814
|
this.broadcast({ type: "activity_update", events: this.activity({ limit: 50 }) });
|
|
@@ -1303,6 +1878,23 @@ export class RelayRuntime {
|
|
|
1303
1878
|
this.subscribers.delete(subscriber);
|
|
1304
1879
|
}
|
|
1305
1880
|
}
|
|
1881
|
+
if (shouldRefreshActiveSessions(event)) {
|
|
1882
|
+
this.scheduleActiveSessionsBroadcast();
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
scheduleActiveSessionsBroadcast() {
|
|
1886
|
+
if (this.activeSessionsBroadcastTimer) {
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
const delayMs = Math.max(0, 1_000 - (Date.now() - this.activeSessionsLastBroadcastAt));
|
|
1890
|
+
this.activeSessionsBroadcastTimer = setTimeout(() => {
|
|
1891
|
+
this.activeSessionsBroadcastTimer = null;
|
|
1892
|
+
this.activeSessionsLastBroadcastAt = Date.now();
|
|
1893
|
+
void this.activeSessions()
|
|
1894
|
+
.then((active) => this.broadcast({ type: "active_sessions_update", active }))
|
|
1895
|
+
.catch(() => { });
|
|
1896
|
+
}, delayMs);
|
|
1897
|
+
this.activeSessionsBroadcastTimer.unref?.();
|
|
1306
1898
|
}
|
|
1307
1899
|
publicInfo(session) {
|
|
1308
1900
|
const info = session.getInfo();
|
|
@@ -1315,92 +1907,3 @@ export class RelayRuntime {
|
|
|
1315
1907
|
};
|
|
1316
1908
|
}
|
|
1317
1909
|
}
|
|
1318
|
-
function cliHealthForAgent(agentId, health) {
|
|
1319
|
-
if (agentId === "pi") {
|
|
1320
|
-
return { path: health.piCliPath, label: health.piCli, version: health.piCliVersion };
|
|
1321
|
-
}
|
|
1322
|
-
if (agentId === "hermes") {
|
|
1323
|
-
return { path: health.hermesCliPath, label: health.hermesCli, version: health.hermesCliVersion };
|
|
1324
|
-
}
|
|
1325
|
-
if (agentId === "openclaw") {
|
|
1326
|
-
return { path: health.openClawCliPath, label: health.openClawCli, version: health.openClawCliVersion };
|
|
1327
|
-
}
|
|
1328
|
-
if (agentId === "claude-code") {
|
|
1329
|
-
return { path: health.claudeCodeCliPath, label: health.claudeCodeCli, version: health.claudeCodeCliVersion };
|
|
1330
|
-
}
|
|
1331
|
-
return { path: health.codexCliPath, label: health.codexCli, version: health.codexCliVersion };
|
|
1332
|
-
}
|
|
1333
|
-
function versionCheckForAgent(agentId, versions) {
|
|
1334
|
-
if (agentId === "pi")
|
|
1335
|
-
return versions.pi;
|
|
1336
|
-
if (agentId === "hermes")
|
|
1337
|
-
return versions.hermes;
|
|
1338
|
-
if (agentId === "openclaw")
|
|
1339
|
-
return versions.openclaw;
|
|
1340
|
-
if (agentId === "claude-code")
|
|
1341
|
-
return versions.claudeCode;
|
|
1342
|
-
return versions.codex;
|
|
1343
|
-
}
|
|
1344
|
-
function hostLoginCommand(info, config) {
|
|
1345
|
-
if (info.agentId === "hermes") {
|
|
1346
|
-
return `${config.hermesCliPath ?? "hermes"} login --no-browser`;
|
|
1347
|
-
}
|
|
1348
|
-
if (info.agentId === "claude-code") {
|
|
1349
|
-
return `${config.claudeCodeCliPath ?? "claude"} auth login`;
|
|
1350
|
-
}
|
|
1351
|
-
if (info.agentId === "pi") {
|
|
1352
|
-
return `${config.piCliPath ?? "pi"} auth login`;
|
|
1353
|
-
}
|
|
1354
|
-
if (info.agentId === "openclaw") {
|
|
1355
|
-
return `${config.openClawCliPath ?? "openclaw"} login`;
|
|
1356
|
-
}
|
|
1357
|
-
return "codex login --device-auth";
|
|
1358
|
-
}
|
|
1359
|
-
function hostLogoutCommand(info, config) {
|
|
1360
|
-
if (info.agentId === "hermes") {
|
|
1361
|
-
return `${config.hermesCliPath ?? "hermes"} logout`;
|
|
1362
|
-
}
|
|
1363
|
-
if (info.agentId === "claude-code") {
|
|
1364
|
-
return `${config.claudeCodeCliPath ?? "claude"} auth logout`;
|
|
1365
|
-
}
|
|
1366
|
-
if (info.agentId === "pi") {
|
|
1367
|
-
return `${config.piCliPath ?? "pi"} auth logout`;
|
|
1368
|
-
}
|
|
1369
|
-
if (info.agentId === "openclaw") {
|
|
1370
|
-
return `${config.openClawCliPath ?? "openclaw"} logout`;
|
|
1371
|
-
}
|
|
1372
|
-
return "codex logout";
|
|
1373
|
-
}
|
|
1374
|
-
function normalizeMimeType(value, name) {
|
|
1375
|
-
const configured = value?.trim();
|
|
1376
|
-
if (configured) {
|
|
1377
|
-
return configured;
|
|
1378
|
-
}
|
|
1379
|
-
const extension = path.extname(name).toLowerCase();
|
|
1380
|
-
if ([".jpg", ".jpeg"].includes(extension))
|
|
1381
|
-
return "image/jpeg";
|
|
1382
|
-
if (extension === ".png")
|
|
1383
|
-
return "image/png";
|
|
1384
|
-
if (extension === ".gif")
|
|
1385
|
-
return "image/gif";
|
|
1386
|
-
if (extension === ".webp")
|
|
1387
|
-
return "image/webp";
|
|
1388
|
-
if (extension === ".mp3")
|
|
1389
|
-
return "audio/mpeg";
|
|
1390
|
-
if (extension === ".wav")
|
|
1391
|
-
return "audio/wav";
|
|
1392
|
-
if (extension === ".ogg" || extension === ".oga")
|
|
1393
|
-
return "audio/ogg";
|
|
1394
|
-
if (extension === ".m4a")
|
|
1395
|
-
return "audio/mp4";
|
|
1396
|
-
if (extension === ".webm")
|
|
1397
|
-
return "audio/webm";
|
|
1398
|
-
return "application/octet-stream";
|
|
1399
|
-
}
|
|
1400
|
-
function uploadFileDtos(files) {
|
|
1401
|
-
return files.map((file) => ({
|
|
1402
|
-
name: file.safeName,
|
|
1403
|
-
mimeType: file.mimeType,
|
|
1404
|
-
sizeBytes: file.sizeBytes,
|
|
1405
|
-
}));
|
|
1406
|
-
}
|