@nordbyte/nordrelay 0.5.1 → 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 +65 -11
- package/README.md +97 -23
- package/dist/access-control.js +1 -0
- package/dist/activity-events.js +44 -0
- package/dist/agent-updates.js +18 -2
- package/dist/audit-log.js +40 -2
- package/dist/bot-rendering.js +10 -7
- package/dist/bot.js +492 -7
- 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/codex-cli.js +1 -1
- package/dist/config-metadata.js +80 -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/operations.js +176 -119
- package/dist/relay-external-activity-monitor.js +47 -6
- package/dist/relay-runtime.js +1003 -268
- package/dist/runtime-cache.js +57 -0
- package/dist/session-locks.js +10 -7
- package/dist/state-backend.js +3 -0
- package/dist/support-bundle.js +18 -1
- 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 +149 -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/.codex-plugin/plugin.json +1 -1
- package/plugins/nordrelay/scripts/nordrelay.mjs +468 -22
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);
|
|
@@ -105,10 +153,16 @@ export class RelayRuntime {
|
|
|
105
153
|
};
|
|
106
154
|
}
|
|
107
155
|
async status() {
|
|
156
|
+
const cliOptions = this.cliPathOptions();
|
|
157
|
+
const [health, versionChecks, snapshot] = await Promise.all([
|
|
158
|
+
getConnectorHealth(cliOptions),
|
|
159
|
+
getVersionChecks(cliOptions),
|
|
160
|
+
this.snapshot(),
|
|
161
|
+
]);
|
|
108
162
|
return {
|
|
109
|
-
health
|
|
110
|
-
versionChecks
|
|
111
|
-
snapshot
|
|
163
|
+
health,
|
|
164
|
+
versionChecks,
|
|
165
|
+
snapshot,
|
|
112
166
|
};
|
|
113
167
|
}
|
|
114
168
|
async bootstrapStatus() {
|
|
@@ -121,13 +175,22 @@ export class RelayRuntime {
|
|
|
121
175
|
};
|
|
122
176
|
}
|
|
123
177
|
async version() {
|
|
124
|
-
return {
|
|
125
|
-
|
|
126
|
-
state
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
});
|
|
129
191
|
}
|
|
130
|
-
updateConnector() {
|
|
192
|
+
updateConnector(actor) {
|
|
193
|
+
this.cache.invalidate("version");
|
|
131
194
|
const update = spawnSelfUpdate();
|
|
132
195
|
this.broadcastStatus(`Update started with ${update.method}. Log: ${update.logPath}`, "warn");
|
|
133
196
|
this.appendActivity({
|
|
@@ -136,12 +199,14 @@ export class RelayRuntime {
|
|
|
136
199
|
type: "update_started",
|
|
137
200
|
threadId: null,
|
|
138
201
|
workspace: this.config.workspace,
|
|
202
|
+
actor,
|
|
139
203
|
detail: `${update.method}: ${update.summary}`,
|
|
140
204
|
});
|
|
141
205
|
this.appendAudit({
|
|
142
206
|
action: "command",
|
|
143
207
|
status: "ok",
|
|
144
208
|
contextKey: WEB_CONTEXT_KEY,
|
|
209
|
+
actor,
|
|
145
210
|
description: "update",
|
|
146
211
|
detail: update.summary,
|
|
147
212
|
});
|
|
@@ -150,13 +215,19 @@ export class RelayRuntime {
|
|
|
150
215
|
agentUpdateJobs() {
|
|
151
216
|
return this.agentUpdates.list();
|
|
152
217
|
}
|
|
153
|
-
startAgentUpdate(agentId, operation = "update") {
|
|
218
|
+
startAgentUpdate(agentId, operation = "update", actor) {
|
|
219
|
+
this.cache.invalidate("adapterHealth");
|
|
220
|
+
this.cache.invalidate("version");
|
|
154
221
|
const job = this.agentUpdates.start(agentId, {
|
|
155
222
|
piCliPath: this.config.piCliPath,
|
|
156
223
|
hermesCliPath: this.config.hermesCliPath,
|
|
157
224
|
openClawCliPath: this.config.openClawCliPath,
|
|
158
225
|
claudeCodeCliPath: this.config.claudeCodeCliPath,
|
|
159
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 });
|
|
160
231
|
this.broadcastStatus(`${job.agentLabel} ${operation} started. Log: ${job.logPath}`, "warn");
|
|
161
232
|
this.appendActivity({
|
|
162
233
|
source: "web",
|
|
@@ -165,6 +236,7 @@ export class RelayRuntime {
|
|
|
165
236
|
agentId,
|
|
166
237
|
threadId: null,
|
|
167
238
|
workspace: this.config.workspace,
|
|
239
|
+
actor,
|
|
168
240
|
detail: `${job.method}: ${job.summary}`,
|
|
169
241
|
});
|
|
170
242
|
this.appendAudit({
|
|
@@ -172,6 +244,7 @@ export class RelayRuntime {
|
|
|
172
244
|
status: "ok",
|
|
173
245
|
contextKey: WEB_CONTEXT_KEY,
|
|
174
246
|
agentId,
|
|
247
|
+
actor,
|
|
175
248
|
description: `${operation} ${agentId}`,
|
|
176
249
|
detail: job.summary,
|
|
177
250
|
});
|
|
@@ -180,83 +253,130 @@ export class RelayRuntime {
|
|
|
180
253
|
agentUpdateLog(id) {
|
|
181
254
|
return this.agentUpdates.readLog(id);
|
|
182
255
|
}
|
|
183
|
-
deleteAgentUpdateLog(id) {
|
|
256
|
+
deleteAgentUpdateLog(id, actor) {
|
|
184
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
|
+
});
|
|
185
268
|
this.appendAudit({
|
|
186
269
|
action: "command",
|
|
187
270
|
status: "ok",
|
|
188
271
|
contextKey: WEB_CONTEXT_KEY,
|
|
189
272
|
agentId: job.agentId,
|
|
273
|
+
actor,
|
|
190
274
|
description: `delete update log ${id}`,
|
|
191
275
|
detail: job.logPath,
|
|
192
276
|
});
|
|
193
277
|
return job;
|
|
194
278
|
}
|
|
195
|
-
sendAgentUpdateInput(id, input) {
|
|
196
|
-
|
|
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;
|
|
197
292
|
}
|
|
198
|
-
cancelAgentUpdate(id) {
|
|
199
|
-
|
|
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;
|
|
200
306
|
}
|
|
201
307
|
async diagnostics() {
|
|
202
|
-
return {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
externalMirror: this.externalActivityMonitor.snapshot(),
|
|
211
|
-
agentDiagnostics: getAgentDiagnostics(await this.getSession(true), this.config),
|
|
212
|
-
},
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
async adapterHealth() {
|
|
216
|
-
const health = await getConnectorHealth({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath });
|
|
217
|
-
const versions = await getVersionChecks({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath });
|
|
218
|
-
return Promise.all(listAgentAdapterDescriptors().map(async (descriptor) => {
|
|
219
|
-
const enabled = enabledAgents(this.config).includes(descriptor.id);
|
|
220
|
-
const auth = descriptor.capabilities.auth && enabled
|
|
221
|
-
? await this.authStatus(descriptor.id).catch((error) => ({
|
|
222
|
-
agentId: descriptor.id,
|
|
223
|
-
agentLabel: descriptor.label,
|
|
224
|
-
supported: descriptor.capabilities.auth,
|
|
225
|
-
authenticated: false,
|
|
226
|
-
detail: friendlyErrorText(error),
|
|
227
|
-
loginSupported: descriptor.capabilities.login,
|
|
228
|
-
logoutSupported: descriptor.capabilities.logout,
|
|
229
|
-
}))
|
|
230
|
-
: null;
|
|
231
|
-
const cli = cliHealthForAgent(descriptor.id, health);
|
|
232
|
-
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
|
+
]);
|
|
233
316
|
return {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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),
|
|
243
326
|
},
|
|
244
|
-
cli,
|
|
245
|
-
version: {
|
|
246
|
-
installed: version.installedLabel,
|
|
247
|
-
latest: version.latestVersion,
|
|
248
|
-
status: version.status,
|
|
249
|
-
detail: version.detail,
|
|
250
|
-
},
|
|
251
|
-
capabilities: descriptor.capabilities,
|
|
252
|
-
notes: descriptor.notes,
|
|
253
327
|
};
|
|
254
|
-
})
|
|
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
|
+
});
|
|
255
375
|
}
|
|
256
376
|
permissions() {
|
|
257
377
|
return {
|
|
258
378
|
mode: "users",
|
|
259
|
-
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.",
|
|
260
380
|
};
|
|
261
381
|
}
|
|
262
382
|
tasks() {
|
|
@@ -268,10 +388,229 @@ export class RelayRuntime {
|
|
|
268
388
|
recent: this.activity({ limit: 20 }),
|
|
269
389
|
};
|
|
270
390
|
}
|
|
271
|
-
|
|
272
|
-
|
|
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
|
+
};
|
|
273
479
|
}
|
|
274
|
-
async
|
|
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
|
+
};
|
|
596
|
+
}
|
|
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) {
|
|
275
614
|
const bundle = await createSupportBundle({
|
|
276
615
|
config: this.config,
|
|
277
616
|
diagnostics: await this.diagnostics(),
|
|
@@ -286,12 +625,14 @@ export class RelayRuntime {
|
|
|
286
625
|
type: "diagnostics_bundle_exported",
|
|
287
626
|
threadId: null,
|
|
288
627
|
workspace: this.config.workspace,
|
|
628
|
+
actor,
|
|
289
629
|
detail: bundle.path,
|
|
290
630
|
});
|
|
291
631
|
this.appendAudit({
|
|
292
632
|
action: "command",
|
|
293
633
|
status: "ok",
|
|
294
634
|
contextKey: WEB_CONTEXT_KEY,
|
|
635
|
+
actor,
|
|
295
636
|
description: "export diagnostics bundle",
|
|
296
637
|
detail: bundle.path,
|
|
297
638
|
});
|
|
@@ -300,23 +641,48 @@ export class RelayRuntime {
|
|
|
300
641
|
locks() {
|
|
301
642
|
return this.lockStore.list();
|
|
302
643
|
}
|
|
303
|
-
lockWebSession(ownerName = "Web dashboard") {
|
|
304
|
-
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
|
+
});
|
|
305
660
|
this.appendAudit({
|
|
306
661
|
action: "lock_updated",
|
|
307
662
|
status: "ok",
|
|
308
663
|
contextKey: WEB_CONTEXT_KEY,
|
|
664
|
+
actor,
|
|
309
665
|
description: "lock",
|
|
310
|
-
detail: `locked by ${
|
|
666
|
+
detail: `locked by ${label}`,
|
|
311
667
|
});
|
|
312
668
|
return lock;
|
|
313
669
|
}
|
|
314
|
-
unlockWebSession() {
|
|
670
|
+
unlockWebSession(actor) {
|
|
315
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
|
+
});
|
|
316
681
|
this.appendAudit({
|
|
317
682
|
action: "lock_updated",
|
|
318
683
|
status: "ok",
|
|
319
684
|
contextKey: WEB_CONTEXT_KEY,
|
|
685
|
+
actor,
|
|
320
686
|
description: "unlock",
|
|
321
687
|
detail: removed ? "unlocked" : "no lock",
|
|
322
688
|
});
|
|
@@ -385,7 +751,7 @@ export class RelayRuntime {
|
|
|
385
751
|
}
|
|
386
752
|
}
|
|
387
753
|
}
|
|
388
|
-
async login(agentId) {
|
|
754
|
+
async login(agentId, actor) {
|
|
389
755
|
const { session, dispose } = await this.getControlSession(agentId);
|
|
390
756
|
try {
|
|
391
757
|
const info = this.publicInfo(session);
|
|
@@ -409,6 +775,16 @@ export class RelayRuntime {
|
|
|
409
775
|
};
|
|
410
776
|
}
|
|
411
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
|
+
});
|
|
412
788
|
this.appendAudit({
|
|
413
789
|
action: "command",
|
|
414
790
|
status: result.success ? "ok" : "failed",
|
|
@@ -416,6 +792,7 @@ export class RelayRuntime {
|
|
|
416
792
|
agentId: info.agentId,
|
|
417
793
|
threadId: info.threadId,
|
|
418
794
|
workspace: info.workspace,
|
|
795
|
+
actor,
|
|
419
796
|
description: "login",
|
|
420
797
|
detail: result.message,
|
|
421
798
|
});
|
|
@@ -427,7 +804,7 @@ export class RelayRuntime {
|
|
|
427
804
|
}
|
|
428
805
|
}
|
|
429
806
|
}
|
|
430
|
-
async logout(agentId) {
|
|
807
|
+
async logout(agentId, actor) {
|
|
431
808
|
const { session, dispose } = await this.getControlSession(agentId);
|
|
432
809
|
try {
|
|
433
810
|
const info = this.publicInfo(session);
|
|
@@ -461,6 +838,16 @@ export class RelayRuntime {
|
|
|
461
838
|
};
|
|
462
839
|
}
|
|
463
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
|
+
});
|
|
464
851
|
this.appendAudit({
|
|
465
852
|
action: "command",
|
|
466
853
|
status: result.success ? "ok" : "failed",
|
|
@@ -468,6 +855,7 @@ export class RelayRuntime {
|
|
|
468
855
|
agentId: info.agentId,
|
|
469
856
|
threadId: info.threadId,
|
|
470
857
|
workspace: info.workspace,
|
|
858
|
+
actor,
|
|
471
859
|
description: "logout",
|
|
472
860
|
detail: result.message,
|
|
473
861
|
});
|
|
@@ -495,18 +883,29 @@ export class RelayRuntime {
|
|
|
495
883
|
activity: this.activity({ limit: 100 }).filter((event) => event.threadId === threadId),
|
|
496
884
|
};
|
|
497
885
|
}
|
|
498
|
-
async clearChatHistory() {
|
|
886
|
+
async clearChatHistory(actor) {
|
|
499
887
|
const session = await this.getSession(true);
|
|
500
|
-
const
|
|
888
|
+
const info = this.publicInfo(session);
|
|
889
|
+
const removed = this.chatStore.clear(info.threadId);
|
|
501
890
|
const messages = await this.chatHistory();
|
|
502
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
|
+
});
|
|
503
902
|
return { removed, messages };
|
|
504
903
|
}
|
|
505
904
|
activity(options = {}) {
|
|
506
905
|
const currentInfo = this.registry.get(WEB_CONTEXT_KEY)?.getInfo();
|
|
507
906
|
return this.activityStore.list(options).map((event) => this.enrichActivityEvent(event, currentInfo));
|
|
508
907
|
}
|
|
509
|
-
async retry() {
|
|
908
|
+
async retry(actor) {
|
|
510
909
|
const cached = this.queueService.getLastPrompt();
|
|
511
910
|
if (!cached) {
|
|
512
911
|
throw new Error("Nothing to retry. Send a message first.");
|
|
@@ -515,12 +914,13 @@ export class RelayRuntime {
|
|
|
515
914
|
action: "command",
|
|
516
915
|
status: "ok",
|
|
517
916
|
contextKey: WEB_CONTEXT_KEY,
|
|
917
|
+
actor,
|
|
518
918
|
description: "retry",
|
|
519
919
|
detail: cached.description,
|
|
520
920
|
});
|
|
521
|
-
return this.sendEnvelope(cached);
|
|
921
|
+
return this.sendEnvelope({ ...cached, activityActor: cached.activityActor ?? actor }, actor);
|
|
522
922
|
}
|
|
523
|
-
async sync() {
|
|
923
|
+
async sync(actor) {
|
|
524
924
|
const session = await this.getSession(true);
|
|
525
925
|
const info = this.publicInfo(session);
|
|
526
926
|
if (!(info.capabilities ?? CODEX_AGENT_CAPABILITIES).externalActivity) {
|
|
@@ -537,6 +937,7 @@ export class RelayRuntime {
|
|
|
537
937
|
threadId: result.info.threadId,
|
|
538
938
|
workspace: result.info.workspace,
|
|
539
939
|
agentId: result.info.agentId,
|
|
940
|
+
actor,
|
|
540
941
|
detail: result.changedFields.length > 0 ? result.changedFields.join(", ") : "already in sync",
|
|
541
942
|
});
|
|
542
943
|
this.appendAudit({
|
|
@@ -546,6 +947,7 @@ export class RelayRuntime {
|
|
|
546
947
|
agentId: result.info.agentId,
|
|
547
948
|
threadId: result.info.threadId,
|
|
548
949
|
workspace: result.info.workspace,
|
|
950
|
+
actor,
|
|
549
951
|
description: "sync",
|
|
550
952
|
detail: result.changedFields.join(", ") || "none",
|
|
551
953
|
});
|
|
@@ -612,15 +1014,26 @@ export class RelayRuntime {
|
|
|
612
1014
|
});
|
|
613
1015
|
return session.listModels();
|
|
614
1016
|
}
|
|
615
|
-
async setAgent(agentId) {
|
|
1017
|
+
async setAgent(agentId, actor) {
|
|
616
1018
|
if (!enabledAgents(this.config).includes(agentId)) {
|
|
617
1019
|
throw new Error(`Agent is not enabled: ${agentId}`);
|
|
618
1020
|
}
|
|
619
1021
|
const session = await this.registry.switchAgent(WEB_CONTEXT_KEY, agentId);
|
|
620
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
|
+
});
|
|
621
1034
|
return this.publicInfo(session);
|
|
622
1035
|
}
|
|
623
|
-
async newSession(options = {}) {
|
|
1036
|
+
async newSession(options = {}, actor) {
|
|
624
1037
|
const session = options.agentId ? await this.registry.switchAgent(WEB_CONTEXT_KEY, options.agentId) : await this.getSession(true);
|
|
625
1038
|
this.ensureIdle(session);
|
|
626
1039
|
if (options.reasoningEffort) {
|
|
@@ -645,11 +1058,12 @@ export class RelayRuntime {
|
|
|
645
1058
|
threadId: info.threadId,
|
|
646
1059
|
workspace: info.workspace,
|
|
647
1060
|
agentId: info.agentId,
|
|
1061
|
+
actor,
|
|
648
1062
|
detail: "New dashboard session created.",
|
|
649
1063
|
});
|
|
650
1064
|
return this.publicInfo(session);
|
|
651
1065
|
}
|
|
652
|
-
async switchSession(threadId) {
|
|
1066
|
+
async switchSession(threadId, actor) {
|
|
653
1067
|
const session = await this.getSession(true);
|
|
654
1068
|
this.ensureIdle(session);
|
|
655
1069
|
const info = await session.switchSession(threadId);
|
|
@@ -662,21 +1076,24 @@ export class RelayRuntime {
|
|
|
662
1076
|
threadId: info.threadId,
|
|
663
1077
|
workspace: info.workspace,
|
|
664
1078
|
agentId: info.agentId,
|
|
1079
|
+
actor,
|
|
665
1080
|
detail: "Dashboard switched session.",
|
|
666
1081
|
});
|
|
667
1082
|
return this.publicInfo(session);
|
|
668
1083
|
}
|
|
669
|
-
async attachSession(threadId) {
|
|
670
|
-
return this.switchSession(threadId);
|
|
1084
|
+
async attachSession(threadId, actor) {
|
|
1085
|
+
return this.switchSession(threadId, actor);
|
|
671
1086
|
}
|
|
672
|
-
async setModel(model) {
|
|
1087
|
+
async setModel(model, actor) {
|
|
673
1088
|
const session = await this.getSession(true);
|
|
674
1089
|
this.ensureIdle(session);
|
|
675
1090
|
await session.setModelForCurrentSession(model);
|
|
676
1091
|
this.updateSession(session);
|
|
677
|
-
|
|
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;
|
|
678
1095
|
}
|
|
679
|
-
async setReasoningEffort(effort) {
|
|
1096
|
+
async setReasoningEffort(effort, actor) {
|
|
680
1097
|
const session = await this.getSession(true);
|
|
681
1098
|
this.ensureIdle(session);
|
|
682
1099
|
const options = agentReasoningOptions(session.getInfo().agentId);
|
|
@@ -685,9 +1102,11 @@ export class RelayRuntime {
|
|
|
685
1102
|
}
|
|
686
1103
|
await session.setReasoningEffortForCurrentSession(effort);
|
|
687
1104
|
this.updateSession(session);
|
|
688
|
-
|
|
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;
|
|
689
1108
|
}
|
|
690
|
-
async setFastMode(enabled) {
|
|
1109
|
+
async setFastMode(enabled, actor) {
|
|
691
1110
|
const session = await this.getSession(true);
|
|
692
1111
|
this.ensureIdle(session);
|
|
693
1112
|
if (!(session.getInfo().capabilities ?? CODEX_AGENT_CAPABILITIES).fastMode) {
|
|
@@ -695,23 +1114,29 @@ export class RelayRuntime {
|
|
|
695
1114
|
}
|
|
696
1115
|
session.setFastMode(enabled);
|
|
697
1116
|
this.updateSession(session);
|
|
698
|
-
|
|
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;
|
|
699
1120
|
}
|
|
700
|
-
async setLaunchProfile(profileId) {
|
|
1121
|
+
async setLaunchProfile(profileId, actor) {
|
|
701
1122
|
const session = await this.getSession(true);
|
|
702
1123
|
this.ensureIdle(session);
|
|
703
1124
|
session.setLaunchProfile(profileId);
|
|
704
1125
|
this.updateSession(session);
|
|
705
|
-
|
|
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;
|
|
706
1129
|
}
|
|
707
|
-
async handback() {
|
|
1130
|
+
async handback(actor) {
|
|
708
1131
|
const session = await this.getSession(true);
|
|
709
1132
|
this.ensureIdle(session);
|
|
710
1133
|
const result = session.handback();
|
|
711
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." });
|
|
712
1137
|
return result;
|
|
713
1138
|
}
|
|
714
|
-
async abort() {
|
|
1139
|
+
async abort(actor) {
|
|
715
1140
|
const session = await this.getSession(true);
|
|
716
1141
|
const snapshot = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
|
|
717
1142
|
if (snapshot?.activity.active && !session.isProcessing()) {
|
|
@@ -721,19 +1146,23 @@ export class RelayRuntime {
|
|
|
721
1146
|
message: `Cannot abort the external ${snapshot.agentLabel} CLI task from NordRelay. Stop it in the terminal where it is running.`,
|
|
722
1147
|
at: new Date().toISOString(),
|
|
723
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.` });
|
|
724
1151
|
return;
|
|
725
1152
|
}
|
|
726
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." });
|
|
727
1156
|
this.broadcast({ type: "status", level: "warn", message: "Current operation aborted.", at: new Date().toISOString() });
|
|
728
1157
|
}
|
|
729
|
-
async sendPrompt(text) {
|
|
1158
|
+
async sendPrompt(text, actor) {
|
|
730
1159
|
const trimmed = text.trim();
|
|
731
1160
|
if (!trimmed) {
|
|
732
1161
|
throw new Error("Prompt is empty.");
|
|
733
1162
|
}
|
|
734
|
-
return this.sendEnvelope(toPromptEnvelope(trimmed));
|
|
1163
|
+
return this.sendEnvelope({ ...toPromptEnvelope(trimmed), activityActor: actor }, actor);
|
|
735
1164
|
}
|
|
736
|
-
async sendUploadPrompt(options) {
|
|
1165
|
+
async sendUploadPrompt(options, actor) {
|
|
737
1166
|
const text = options.text?.trim() ?? "";
|
|
738
1167
|
const files = options.files.filter((file) => file.data.byteLength > 0);
|
|
739
1168
|
if (!text && files.length === 0) {
|
|
@@ -768,9 +1197,32 @@ export class RelayRuntime {
|
|
|
768
1197
|
const transcript = result.text.trim();
|
|
769
1198
|
if (transcript) {
|
|
770
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
|
+
});
|
|
771
1211
|
}
|
|
772
1212
|
}
|
|
773
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
|
+
}
|
|
774
1226
|
const audioOnly = stagedFiles.length > 0 && stagedFiles.every((file) => file.mimeType.startsWith("audio/"));
|
|
775
1227
|
if (this.config.voiceTranscribeOnly && audioOnly && !text) {
|
|
776
1228
|
return {
|
|
@@ -791,14 +1243,15 @@ export class RelayRuntime {
|
|
|
791
1243
|
if (stagedFiles.length > 0) {
|
|
792
1244
|
promptInput.stagedFileInstructions = buildFileInstructions(stagedFiles, outDir);
|
|
793
1245
|
}
|
|
794
|
-
const result = await this.sendEnvelope(toPromptEnvelope(promptInput, outDir));
|
|
1246
|
+
const result = await this.sendEnvelope({ ...toPromptEnvelope(promptInput, outDir), activityActor: actor }, actor);
|
|
795
1247
|
return {
|
|
796
1248
|
...result,
|
|
797
1249
|
transcript: transcriptParts.join("\n\n") || undefined,
|
|
798
1250
|
files: uploadFileDtos(stagedFiles),
|
|
799
1251
|
};
|
|
800
1252
|
}
|
|
801
|
-
async sendEnvelope(envelope) {
|
|
1253
|
+
async sendEnvelope(envelope, actor) {
|
|
1254
|
+
const activityActor = envelope.activityActor ?? actor;
|
|
802
1255
|
const session = await this.getSession(false);
|
|
803
1256
|
const external = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
|
|
804
1257
|
if (session.isProcessing() || external?.activity.active) {
|
|
@@ -811,6 +1264,7 @@ export class RelayRuntime {
|
|
|
811
1264
|
threadId: info.threadId,
|
|
812
1265
|
workspace: info.workspace,
|
|
813
1266
|
agentId: info.agentId,
|
|
1267
|
+
actor: activityActor,
|
|
814
1268
|
prompt: envelope.description,
|
|
815
1269
|
detail: external?.activity.active
|
|
816
1270
|
? `Queued because ${external.agentLabel} CLI is still processing another task.`
|
|
@@ -824,6 +1278,7 @@ export class RelayRuntime {
|
|
|
824
1278
|
threadId: info.threadId,
|
|
825
1279
|
workspace: info.workspace,
|
|
826
1280
|
promptId: queued.id,
|
|
1281
|
+
actor: activityActor,
|
|
827
1282
|
description: envelope.description,
|
|
828
1283
|
});
|
|
829
1284
|
if (external?.activity.active) {
|
|
@@ -832,7 +1287,7 @@ export class RelayRuntime {
|
|
|
832
1287
|
this.broadcastQueue();
|
|
833
1288
|
return { queued: true, queueId: queued.id };
|
|
834
1289
|
}
|
|
835
|
-
void this.runPrompt(session, envelope).catch((error) => {
|
|
1290
|
+
void this.runPrompt(session, { ...envelope, activityActor }).catch((error) => {
|
|
836
1291
|
this.broadcast({ type: "turn_error", id: this.currentTurnId ?? "turn", error: friendlyErrorText(error), at: new Date().toISOString() });
|
|
837
1292
|
});
|
|
838
1293
|
return { queued: false };
|
|
@@ -843,7 +1298,9 @@ export class RelayRuntime {
|
|
|
843
1298
|
queuePaused() {
|
|
844
1299
|
return this.queueService.isPaused();
|
|
845
1300
|
}
|
|
846
|
-
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;
|
|
847
1304
|
this.queueService.apply(action, id);
|
|
848
1305
|
if (id && action === "run") {
|
|
849
1306
|
void this.drainQueue().catch((error) => this.broadcastStatus(friendlyErrorText(error), "error"));
|
|
@@ -851,15 +1308,18 @@ export class RelayRuntime {
|
|
|
851
1308
|
this.appendActivity({
|
|
852
1309
|
source: "web",
|
|
853
1310
|
status: "info",
|
|
854
|
-
type:
|
|
1311
|
+
type: `queue_${action}`,
|
|
855
1312
|
threadId: null,
|
|
856
1313
|
workspace: this.config.workspace,
|
|
857
|
-
|
|
1314
|
+
actor,
|
|
1315
|
+
prompt: affected?.description,
|
|
1316
|
+
detail: id ? `${action}: ${id}` : `${action}: ${before.length} queued`,
|
|
858
1317
|
});
|
|
859
1318
|
this.appendAudit({
|
|
860
1319
|
action: "queue_updated",
|
|
861
1320
|
status: "ok",
|
|
862
1321
|
contextKey: WEB_CONTEXT_KEY,
|
|
1322
|
+
actor,
|
|
863
1323
|
description: id ? `${action}: ${id}` : action,
|
|
864
1324
|
});
|
|
865
1325
|
this.broadcastQueue();
|
|
@@ -873,13 +1333,39 @@ export class RelayRuntime {
|
|
|
873
1333
|
const session = await this.getSession(true);
|
|
874
1334
|
return this.artifactService.get(session.getInfo().workspace, turnId);
|
|
875
1335
|
}
|
|
876
|
-
async deleteArtifact(turnId) {
|
|
1336
|
+
async deleteArtifact(turnId, actor) {
|
|
877
1337
|
const session = await this.getSession(true);
|
|
878
|
-
|
|
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;
|
|
879
1351
|
}
|
|
880
|
-
async createArtifactZip(turnId) {
|
|
1352
|
+
async createArtifactZip(turnId, actor) {
|
|
881
1353
|
const session = await this.getSession(true);
|
|
882
|
-
|
|
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;
|
|
883
1369
|
}
|
|
884
1370
|
async artifactPreview(turnId, relativePath) {
|
|
885
1371
|
const session = await this.getSession(true);
|
|
@@ -894,7 +1380,7 @@ export class RelayRuntime {
|
|
|
894
1380
|
}
|
|
895
1381
|
return readFormattedLogTail(lines);
|
|
896
1382
|
}
|
|
897
|
-
clearLogs(target = "connector") {
|
|
1383
|
+
clearLogs(target = "connector", actor) {
|
|
898
1384
|
const result = clearLogFile(target === "update" ? getUpdateLogPath() : target === "agent-updates" ? getAgentUpdateLogPath() : getConnectorLogPath());
|
|
899
1385
|
this.appendActivity({
|
|
900
1386
|
source: "web",
|
|
@@ -902,11 +1388,20 @@ export class RelayRuntime {
|
|
|
902
1388
|
type: "logs_cleared",
|
|
903
1389
|
threadId: null,
|
|
904
1390
|
workspace: this.config.workspace,
|
|
1391
|
+
actor,
|
|
905
1392
|
detail: `Cleared ${target} log.`,
|
|
906
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
|
+
});
|
|
907
1402
|
return { ok: true, filePath: result.filePath, clearedAt: result.clearedAt.toISOString() };
|
|
908
1403
|
}
|
|
909
|
-
restartConnector() {
|
|
1404
|
+
restartConnector(actor) {
|
|
910
1405
|
spawnConnectorRestart();
|
|
911
1406
|
this.broadcastStatus("Restart requested. The dashboard may disconnect briefly.", "warn");
|
|
912
1407
|
this.appendActivity({
|
|
@@ -915,8 +1410,16 @@ export class RelayRuntime {
|
|
|
915
1410
|
type: "restart_requested",
|
|
916
1411
|
threadId: null,
|
|
917
1412
|
workspace: this.config.workspace,
|
|
1413
|
+
actor,
|
|
918
1414
|
detail: "Dashboard requested a connector restart.",
|
|
919
1415
|
});
|
|
1416
|
+
this.appendAudit({
|
|
1417
|
+
action: "command",
|
|
1418
|
+
status: "ok",
|
|
1419
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
1420
|
+
actor,
|
|
1421
|
+
description: "restart connector",
|
|
1422
|
+
});
|
|
920
1423
|
return { ok: true, message: "Restart requested." };
|
|
921
1424
|
}
|
|
922
1425
|
dispose() {
|
|
@@ -930,6 +1433,234 @@ export class RelayRuntime {
|
|
|
930
1433
|
async getSession(deferThreadStart) {
|
|
931
1434
|
return this.registry.getOrCreate(WEB_CONTEXT_KEY, { deferThreadStart });
|
|
932
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
|
+
}
|
|
933
1664
|
async getControlSession(agentId) {
|
|
934
1665
|
const active = await this.getSession(true);
|
|
935
1666
|
const activeInfo = this.publicInfo(active);
|
|
@@ -945,6 +1676,14 @@ export class RelayRuntime {
|
|
|
945
1676
|
});
|
|
946
1677
|
return { session, dispose: true };
|
|
947
1678
|
}
|
|
1679
|
+
cliPathOptions() {
|
|
1680
|
+
return {
|
|
1681
|
+
piCliPath: this.config.piCliPath,
|
|
1682
|
+
hermesCliPath: this.config.hermesCliPath,
|
|
1683
|
+
openClawCliPath: this.config.openClawCliPath,
|
|
1684
|
+
claudeCodeCliPath: this.config.claudeCodeCliPath,
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
948
1687
|
async ensureActiveThread(session) {
|
|
949
1688
|
if (!session.hasActiveThread()) {
|
|
950
1689
|
await session.newThread();
|
|
@@ -1009,169 +1748,14 @@ export class RelayRuntime {
|
|
|
1009
1748
|
}
|
|
1010
1749
|
}
|
|
1011
1750
|
async runPrompt(session, envelope) {
|
|
1012
|
-
await this.ensureActiveThread(session);
|
|
1013
|
-
const info = session.getInfo();
|
|
1014
|
-
if ((info.capabilities ?? CODEX_AGENT_CAPABILITIES).auth) {
|
|
1015
|
-
const auth = await this.checkAgentAuth(info);
|
|
1016
|
-
if (!auth.authenticated) {
|
|
1017
|
-
throw new Error(`${agentLabel(info.agentId)} is not authenticated: ${auth.detail}`);
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
1751
|
const workspacePolicy = evaluateWorkspacePolicy(session.getInfo().workspace, this.config);
|
|
1021
1752
|
if (!workspacePolicy.allowed) {
|
|
1022
1753
|
throw new Error(workspacePolicy.warning ?? "Current workspace is blocked by policy.");
|
|
1023
1754
|
}
|
|
1024
|
-
const turnId = randomUUID().slice(0, 12);
|
|
1025
|
-
this.currentTurnId = turnId;
|
|
1026
|
-
this.currentTurnStartedAt = Date.now();
|
|
1027
|
-
this.accumulatedText = "";
|
|
1028
|
-
this.currentProgress = {
|
|
1029
|
-
id: turnId,
|
|
1030
|
-
source: "web",
|
|
1031
|
-
status: "running",
|
|
1032
|
-
prompt: envelope.description,
|
|
1033
|
-
agentId: info.agentId,
|
|
1034
|
-
agentLabel: info.agentLabel,
|
|
1035
|
-
threadId: info.threadId,
|
|
1036
|
-
workspace: info.workspace,
|
|
1037
|
-
startedAt: new Date(this.currentTurnStartedAt).toISOString(),
|
|
1038
|
-
updatedAt: new Date(this.currentTurnStartedAt).toISOString(),
|
|
1039
|
-
durationMs: 0,
|
|
1040
|
-
outputChars: 0,
|
|
1041
|
-
tools: [],
|
|
1042
|
-
};
|
|
1043
|
-
this.queueService.setLastPrompt(envelope);
|
|
1044
|
-
const startedDate = new Date();
|
|
1045
|
-
const startedAt = startedDate.toISOString();
|
|
1046
|
-
this.chatStore.append({
|
|
1047
|
-
threadId: info.threadId ?? "pending",
|
|
1048
|
-
role: "user",
|
|
1049
|
-
text: envelope.description,
|
|
1050
|
-
source: "web",
|
|
1051
|
-
turnId,
|
|
1052
|
-
timestamp: startedAt,
|
|
1053
|
-
});
|
|
1054
|
-
this.appendActivity({
|
|
1055
|
-
source: "web",
|
|
1056
|
-
status: "running",
|
|
1057
|
-
type: "prompt_started",
|
|
1058
|
-
threadId: info.threadId,
|
|
1059
|
-
workspace: info.workspace,
|
|
1060
|
-
agentId: info.agentId,
|
|
1061
|
-
prompt: envelope.description,
|
|
1062
|
-
});
|
|
1063
|
-
this.appendAudit({
|
|
1064
|
-
action: "prompt_started",
|
|
1065
|
-
status: "ok",
|
|
1066
|
-
contextKey: WEB_CONTEXT_KEY,
|
|
1067
|
-
agentId: info.agentId,
|
|
1068
|
-
threadId: info.threadId,
|
|
1069
|
-
workspace: info.workspace,
|
|
1070
|
-
description: envelope.description,
|
|
1071
|
-
});
|
|
1072
|
-
this.broadcast({ type: "turn_start", id: turnId, prompt: envelope.description, at: startedAt, source: "web" });
|
|
1073
|
-
const callbacks = {
|
|
1074
|
-
onTextDelta: (delta) => {
|
|
1075
|
-
this.accumulatedText += delta;
|
|
1076
|
-
this.updateCurrentProgress({ outputChars: this.accumulatedText.length });
|
|
1077
|
-
this.broadcast({ type: "text_delta", id: turnId, delta });
|
|
1078
|
-
},
|
|
1079
|
-
onToolStart: (toolName, toolCallId) => {
|
|
1080
|
-
this.addCurrentTool(toolName);
|
|
1081
|
-
this.broadcast({ type: "tool_start", id: turnId, toolCallId, toolName });
|
|
1082
|
-
},
|
|
1083
|
-
onToolUpdate: (toolCallId, partialResult) => {
|
|
1084
|
-
this.updateCurrentProgress();
|
|
1085
|
-
this.broadcast({ type: "tool_update", id: turnId, toolCallId, partialResult });
|
|
1086
|
-
},
|
|
1087
|
-
onToolEnd: (toolCallId, isError) => {
|
|
1088
|
-
this.updateCurrentProgress({ currentTool: undefined });
|
|
1089
|
-
this.broadcast({ type: "tool_end", id: turnId, toolCallId, isError });
|
|
1090
|
-
},
|
|
1091
|
-
onTodoUpdate: (items) => {
|
|
1092
|
-
this.updateCurrentProgress({ detail: `Plan: ${items.filter((item) => item.completed).length}/${items.length} done` });
|
|
1093
|
-
this.broadcast({ type: "todo_update", id: turnId, items });
|
|
1094
|
-
},
|
|
1095
|
-
onTurnComplete: () => { },
|
|
1096
|
-
onAgentEnd: () => this.broadcast({ type: "turn_complete", id: turnId, at: new Date().toISOString() }),
|
|
1097
|
-
};
|
|
1098
1755
|
try {
|
|
1099
|
-
await
|
|
1100
|
-
this.updateSession(session);
|
|
1101
|
-
await this.artifactService.persistWorkspaceArtifactsForTurn(session.getInfo().workspace, turnId, startedDate);
|
|
1102
|
-
if (this.accumulatedText.trim()) {
|
|
1103
|
-
this.chatStore.append({
|
|
1104
|
-
threadId: info.threadId ?? "pending",
|
|
1105
|
-
role: "agent",
|
|
1106
|
-
text: this.accumulatedText,
|
|
1107
|
-
source: "web",
|
|
1108
|
-
turnId,
|
|
1109
|
-
});
|
|
1110
|
-
}
|
|
1111
|
-
this.appendActivity({
|
|
1112
|
-
source: "web",
|
|
1113
|
-
status: "completed",
|
|
1114
|
-
type: "prompt_completed",
|
|
1115
|
-
threadId: info.threadId,
|
|
1116
|
-
workspace: info.workspace,
|
|
1117
|
-
agentId: info.agentId,
|
|
1118
|
-
prompt: envelope.description,
|
|
1119
|
-
durationMs: Date.now() - this.currentTurnStartedAt,
|
|
1120
|
-
});
|
|
1121
|
-
this.appendAudit({
|
|
1122
|
-
action: "prompt_completed",
|
|
1123
|
-
status: "ok",
|
|
1124
|
-
contextKey: WEB_CONTEXT_KEY,
|
|
1125
|
-
agentId: info.agentId,
|
|
1126
|
-
threadId: info.threadId,
|
|
1127
|
-
workspace: info.workspace,
|
|
1128
|
-
description: envelope.description,
|
|
1129
|
-
});
|
|
1130
|
-
this.updateCurrentProgress({ status: "completed" });
|
|
1131
|
-
this.broadcast({ type: "turn_complete", id: turnId, at: new Date().toISOString() });
|
|
1132
|
-
this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
|
|
1133
|
-
}
|
|
1134
|
-
catch (error) {
|
|
1135
|
-
const errorText = friendlyErrorText(error);
|
|
1136
|
-
this.chatStore.append({
|
|
1137
|
-
threadId: info.threadId ?? "pending",
|
|
1138
|
-
role: "system",
|
|
1139
|
-
text: `Error: ${errorText}`,
|
|
1140
|
-
source: "web",
|
|
1141
|
-
turnId,
|
|
1142
|
-
});
|
|
1143
|
-
this.appendActivity({
|
|
1144
|
-
source: "web",
|
|
1145
|
-
status: "failed",
|
|
1146
|
-
type: "prompt_failed",
|
|
1147
|
-
threadId: info.threadId,
|
|
1148
|
-
workspace: info.workspace,
|
|
1149
|
-
agentId: info.agentId,
|
|
1150
|
-
prompt: envelope.description,
|
|
1151
|
-
detail: errorText,
|
|
1152
|
-
durationMs: Date.now() - this.currentTurnStartedAt,
|
|
1153
|
-
});
|
|
1154
|
-
this.appendAudit({
|
|
1155
|
-
action: "prompt_failed",
|
|
1156
|
-
status: "failed",
|
|
1157
|
-
contextKey: WEB_CONTEXT_KEY,
|
|
1158
|
-
agentId: info.agentId,
|
|
1159
|
-
threadId: info.threadId,
|
|
1160
|
-
workspace: info.workspace,
|
|
1161
|
-
description: envelope.description,
|
|
1162
|
-
detail: errorText,
|
|
1163
|
-
});
|
|
1164
|
-
this.updateCurrentProgress({ status: "failed", detail: errorText });
|
|
1165
|
-
this.broadcast({ type: "turn_error", id: turnId, error: errorText, at: new Date().toISOString() });
|
|
1166
|
-
this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
|
|
1167
|
-
throw error;
|
|
1756
|
+
await this.turnService.run(session, envelope);
|
|
1168
1757
|
}
|
|
1169
1758
|
finally {
|
|
1170
|
-
this.currentTurnId = null;
|
|
1171
|
-
if (this.currentProgress) {
|
|
1172
|
-
this.currentProgress.durationMs = Date.now() - this.currentTurnStartedAt;
|
|
1173
|
-
this.currentProgress.updatedAt = new Date().toISOString();
|
|
1174
|
-
}
|
|
1175
1759
|
await this.drainQueue();
|
|
1176
1760
|
}
|
|
1177
1761
|
}
|
|
@@ -1204,6 +1788,40 @@ export class RelayRuntime {
|
|
|
1204
1788
|
this.registry.updateMetadata(WEB_CONTEXT_KEY, session);
|
|
1205
1789
|
this.broadcast({ type: "session_update", session: this.publicInfo(session) });
|
|
1206
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
|
+
}
|
|
1207
1825
|
appendActivity(input) {
|
|
1208
1826
|
const event = this.activityStore.append(this.enrichActivityInput(input));
|
|
1209
1827
|
this.broadcast({ type: "activity_update", events: this.activity({ limit: 50 }) });
|
|
@@ -1341,6 +1959,123 @@ function hostLogoutCommand(info, config) {
|
|
|
1341
1959
|
}
|
|
1342
1960
|
return "codex logout";
|
|
1343
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
|
+
}
|
|
1344
2079
|
function normalizeMimeType(value, name) {
|
|
1345
2080
|
const configured = value?.trim();
|
|
1346
2081
|
if (configured) {
|