@nordbyte/nordrelay 0.5.2 → 0.6.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 +63 -11
- package/README.md +90 -19
- package/dist/access-control.js +1 -0
- package/dist/activity-events.js +44 -0
- package/dist/audit-log.js +40 -2
- package/dist/bot-rendering.js +10 -7
- package/dist/bot.js +458 -5
- package/dist/channel-actions.js +7 -2
- package/dist/channel-adapter.js +34 -7
- package/dist/channel-command-service.js +156 -0
- package/dist/channel-turn-service.js +237 -0
- package/dist/config-metadata.js +78 -13
- package/dist/config.js +77 -7
- package/dist/context-key.js +77 -5
- package/dist/discord-artifacts.js +165 -0
- package/dist/discord-bot.js +2014 -0
- package/dist/discord-channel-runtime.js +133 -0
- package/dist/discord-command-surface.js +119 -0
- package/dist/discord-rate-limit.js +141 -0
- package/dist/index.js +16 -5
- package/dist/job-store.js +127 -0
- package/dist/metrics.js +41 -0
- package/dist/relay-external-activity-monitor.js +47 -6
- package/dist/relay-runtime.js +986 -281
- 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-diagnostics-command.js +5 -50
- package/dist/telegram-general-commands.js +2 -6
- package/dist/telegram-operational-commands.js +14 -6
- 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 +9 -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 +97 -13
- package/dist/web-dashboard-runtime-routes.js +53 -8
- package/dist/web-dashboard-session-routes.js +27 -20
- package/dist/web-dashboard-ui.js +1 -0
- package/dist/web-dashboard.js +148 -6
- package/dist/web-state.js +33 -2
- package/dist/webui-assets/dashboard.css +75 -1
- package/dist/webui-assets/dashboard.js +358 -47
- package/package.json +3 -1
- package/plugins/nordrelay/scripts/nordrelay.mjs +210 -17
package/dist/relay-runtime.js
CHANGED
|
@@ -2,31 +2,40 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { ensureOutDir } from "./artifacts.js";
|
|
4
4
|
import { buildFileInstructions, outboxPath, stageFile, } from "./attachments.js";
|
|
5
|
-
import { CODEX_AGENT_CAPABILITIES, agentLabel, agentReasoningLabel, agentReasoningOptions, } from "./agent.js";
|
|
5
|
+
import { CODEX_AGENT_CAPABILITIES, agentLabel, agentReasoningLabel, agentReasoningOptions, isAgentId, } from "./agent.js";
|
|
6
6
|
import { getAgentDiagnostics, getExternalSnapshotForSession, } from "./agent-activity.js";
|
|
7
7
|
import { listAgentAdapterDescriptors } from "./agent-adapter.js";
|
|
8
8
|
import { AgentUpdateManager } from "./agent-updates.js";
|
|
9
9
|
import { createAgentSessionService, enabledAgents } from "./agent-factory.js";
|
|
10
10
|
import { AuditLogStore } from "./audit-log.js";
|
|
11
|
+
import { BotPreferencesStore } from "./bot-preferences.js";
|
|
12
|
+
import { ChannelTurnService } from "./channel-turn-service.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";
|
|
22
28
|
import { renderSessionInfoPlain, renderSessionUsageRows } from "./session-format.js";
|
|
23
29
|
import { SessionLockStore } from "./session-locks.js";
|
|
24
30
|
import { SessionRegistry } from "./session-registry.js";
|
|
25
31
|
import { createSupportBundle } from "./support-bundle.js";
|
|
26
32
|
import { transcribeAudio } from "./voice.js";
|
|
27
33
|
import { WebActivityStore, WebChatStore, } from "./web-state.js";
|
|
34
|
+
import { channelIdForContextKey } from "./context-key.js";
|
|
28
35
|
import { evaluateWorkspacePolicy, filterAllowedWorkspaces } from "./workspace-policy.js";
|
|
29
36
|
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 {
|
|
@@ -39,9 +48,14 @@ export class RelayRuntime {
|
|
|
39
48
|
lockStore;
|
|
40
49
|
agentUpdates;
|
|
41
50
|
queueService;
|
|
51
|
+
jobStore;
|
|
42
52
|
artifactService;
|
|
43
53
|
externalActivityMonitor;
|
|
54
|
+
cache = new RuntimeSnapshotCache();
|
|
55
|
+
turnService;
|
|
44
56
|
subscribers = new Set();
|
|
57
|
+
agentUpdateActors = new Map();
|
|
58
|
+
agentUpdateStates = new Map();
|
|
45
59
|
externalMonitor;
|
|
46
60
|
draining = false;
|
|
47
61
|
currentTurnId = null;
|
|
@@ -60,9 +74,13 @@ export class RelayRuntime {
|
|
|
60
74
|
this.auditStore = new AuditLogStore(config.workspace, config.stateBackend, config.auditMaxEvents);
|
|
61
75
|
this.lockStore = new SessionLockStore(config.workspace, config.stateBackend);
|
|
62
76
|
this.queueService = new RelayQueueService(this.promptStore, WEB_CONTEXT_KEY);
|
|
77
|
+
this.jobStore = new UnifiedJobStore(config.workspace, config.stateBackend, config.unifiedJobMaxItems);
|
|
63
78
|
this.artifactService = new RelayArtifactService(config);
|
|
64
79
|
this.agentUpdates = new AgentUpdateManager({
|
|
65
|
-
onUpdate: (job) =>
|
|
80
|
+
onUpdate: (job) => {
|
|
81
|
+
this.broadcast({ type: "agent_update", job });
|
|
82
|
+
this.recordAgentUpdateLifecycle(job);
|
|
83
|
+
},
|
|
66
84
|
});
|
|
67
85
|
this.externalActivityMonitor = new RelayExternalActivityMonitor({
|
|
68
86
|
config,
|
|
@@ -83,6 +101,36 @@ export class RelayRuntime {
|
|
|
83
101
|
}, config.codexExternalBusyCheckMs);
|
|
84
102
|
this.externalMonitor.unref?.();
|
|
85
103
|
}
|
|
104
|
+
this.turnService = new ChannelTurnService({
|
|
105
|
+
source: "web",
|
|
106
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
107
|
+
chatStore: this.chatStore,
|
|
108
|
+
artifactService: this.artifactService,
|
|
109
|
+
checkAuth: (info) => this.checkAgentAuth(info),
|
|
110
|
+
ensureActiveThread: (session) => this.ensureActiveThread(session),
|
|
111
|
+
updateSession: (session) => this.updateSession(session),
|
|
112
|
+
appendActivity: (input) => this.appendActivity(input),
|
|
113
|
+
appendAudit: (input) => this.appendAudit(input),
|
|
114
|
+
broadcast: (event) => this.broadcast(event),
|
|
115
|
+
chatHistory: () => this.chatHistory(),
|
|
116
|
+
setLastPrompt: (envelope) => this.queueService.setLastPrompt(envelope),
|
|
117
|
+
getCurrentProgress: () => this.currentProgress,
|
|
118
|
+
setCurrentProgress: (progress) => {
|
|
119
|
+
this.currentProgress = progress;
|
|
120
|
+
},
|
|
121
|
+
setCurrentTurn: (id, startedAt, accumulatedText) => {
|
|
122
|
+
this.currentTurnId = id;
|
|
123
|
+
if (startedAt !== undefined)
|
|
124
|
+
this.currentTurnStartedAt = startedAt;
|
|
125
|
+
if (accumulatedText !== undefined)
|
|
126
|
+
this.accumulatedText = accumulatedText;
|
|
127
|
+
},
|
|
128
|
+
getCurrentTurnStartedAt: () => this.currentTurnStartedAt,
|
|
129
|
+
getAccumulatedText: () => this.accumulatedText,
|
|
130
|
+
setAccumulatedText: (text) => {
|
|
131
|
+
this.accumulatedText = text;
|
|
132
|
+
},
|
|
133
|
+
});
|
|
86
134
|
}
|
|
87
135
|
subscribe(callback) {
|
|
88
136
|
this.subscribers.add(callback);
|
|
@@ -127,19 +175,22 @@ export class RelayRuntime {
|
|
|
127
175
|
};
|
|
128
176
|
}
|
|
129
177
|
async version() {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
178
|
+
return this.cached("version", async () => {
|
|
179
|
+
const cliOptions = this.cliPathOptions();
|
|
180
|
+
const [health, state, versionChecks] = await Promise.all([
|
|
181
|
+
getConnectorHealth(cliOptions),
|
|
182
|
+
readConnectorState(),
|
|
183
|
+
getVersionChecks(cliOptions),
|
|
184
|
+
]);
|
|
185
|
+
return {
|
|
186
|
+
health,
|
|
187
|
+
state,
|
|
188
|
+
versionChecks,
|
|
189
|
+
};
|
|
190
|
+
});
|
|
141
191
|
}
|
|
142
|
-
updateConnector() {
|
|
192
|
+
updateConnector(actor) {
|
|
193
|
+
this.cache.invalidate("version");
|
|
143
194
|
const update = spawnSelfUpdate();
|
|
144
195
|
this.broadcastStatus(`Update started with ${update.method}. Log: ${update.logPath}`, "warn");
|
|
145
196
|
this.appendActivity({
|
|
@@ -148,12 +199,14 @@ export class RelayRuntime {
|
|
|
148
199
|
type: "update_started",
|
|
149
200
|
threadId: null,
|
|
150
201
|
workspace: this.config.workspace,
|
|
202
|
+
actor,
|
|
151
203
|
detail: `${update.method}: ${update.summary}`,
|
|
152
204
|
});
|
|
153
205
|
this.appendAudit({
|
|
154
206
|
action: "command",
|
|
155
207
|
status: "ok",
|
|
156
208
|
contextKey: WEB_CONTEXT_KEY,
|
|
209
|
+
actor,
|
|
157
210
|
description: "update",
|
|
158
211
|
detail: update.summary,
|
|
159
212
|
});
|
|
@@ -162,13 +215,19 @@ export class RelayRuntime {
|
|
|
162
215
|
agentUpdateJobs() {
|
|
163
216
|
return this.agentUpdates.list();
|
|
164
217
|
}
|
|
165
|
-
startAgentUpdate(agentId, operation = "update") {
|
|
218
|
+
startAgentUpdate(agentId, operation = "update", actor) {
|
|
219
|
+
this.cache.invalidate("adapterHealth");
|
|
220
|
+
this.cache.invalidate("version");
|
|
166
221
|
const job = this.agentUpdates.start(agentId, {
|
|
167
222
|
piCliPath: this.config.piCliPath,
|
|
168
223
|
hermesCliPath: this.config.hermesCliPath,
|
|
169
224
|
openClawCliPath: this.config.openClawCliPath,
|
|
170
225
|
claudeCodeCliPath: this.config.claudeCodeCliPath,
|
|
171
226
|
}, operation);
|
|
227
|
+
if (actor) {
|
|
228
|
+
this.agentUpdateActors.set(job.id, actor);
|
|
229
|
+
}
|
|
230
|
+
this.agentUpdateStates.set(job.id, { status: job.status, needsInput: job.needsInput });
|
|
172
231
|
this.broadcastStatus(`${job.agentLabel} ${operation} started. Log: ${job.logPath}`, "warn");
|
|
173
232
|
this.appendActivity({
|
|
174
233
|
source: "web",
|
|
@@ -177,6 +236,7 @@ export class RelayRuntime {
|
|
|
177
236
|
agentId,
|
|
178
237
|
threadId: null,
|
|
179
238
|
workspace: this.config.workspace,
|
|
239
|
+
actor,
|
|
180
240
|
detail: `${job.method}: ${job.summary}`,
|
|
181
241
|
});
|
|
182
242
|
this.appendAudit({
|
|
@@ -184,6 +244,7 @@ export class RelayRuntime {
|
|
|
184
244
|
status: "ok",
|
|
185
245
|
contextKey: WEB_CONTEXT_KEY,
|
|
186
246
|
agentId,
|
|
247
|
+
actor,
|
|
187
248
|
description: `${operation} ${agentId}`,
|
|
188
249
|
detail: job.summary,
|
|
189
250
|
});
|
|
@@ -192,93 +253,130 @@ export class RelayRuntime {
|
|
|
192
253
|
agentUpdateLog(id) {
|
|
193
254
|
return this.agentUpdates.readLog(id);
|
|
194
255
|
}
|
|
195
|
-
deleteAgentUpdateLog(id) {
|
|
256
|
+
deleteAgentUpdateLog(id, actor) {
|
|
196
257
|
const job = this.agentUpdates.deleteLog(id);
|
|
258
|
+
this.appendActivity({
|
|
259
|
+
source: "web",
|
|
260
|
+
status: "info",
|
|
261
|
+
type: "agent_update_log_deleted",
|
|
262
|
+
agentId: job.agentId,
|
|
263
|
+
threadId: null,
|
|
264
|
+
workspace: this.config.workspace,
|
|
265
|
+
actor,
|
|
266
|
+
detail: job.logPath,
|
|
267
|
+
});
|
|
197
268
|
this.appendAudit({
|
|
198
269
|
action: "command",
|
|
199
270
|
status: "ok",
|
|
200
271
|
contextKey: WEB_CONTEXT_KEY,
|
|
201
272
|
agentId: job.agentId,
|
|
273
|
+
actor,
|
|
202
274
|
description: `delete update log ${id}`,
|
|
203
275
|
detail: job.logPath,
|
|
204
276
|
});
|
|
205
277
|
return job;
|
|
206
278
|
}
|
|
207
|
-
sendAgentUpdateInput(id, input) {
|
|
208
|
-
|
|
279
|
+
sendAgentUpdateInput(id, input, actor) {
|
|
280
|
+
const job = this.agentUpdates.sendInput(id, input);
|
|
281
|
+
this.appendActivity({
|
|
282
|
+
source: "web",
|
|
283
|
+
status: "info",
|
|
284
|
+
type: "agent_update_input_sent",
|
|
285
|
+
agentId: job.agentId,
|
|
286
|
+
threadId: null,
|
|
287
|
+
workspace: this.config.workspace,
|
|
288
|
+
actor: actor ?? this.agentUpdateActors.get(id),
|
|
289
|
+
detail: `Input sent to ${job.agentLabel} ${job.operation}.`,
|
|
290
|
+
});
|
|
291
|
+
return job;
|
|
209
292
|
}
|
|
210
|
-
cancelAgentUpdate(id) {
|
|
211
|
-
|
|
293
|
+
cancelAgentUpdate(id, actor) {
|
|
294
|
+
const job = this.agentUpdates.cancel(id);
|
|
295
|
+
this.appendActivity({
|
|
296
|
+
source: "web",
|
|
297
|
+
status: "aborted",
|
|
298
|
+
type: "agent_update_cancel_requested",
|
|
299
|
+
agentId: job.agentId,
|
|
300
|
+
threadId: null,
|
|
301
|
+
workspace: this.config.workspace,
|
|
302
|
+
actor: actor ?? this.agentUpdateActors.get(id),
|
|
303
|
+
detail: `${job.agentLabel} ${job.operation} cancellation requested.`,
|
|
304
|
+
});
|
|
305
|
+
return job;
|
|
212
306
|
}
|
|
213
307
|
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);
|
|
308
|
+
return this.cached("diagnostics", async () => {
|
|
309
|
+
const cliOptions = this.cliPathOptions();
|
|
310
|
+
const [health, versionChecks, snapshot, session] = await Promise.all([
|
|
311
|
+
getConnectorHealth(cliOptions),
|
|
312
|
+
getVersionChecks(cliOptions),
|
|
313
|
+
this.snapshot(),
|
|
314
|
+
this.getSession(true),
|
|
315
|
+
]);
|
|
255
316
|
return {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
317
|
+
health,
|
|
318
|
+
versionChecks,
|
|
319
|
+
snapshot,
|
|
320
|
+
runtime: {
|
|
321
|
+
stateBackend: this.config.stateBackend,
|
|
322
|
+
sourceWorkspace: this.config.workspace,
|
|
323
|
+
queuePaused: this.queueService.isPaused(),
|
|
324
|
+
externalMirror: this.externalActivityMonitor.snapshot(),
|
|
325
|
+
agentDiagnostics: getAgentDiagnostics(session, this.config),
|
|
265
326
|
},
|
|
266
|
-
cli,
|
|
267
|
-
version: {
|
|
268
|
-
installed: version.installedLabel,
|
|
269
|
-
latest: version.latestVersion,
|
|
270
|
-
status: version.status,
|
|
271
|
-
detail: version.detail,
|
|
272
|
-
},
|
|
273
|
-
capabilities: descriptor.capabilities,
|
|
274
|
-
notes: descriptor.notes,
|
|
275
327
|
};
|
|
276
|
-
})
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
async adapterHealth() {
|
|
331
|
+
return this.cached("adapterHealth", async () => {
|
|
332
|
+
const cliOptions = this.cliPathOptions();
|
|
333
|
+
const [health, versions] = await Promise.all([
|
|
334
|
+
getConnectorHealth(cliOptions),
|
|
335
|
+
getVersionChecks(cliOptions),
|
|
336
|
+
]);
|
|
337
|
+
return Promise.all(listAgentAdapterDescriptors().map(async (descriptor) => {
|
|
338
|
+
const enabled = enabledAgents(this.config).includes(descriptor.id);
|
|
339
|
+
const auth = descriptor.capabilities.auth && enabled
|
|
340
|
+
? await this.authStatus(descriptor.id).catch((error) => ({
|
|
341
|
+
agentId: descriptor.id,
|
|
342
|
+
agentLabel: descriptor.label,
|
|
343
|
+
supported: descriptor.capabilities.auth,
|
|
344
|
+
authenticated: false,
|
|
345
|
+
detail: friendlyErrorText(error),
|
|
346
|
+
loginSupported: descriptor.capabilities.login,
|
|
347
|
+
logoutSupported: descriptor.capabilities.logout,
|
|
348
|
+
}))
|
|
349
|
+
: null;
|
|
350
|
+
const cli = cliHealthForAgent(descriptor.id, health);
|
|
351
|
+
const version = versionCheckForAgent(descriptor.id, versions);
|
|
352
|
+
return {
|
|
353
|
+
id: descriptor.id,
|
|
354
|
+
label: descriptor.label,
|
|
355
|
+
enabled,
|
|
356
|
+
status: descriptor.status === "available" ? (enabled ? "enabled" : "disabled") : "planned",
|
|
357
|
+
auth: {
|
|
358
|
+
supported: descriptor.capabilities.auth,
|
|
359
|
+
authenticated: auth ? auth.authenticated : null,
|
|
360
|
+
method: auth?.method,
|
|
361
|
+
detail: auth?.detail,
|
|
362
|
+
},
|
|
363
|
+
cli,
|
|
364
|
+
version: {
|
|
365
|
+
installed: version.installedLabel,
|
|
366
|
+
latest: version.latestVersion,
|
|
367
|
+
status: version.status,
|
|
368
|
+
detail: version.detail,
|
|
369
|
+
},
|
|
370
|
+
capabilities: descriptor.capabilities,
|
|
371
|
+
notes: descriptor.notes,
|
|
372
|
+
};
|
|
373
|
+
}));
|
|
374
|
+
});
|
|
277
375
|
}
|
|
278
376
|
permissions() {
|
|
279
377
|
return {
|
|
280
378
|
mode: "users",
|
|
281
|
-
message: "Access is managed by NordRelay users, groups, Telegram identities,
|
|
379
|
+
message: "Access is managed by NordRelay users, groups, Telegram identities, Telegram chat access records, Discord identities, and Discord channel access records.",
|
|
282
380
|
};
|
|
283
381
|
}
|
|
284
382
|
tasks() {
|
|
@@ -290,10 +388,229 @@ export class RelayRuntime {
|
|
|
290
388
|
recent: this.activity({ limit: 20 }),
|
|
291
389
|
};
|
|
292
390
|
}
|
|
293
|
-
|
|
294
|
-
|
|
391
|
+
async jobs() {
|
|
392
|
+
const jobs = [];
|
|
393
|
+
const current = this.currentProgress;
|
|
394
|
+
if (current) {
|
|
395
|
+
jobs.push(taskToUnifiedJob("web:current", "web-turn", "Current WebUI turn", current, {
|
|
396
|
+
canCancel: current.status === "running",
|
|
397
|
+
canRetry: false,
|
|
398
|
+
canReadLog: false,
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
const external = this.externalActivityMonitor.task();
|
|
402
|
+
if (external) {
|
|
403
|
+
jobs.push(taskToUnifiedJob(`external:${external.agentId ?? "agent"}:${external.threadId ?? "pending"}`, "external-turn", "External CLI turn", external, {
|
|
404
|
+
canCancel: false,
|
|
405
|
+
canRetry: false,
|
|
406
|
+
canReadLog: false,
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
for (const item of this.queueService.rawList()) {
|
|
410
|
+
const createdAt = new Date(item.createdAt).toISOString();
|
|
411
|
+
jobs.push({
|
|
412
|
+
id: `queue:${item.id}`,
|
|
413
|
+
kind: "queued-prompt",
|
|
414
|
+
title: `Queued prompt ${item.id}`,
|
|
415
|
+
status: "queued",
|
|
416
|
+
source: "web",
|
|
417
|
+
threadId: null,
|
|
418
|
+
workspace: this.config.workspace,
|
|
419
|
+
owner: item.activityActor,
|
|
420
|
+
startedAt: createdAt,
|
|
421
|
+
updatedAt: createdAt,
|
|
422
|
+
summary: item.description,
|
|
423
|
+
queueId: item.id,
|
|
424
|
+
logTail: item.lastError,
|
|
425
|
+
canCancel: true,
|
|
426
|
+
canRetry: true,
|
|
427
|
+
canReadLog: true,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
for (const job of this.agentUpdates.list()) {
|
|
431
|
+
jobs.push({
|
|
432
|
+
id: `agent-update:${job.id}`,
|
|
433
|
+
kind: "agent-update",
|
|
434
|
+
title: `${job.agentLabel} ${job.operation}`,
|
|
435
|
+
status: agentUpdateStatusToUnified(job.status),
|
|
436
|
+
source: "web",
|
|
437
|
+
agentId: job.agentId,
|
|
438
|
+
agentLabel: job.agentLabel,
|
|
439
|
+
threadId: null,
|
|
440
|
+
workspace: this.config.workspace,
|
|
441
|
+
owner: this.agentUpdateActors.get(job.id),
|
|
442
|
+
startedAt: job.startedAt,
|
|
443
|
+
updatedAt: job.updatedAt,
|
|
444
|
+
finishedAt: job.finishedAt,
|
|
445
|
+
summary: job.error || job.summary,
|
|
446
|
+
logPath: job.logPath,
|
|
447
|
+
logTail: job.outputTail,
|
|
448
|
+
updateJobId: job.id,
|
|
449
|
+
canCancel: job.status === "running",
|
|
450
|
+
canRetry: job.status !== "running",
|
|
451
|
+
canReadLog: true,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
for (const event of this.activity({ limit: 100 })) {
|
|
455
|
+
if (event.type === "diagnostics_bundle_exported") {
|
|
456
|
+
jobs.push(activityToUnifiedJob(event, "support-bundle", "Diagnostics support bundle", {
|
|
457
|
+
canCancel: false,
|
|
458
|
+
canRetry: true,
|
|
459
|
+
canReadLog: Boolean(event.detail),
|
|
460
|
+
}));
|
|
461
|
+
}
|
|
462
|
+
else if (event.type === "update_started") {
|
|
463
|
+
jobs.push(activityToUnifiedJob(event, "connector-update", "NordRelay update", {
|
|
464
|
+
canCancel: false,
|
|
465
|
+
canRetry: true,
|
|
466
|
+
canReadLog: Boolean(event.detail),
|
|
467
|
+
}));
|
|
468
|
+
}
|
|
469
|
+
else if (event.category === "prompt" && event.type.startsWith("prompt_")) {
|
|
470
|
+
jobs.push(promptActivityToUnifiedJob(event));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const liveJobs = dedupeJobs(jobs);
|
|
474
|
+
const storedJobs = this.jobStore.upsertMany(liveJobs);
|
|
475
|
+
return {
|
|
476
|
+
jobs: dedupeJobs([...liveJobs, ...storedJobs]).sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt)),
|
|
477
|
+
updatedAt: new Date().toISOString(),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
async jobLog(id) {
|
|
481
|
+
if (id.startsWith("agent-update:")) {
|
|
482
|
+
const updateId = id.slice("agent-update:".length);
|
|
483
|
+
const log = this.agentUpdates.readLog(updateId);
|
|
484
|
+
return { job: (await this.jobs()).jobs.find((job) => job.id === id) ?? null, plain: log.plain };
|
|
485
|
+
}
|
|
486
|
+
if (id.startsWith("queue:")) {
|
|
487
|
+
const queueId = id.slice("queue:".length);
|
|
488
|
+
const item = this.queueService.rawList().find((candidate) => candidate.id === queueId);
|
|
489
|
+
return {
|
|
490
|
+
job: (await this.jobs()).jobs.find((job) => job.id === id) ?? null,
|
|
491
|
+
plain: item ? [
|
|
492
|
+
`Queued prompt: ${item.id}`,
|
|
493
|
+
`Created: ${new Date(item.createdAt).toISOString()}`,
|
|
494
|
+
`Attempts: ${item.attempts ?? 0}`,
|
|
495
|
+
`Description: ${item.description}`,
|
|
496
|
+
item.lastError ? `Last error: ${item.lastError}` : "",
|
|
497
|
+
].filter(Boolean).join("\n") : "Queued prompt not found.",
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
const job = (await this.jobs()).jobs.find((candidate) => candidate.id === id) ?? null;
|
|
501
|
+
return { job, plain: job?.logTail || job?.logPath || job?.summary || this.jobStore.get(id)?.summary || "No log available for this job." };
|
|
502
|
+
}
|
|
503
|
+
async jobAction(id, action, actor) {
|
|
504
|
+
if (id === "web:current" && action === "cancel") {
|
|
505
|
+
await this.abort(actor);
|
|
506
|
+
return this.jobs();
|
|
507
|
+
}
|
|
508
|
+
if (id.startsWith("queue:")) {
|
|
509
|
+
const queueId = id.slice("queue:".length);
|
|
510
|
+
this.queueService.apply(action === "cancel" ? "cancel" : "run", queueId);
|
|
511
|
+
this.jobStore.patch(id, {
|
|
512
|
+
status: action === "cancel" ? "aborted" : "queued",
|
|
513
|
+
summary: action === "cancel" ? `Cancelled queued prompt ${queueId}.` : `Queued prompt ${queueId} moved to the front.`,
|
|
514
|
+
canCancel: action !== "cancel",
|
|
515
|
+
canRetry: action === "cancel",
|
|
516
|
+
finishedAt: action === "cancel" ? new Date().toISOString() : undefined,
|
|
517
|
+
});
|
|
518
|
+
this.broadcast({ type: "queue_update", queue: this.queue(), paused: this.queuePaused() });
|
|
519
|
+
this.appendActivity({
|
|
520
|
+
source: "web",
|
|
521
|
+
status: action === "cancel" ? "aborted" : "queued",
|
|
522
|
+
type: action === "cancel" ? "job_cancelled" : "job_retried",
|
|
523
|
+
threadId: null,
|
|
524
|
+
workspace: this.config.workspace,
|
|
525
|
+
actor,
|
|
526
|
+
detail: `queue:${queueId}`,
|
|
527
|
+
});
|
|
528
|
+
if (action === "retry") {
|
|
529
|
+
void this.drainQueue();
|
|
530
|
+
}
|
|
531
|
+
return this.jobs();
|
|
532
|
+
}
|
|
533
|
+
if (id.startsWith("agent-update:")) {
|
|
534
|
+
const updateId = id.slice("agent-update:".length);
|
|
535
|
+
const current = this.agentUpdates.get(updateId);
|
|
536
|
+
if (!current) {
|
|
537
|
+
throw new Error("Unknown agent update job.");
|
|
538
|
+
}
|
|
539
|
+
if (action === "cancel") {
|
|
540
|
+
this.cancelAgentUpdate(updateId, actor);
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
this.startAgentUpdate(current.agentId, current.operation, actor);
|
|
544
|
+
}
|
|
545
|
+
return this.jobs();
|
|
546
|
+
}
|
|
547
|
+
if (id.startsWith("support-bundle:") && action === "retry") {
|
|
548
|
+
await this.supportBundle(actor);
|
|
549
|
+
return this.jobs();
|
|
550
|
+
}
|
|
551
|
+
if (id.startsWith("connector-update:") && action === "retry") {
|
|
552
|
+
this.updateConnector(actor);
|
|
553
|
+
return this.jobs();
|
|
554
|
+
}
|
|
555
|
+
throw new Error(`Unsupported job action: ${action} ${id}`);
|
|
556
|
+
}
|
|
557
|
+
async activeSessions() {
|
|
558
|
+
const sessions = new Map();
|
|
559
|
+
const knownContexts = this.listKnownContextMetadata();
|
|
560
|
+
const preferences = new BotPreferencesStore(this.config.workspace, this.config.stateBackend);
|
|
561
|
+
const addActiveSession = (session) => {
|
|
562
|
+
const key = this.activeSessionKey(session);
|
|
563
|
+
const existing = sessions.get(key);
|
|
564
|
+
sessions.set(key, this.preferredActiveSession(existing, session));
|
|
565
|
+
};
|
|
566
|
+
if (this.currentProgress?.status === "running") {
|
|
567
|
+
addActiveSession({
|
|
568
|
+
...this.currentProgress,
|
|
569
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
570
|
+
sourceContextKey: WEB_CONTEXT_KEY,
|
|
571
|
+
source: "web",
|
|
572
|
+
status: "running",
|
|
573
|
+
queueLength: this.queueService.length(),
|
|
574
|
+
queuePaused: this.queueService.isPaused(),
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
for (const active of this.discoverRunningConnectorSessions()) {
|
|
578
|
+
addActiveSession(active);
|
|
579
|
+
}
|
|
580
|
+
for (const active of this.discoverActiveCodexSessions(knownContexts, preferences)) {
|
|
581
|
+
addActiveSession(active);
|
|
582
|
+
}
|
|
583
|
+
for (const meta of knownContexts) {
|
|
584
|
+
if (meta.contextKey === WEB_CONTEXT_KEY && this.currentProgress?.status === "running") {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
const active = this.externalActiveSession(meta, knownContexts, preferences);
|
|
588
|
+
if (active) {
|
|
589
|
+
addActiveSession(active);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
sessions: [...sessions.values()].sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt)),
|
|
594
|
+
updatedAt: new Date().toISOString(),
|
|
595
|
+
};
|
|
295
596
|
}
|
|
296
|
-
async
|
|
597
|
+
async metrics() {
|
|
598
|
+
const [active, jobs] = await Promise.all([
|
|
599
|
+
this.activeSessions(),
|
|
600
|
+
this.jobs(),
|
|
601
|
+
]);
|
|
602
|
+
return buildRuntimeMetrics({
|
|
603
|
+
queueLength: this.queueService.length(),
|
|
604
|
+
queuePaused: this.queueService.isPaused(),
|
|
605
|
+
activeTurnCount: active.sessions.length,
|
|
606
|
+
jobs: jobs.jobs,
|
|
607
|
+
activity: this.activity({ limit: 500 }),
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
audit(options = 50) {
|
|
611
|
+
return this.auditStore.list(options);
|
|
612
|
+
}
|
|
613
|
+
async supportBundle(actor) {
|
|
297
614
|
const bundle = await createSupportBundle({
|
|
298
615
|
config: this.config,
|
|
299
616
|
diagnostics: await this.diagnostics(),
|
|
@@ -308,12 +625,14 @@ export class RelayRuntime {
|
|
|
308
625
|
type: "diagnostics_bundle_exported",
|
|
309
626
|
threadId: null,
|
|
310
627
|
workspace: this.config.workspace,
|
|
628
|
+
actor,
|
|
311
629
|
detail: bundle.path,
|
|
312
630
|
});
|
|
313
631
|
this.appendAudit({
|
|
314
632
|
action: "command",
|
|
315
633
|
status: "ok",
|
|
316
634
|
contextKey: WEB_CONTEXT_KEY,
|
|
635
|
+
actor,
|
|
317
636
|
description: "export diagnostics bundle",
|
|
318
637
|
detail: bundle.path,
|
|
319
638
|
});
|
|
@@ -322,23 +641,48 @@ export class RelayRuntime {
|
|
|
322
641
|
locks() {
|
|
323
642
|
return this.lockStore.list();
|
|
324
643
|
}
|
|
325
|
-
lockWebSession(ownerName = "Web dashboard") {
|
|
326
|
-
const
|
|
644
|
+
lockWebSession(ownerName = "Web dashboard", actor) {
|
|
645
|
+
const label = ownerName || actor?.label || "Web dashboard";
|
|
646
|
+
const lock = this.lockStore.set(WEB_CONTEXT_KEY, {
|
|
647
|
+
userId: actor?.id ?? "web",
|
|
648
|
+
label,
|
|
649
|
+
channel: "web",
|
|
650
|
+
}, this.config.sessionLockTtlMs);
|
|
651
|
+
this.appendActivity({
|
|
652
|
+
source: "web",
|
|
653
|
+
status: "info",
|
|
654
|
+
type: "lock_created",
|
|
655
|
+
threadId: null,
|
|
656
|
+
workspace: this.config.workspace,
|
|
657
|
+
actor,
|
|
658
|
+
detail: `locked by ${label}`,
|
|
659
|
+
});
|
|
327
660
|
this.appendAudit({
|
|
328
661
|
action: "lock_updated",
|
|
329
662
|
status: "ok",
|
|
330
663
|
contextKey: WEB_CONTEXT_KEY,
|
|
664
|
+
actor,
|
|
331
665
|
description: "lock",
|
|
332
|
-
detail: `locked by ${
|
|
666
|
+
detail: `locked by ${label}`,
|
|
333
667
|
});
|
|
334
668
|
return lock;
|
|
335
669
|
}
|
|
336
|
-
unlockWebSession() {
|
|
670
|
+
unlockWebSession(actor) {
|
|
337
671
|
const removed = this.lockStore.clear(WEB_CONTEXT_KEY);
|
|
672
|
+
this.appendActivity({
|
|
673
|
+
source: "web",
|
|
674
|
+
status: "info",
|
|
675
|
+
type: "lock_removed",
|
|
676
|
+
threadId: null,
|
|
677
|
+
workspace: this.config.workspace,
|
|
678
|
+
actor,
|
|
679
|
+
detail: removed ? "unlocked" : "no lock",
|
|
680
|
+
});
|
|
338
681
|
this.appendAudit({
|
|
339
682
|
action: "lock_updated",
|
|
340
683
|
status: "ok",
|
|
341
684
|
contextKey: WEB_CONTEXT_KEY,
|
|
685
|
+
actor,
|
|
342
686
|
description: "unlock",
|
|
343
687
|
detail: removed ? "unlocked" : "no lock",
|
|
344
688
|
});
|
|
@@ -407,7 +751,7 @@ export class RelayRuntime {
|
|
|
407
751
|
}
|
|
408
752
|
}
|
|
409
753
|
}
|
|
410
|
-
async login(agentId) {
|
|
754
|
+
async login(agentId, actor) {
|
|
411
755
|
const { session, dispose } = await this.getControlSession(agentId);
|
|
412
756
|
try {
|
|
413
757
|
const info = this.publicInfo(session);
|
|
@@ -431,6 +775,16 @@ export class RelayRuntime {
|
|
|
431
775
|
};
|
|
432
776
|
}
|
|
433
777
|
const result = await this.startAgentLogin(info);
|
|
778
|
+
this.appendActivity({
|
|
779
|
+
source: "web",
|
|
780
|
+
status: result.success ? "info" : "failed",
|
|
781
|
+
type: result.success ? "login_started" : "login_failed",
|
|
782
|
+
threadId: info.threadId,
|
|
783
|
+
workspace: info.workspace,
|
|
784
|
+
agentId: info.agentId,
|
|
785
|
+
actor,
|
|
786
|
+
detail: result.message,
|
|
787
|
+
});
|
|
434
788
|
this.appendAudit({
|
|
435
789
|
action: "command",
|
|
436
790
|
status: result.success ? "ok" : "failed",
|
|
@@ -438,6 +792,7 @@ export class RelayRuntime {
|
|
|
438
792
|
agentId: info.agentId,
|
|
439
793
|
threadId: info.threadId,
|
|
440
794
|
workspace: info.workspace,
|
|
795
|
+
actor,
|
|
441
796
|
description: "login",
|
|
442
797
|
detail: result.message,
|
|
443
798
|
});
|
|
@@ -449,7 +804,7 @@ export class RelayRuntime {
|
|
|
449
804
|
}
|
|
450
805
|
}
|
|
451
806
|
}
|
|
452
|
-
async logout(agentId) {
|
|
807
|
+
async logout(agentId, actor) {
|
|
453
808
|
const { session, dispose } = await this.getControlSession(agentId);
|
|
454
809
|
try {
|
|
455
810
|
const info = this.publicInfo(session);
|
|
@@ -483,6 +838,16 @@ export class RelayRuntime {
|
|
|
483
838
|
};
|
|
484
839
|
}
|
|
485
840
|
const result = await this.startAgentLogout(info);
|
|
841
|
+
this.appendActivity({
|
|
842
|
+
source: "web",
|
|
843
|
+
status: result.success ? "info" : "failed",
|
|
844
|
+
type: result.success ? "logout_completed" : "logout_failed",
|
|
845
|
+
threadId: info.threadId,
|
|
846
|
+
workspace: info.workspace,
|
|
847
|
+
agentId: info.agentId,
|
|
848
|
+
actor,
|
|
849
|
+
detail: result.message,
|
|
850
|
+
});
|
|
486
851
|
this.appendAudit({
|
|
487
852
|
action: "command",
|
|
488
853
|
status: result.success ? "ok" : "failed",
|
|
@@ -490,6 +855,7 @@ export class RelayRuntime {
|
|
|
490
855
|
agentId: info.agentId,
|
|
491
856
|
threadId: info.threadId,
|
|
492
857
|
workspace: info.workspace,
|
|
858
|
+
actor,
|
|
493
859
|
description: "logout",
|
|
494
860
|
detail: result.message,
|
|
495
861
|
});
|
|
@@ -517,18 +883,29 @@ export class RelayRuntime {
|
|
|
517
883
|
activity: this.activity({ limit: 100 }).filter((event) => event.threadId === threadId),
|
|
518
884
|
};
|
|
519
885
|
}
|
|
520
|
-
async clearChatHistory() {
|
|
886
|
+
async clearChatHistory(actor) {
|
|
521
887
|
const session = await this.getSession(true);
|
|
522
|
-
const
|
|
888
|
+
const info = this.publicInfo(session);
|
|
889
|
+
const removed = this.chatStore.clear(info.threadId);
|
|
523
890
|
const messages = await this.chatHistory();
|
|
524
891
|
this.broadcast({ type: "chat_history", messages });
|
|
892
|
+
this.appendActivity({
|
|
893
|
+
source: "web",
|
|
894
|
+
status: "info",
|
|
895
|
+
type: "chat_history_cleared",
|
|
896
|
+
threadId: info.threadId,
|
|
897
|
+
workspace: info.workspace,
|
|
898
|
+
agentId: info.agentId,
|
|
899
|
+
actor,
|
|
900
|
+
detail: `${removed} messages removed.`,
|
|
901
|
+
});
|
|
525
902
|
return { removed, messages };
|
|
526
903
|
}
|
|
527
904
|
activity(options = {}) {
|
|
528
905
|
const currentInfo = this.registry.get(WEB_CONTEXT_KEY)?.getInfo();
|
|
529
906
|
return this.activityStore.list(options).map((event) => this.enrichActivityEvent(event, currentInfo));
|
|
530
907
|
}
|
|
531
|
-
async retry() {
|
|
908
|
+
async retry(actor) {
|
|
532
909
|
const cached = this.queueService.getLastPrompt();
|
|
533
910
|
if (!cached) {
|
|
534
911
|
throw new Error("Nothing to retry. Send a message first.");
|
|
@@ -537,12 +914,13 @@ export class RelayRuntime {
|
|
|
537
914
|
action: "command",
|
|
538
915
|
status: "ok",
|
|
539
916
|
contextKey: WEB_CONTEXT_KEY,
|
|
917
|
+
actor,
|
|
540
918
|
description: "retry",
|
|
541
919
|
detail: cached.description,
|
|
542
920
|
});
|
|
543
|
-
return this.sendEnvelope(cached);
|
|
921
|
+
return this.sendEnvelope({ ...cached, activityActor: cached.activityActor ?? actor }, actor);
|
|
544
922
|
}
|
|
545
|
-
async sync() {
|
|
923
|
+
async sync(actor) {
|
|
546
924
|
const session = await this.getSession(true);
|
|
547
925
|
const info = this.publicInfo(session);
|
|
548
926
|
if (!(info.capabilities ?? CODEX_AGENT_CAPABILITIES).externalActivity) {
|
|
@@ -559,6 +937,7 @@ export class RelayRuntime {
|
|
|
559
937
|
threadId: result.info.threadId,
|
|
560
938
|
workspace: result.info.workspace,
|
|
561
939
|
agentId: result.info.agentId,
|
|
940
|
+
actor,
|
|
562
941
|
detail: result.changedFields.length > 0 ? result.changedFields.join(", ") : "already in sync",
|
|
563
942
|
});
|
|
564
943
|
this.appendAudit({
|
|
@@ -568,6 +947,7 @@ export class RelayRuntime {
|
|
|
568
947
|
agentId: result.info.agentId,
|
|
569
948
|
threadId: result.info.threadId,
|
|
570
949
|
workspace: result.info.workspace,
|
|
950
|
+
actor,
|
|
571
951
|
description: "sync",
|
|
572
952
|
detail: result.changedFields.join(", ") || "none",
|
|
573
953
|
});
|
|
@@ -634,15 +1014,26 @@ export class RelayRuntime {
|
|
|
634
1014
|
});
|
|
635
1015
|
return session.listModels();
|
|
636
1016
|
}
|
|
637
|
-
async setAgent(agentId) {
|
|
1017
|
+
async setAgent(agentId, actor) {
|
|
638
1018
|
if (!enabledAgents(this.config).includes(agentId)) {
|
|
639
1019
|
throw new Error(`Agent is not enabled: ${agentId}`);
|
|
640
1020
|
}
|
|
641
1021
|
const session = await this.registry.switchAgent(WEB_CONTEXT_KEY, agentId);
|
|
642
1022
|
this.updateSession(session);
|
|
1023
|
+
const info = this.publicInfo(session);
|
|
1024
|
+
this.appendActivity({
|
|
1025
|
+
source: "web",
|
|
1026
|
+
status: "info",
|
|
1027
|
+
type: "agent_switch",
|
|
1028
|
+
threadId: info.threadId,
|
|
1029
|
+
workspace: info.workspace,
|
|
1030
|
+
agentId: info.agentId,
|
|
1031
|
+
actor,
|
|
1032
|
+
detail: `Dashboard switched agent to ${info.agentLabel}.`,
|
|
1033
|
+
});
|
|
643
1034
|
return this.publicInfo(session);
|
|
644
1035
|
}
|
|
645
|
-
async newSession(options = {}) {
|
|
1036
|
+
async newSession(options = {}, actor) {
|
|
646
1037
|
const session = options.agentId ? await this.registry.switchAgent(WEB_CONTEXT_KEY, options.agentId) : await this.getSession(true);
|
|
647
1038
|
this.ensureIdle(session);
|
|
648
1039
|
if (options.reasoningEffort) {
|
|
@@ -667,11 +1058,12 @@ export class RelayRuntime {
|
|
|
667
1058
|
threadId: info.threadId,
|
|
668
1059
|
workspace: info.workspace,
|
|
669
1060
|
agentId: info.agentId,
|
|
1061
|
+
actor,
|
|
670
1062
|
detail: "New dashboard session created.",
|
|
671
1063
|
});
|
|
672
1064
|
return this.publicInfo(session);
|
|
673
1065
|
}
|
|
674
|
-
async switchSession(threadId) {
|
|
1066
|
+
async switchSession(threadId, actor) {
|
|
675
1067
|
const session = await this.getSession(true);
|
|
676
1068
|
this.ensureIdle(session);
|
|
677
1069
|
const info = await session.switchSession(threadId);
|
|
@@ -684,21 +1076,24 @@ export class RelayRuntime {
|
|
|
684
1076
|
threadId: info.threadId,
|
|
685
1077
|
workspace: info.workspace,
|
|
686
1078
|
agentId: info.agentId,
|
|
1079
|
+
actor,
|
|
687
1080
|
detail: "Dashboard switched session.",
|
|
688
1081
|
});
|
|
689
1082
|
return this.publicInfo(session);
|
|
690
1083
|
}
|
|
691
|
-
async attachSession(threadId) {
|
|
692
|
-
return this.switchSession(threadId);
|
|
1084
|
+
async attachSession(threadId, actor) {
|
|
1085
|
+
return this.switchSession(threadId, actor);
|
|
693
1086
|
}
|
|
694
|
-
async setModel(model) {
|
|
1087
|
+
async setModel(model, actor) {
|
|
695
1088
|
const session = await this.getSession(true);
|
|
696
1089
|
this.ensureIdle(session);
|
|
697
1090
|
await session.setModelForCurrentSession(model);
|
|
698
1091
|
this.updateSession(session);
|
|
699
|
-
|
|
1092
|
+
const info = this.publicInfo(session);
|
|
1093
|
+
this.appendActivity({ source: "web", status: "info", type: "model_changed", threadId: info.threadId, workspace: info.workspace, agentId: info.agentId, actor, detail: model });
|
|
1094
|
+
return info;
|
|
700
1095
|
}
|
|
701
|
-
async setReasoningEffort(effort) {
|
|
1096
|
+
async setReasoningEffort(effort, actor) {
|
|
702
1097
|
const session = await this.getSession(true);
|
|
703
1098
|
this.ensureIdle(session);
|
|
704
1099
|
const options = agentReasoningOptions(session.getInfo().agentId);
|
|
@@ -707,9 +1102,11 @@ export class RelayRuntime {
|
|
|
707
1102
|
}
|
|
708
1103
|
await session.setReasoningEffortForCurrentSession(effort);
|
|
709
1104
|
this.updateSession(session);
|
|
710
|
-
|
|
1105
|
+
const info = this.publicInfo(session);
|
|
1106
|
+
this.appendActivity({ source: "web", status: "info", type: "reasoning_changed", threadId: info.threadId, workspace: info.workspace, agentId: info.agentId, actor, detail: effort });
|
|
1107
|
+
return info;
|
|
711
1108
|
}
|
|
712
|
-
async setFastMode(enabled) {
|
|
1109
|
+
async setFastMode(enabled, actor) {
|
|
713
1110
|
const session = await this.getSession(true);
|
|
714
1111
|
this.ensureIdle(session);
|
|
715
1112
|
if (!(session.getInfo().capabilities ?? CODEX_AGENT_CAPABILITIES).fastMode) {
|
|
@@ -717,23 +1114,29 @@ export class RelayRuntime {
|
|
|
717
1114
|
}
|
|
718
1115
|
session.setFastMode(enabled);
|
|
719
1116
|
this.updateSession(session);
|
|
720
|
-
|
|
1117
|
+
const info = this.publicInfo(session);
|
|
1118
|
+
this.appendActivity({ source: "web", status: "info", type: "fast_mode_changed", threadId: info.threadId, workspace: info.workspace, agentId: info.agentId, actor, detail: enabled ? "on" : "off" });
|
|
1119
|
+
return info;
|
|
721
1120
|
}
|
|
722
|
-
async setLaunchProfile(profileId) {
|
|
1121
|
+
async setLaunchProfile(profileId, actor) {
|
|
723
1122
|
const session = await this.getSession(true);
|
|
724
1123
|
this.ensureIdle(session);
|
|
725
1124
|
session.setLaunchProfile(profileId);
|
|
726
1125
|
this.updateSession(session);
|
|
727
|
-
|
|
1126
|
+
const info = this.publicInfo(session);
|
|
1127
|
+
this.appendActivity({ source: "web", status: "info", type: "launch_profile_changed", threadId: info.threadId, workspace: info.workspace, agentId: info.agentId, actor, detail: info.launchProfileLabel ?? profileId });
|
|
1128
|
+
return info;
|
|
728
1129
|
}
|
|
729
|
-
async handback() {
|
|
1130
|
+
async handback(actor) {
|
|
730
1131
|
const session = await this.getSession(true);
|
|
731
1132
|
this.ensureIdle(session);
|
|
732
1133
|
const result = session.handback();
|
|
733
1134
|
this.updateSession(session);
|
|
1135
|
+
const info = this.publicInfo(session);
|
|
1136
|
+
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
1137
|
return result;
|
|
735
1138
|
}
|
|
736
|
-
async abort() {
|
|
1139
|
+
async abort(actor) {
|
|
737
1140
|
const session = await this.getSession(true);
|
|
738
1141
|
const snapshot = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
|
|
739
1142
|
if (snapshot?.activity.active && !session.isProcessing()) {
|
|
@@ -743,19 +1146,23 @@ export class RelayRuntime {
|
|
|
743
1146
|
message: `Cannot abort the external ${snapshot.agentLabel} CLI task from NordRelay. Stop it in the terminal where it is running.`,
|
|
744
1147
|
at: new Date().toISOString(),
|
|
745
1148
|
});
|
|
1149
|
+
const info = this.publicInfo(session);
|
|
1150
|
+
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
1151
|
return;
|
|
747
1152
|
}
|
|
748
1153
|
await session.abort();
|
|
1154
|
+
const info = this.publicInfo(session);
|
|
1155
|
+
this.appendActivity({ source: "web", status: "aborted", type: "prompt_aborted", threadId: info.threadId, workspace: info.workspace, agentId: info.agentId, actor, detail: "Current operation aborted." });
|
|
749
1156
|
this.broadcast({ type: "status", level: "warn", message: "Current operation aborted.", at: new Date().toISOString() });
|
|
750
1157
|
}
|
|
751
|
-
async sendPrompt(text) {
|
|
1158
|
+
async sendPrompt(text, actor) {
|
|
752
1159
|
const trimmed = text.trim();
|
|
753
1160
|
if (!trimmed) {
|
|
754
1161
|
throw new Error("Prompt is empty.");
|
|
755
1162
|
}
|
|
756
|
-
return this.sendEnvelope(toPromptEnvelope(trimmed));
|
|
1163
|
+
return this.sendEnvelope({ ...toPromptEnvelope(trimmed), activityActor: actor }, actor);
|
|
757
1164
|
}
|
|
758
|
-
async sendUploadPrompt(options) {
|
|
1165
|
+
async sendUploadPrompt(options, actor) {
|
|
759
1166
|
const text = options.text?.trim() ?? "";
|
|
760
1167
|
const files = options.files.filter((file) => file.data.byteLength > 0);
|
|
761
1168
|
if (!text && files.length === 0) {
|
|
@@ -790,9 +1197,32 @@ export class RelayRuntime {
|
|
|
790
1197
|
const transcript = result.text.trim();
|
|
791
1198
|
if (transcript) {
|
|
792
1199
|
transcriptParts.push(`Audio transcript (${staged.safeName}, via ${result.backend}):\n${transcript}`);
|
|
1200
|
+
this.appendActivity({
|
|
1201
|
+
source: "web",
|
|
1202
|
+
status: "completed",
|
|
1203
|
+
type: "voice_transcribed",
|
|
1204
|
+
threadId: session.getInfo().threadId,
|
|
1205
|
+
workspace,
|
|
1206
|
+
agentId: session.getInfo().agentId,
|
|
1207
|
+
actor,
|
|
1208
|
+
detail: `${staged.safeName} via ${result.backend}`,
|
|
1209
|
+
durationMs: result.durationMs,
|
|
1210
|
+
});
|
|
793
1211
|
}
|
|
794
1212
|
}
|
|
795
1213
|
}
|
|
1214
|
+
if (stagedFiles.length > 0) {
|
|
1215
|
+
this.appendActivity({
|
|
1216
|
+
source: "web",
|
|
1217
|
+
status: "info",
|
|
1218
|
+
type: "attachment_staged",
|
|
1219
|
+
threadId: session.getInfo().threadId,
|
|
1220
|
+
workspace,
|
|
1221
|
+
agentId: session.getInfo().agentId,
|
|
1222
|
+
actor,
|
|
1223
|
+
detail: `${stagedFiles.length} file(s): ${stagedFiles.map((file) => file.safeName).join(", ")}`,
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
796
1226
|
const audioOnly = stagedFiles.length > 0 && stagedFiles.every((file) => file.mimeType.startsWith("audio/"));
|
|
797
1227
|
if (this.config.voiceTranscribeOnly && audioOnly && !text) {
|
|
798
1228
|
return {
|
|
@@ -813,14 +1243,15 @@ export class RelayRuntime {
|
|
|
813
1243
|
if (stagedFiles.length > 0) {
|
|
814
1244
|
promptInput.stagedFileInstructions = buildFileInstructions(stagedFiles, outDir);
|
|
815
1245
|
}
|
|
816
|
-
const result = await this.sendEnvelope(toPromptEnvelope(promptInput, outDir));
|
|
1246
|
+
const result = await this.sendEnvelope({ ...toPromptEnvelope(promptInput, outDir), activityActor: actor }, actor);
|
|
817
1247
|
return {
|
|
818
1248
|
...result,
|
|
819
1249
|
transcript: transcriptParts.join("\n\n") || undefined,
|
|
820
1250
|
files: uploadFileDtos(stagedFiles),
|
|
821
1251
|
};
|
|
822
1252
|
}
|
|
823
|
-
async sendEnvelope(envelope) {
|
|
1253
|
+
async sendEnvelope(envelope, actor) {
|
|
1254
|
+
const activityActor = envelope.activityActor ?? actor;
|
|
824
1255
|
const session = await this.getSession(false);
|
|
825
1256
|
const external = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
|
|
826
1257
|
if (session.isProcessing() || external?.activity.active) {
|
|
@@ -833,6 +1264,7 @@ export class RelayRuntime {
|
|
|
833
1264
|
threadId: info.threadId,
|
|
834
1265
|
workspace: info.workspace,
|
|
835
1266
|
agentId: info.agentId,
|
|
1267
|
+
actor: activityActor,
|
|
836
1268
|
prompt: envelope.description,
|
|
837
1269
|
detail: external?.activity.active
|
|
838
1270
|
? `Queued because ${external.agentLabel} CLI is still processing another task.`
|
|
@@ -846,6 +1278,7 @@ export class RelayRuntime {
|
|
|
846
1278
|
threadId: info.threadId,
|
|
847
1279
|
workspace: info.workspace,
|
|
848
1280
|
promptId: queued.id,
|
|
1281
|
+
actor: activityActor,
|
|
849
1282
|
description: envelope.description,
|
|
850
1283
|
});
|
|
851
1284
|
if (external?.activity.active) {
|
|
@@ -854,7 +1287,7 @@ export class RelayRuntime {
|
|
|
854
1287
|
this.broadcastQueue();
|
|
855
1288
|
return { queued: true, queueId: queued.id };
|
|
856
1289
|
}
|
|
857
|
-
void this.runPrompt(session, envelope).catch((error) => {
|
|
1290
|
+
void this.runPrompt(session, { ...envelope, activityActor }).catch((error) => {
|
|
858
1291
|
this.broadcast({ type: "turn_error", id: this.currentTurnId ?? "turn", error: friendlyErrorText(error), at: new Date().toISOString() });
|
|
859
1292
|
});
|
|
860
1293
|
return { queued: false };
|
|
@@ -865,7 +1298,9 @@ export class RelayRuntime {
|
|
|
865
1298
|
queuePaused() {
|
|
866
1299
|
return this.queueService.isPaused();
|
|
867
1300
|
}
|
|
868
|
-
queueAction(action, id) {
|
|
1301
|
+
queueAction(action, id, actor) {
|
|
1302
|
+
const before = this.queueService.rawList();
|
|
1303
|
+
const affected = id ? before.find((item) => item.id === id) : undefined;
|
|
869
1304
|
this.queueService.apply(action, id);
|
|
870
1305
|
if (id && action === "run") {
|
|
871
1306
|
void this.drainQueue().catch((error) => this.broadcastStatus(friendlyErrorText(error), "error"));
|
|
@@ -873,15 +1308,18 @@ export class RelayRuntime {
|
|
|
873
1308
|
this.appendActivity({
|
|
874
1309
|
source: "web",
|
|
875
1310
|
status: "info",
|
|
876
|
-
type:
|
|
1311
|
+
type: `queue_${action}`,
|
|
877
1312
|
threadId: null,
|
|
878
1313
|
workspace: this.config.workspace,
|
|
879
|
-
|
|
1314
|
+
actor,
|
|
1315
|
+
prompt: affected?.description,
|
|
1316
|
+
detail: id ? `${action}: ${id}` : `${action}: ${before.length} queued`,
|
|
880
1317
|
});
|
|
881
1318
|
this.appendAudit({
|
|
882
1319
|
action: "queue_updated",
|
|
883
1320
|
status: "ok",
|
|
884
1321
|
contextKey: WEB_CONTEXT_KEY,
|
|
1322
|
+
actor,
|
|
885
1323
|
description: id ? `${action}: ${id}` : action,
|
|
886
1324
|
});
|
|
887
1325
|
this.broadcastQueue();
|
|
@@ -895,13 +1333,39 @@ export class RelayRuntime {
|
|
|
895
1333
|
const session = await this.getSession(true);
|
|
896
1334
|
return this.artifactService.get(session.getInfo().workspace, turnId);
|
|
897
1335
|
}
|
|
898
|
-
async deleteArtifact(turnId) {
|
|
1336
|
+
async deleteArtifact(turnId, actor) {
|
|
899
1337
|
const session = await this.getSession(true);
|
|
900
|
-
|
|
1338
|
+
const info = this.publicInfo(session);
|
|
1339
|
+
const removed = await this.artifactService.delete(info.workspace, turnId);
|
|
1340
|
+
this.appendActivity({
|
|
1341
|
+
source: "web",
|
|
1342
|
+
status: removed ? "info" : "failed",
|
|
1343
|
+
type: "artifact_deleted",
|
|
1344
|
+
threadId: info.threadId,
|
|
1345
|
+
workspace: info.workspace,
|
|
1346
|
+
agentId: info.agentId,
|
|
1347
|
+
actor,
|
|
1348
|
+
detail: turnId,
|
|
1349
|
+
});
|
|
1350
|
+
return removed;
|
|
901
1351
|
}
|
|
902
|
-
async createArtifactZip(turnId) {
|
|
1352
|
+
async createArtifactZip(turnId, actor) {
|
|
903
1353
|
const session = await this.getSession(true);
|
|
904
|
-
|
|
1354
|
+
const info = this.publicInfo(session);
|
|
1355
|
+
const zip = await this.artifactService.createZip(info.workspace, turnId);
|
|
1356
|
+
if (zip) {
|
|
1357
|
+
this.appendActivity({
|
|
1358
|
+
source: "web",
|
|
1359
|
+
status: "info",
|
|
1360
|
+
type: "artifact_zip_created",
|
|
1361
|
+
threadId: info.threadId,
|
|
1362
|
+
workspace: info.workspace,
|
|
1363
|
+
agentId: info.agentId,
|
|
1364
|
+
actor,
|
|
1365
|
+
detail: zip.name,
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
return zip;
|
|
905
1369
|
}
|
|
906
1370
|
async artifactPreview(turnId, relativePath) {
|
|
907
1371
|
const session = await this.getSession(true);
|
|
@@ -916,7 +1380,7 @@ export class RelayRuntime {
|
|
|
916
1380
|
}
|
|
917
1381
|
return readFormattedLogTail(lines);
|
|
918
1382
|
}
|
|
919
|
-
clearLogs(target = "connector") {
|
|
1383
|
+
clearLogs(target = "connector", actor) {
|
|
920
1384
|
const result = clearLogFile(target === "update" ? getUpdateLogPath() : target === "agent-updates" ? getAgentUpdateLogPath() : getConnectorLogPath());
|
|
921
1385
|
this.appendActivity({
|
|
922
1386
|
source: "web",
|
|
@@ -924,11 +1388,20 @@ export class RelayRuntime {
|
|
|
924
1388
|
type: "logs_cleared",
|
|
925
1389
|
threadId: null,
|
|
926
1390
|
workspace: this.config.workspace,
|
|
1391
|
+
actor,
|
|
927
1392
|
detail: `Cleared ${target} log.`,
|
|
928
1393
|
});
|
|
1394
|
+
this.appendAudit({
|
|
1395
|
+
action: "command",
|
|
1396
|
+
status: "ok",
|
|
1397
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
1398
|
+
actor,
|
|
1399
|
+
description: `clear ${target} log`,
|
|
1400
|
+
detail: result.filePath,
|
|
1401
|
+
});
|
|
929
1402
|
return { ok: true, filePath: result.filePath, clearedAt: result.clearedAt.toISOString() };
|
|
930
1403
|
}
|
|
931
|
-
restartConnector() {
|
|
1404
|
+
restartConnector(actor) {
|
|
932
1405
|
spawnConnectorRestart();
|
|
933
1406
|
this.broadcastStatus("Restart requested. The dashboard may disconnect briefly.", "warn");
|
|
934
1407
|
this.appendActivity({
|
|
@@ -937,8 +1410,16 @@ export class RelayRuntime {
|
|
|
937
1410
|
type: "restart_requested",
|
|
938
1411
|
threadId: null,
|
|
939
1412
|
workspace: this.config.workspace,
|
|
1413
|
+
actor,
|
|
940
1414
|
detail: "Dashboard requested a connector restart.",
|
|
941
1415
|
});
|
|
1416
|
+
this.appendAudit({
|
|
1417
|
+
action: "command",
|
|
1418
|
+
status: "ok",
|
|
1419
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
1420
|
+
actor,
|
|
1421
|
+
description: "restart connector",
|
|
1422
|
+
});
|
|
942
1423
|
return { ok: true, message: "Restart requested." };
|
|
943
1424
|
}
|
|
944
1425
|
dispose() {
|
|
@@ -952,6 +1433,234 @@ export class RelayRuntime {
|
|
|
952
1433
|
async getSession(deferThreadStart) {
|
|
953
1434
|
return this.registry.getOrCreate(WEB_CONTEXT_KEY, { deferThreadStart });
|
|
954
1435
|
}
|
|
1436
|
+
async cached(key, producer) {
|
|
1437
|
+
return (await this.cache.get(key, this.config.dashboardCacheTtlMs, producer)).value;
|
|
1438
|
+
}
|
|
1439
|
+
listKnownContextMetadata() {
|
|
1440
|
+
const contexts = new Map();
|
|
1441
|
+
const add = (meta) => {
|
|
1442
|
+
if (meta?.contextKey) {
|
|
1443
|
+
contexts.set(meta.contextKey, meta);
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1446
|
+
for (const meta of this.registry.listContexts()) {
|
|
1447
|
+
add(meta);
|
|
1448
|
+
}
|
|
1449
|
+
const sharedRegistry = new SessionRegistry(this.config);
|
|
1450
|
+
try {
|
|
1451
|
+
for (const meta of sharedRegistry.listContexts()) {
|
|
1452
|
+
add(meta);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
finally {
|
|
1456
|
+
sharedRegistry.disposeAll();
|
|
1457
|
+
}
|
|
1458
|
+
const current = this.registry.get(WEB_CONTEXT_KEY)?.getInfo();
|
|
1459
|
+
if (current) {
|
|
1460
|
+
add({
|
|
1461
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
1462
|
+
agentId: current.agentId,
|
|
1463
|
+
threadId: current.threadId,
|
|
1464
|
+
workspace: current.workspace,
|
|
1465
|
+
model: current.model,
|
|
1466
|
+
reasoningEffort: current.reasoningEffort,
|
|
1467
|
+
launchProfileId: current.nextLaunchProfileId ?? current.launchProfileId,
|
|
1468
|
+
sessionPath: current.sessionPath,
|
|
1469
|
+
updatedAt: Date.now(),
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
return [...contexts.values()];
|
|
1473
|
+
}
|
|
1474
|
+
discoverRunningConnectorSessions() {
|
|
1475
|
+
const active = [];
|
|
1476
|
+
const terminal = new Set();
|
|
1477
|
+
const now = Date.now();
|
|
1478
|
+
for (const event of this.activityStore.list({ limit: 500 })) {
|
|
1479
|
+
if (!event.threadId || !event.agentId || !event.contextKey) {
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
const key = `${event.source}:${event.contextKey}:${event.agentId}:${event.threadId}`;
|
|
1483
|
+
if (isPromptTerminalActivity(event)) {
|
|
1484
|
+
terminal.add(key);
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
if (event.type !== "prompt_started" || event.status !== "running" || event.source === "cli") {
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
if (terminal.has(key)) {
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
const startedMs = Date.parse(event.timestamp);
|
|
1494
|
+
if (!Number.isFinite(startedMs) || now - startedMs > ACTIVE_ACTIVITY_TTL_MS) {
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
active.push({
|
|
1498
|
+
id: `${event.contextKey}:${event.id}`,
|
|
1499
|
+
contextKey: event.contextKey,
|
|
1500
|
+
sourceContextKey: event.contextKey,
|
|
1501
|
+
source: event.source,
|
|
1502
|
+
status: "running",
|
|
1503
|
+
agentId: event.agentId,
|
|
1504
|
+
agentLabel: event.agentId ? agentLabel(event.agentId) : undefined,
|
|
1505
|
+
threadId: event.threadId,
|
|
1506
|
+
workspace: event.workspace,
|
|
1507
|
+
prompt: event.prompt,
|
|
1508
|
+
startedAt: event.timestamp,
|
|
1509
|
+
updatedAt: event.timestamp,
|
|
1510
|
+
durationMs: Math.max(0, now - startedMs),
|
|
1511
|
+
queueLength: this.promptStore.list(event.contextKey).length,
|
|
1512
|
+
queuePaused: this.promptStore.isPaused(event.contextKey),
|
|
1513
|
+
detail: event.actor?.label ? `Started by ${event.actor.label}` : undefined,
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
return active;
|
|
1517
|
+
}
|
|
1518
|
+
discoverActiveCodexSessions(knownContexts, preferences) {
|
|
1519
|
+
if (!this.config.codexEnabled || !enabledAgents(this.config).includes("codex")) {
|
|
1520
|
+
return [];
|
|
1521
|
+
}
|
|
1522
|
+
const capabilities = this.capabilitiesForAgent("codex");
|
|
1523
|
+
if (!capabilities.externalActivity) {
|
|
1524
|
+
return [];
|
|
1525
|
+
}
|
|
1526
|
+
const active = [];
|
|
1527
|
+
for (const thread of listCodexThreads(ACTIVE_CODEX_DISCOVERY_LIMIT)) {
|
|
1528
|
+
const meta = {
|
|
1529
|
+
contextKey: `cli:codex:${thread.id}`,
|
|
1530
|
+
agentId: "codex",
|
|
1531
|
+
threadId: thread.id,
|
|
1532
|
+
workspace: thread.cwd,
|
|
1533
|
+
model: thread.model ?? undefined,
|
|
1534
|
+
reasoningEffort: thread.reasoningEffort ?? undefined,
|
|
1535
|
+
updatedAt: thread.updatedAt.getTime(),
|
|
1536
|
+
};
|
|
1537
|
+
const session = this.externalActiveSession(meta, knownContexts, preferences);
|
|
1538
|
+
if (session) {
|
|
1539
|
+
active.push(session);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
return active;
|
|
1543
|
+
}
|
|
1544
|
+
externalActiveSession(meta, knownContexts, preferences) {
|
|
1545
|
+
if (!meta.threadId) {
|
|
1546
|
+
return null;
|
|
1547
|
+
}
|
|
1548
|
+
const agentId = isAgentId(meta.agentId) ? meta.agentId : this.config.defaultAgent;
|
|
1549
|
+
if (!enabledAgents(this.config).includes(agentId)) {
|
|
1550
|
+
return null;
|
|
1551
|
+
}
|
|
1552
|
+
const capabilities = this.capabilitiesForAgent(agentId);
|
|
1553
|
+
if (!capabilities.externalActivity) {
|
|
1554
|
+
return null;
|
|
1555
|
+
}
|
|
1556
|
+
const snapshot = getExternalSnapshotForSession(this.sessionStubForMetadata(meta, agentId, capabilities), this.config, {
|
|
1557
|
+
maxEvents: 8,
|
|
1558
|
+
});
|
|
1559
|
+
if (!snapshot?.activity.active) {
|
|
1560
|
+
return null;
|
|
1561
|
+
}
|
|
1562
|
+
const startedAt = snapshot.activity.startedAt?.toISOString() ?? new Date().toISOString();
|
|
1563
|
+
const updatedAt = snapshot.activity.updatedAt?.toISOString() ?? new Date().toISOString();
|
|
1564
|
+
const startedMs = Date.parse(startedAt);
|
|
1565
|
+
const sourceContextKey = `cli:${snapshot.agentId}:${snapshot.threadId}`;
|
|
1566
|
+
const mirrorChannels = this.activeMirrorChannels(snapshot.agentId, snapshot.threadId, knownContexts, preferences);
|
|
1567
|
+
const queueLength = mirrorChannels.reduce((sum, mirror) => sum + mirror.queueLength, this.promptStore.list(sourceContextKey).length);
|
|
1568
|
+
const mirrorDetail = mirrorChannels.length > 0
|
|
1569
|
+
? `Mirroring: ${mirrorChannels.map((mirror) => `${mirror.source} ${mirror.mode}`).join(", ")}`
|
|
1570
|
+
: "Mirroring: none";
|
|
1571
|
+
return {
|
|
1572
|
+
id: `${sourceContextKey}:${snapshot.activity.turnId ?? snapshot.threadId}`,
|
|
1573
|
+
contextKey: sourceContextKey,
|
|
1574
|
+
sourceContextKey,
|
|
1575
|
+
source: "cli",
|
|
1576
|
+
status: "external",
|
|
1577
|
+
agentId: snapshot.agentId,
|
|
1578
|
+
agentLabel: snapshot.agentLabel,
|
|
1579
|
+
threadId: snapshot.threadId,
|
|
1580
|
+
workspace: meta.workspace,
|
|
1581
|
+
prompt: snapshot.latestUserMessage ?? undefined,
|
|
1582
|
+
currentTool: snapshot.latestToolName ?? undefined,
|
|
1583
|
+
lastTool: snapshot.latestToolName ?? undefined,
|
|
1584
|
+
startedAt,
|
|
1585
|
+
updatedAt,
|
|
1586
|
+
durationMs: Number.isFinite(startedMs) ? Math.max(0, Date.now() - startedMs) : 0,
|
|
1587
|
+
queueLength,
|
|
1588
|
+
queuePaused: mirrorChannels.some((mirror) => mirror.queuePaused) || this.promptStore.isPaused(sourceContextKey),
|
|
1589
|
+
mirrorChannels,
|
|
1590
|
+
detail: `${mirrorDetail} | ${snapshot.sourceLabel}: ${snapshot.sourcePath}`,
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
activeMirrorChannels(agentId, threadId, knownContexts, preferences) {
|
|
1594
|
+
const mirrors = [];
|
|
1595
|
+
const seen = new Set();
|
|
1596
|
+
for (const meta of knownContexts) {
|
|
1597
|
+
const metaAgentId = meta.agentId ?? this.config.defaultAgent ?? "codex";
|
|
1598
|
+
if (meta.threadId !== threadId || metaAgentId !== agentId) {
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
const source = activeSessionSourceForContext(meta.contextKey);
|
|
1602
|
+
if (source !== "telegram" && source !== "discord") {
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
const mode = this.effectiveMirrorMode(meta.contextKey, source, preferences);
|
|
1606
|
+
if (mode === "off" || seen.has(meta.contextKey)) {
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1609
|
+
seen.add(meta.contextKey);
|
|
1610
|
+
mirrors.push({
|
|
1611
|
+
source,
|
|
1612
|
+
contextKey: meta.contextKey,
|
|
1613
|
+
mode,
|
|
1614
|
+
queueLength: this.promptStore.list(meta.contextKey).length,
|
|
1615
|
+
queuePaused: this.promptStore.isPaused(meta.contextKey),
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
return mirrors;
|
|
1619
|
+
}
|
|
1620
|
+
effectiveMirrorMode(contextKey, source, preferences) {
|
|
1621
|
+
const configured = source === "telegram" ? this.config.telegramMirrorMode : this.config.discordMirrorMode;
|
|
1622
|
+
return preferences.get(contextKey).mirrorMode ?? configured;
|
|
1623
|
+
}
|
|
1624
|
+
sessionStubForMetadata(meta, agentId, capabilities) {
|
|
1625
|
+
const info = {
|
|
1626
|
+
agentId,
|
|
1627
|
+
agentLabel: agentLabel(agentId),
|
|
1628
|
+
threadId: meta.threadId,
|
|
1629
|
+
workspace: meta.workspace,
|
|
1630
|
+
model: meta.model,
|
|
1631
|
+
reasoningEffort: meta.reasoningEffort,
|
|
1632
|
+
launchProfileId: meta.launchProfileId ?? this.config.defaultLaunchProfileId,
|
|
1633
|
+
launchProfileLabel: meta.launchProfileId ?? this.config.defaultLaunchProfileId,
|
|
1634
|
+
launchProfileBehavior: "-",
|
|
1635
|
+
sandboxMode: "-",
|
|
1636
|
+
approvalPolicy: "-",
|
|
1637
|
+
fastMode: false,
|
|
1638
|
+
unsafeLaunch: false,
|
|
1639
|
+
sessionPath: meta.sessionPath,
|
|
1640
|
+
capabilities,
|
|
1641
|
+
};
|
|
1642
|
+
return {
|
|
1643
|
+
getInfo: () => info,
|
|
1644
|
+
getActiveThreadId: () => meta.threadId,
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
capabilitiesForAgent(agentId) {
|
|
1648
|
+
return listAgentAdapterDescriptors().find((descriptor) => descriptor.id === agentId)?.capabilities ?? CODEX_AGENT_CAPABILITIES;
|
|
1649
|
+
}
|
|
1650
|
+
activeSessionKey(session) {
|
|
1651
|
+
return session.threadId ? `${session.agentId ?? "unknown"}:${session.threadId}` : session.id;
|
|
1652
|
+
}
|
|
1653
|
+
preferredActiveSession(existing, candidate) {
|
|
1654
|
+
if (!existing) {
|
|
1655
|
+
return candidate;
|
|
1656
|
+
}
|
|
1657
|
+
const existingPriority = activeSessionPriority(existing);
|
|
1658
|
+
const candidatePriority = activeSessionPriority(candidate);
|
|
1659
|
+
if (candidatePriority !== existingPriority) {
|
|
1660
|
+
return candidatePriority > existingPriority ? candidate : existing;
|
|
1661
|
+
}
|
|
1662
|
+
return Date.parse(candidate.updatedAt) >= Date.parse(existing.updatedAt) ? candidate : existing;
|
|
1663
|
+
}
|
|
955
1664
|
async getControlSession(agentId) {
|
|
956
1665
|
const active = await this.getSession(true);
|
|
957
1666
|
const activeInfo = this.publicInfo(active);
|
|
@@ -1039,169 +1748,14 @@ export class RelayRuntime {
|
|
|
1039
1748
|
}
|
|
1040
1749
|
}
|
|
1041
1750
|
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
1751
|
const workspacePolicy = evaluateWorkspacePolicy(session.getInfo().workspace, this.config);
|
|
1051
1752
|
if (!workspacePolicy.allowed) {
|
|
1052
1753
|
throw new Error(workspacePolicy.warning ?? "Current workspace is blocked by policy.");
|
|
1053
1754
|
}
|
|
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
1755
|
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;
|
|
1756
|
+
await this.turnService.run(session, envelope);
|
|
1198
1757
|
}
|
|
1199
1758
|
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
1759
|
await this.drainQueue();
|
|
1206
1760
|
}
|
|
1207
1761
|
}
|
|
@@ -1234,6 +1788,40 @@ export class RelayRuntime {
|
|
|
1234
1788
|
this.registry.updateMetadata(WEB_CONTEXT_KEY, session);
|
|
1235
1789
|
this.broadcast({ type: "session_update", session: this.publicInfo(session) });
|
|
1236
1790
|
}
|
|
1791
|
+
recordActivity(input) {
|
|
1792
|
+
return this.appendActivity(input);
|
|
1793
|
+
}
|
|
1794
|
+
recordAgentUpdateLifecycle(job) {
|
|
1795
|
+
const previous = this.agentUpdateStates.get(job.id);
|
|
1796
|
+
const actor = this.agentUpdateActors.get(job.id);
|
|
1797
|
+
if (job.needsInput && !previous?.needsInput) {
|
|
1798
|
+
this.appendActivity({
|
|
1799
|
+
source: "web",
|
|
1800
|
+
status: "info",
|
|
1801
|
+
type: "agent_update_input_required",
|
|
1802
|
+
agentId: job.agentId,
|
|
1803
|
+
threadId: null,
|
|
1804
|
+
workspace: this.config.workspace,
|
|
1805
|
+
actor,
|
|
1806
|
+
detail: `${job.agentLabel} ${job.operation} may require input.`,
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
if (job.status !== "running" && previous?.status === "running") {
|
|
1810
|
+
this.appendActivity({
|
|
1811
|
+
source: "web",
|
|
1812
|
+
status: job.status === "completed" ? "completed" : job.status === "cancelled" ? "aborted" : "failed",
|
|
1813
|
+
type: job.operation === "install" ? `agent_install_${job.status}` : `agent_update_${job.status}`,
|
|
1814
|
+
agentId: job.agentId,
|
|
1815
|
+
threadId: null,
|
|
1816
|
+
workspace: this.config.workspace,
|
|
1817
|
+
actor,
|
|
1818
|
+
detail: job.error ?? `${job.agentLabel} ${job.operation} ${job.status}.`,
|
|
1819
|
+
durationMs: Math.max(0, Date.parse(job.finishedAt ?? job.updatedAt) - Date.parse(job.startedAt)),
|
|
1820
|
+
});
|
|
1821
|
+
this.agentUpdateActors.delete(job.id);
|
|
1822
|
+
}
|
|
1823
|
+
this.agentUpdateStates.set(job.id, { status: job.status, needsInput: job.needsInput });
|
|
1824
|
+
}
|
|
1237
1825
|
appendActivity(input) {
|
|
1238
1826
|
const event = this.activityStore.append(this.enrichActivityInput(input));
|
|
1239
1827
|
this.broadcast({ type: "activity_update", events: this.activity({ limit: 50 }) });
|
|
@@ -1371,6 +1959,123 @@ function hostLogoutCommand(info, config) {
|
|
|
1371
1959
|
}
|
|
1372
1960
|
return "codex logout";
|
|
1373
1961
|
}
|
|
1962
|
+
function activeSessionSourceForContext(contextKey) {
|
|
1963
|
+
const channelId = channelIdForContextKey(contextKey);
|
|
1964
|
+
if (channelId === "telegram") {
|
|
1965
|
+
return "telegram";
|
|
1966
|
+
}
|
|
1967
|
+
if (channelId === "discord") {
|
|
1968
|
+
return "discord";
|
|
1969
|
+
}
|
|
1970
|
+
if (channelId === "web") {
|
|
1971
|
+
return "web";
|
|
1972
|
+
}
|
|
1973
|
+
return "cli";
|
|
1974
|
+
}
|
|
1975
|
+
function activeSessionPriority(session) {
|
|
1976
|
+
if (session.status === "running") {
|
|
1977
|
+
return 3;
|
|
1978
|
+
}
|
|
1979
|
+
return session.contextKey.startsWith("cli:") ? 1 : 2;
|
|
1980
|
+
}
|
|
1981
|
+
function isPromptTerminalActivity(event) {
|
|
1982
|
+
return event.status === "completed" ||
|
|
1983
|
+
event.status === "failed" ||
|
|
1984
|
+
event.status === "aborted" ||
|
|
1985
|
+
event.type === "prompt_completed" ||
|
|
1986
|
+
event.type === "prompt_failed" ||
|
|
1987
|
+
event.type === "prompt_aborted";
|
|
1988
|
+
}
|
|
1989
|
+
function taskToUnifiedJob(id, kind, title, task, options) {
|
|
1990
|
+
return {
|
|
1991
|
+
id,
|
|
1992
|
+
kind,
|
|
1993
|
+
title,
|
|
1994
|
+
status: task.status,
|
|
1995
|
+
source: task.source,
|
|
1996
|
+
agentId: task.agentId,
|
|
1997
|
+
agentLabel: task.agentLabel,
|
|
1998
|
+
threadId: task.threadId,
|
|
1999
|
+
workspace: task.workspace,
|
|
2000
|
+
startedAt: task.startedAt,
|
|
2001
|
+
updatedAt: task.updatedAt,
|
|
2002
|
+
durationMs: task.durationMs,
|
|
2003
|
+
summary: task.prompt || task.detail,
|
|
2004
|
+
logTail: task.currentTool || task.lastTool ? `Current tool: ${task.currentTool ?? "-"}\nLast tool: ${task.lastTool ?? "-"}` : undefined,
|
|
2005
|
+
...options,
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
function activityToUnifiedJob(event, kind, title, options) {
|
|
2009
|
+
return {
|
|
2010
|
+
id: `${kind}:${event.id}`,
|
|
2011
|
+
kind,
|
|
2012
|
+
title,
|
|
2013
|
+
status: event.status,
|
|
2014
|
+
source: event.source,
|
|
2015
|
+
agentId: event.agentId,
|
|
2016
|
+
threadId: event.threadId,
|
|
2017
|
+
workspace: event.workspace,
|
|
2018
|
+
owner: event.actor,
|
|
2019
|
+
startedAt: event.timestamp,
|
|
2020
|
+
updatedAt: event.timestamp,
|
|
2021
|
+
finishedAt: event.timestamp,
|
|
2022
|
+
durationMs: event.durationMs,
|
|
2023
|
+
summary: event.prompt || event.detail,
|
|
2024
|
+
logPath: event.detail,
|
|
2025
|
+
logTail: event.detail,
|
|
2026
|
+
...options,
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
function promptActivityToUnifiedJob(event) {
|
|
2030
|
+
const status = event.status === "info" ? "completed" : event.status;
|
|
2031
|
+
const sourceLabel = event.source === "web"
|
|
2032
|
+
? "WebUI"
|
|
2033
|
+
: event.source === "telegram"
|
|
2034
|
+
? "Telegram"
|
|
2035
|
+
: event.source === "discord"
|
|
2036
|
+
? "Discord"
|
|
2037
|
+
: "CLI";
|
|
2038
|
+
const promptKey = event.threadId ?? event.contextKey ?? event.id;
|
|
2039
|
+
return {
|
|
2040
|
+
id: `prompt:${event.source}:${promptKey}:${event.id}`,
|
|
2041
|
+
kind: event.source === "cli" ? "external-turn" : "web-turn",
|
|
2042
|
+
title: `${sourceLabel} prompt`,
|
|
2043
|
+
status,
|
|
2044
|
+
source: event.source,
|
|
2045
|
+
agentId: event.agentId,
|
|
2046
|
+
threadId: event.threadId,
|
|
2047
|
+
workspace: event.workspace,
|
|
2048
|
+
owner: event.actor,
|
|
2049
|
+
startedAt: event.timestamp,
|
|
2050
|
+
updatedAt: event.timestamp,
|
|
2051
|
+
finishedAt: status === "running" || status === "queued" ? undefined : event.timestamp,
|
|
2052
|
+
durationMs: event.durationMs,
|
|
2053
|
+
summary: event.prompt || event.detail,
|
|
2054
|
+
logTail: event.detail,
|
|
2055
|
+
canCancel: status === "running" && event.source === "web",
|
|
2056
|
+
canRetry: status !== "running",
|
|
2057
|
+
canReadLog: Boolean(event.detail || event.prompt),
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
function agentUpdateStatusToUnified(status) {
|
|
2061
|
+
if (status === "cancelled")
|
|
2062
|
+
return "aborted";
|
|
2063
|
+
if (status === "running")
|
|
2064
|
+
return "running";
|
|
2065
|
+
if (status === "completed")
|
|
2066
|
+
return "completed";
|
|
2067
|
+
return "failed";
|
|
2068
|
+
}
|
|
2069
|
+
function dedupeJobs(jobs) {
|
|
2070
|
+
const seen = new Set();
|
|
2071
|
+
return jobs.filter((job) => {
|
|
2072
|
+
if (seen.has(job.id)) {
|
|
2073
|
+
return false;
|
|
2074
|
+
}
|
|
2075
|
+
seen.add(job.id);
|
|
2076
|
+
return true;
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
1374
2079
|
function normalizeMimeType(value, name) {
|
|
1375
2080
|
const configured = value?.trim();
|
|
1376
2081
|
if (configured) {
|