@nordbyte/nordrelay 0.3.1 → 0.4.1
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 +45 -2
- package/README.md +221 -35
- package/dist/access-control.js +3 -0
- package/dist/agent-activity.js +300 -0
- package/dist/agent-adapter.js +17 -30
- package/dist/agent-factory.js +27 -0
- package/dist/agent-feature-matrix.js +42 -0
- package/dist/agent-updates.js +294 -0
- package/dist/agent.js +123 -9
- package/dist/artifacts.js +1 -1
- package/dist/audit-log.js +1 -1
- package/dist/bot-ui.js +1 -1
- package/dist/bot.js +483 -354
- package/dist/channel-actions.js +372 -0
- package/dist/claude-code-auth.js +121 -0
- package/dist/claude-code-cli.js +19 -0
- package/dist/claude-code-launch.js +73 -0
- package/dist/claude-code-session.js +660 -0
- package/dist/claude-code-state.js +590 -0
- package/dist/codex-session.js +12 -1
- package/dist/config.js +113 -9
- package/dist/hermes-api.js +150 -0
- package/dist/hermes-auth.js +96 -0
- package/dist/hermes-cli.js +19 -0
- package/dist/hermes-launch.js +57 -0
- package/dist/hermes-session.js +477 -0
- package/dist/hermes-state.js +609 -0
- package/dist/index.js +51 -8
- package/dist/openclaw-auth.js +27 -0
- package/dist/openclaw-cli.js +19 -0
- package/dist/openclaw-gateway.js +285 -0
- package/dist/openclaw-launch.js +65 -0
- package/dist/openclaw-session.js +549 -0
- package/dist/openclaw-state.js +409 -0
- package/dist/operations.js +115 -9
- package/dist/pi-auth.js +59 -0
- package/dist/pi-launch.js +61 -0
- package/dist/pi-rpc.js +18 -0
- package/dist/pi-session.js +103 -15
- package/dist/pi-state.js +253 -0
- package/dist/relay-runtime.js +798 -72
- package/dist/session-format.js +98 -19
- package/dist/session-registry.js +40 -15
- package/dist/settings-service.js +35 -4
- package/dist/web-dashboard-assets.js +2 -0
- package/dist/web-dashboard-client.js +275 -0
- package/dist/web-dashboard-style.js +9 -0
- package/dist/web-dashboard-ui.js +18 -0
- package/dist/web-dashboard.js +296 -196
- package/package.json +8 -3
- package/plugins/nordrelay/.codex-plugin/plugin.json +7 -4
- package/plugins/nordrelay/commands/remote.md +2 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +187 -12
- package/plugins/nordrelay/skills/telegram-remote/SKILL.md +2 -2
- package/CHANGELOG.md +0 -26
package/dist/relay-runtime.js
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { createArtifactZipBundle, getArtifactTurnReport, ensureOutDir, listRecentArtifactReports, removeArtifactTurn, totalArtifactSize, } from "./artifacts.js";
|
|
4
|
+
import { createArtifactZipBundle, collectRecentWorkspaceArtifacts, getArtifactTurnReport, ensureOutDir, listRecentArtifactReports, persistWorkspaceArtifactReport, removeArtifactTurn, totalArtifactSize, } from "./artifacts.js";
|
|
5
5
|
import { buildFileInstructions, outboxPath, stageFile, } from "./attachments.js";
|
|
6
|
-
import { CODEX_AGENT_CAPABILITIES,
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
6
|
+
import { CODEX_AGENT_CAPABILITIES, agentLabel, agentReasoningLabel, agentReasoningOptions, } from "./agent.js";
|
|
7
|
+
import { getAgentDiagnostics, getExternalSnapshotForSession, } from "./agent-activity.js";
|
|
8
|
+
import { listAgentAdapterDescriptors } from "./agent-adapter.js";
|
|
9
|
+
import { AgentUpdateManager } from "./agent-updates.js";
|
|
10
|
+
import { createAgentSessionService, enabledAgents } from "./agent-factory.js";
|
|
11
|
+
import { AuditLogStore } from "./audit-log.js";
|
|
12
|
+
import { checkAuthStatus, startLogin as startCodexLogin, startLogout as startCodexLogout } from "./codex-auth.js";
|
|
13
|
+
import { checkClaudeCodeAuthStatus, startClaudeCodeLogin, startClaudeCodeLogout } from "./claude-code-auth.js";
|
|
10
14
|
import { friendlyErrorText } from "./error-messages.js";
|
|
11
|
-
import {
|
|
15
|
+
import { checkHermesAuthStatus, startHermesLogin, startHermesLogout } from "./hermes-auth.js";
|
|
16
|
+
import { checkOpenClawAuthStatus } from "./openclaw-auth.js";
|
|
17
|
+
import { clearLogFile, getAgentUpdateLogPath, getConnectorHealth, getConnectorLogPath, getPackageVersion, getUpdateLogPath, getVersionChecks, readConnectorState, readFormattedLogTail, spawnConnectorRestart, spawnSelfUpdate } from "./operations.js";
|
|
18
|
+
import { checkPiAuthStatus } from "./pi-auth.js";
|
|
12
19
|
import { PromptStore, toPromptEnvelope } from "./prompt-store.js";
|
|
13
|
-
import { renderSessionInfoPlain } from "./session-format.js";
|
|
20
|
+
import { renderSessionInfoPlain, renderSessionUsageRows } from "./session-format.js";
|
|
21
|
+
import { SessionLockStore } from "./session-locks.js";
|
|
14
22
|
import { SessionRegistry } from "./session-registry.js";
|
|
15
23
|
import { transcribeAudio } from "./voice.js";
|
|
16
24
|
import { WebActivityStore, WebChatStore, } from "./web-state.js";
|
|
@@ -25,12 +33,16 @@ export class RelayRuntime {
|
|
|
25
33
|
promptStore;
|
|
26
34
|
chatStore;
|
|
27
35
|
activityStore;
|
|
36
|
+
auditStore;
|
|
37
|
+
lockStore;
|
|
38
|
+
agentUpdates;
|
|
28
39
|
subscribers = new Set();
|
|
29
40
|
externalMonitor;
|
|
30
41
|
draining = false;
|
|
31
42
|
currentTurnId = null;
|
|
32
43
|
accumulatedText = "";
|
|
33
44
|
currentTurnStartedAt = 0;
|
|
45
|
+
currentProgress = null;
|
|
34
46
|
externalMirror = null;
|
|
35
47
|
constructor(config) {
|
|
36
48
|
this.config = config;
|
|
@@ -41,6 +53,11 @@ export class RelayRuntime {
|
|
|
41
53
|
this.promptStore = new PromptStore(config.workspace, config.stateBackend);
|
|
42
54
|
this.chatStore = new WebChatStore(config.workspace, config.stateBackend, MAX_CHAT_HISTORY);
|
|
43
55
|
this.activityStore = new WebActivityStore(config.workspace, config.stateBackend, config.auditMaxEvents);
|
|
56
|
+
this.auditStore = new AuditLogStore(config.workspace, config.stateBackend, config.auditMaxEvents);
|
|
57
|
+
this.lockStore = new SessionLockStore(config.workspace, config.stateBackend);
|
|
58
|
+
this.agentUpdates = new AgentUpdateManager({
|
|
59
|
+
onUpdate: (job) => this.broadcast({ type: "agent_update", job }),
|
|
60
|
+
});
|
|
44
61
|
if (config.codexExternalBusyCheckMs > 0) {
|
|
45
62
|
this.externalMonitor = setInterval(() => {
|
|
46
63
|
void this.monitorExternalActivity().catch((error) => this.broadcastStatus(friendlyErrorText(error), "error"));
|
|
@@ -70,48 +87,361 @@ export class RelayRuntime {
|
|
|
70
87
|
}
|
|
71
88
|
async status() {
|
|
72
89
|
return {
|
|
73
|
-
health: await getConnectorHealth(),
|
|
74
|
-
versionChecks: await getVersionChecks({ piCliPath: this.config.piCliPath }),
|
|
90
|
+
health: await getConnectorHealth({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath }),
|
|
91
|
+
versionChecks: await getVersionChecks({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath }),
|
|
92
|
+
snapshot: await this.snapshot(),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
async bootstrapStatus() {
|
|
96
|
+
return {
|
|
97
|
+
health: {
|
|
98
|
+
version: await getPackageVersion(),
|
|
99
|
+
state: await readConnectorState(),
|
|
100
|
+
},
|
|
75
101
|
snapshot: await this.snapshot(),
|
|
76
102
|
};
|
|
77
103
|
}
|
|
104
|
+
async version() {
|
|
105
|
+
return {
|
|
106
|
+
health: await getConnectorHealth({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath }),
|
|
107
|
+
state: await readConnectorState(),
|
|
108
|
+
versionChecks: await getVersionChecks({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath }),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
updateConnector() {
|
|
112
|
+
const update = spawnSelfUpdate();
|
|
113
|
+
this.broadcastStatus(`Update started with ${update.method}. Log: ${update.logPath}`, "warn");
|
|
114
|
+
this.appendActivity({
|
|
115
|
+
source: "web",
|
|
116
|
+
status: "info",
|
|
117
|
+
type: "update_started",
|
|
118
|
+
threadId: null,
|
|
119
|
+
workspace: this.config.workspace,
|
|
120
|
+
detail: `${update.method}: ${update.summary}`,
|
|
121
|
+
});
|
|
122
|
+
this.appendAudit({
|
|
123
|
+
action: "command",
|
|
124
|
+
status: "ok",
|
|
125
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
126
|
+
description: "update",
|
|
127
|
+
detail: update.summary,
|
|
128
|
+
});
|
|
129
|
+
return update;
|
|
130
|
+
}
|
|
131
|
+
agentUpdateJobs() {
|
|
132
|
+
return this.agentUpdates.list();
|
|
133
|
+
}
|
|
134
|
+
startAgentUpdate(agentId) {
|
|
135
|
+
const job = this.agentUpdates.start(agentId, {
|
|
136
|
+
piCliPath: this.config.piCliPath,
|
|
137
|
+
hermesCliPath: this.config.hermesCliPath,
|
|
138
|
+
openClawCliPath: this.config.openClawCliPath,
|
|
139
|
+
claudeCodeCliPath: this.config.claudeCodeCliPath,
|
|
140
|
+
});
|
|
141
|
+
this.broadcastStatus(`${job.agentLabel} update started. Log: ${job.logPath}`, "warn");
|
|
142
|
+
this.appendActivity({
|
|
143
|
+
source: "web",
|
|
144
|
+
status: "info",
|
|
145
|
+
type: "agent_update_started",
|
|
146
|
+
agentId,
|
|
147
|
+
threadId: null,
|
|
148
|
+
workspace: this.config.workspace,
|
|
149
|
+
detail: `${job.method}: ${job.summary}`,
|
|
150
|
+
});
|
|
151
|
+
this.appendAudit({
|
|
152
|
+
action: "command",
|
|
153
|
+
status: "ok",
|
|
154
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
155
|
+
agentId,
|
|
156
|
+
description: `update ${agentId}`,
|
|
157
|
+
detail: job.summary,
|
|
158
|
+
});
|
|
159
|
+
return job;
|
|
160
|
+
}
|
|
161
|
+
agentUpdateLog(id) {
|
|
162
|
+
return this.agentUpdates.readLog(id);
|
|
163
|
+
}
|
|
164
|
+
sendAgentUpdateInput(id, input) {
|
|
165
|
+
return this.agentUpdates.sendInput(id, input);
|
|
166
|
+
}
|
|
167
|
+
cancelAgentUpdate(id) {
|
|
168
|
+
return this.agentUpdates.cancel(id);
|
|
169
|
+
}
|
|
78
170
|
async diagnostics() {
|
|
79
171
|
return {
|
|
80
|
-
health: await getConnectorHealth(),
|
|
81
|
-
versionChecks: await getVersionChecks({ piCliPath: this.config.piCliPath }),
|
|
172
|
+
health: await getConnectorHealth({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath }),
|
|
173
|
+
versionChecks: await getVersionChecks({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath }),
|
|
82
174
|
snapshot: await this.snapshot(),
|
|
83
175
|
runtime: {
|
|
84
176
|
stateBackend: this.config.stateBackend,
|
|
85
177
|
sourceWorkspace: this.config.workspace,
|
|
86
178
|
queuePaused: this.promptStore.isPaused(WEB_CONTEXT_KEY),
|
|
87
179
|
externalMirror: this.externalMirror ? { ...this.externalMirror } : null,
|
|
180
|
+
agentDiagnostics: getAgentDiagnostics(await this.getSession(true), this.config),
|
|
88
181
|
},
|
|
89
182
|
};
|
|
90
183
|
}
|
|
91
|
-
async
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
184
|
+
async adapterHealth() {
|
|
185
|
+
const health = await getConnectorHealth({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath });
|
|
186
|
+
const versions = await getVersionChecks({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath });
|
|
187
|
+
return Promise.all(listAgentAdapterDescriptors().map(async (descriptor) => {
|
|
188
|
+
const enabled = enabledAgents(this.config).includes(descriptor.id);
|
|
189
|
+
const auth = descriptor.capabilities.auth && enabled
|
|
190
|
+
? await this.authStatus(descriptor.id).catch((error) => ({
|
|
191
|
+
agentId: descriptor.id,
|
|
192
|
+
agentLabel: descriptor.label,
|
|
193
|
+
supported: descriptor.capabilities.auth,
|
|
194
|
+
authenticated: false,
|
|
195
|
+
detail: friendlyErrorText(error),
|
|
196
|
+
loginSupported: descriptor.capabilities.login,
|
|
197
|
+
logoutSupported: descriptor.capabilities.logout,
|
|
105
198
|
}))
|
|
106
|
-
:
|
|
107
|
-
|
|
108
|
-
|
|
199
|
+
: null;
|
|
200
|
+
const cli = cliHealthForAgent(descriptor.id, health);
|
|
201
|
+
const version = versionCheckForAgent(descriptor.id, versions);
|
|
202
|
+
return {
|
|
203
|
+
id: descriptor.id,
|
|
204
|
+
label: descriptor.label,
|
|
205
|
+
enabled,
|
|
206
|
+
status: descriptor.status === "available" ? (enabled ? "enabled" : "disabled") : "planned",
|
|
207
|
+
auth: {
|
|
208
|
+
supported: descriptor.capabilities.auth,
|
|
209
|
+
authenticated: auth ? auth.authenticated : null,
|
|
210
|
+
method: auth?.method,
|
|
211
|
+
detail: auth?.detail,
|
|
212
|
+
},
|
|
213
|
+
cli,
|
|
214
|
+
version: {
|
|
215
|
+
installed: version.installedLabel,
|
|
216
|
+
latest: version.latestVersion,
|
|
217
|
+
status: version.status,
|
|
218
|
+
detail: version.detail,
|
|
219
|
+
},
|
|
220
|
+
capabilities: descriptor.capabilities,
|
|
221
|
+
notes: descriptor.notes,
|
|
222
|
+
};
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
permissions() {
|
|
226
|
+
return {
|
|
227
|
+
telegramAllowAnyChat: this.config.telegramAllowAnyChat,
|
|
228
|
+
telegramAdminUserIds: this.config.telegramAdminUserIds,
|
|
229
|
+
telegramAllowedUserIds: this.config.telegramAllowedUserIds,
|
|
230
|
+
telegramReadOnlyUserIds: this.config.telegramReadOnlyUserIds,
|
|
231
|
+
telegramAllowedChatIds: this.config.telegramAllowedChatIds,
|
|
232
|
+
telegramRolePolicies: this.config.telegramRolePolicies,
|
|
109
233
|
};
|
|
110
234
|
}
|
|
235
|
+
tasks() {
|
|
236
|
+
return {
|
|
237
|
+
current: this.currentProgress ? { ...this.currentProgress, tools: [...this.currentProgress.tools] } : null,
|
|
238
|
+
external: this.externalTask(),
|
|
239
|
+
queue: this.queue(),
|
|
240
|
+
queuePaused: this.queuePaused(),
|
|
241
|
+
recent: this.activity({ limit: 20 }),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
audit(limit = 50) {
|
|
245
|
+
return this.auditStore.list(limit);
|
|
246
|
+
}
|
|
247
|
+
locks() {
|
|
248
|
+
return this.lockStore.list();
|
|
249
|
+
}
|
|
250
|
+
lockWebSession(ownerName = "Web dashboard") {
|
|
251
|
+
const lock = this.lockStore.set(WEB_CONTEXT_KEY, 0, ownerName, this.config.sessionLockTtlMs);
|
|
252
|
+
this.appendAudit({
|
|
253
|
+
action: "lock_updated",
|
|
254
|
+
status: "ok",
|
|
255
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
256
|
+
description: "lock",
|
|
257
|
+
detail: `locked by ${ownerName}`,
|
|
258
|
+
});
|
|
259
|
+
return lock;
|
|
260
|
+
}
|
|
261
|
+
unlockWebSession() {
|
|
262
|
+
const removed = this.lockStore.clear(WEB_CONTEXT_KEY);
|
|
263
|
+
this.appendAudit({
|
|
264
|
+
action: "lock_updated",
|
|
265
|
+
status: "ok",
|
|
266
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
267
|
+
description: "unlock",
|
|
268
|
+
detail: removed ? "unlocked" : "no lock",
|
|
269
|
+
});
|
|
270
|
+
return { removed, locks: this.locks() };
|
|
271
|
+
}
|
|
272
|
+
async controlOptions(agentId) {
|
|
273
|
+
const { session, dispose } = await this.getControlSession(agentId);
|
|
274
|
+
try {
|
|
275
|
+
const info = this.publicInfo(session);
|
|
276
|
+
const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
|
|
277
|
+
if (capabilities.modelSelection) {
|
|
278
|
+
await session.refreshModels().catch((error) => {
|
|
279
|
+
console.warn(`Failed to refresh ${agentLabel(info.agentId)} models: ${error instanceof Error ? error.message : String(error)}`);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
models: capabilities.modelSelection ? session.listModels() : [],
|
|
284
|
+
reasoningLabel: agentReasoningLabel(info.agentId),
|
|
285
|
+
reasoningOptions: agentReasoningOptions(info.agentId),
|
|
286
|
+
launchProfiles: capabilities.launchProfiles ? session.listLaunchProfiles() : [],
|
|
287
|
+
workspaces: filterAllowedWorkspaces(session.listWorkspaces(), this.config),
|
|
288
|
+
capabilities,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
if (dispose) {
|
|
293
|
+
session.dispose();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async authStatus(agentId) {
|
|
298
|
+
const { session, dispose } = await this.getControlSession(agentId);
|
|
299
|
+
try {
|
|
300
|
+
const info = this.publicInfo(session);
|
|
301
|
+
const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
|
|
302
|
+
if (!capabilities.auth) {
|
|
303
|
+
return {
|
|
304
|
+
agentId: info.agentId,
|
|
305
|
+
agentLabel: info.agentLabel,
|
|
306
|
+
supported: false,
|
|
307
|
+
authenticated: null,
|
|
308
|
+
detail: `${info.agentLabel} authentication is managed outside NordRelay.`,
|
|
309
|
+
loginSupported: false,
|
|
310
|
+
logoutSupported: false,
|
|
311
|
+
hostLoginCommand: hostLoginCommand(info, this.config),
|
|
312
|
+
hostLogoutCommand: hostLogoutCommand(info, this.config),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const status = await this.checkAgentAuth(info);
|
|
316
|
+
return {
|
|
317
|
+
agentId: info.agentId,
|
|
318
|
+
agentLabel: info.agentLabel,
|
|
319
|
+
supported: true,
|
|
320
|
+
authenticated: status.authenticated,
|
|
321
|
+
method: status.method,
|
|
322
|
+
detail: status.detail,
|
|
323
|
+
loginSupported: capabilities.login,
|
|
324
|
+
logoutSupported: capabilities.logout,
|
|
325
|
+
hostLoginCommand: hostLoginCommand(info, this.config),
|
|
326
|
+
hostLogoutCommand: hostLogoutCommand(info, this.config),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
finally {
|
|
330
|
+
if (dispose) {
|
|
331
|
+
session.dispose();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async login(agentId) {
|
|
336
|
+
const { session, dispose } = await this.getControlSession(agentId);
|
|
337
|
+
try {
|
|
338
|
+
const info = this.publicInfo(session);
|
|
339
|
+
const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
|
|
340
|
+
if (!capabilities.login) {
|
|
341
|
+
return {
|
|
342
|
+
...(await this.authStatus(info.agentId)),
|
|
343
|
+
result: {
|
|
344
|
+
success: false,
|
|
345
|
+
message: `${info.agentLabel} login is not managed by NordRelay. Run ${hostLoginCommand(info, this.config)} on the host.`,
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
if (!this.config.enableTelegramLogin) {
|
|
350
|
+
return {
|
|
351
|
+
...(await this.authStatus(info.agentId)),
|
|
352
|
+
result: {
|
|
353
|
+
success: false,
|
|
354
|
+
message: `Remote login is disabled. Run ${hostLoginCommand(info, this.config)} on the host.`,
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
const result = await this.startAgentLogin(info);
|
|
359
|
+
this.appendAudit({
|
|
360
|
+
action: "command",
|
|
361
|
+
status: result.success ? "ok" : "failed",
|
|
362
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
363
|
+
agentId: info.agentId,
|
|
364
|
+
threadId: info.threadId,
|
|
365
|
+
workspace: info.workspace,
|
|
366
|
+
description: "login",
|
|
367
|
+
detail: result.message,
|
|
368
|
+
});
|
|
369
|
+
return { ...(await this.authStatus(info.agentId)), result };
|
|
370
|
+
}
|
|
371
|
+
finally {
|
|
372
|
+
if (dispose) {
|
|
373
|
+
session.dispose();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async logout(agentId) {
|
|
378
|
+
const { session, dispose } = await this.getControlSession(agentId);
|
|
379
|
+
try {
|
|
380
|
+
const info = this.publicInfo(session);
|
|
381
|
+
const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
|
|
382
|
+
if (!capabilities.logout) {
|
|
383
|
+
return {
|
|
384
|
+
...(await this.authStatus(info.agentId)),
|
|
385
|
+
result: {
|
|
386
|
+
success: false,
|
|
387
|
+
message: `${info.agentLabel} logout is not managed by NordRelay. Run ${hostLogoutCommand(info, this.config)} on the host.`,
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (!this.config.enableTelegramLogin) {
|
|
392
|
+
return {
|
|
393
|
+
...(await this.authStatus(info.agentId)),
|
|
394
|
+
result: {
|
|
395
|
+
success: false,
|
|
396
|
+
message: `Remote auth management is disabled. Run ${hostLogoutCommand(info, this.config)} on the host.`,
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
const current = await this.checkAgentAuth(info);
|
|
401
|
+
if (current.method === "api-key") {
|
|
402
|
+
return {
|
|
403
|
+
...(await this.authStatus(info.agentId)),
|
|
404
|
+
result: {
|
|
405
|
+
success: false,
|
|
406
|
+
message: "Cannot logout while API-key authentication is configured. Remove the API key from .env to use CLI auth.",
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
const result = await this.startAgentLogout(info);
|
|
411
|
+
this.appendAudit({
|
|
412
|
+
action: "command",
|
|
413
|
+
status: result.success ? "ok" : "failed",
|
|
414
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
415
|
+
agentId: info.agentId,
|
|
416
|
+
threadId: info.threadId,
|
|
417
|
+
workspace: info.workspace,
|
|
418
|
+
description: "logout",
|
|
419
|
+
detail: result.message,
|
|
420
|
+
});
|
|
421
|
+
return { ...(await this.authStatus(info.agentId)), result };
|
|
422
|
+
}
|
|
423
|
+
finally {
|
|
424
|
+
if (dispose) {
|
|
425
|
+
session.dispose();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
111
429
|
async chatHistory(limit = 200) {
|
|
112
430
|
const session = await this.getSession(true);
|
|
113
431
|
return this.chatStore.list(this.publicInfo(session).threadId, limit);
|
|
114
432
|
}
|
|
433
|
+
async sessionDetail(threadId) {
|
|
434
|
+
const session = await this.getSession(true);
|
|
435
|
+
const record = session.getSessionRecord(threadId);
|
|
436
|
+
const active = this.publicInfo(session);
|
|
437
|
+
return {
|
|
438
|
+
record,
|
|
439
|
+
active,
|
|
440
|
+
usageRows: active.threadId === threadId ? renderSessionUsageRows(active) : [],
|
|
441
|
+
messages: this.chatStore.list(threadId, 100),
|
|
442
|
+
activity: this.activity({ limit: 100 }).filter((event) => event.threadId === threadId),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
115
445
|
async clearChatHistory() {
|
|
116
446
|
const session = await this.getSession(true);
|
|
117
447
|
const removed = this.chatStore.clear(this.publicInfo(session).threadId);
|
|
@@ -120,27 +450,87 @@ export class RelayRuntime {
|
|
|
120
450
|
return { removed, messages };
|
|
121
451
|
}
|
|
122
452
|
activity(options = {}) {
|
|
123
|
-
return this.activityStore.list(options);
|
|
453
|
+
return this.activityStore.list(options).map((event) => this.enrichActivityEvent(event));
|
|
124
454
|
}
|
|
125
|
-
async
|
|
126
|
-
|
|
455
|
+
async retry() {
|
|
456
|
+
const cached = this.promptStore.getLastPrompt(WEB_CONTEXT_KEY);
|
|
457
|
+
if (!cached) {
|
|
458
|
+
throw new Error("Nothing to retry. Send a message first.");
|
|
459
|
+
}
|
|
460
|
+
this.appendAudit({
|
|
461
|
+
action: "command",
|
|
462
|
+
status: "ok",
|
|
463
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
464
|
+
description: "retry",
|
|
465
|
+
detail: cached.description,
|
|
466
|
+
});
|
|
467
|
+
return this.sendEnvelope(cached);
|
|
127
468
|
}
|
|
128
|
-
async
|
|
469
|
+
async sync() {
|
|
129
470
|
const session = await this.getSession(true);
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
471
|
+
const info = this.publicInfo(session);
|
|
472
|
+
if (!(info.capabilities ?? CODEX_AGENT_CAPABILITIES).externalActivity) {
|
|
473
|
+
throw new Error(`${info.agentLabel} has no external state watcher to sync.`);
|
|
474
|
+
}
|
|
475
|
+
const result = session.syncFromAgentState({ reattach: true });
|
|
476
|
+
if (result.changed) {
|
|
477
|
+
this.updateSession(session);
|
|
478
|
+
}
|
|
479
|
+
this.appendActivity({
|
|
480
|
+
source: "web",
|
|
481
|
+
status: "info",
|
|
482
|
+
type: "session_sync",
|
|
483
|
+
threadId: result.info.threadId,
|
|
484
|
+
workspace: result.info.workspace,
|
|
485
|
+
agentId: result.info.agentId,
|
|
486
|
+
detail: result.changedFields.length > 0 ? result.changedFields.join(", ") : "already in sync",
|
|
487
|
+
});
|
|
488
|
+
this.appendAudit({
|
|
489
|
+
action: "command",
|
|
490
|
+
status: "ok",
|
|
491
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
492
|
+
agentId: result.info.agentId,
|
|
493
|
+
threadId: result.info.threadId,
|
|
494
|
+
workspace: result.info.workspace,
|
|
495
|
+
description: "sync",
|
|
496
|
+
detail: result.changedFields.join(", ") || "none",
|
|
497
|
+
});
|
|
498
|
+
return result;
|
|
499
|
+
}
|
|
500
|
+
async listSessions(limit = 80, query = "", agentId) {
|
|
501
|
+
const { session, dispose } = await this.getControlSession(agentId);
|
|
502
|
+
try {
|
|
503
|
+
return this.filteredSessions(session, query, Math.max(1, limit * 3)).slice(0, limit);
|
|
504
|
+
}
|
|
505
|
+
finally {
|
|
506
|
+
if (dispose) {
|
|
507
|
+
session.dispose();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async listSessionsPage(page = 1, pageSize = MAX_WEB_SESSION_PAGE_SIZE, query = "", agentId) {
|
|
512
|
+
const { session, dispose } = await this.getControlSession(agentId);
|
|
513
|
+
try {
|
|
514
|
+
const effectivePage = Math.max(1, Math.floor(page));
|
|
515
|
+
const effectivePageSize = Math.min(MAX_WEB_SESSION_PAGE_SIZE, Math.max(1, Math.floor(pageSize)));
|
|
516
|
+
const offset = (effectivePage - 1) * effectivePageSize;
|
|
517
|
+
const requested = Math.min(5_000, Math.max(100, (offset + effectivePageSize + 1) * 3));
|
|
518
|
+
const records = this.filteredSessions(session, query, requested);
|
|
519
|
+
return {
|
|
520
|
+
sessions: records.slice(offset, offset + effectivePageSize),
|
|
521
|
+
pagination: {
|
|
522
|
+
page: effectivePage,
|
|
523
|
+
pageSize: effectivePageSize,
|
|
524
|
+
hasPrevious: effectivePage > 1,
|
|
525
|
+
hasNext: records.length > offset + effectivePageSize,
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
finally {
|
|
530
|
+
if (dispose) {
|
|
531
|
+
session.dispose();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
144
534
|
}
|
|
145
535
|
filteredSessions(session, query, limit) {
|
|
146
536
|
const normalized = query.trim().toLowerCase();
|
|
@@ -161,7 +551,12 @@ export class RelayRuntime {
|
|
|
161
551
|
});
|
|
162
552
|
}
|
|
163
553
|
async listModels() {
|
|
164
|
-
|
|
554
|
+
const session = await this.getSession(true);
|
|
555
|
+
const info = this.publicInfo(session);
|
|
556
|
+
await session.refreshModels({ force: true }).catch((error) => {
|
|
557
|
+
console.warn(`Failed to refresh ${agentLabel(info.agentId)} models: ${error instanceof Error ? error.message : String(error)}`);
|
|
558
|
+
});
|
|
559
|
+
return session.listModels();
|
|
165
560
|
}
|
|
166
561
|
async setAgent(agentId) {
|
|
167
562
|
if (!enabledAgents(this.config).includes(agentId)) {
|
|
@@ -175,7 +570,7 @@ export class RelayRuntime {
|
|
|
175
570
|
const session = options.agentId ? await this.registry.switchAgent(WEB_CONTEXT_KEY, options.agentId) : await this.getSession(true);
|
|
176
571
|
this.ensureIdle(session);
|
|
177
572
|
if (options.reasoningEffort) {
|
|
178
|
-
const reasoningOptions = session.getInfo().agentId
|
|
573
|
+
const reasoningOptions = agentReasoningOptions(session.getInfo().agentId);
|
|
179
574
|
if (!reasoningOptions.includes(options.reasoningEffort)) {
|
|
180
575
|
throw new Error(`Invalid ${agentReasoningLabel(session.getInfo().agentId)} value: ${options.reasoningEffort}`);
|
|
181
576
|
}
|
|
@@ -230,7 +625,7 @@ export class RelayRuntime {
|
|
|
230
625
|
async setReasoningEffort(effort) {
|
|
231
626
|
const session = await this.getSession(true);
|
|
232
627
|
this.ensureIdle(session);
|
|
233
|
-
const options = session.getInfo().agentId
|
|
628
|
+
const options = agentReasoningOptions(session.getInfo().agentId);
|
|
234
629
|
if (!options.includes(effort)) {
|
|
235
630
|
throw new Error(`Invalid ${agentReasoningLabel(session.getInfo().agentId)} value: ${effort}`);
|
|
236
631
|
}
|
|
@@ -264,6 +659,16 @@ export class RelayRuntime {
|
|
|
264
659
|
}
|
|
265
660
|
async abort() {
|
|
266
661
|
const session = await this.getSession(true);
|
|
662
|
+
const snapshot = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
|
|
663
|
+
if (snapshot?.activity.active && !session.isProcessing()) {
|
|
664
|
+
this.broadcast({
|
|
665
|
+
type: "status",
|
|
666
|
+
level: "warn",
|
|
667
|
+
message: `Cannot abort the external ${snapshot.agentLabel} CLI task from NordRelay. Stop it in the terminal where it is running.`,
|
|
668
|
+
at: new Date().toISOString(),
|
|
669
|
+
});
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
267
672
|
await session.abort();
|
|
268
673
|
this.broadcast({ type: "status", level: "warn", message: "Current operation aborted.", at: new Date().toISOString() });
|
|
269
674
|
}
|
|
@@ -341,7 +746,8 @@ export class RelayRuntime {
|
|
|
341
746
|
}
|
|
342
747
|
async sendEnvelope(envelope) {
|
|
343
748
|
const session = await this.getSession(false);
|
|
344
|
-
|
|
749
|
+
const external = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
|
|
750
|
+
if (session.isProcessing() || external?.activity.active) {
|
|
345
751
|
const queued = this.promptStore.enqueue(WEB_CONTEXT_KEY, envelope);
|
|
346
752
|
const info = this.publicInfo(session);
|
|
347
753
|
this.appendActivity({
|
|
@@ -352,8 +758,23 @@ export class RelayRuntime {
|
|
|
352
758
|
workspace: info.workspace,
|
|
353
759
|
agentId: info.agentId,
|
|
354
760
|
prompt: envelope.description,
|
|
355
|
-
detail:
|
|
761
|
+
detail: external?.activity.active
|
|
762
|
+
? `Queued because ${external.agentLabel} CLI is still processing another task.`
|
|
763
|
+
: `Queued at position ${this.promptStore.list(WEB_CONTEXT_KEY).length}.`,
|
|
764
|
+
});
|
|
765
|
+
this.appendAudit({
|
|
766
|
+
action: "prompt_queued",
|
|
767
|
+
status: "ok",
|
|
768
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
769
|
+
agentId: info.agentId,
|
|
770
|
+
threadId: info.threadId,
|
|
771
|
+
workspace: info.workspace,
|
|
772
|
+
promptId: queued.id,
|
|
773
|
+
description: envelope.description,
|
|
356
774
|
});
|
|
775
|
+
if (external?.activity.active) {
|
|
776
|
+
this.broadcastStatus(`Waiting for ${external.agentLabel} CLI task... ${this.promptStore.list(WEB_CONTEXT_KEY).length} queued.`, "info");
|
|
777
|
+
}
|
|
357
778
|
this.broadcastQueue();
|
|
358
779
|
return { queued: true, queueId: queued.id };
|
|
359
780
|
}
|
|
@@ -397,6 +818,12 @@ export class RelayRuntime {
|
|
|
397
818
|
workspace: this.config.workspace,
|
|
398
819
|
detail: id ? `${action}: ${id}` : action,
|
|
399
820
|
});
|
|
821
|
+
this.appendAudit({
|
|
822
|
+
action: "queue_updated",
|
|
823
|
+
status: "ok",
|
|
824
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
825
|
+
description: id ? `${action}: ${id}` : action,
|
|
826
|
+
});
|
|
400
827
|
this.broadcastQueue();
|
|
401
828
|
return this.queue();
|
|
402
829
|
}
|
|
@@ -457,11 +884,25 @@ export class RelayRuntime {
|
|
|
457
884
|
}
|
|
458
885
|
async logs(target = "connector", lines = 100) {
|
|
459
886
|
if (target === "update") {
|
|
460
|
-
const { getUpdateLogPath } = await import("./operations.js");
|
|
461
887
|
return readFormattedLogTail(lines, getUpdateLogPath());
|
|
462
888
|
}
|
|
889
|
+
if (target === "agent-updates") {
|
|
890
|
+
return readFormattedLogTail(lines, getAgentUpdateLogPath());
|
|
891
|
+
}
|
|
463
892
|
return readFormattedLogTail(lines);
|
|
464
893
|
}
|
|
894
|
+
clearLogs(target = "connector") {
|
|
895
|
+
const result = clearLogFile(target === "update" ? getUpdateLogPath() : target === "agent-updates" ? getAgentUpdateLogPath() : getConnectorLogPath());
|
|
896
|
+
this.appendActivity({
|
|
897
|
+
source: "web",
|
|
898
|
+
status: "info",
|
|
899
|
+
type: "logs_cleared",
|
|
900
|
+
threadId: null,
|
|
901
|
+
workspace: this.config.workspace,
|
|
902
|
+
detail: `Cleared ${target} log.`,
|
|
903
|
+
});
|
|
904
|
+
return { ok: true, filePath: result.filePath, clearedAt: result.clearedAt.toISOString() };
|
|
905
|
+
}
|
|
465
906
|
restartConnector() {
|
|
466
907
|
spawnConnectorRestart();
|
|
467
908
|
this.broadcastStatus("Restart requested. The dashboard may disconnect briefly.", "warn");
|
|
@@ -479,29 +920,28 @@ export class RelayRuntime {
|
|
|
479
920
|
if (this.externalMonitor) {
|
|
480
921
|
clearInterval(this.externalMonitor);
|
|
481
922
|
}
|
|
923
|
+
this.agentUpdates.cancelAll();
|
|
482
924
|
this.registry.disposeAll();
|
|
483
925
|
this.subscribers.clear();
|
|
484
926
|
}
|
|
485
927
|
async monitorExternalActivity() {
|
|
486
928
|
const session = await this.getSession(true);
|
|
487
929
|
const info = this.publicInfo(session);
|
|
488
|
-
if (!info.capabilities.externalActivity ||
|
|
930
|
+
if (!info.capabilities.externalActivity || !info.threadId || session.isProcessing()) {
|
|
489
931
|
return;
|
|
490
932
|
}
|
|
491
|
-
const snapshot =
|
|
933
|
+
const snapshot = getExternalSnapshotForSession(session, this.config, {
|
|
492
934
|
afterLine: this.externalMirror?.threadId === info.threadId ? this.externalMirror.lastLine : Number.MAX_SAFE_INTEGER,
|
|
493
|
-
|
|
494
|
-
}) ?? getThreadRolloutSnapshot(info.threadId, {
|
|
495
|
-
staleAfterMs: this.config.codexExternalBusyStaleMs,
|
|
935
|
+
}) ?? getExternalSnapshotForSession(session, this.config, {
|
|
496
936
|
maxEvents: 0,
|
|
497
937
|
});
|
|
498
938
|
if (!snapshot) {
|
|
499
939
|
return;
|
|
500
940
|
}
|
|
501
|
-
if (!this.externalMirror || this.externalMirror.threadId !== snapshot.threadId || this.externalMirror.rolloutPath !== snapshot.
|
|
941
|
+
if (!this.externalMirror || this.externalMirror.threadId !== snapshot.threadId || this.externalMirror.rolloutPath !== snapshot.sourcePath) {
|
|
502
942
|
this.externalMirror = {
|
|
503
943
|
threadId: snapshot.threadId,
|
|
504
|
-
rolloutPath: snapshot.
|
|
944
|
+
rolloutPath: snapshot.sourcePath,
|
|
505
945
|
lastLine: snapshot.lineCount,
|
|
506
946
|
turnId: snapshot.activity.turnId,
|
|
507
947
|
startedAt: snapshot.activity.startedAt?.toISOString() ?? null,
|
|
@@ -555,16 +995,21 @@ export class RelayRuntime {
|
|
|
555
995
|
workspace: info.workspace,
|
|
556
996
|
agentId: info.agentId,
|
|
557
997
|
prompt: snapshot.latestUserMessage ?? undefined,
|
|
558
|
-
detail:
|
|
998
|
+
detail: `${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`,
|
|
559
999
|
durationMs: durationFromDates(externalStartedAt, terminalEvent.timestamp),
|
|
560
1000
|
});
|
|
561
|
-
|
|
1001
|
+
if (externalStartedAt && terminalEvent.turnId) {
|
|
1002
|
+
await this.persistWorkspaceArtifactsForTurn(info.workspace, terminalEvent.turnId, externalStartedAt);
|
|
1003
|
+
}
|
|
1004
|
+
mirror.latestStatus = `${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`;
|
|
1005
|
+
this.broadcastStatus(`${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`, terminalEvent.status === "failed" ? "error" : terminalEvent.status === "aborted" ? "warn" : "info");
|
|
562
1006
|
this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
|
|
1007
|
+
await this.drainQueue();
|
|
563
1008
|
}
|
|
564
1009
|
mirror.lastLine = Math.max(mirror.lastLine, snapshot.lineCount);
|
|
565
1010
|
}
|
|
566
1011
|
startExternalTurn(snapshot) {
|
|
567
|
-
const prompt = snapshot.latestUserMessage ??
|
|
1012
|
+
const prompt = snapshot.latestUserMessage ?? `${snapshot.agentLabel} CLI task`;
|
|
568
1013
|
this.chatStore.append({
|
|
569
1014
|
threadId: snapshot.threadId,
|
|
570
1015
|
role: "user",
|
|
@@ -586,7 +1031,7 @@ export class RelayRuntime {
|
|
|
586
1031
|
type: "cli_turn_started",
|
|
587
1032
|
threadId: snapshot.threadId,
|
|
588
1033
|
prompt,
|
|
589
|
-
detail:
|
|
1034
|
+
detail: `${snapshot.sourceLabel}: ${snapshot.sourcePath}`,
|
|
590
1035
|
});
|
|
591
1036
|
}
|
|
592
1037
|
broadcastExternalEvents(snapshot, events) {
|
|
@@ -619,12 +1064,79 @@ export class RelayRuntime {
|
|
|
619
1064
|
async getSession(deferThreadStart) {
|
|
620
1065
|
return this.registry.getOrCreate(WEB_CONTEXT_KEY, { deferThreadStart });
|
|
621
1066
|
}
|
|
1067
|
+
async getControlSession(agentId) {
|
|
1068
|
+
const active = await this.getSession(true);
|
|
1069
|
+
const activeInfo = this.publicInfo(active);
|
|
1070
|
+
if (!agentId || agentId === activeInfo.agentId) {
|
|
1071
|
+
return { session: active, dispose: false };
|
|
1072
|
+
}
|
|
1073
|
+
if (!enabledAgents(this.config).includes(agentId)) {
|
|
1074
|
+
throw new Error(`Agent is not enabled: ${agentId}`);
|
|
1075
|
+
}
|
|
1076
|
+
const session = await createAgentSessionService(this.config, agentId, {
|
|
1077
|
+
deferThreadStart: true,
|
|
1078
|
+
workspace: activeInfo.workspace,
|
|
1079
|
+
});
|
|
1080
|
+
return { session, dispose: true };
|
|
1081
|
+
}
|
|
622
1082
|
async ensureActiveThread(session) {
|
|
623
1083
|
if (!session.hasActiveThread()) {
|
|
624
1084
|
await session.newThread();
|
|
625
1085
|
this.updateSession(session);
|
|
626
1086
|
}
|
|
627
1087
|
}
|
|
1088
|
+
async checkAgentAuth(info) {
|
|
1089
|
+
if (info.agentId === "pi") {
|
|
1090
|
+
return checkPiAuthStatus(info.model);
|
|
1091
|
+
}
|
|
1092
|
+
if (info.agentId === "hermes") {
|
|
1093
|
+
return checkHermesAuthStatus({
|
|
1094
|
+
baseUrl: this.config.hermesApiBaseUrl,
|
|
1095
|
+
apiKey: this.config.hermesApiKey,
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
if (info.agentId === "openclaw") {
|
|
1099
|
+
return checkOpenClawAuthStatus({
|
|
1100
|
+
gatewayUrl: this.config.openClawGatewayUrl,
|
|
1101
|
+
token: this.config.openClawGatewayToken,
|
|
1102
|
+
password: this.config.openClawGatewayPassword,
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
if (info.agentId === "claude-code") {
|
|
1106
|
+
return checkClaudeCodeAuthStatus(this.config.claudeCodeCliPath);
|
|
1107
|
+
}
|
|
1108
|
+
return checkAuthStatus(this.config.codexApiKey);
|
|
1109
|
+
}
|
|
1110
|
+
async startAgentLogin(info) {
|
|
1111
|
+
if (info.agentId === "hermes") {
|
|
1112
|
+
return startHermesLogin(this.config.hermesCliPath);
|
|
1113
|
+
}
|
|
1114
|
+
if (info.agentId === "claude-code") {
|
|
1115
|
+
return startClaudeCodeLogin(this.config.claudeCodeCliPath);
|
|
1116
|
+
}
|
|
1117
|
+
if (info.agentId === "codex") {
|
|
1118
|
+
return startCodexLogin();
|
|
1119
|
+
}
|
|
1120
|
+
return {
|
|
1121
|
+
success: false,
|
|
1122
|
+
message: `${info.agentLabel} login is not managed by NordRelay. Run the agent login flow on the host.`,
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
async startAgentLogout(info) {
|
|
1126
|
+
if (info.agentId === "hermes") {
|
|
1127
|
+
return startHermesLogout(this.config.hermesCliPath);
|
|
1128
|
+
}
|
|
1129
|
+
if (info.agentId === "claude-code") {
|
|
1130
|
+
return startClaudeCodeLogout(this.config.claudeCodeCliPath);
|
|
1131
|
+
}
|
|
1132
|
+
if (info.agentId === "codex") {
|
|
1133
|
+
return startCodexLogout();
|
|
1134
|
+
}
|
|
1135
|
+
return {
|
|
1136
|
+
success: false,
|
|
1137
|
+
message: `${info.agentLabel} logout is not managed by NordRelay. Run the agent logout flow on the host.`,
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
628
1140
|
ensureIdle(session) {
|
|
629
1141
|
if (session.isProcessing()) {
|
|
630
1142
|
throw new Error("The active session is still processing a turn.");
|
|
@@ -634,9 +1146,9 @@ export class RelayRuntime {
|
|
|
634
1146
|
await this.ensureActiveThread(session);
|
|
635
1147
|
const info = session.getInfo();
|
|
636
1148
|
if ((info.capabilities ?? CODEX_AGENT_CAPABILITIES).auth) {
|
|
637
|
-
const auth = await
|
|
1149
|
+
const auth = await this.checkAgentAuth(info);
|
|
638
1150
|
if (!auth.authenticated) {
|
|
639
|
-
throw new Error(
|
|
1151
|
+
throw new Error(`${agentLabel(info.agentId)} is not authenticated: ${auth.detail}`);
|
|
640
1152
|
}
|
|
641
1153
|
}
|
|
642
1154
|
const workspacePolicy = evaluateWorkspacePolicy(session.getInfo().workspace, this.config);
|
|
@@ -647,8 +1159,24 @@ export class RelayRuntime {
|
|
|
647
1159
|
this.currentTurnId = turnId;
|
|
648
1160
|
this.currentTurnStartedAt = Date.now();
|
|
649
1161
|
this.accumulatedText = "";
|
|
1162
|
+
this.currentProgress = {
|
|
1163
|
+
id: turnId,
|
|
1164
|
+
source: "web",
|
|
1165
|
+
status: "running",
|
|
1166
|
+
prompt: envelope.description,
|
|
1167
|
+
agentId: info.agentId,
|
|
1168
|
+
agentLabel: info.agentLabel,
|
|
1169
|
+
threadId: info.threadId,
|
|
1170
|
+
workspace: info.workspace,
|
|
1171
|
+
startedAt: new Date(this.currentTurnStartedAt).toISOString(),
|
|
1172
|
+
updatedAt: new Date(this.currentTurnStartedAt).toISOString(),
|
|
1173
|
+
durationMs: 0,
|
|
1174
|
+
outputChars: 0,
|
|
1175
|
+
tools: [],
|
|
1176
|
+
};
|
|
650
1177
|
this.promptStore.setLastPrompt(WEB_CONTEXT_KEY, envelope);
|
|
651
|
-
const
|
|
1178
|
+
const startedDate = new Date();
|
|
1179
|
+
const startedAt = startedDate.toISOString();
|
|
652
1180
|
this.chatStore.append({
|
|
653
1181
|
threadId: info.threadId ?? "pending",
|
|
654
1182
|
role: "user",
|
|
@@ -666,22 +1194,45 @@ export class RelayRuntime {
|
|
|
666
1194
|
agentId: info.agentId,
|
|
667
1195
|
prompt: envelope.description,
|
|
668
1196
|
});
|
|
1197
|
+
this.appendAudit({
|
|
1198
|
+
action: "prompt_started",
|
|
1199
|
+
status: "ok",
|
|
1200
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
1201
|
+
agentId: info.agentId,
|
|
1202
|
+
threadId: info.threadId,
|
|
1203
|
+
workspace: info.workspace,
|
|
1204
|
+
description: envelope.description,
|
|
1205
|
+
});
|
|
669
1206
|
this.broadcast({ type: "turn_start", id: turnId, prompt: envelope.description, at: startedAt, source: "web" });
|
|
670
1207
|
const callbacks = {
|
|
671
1208
|
onTextDelta: (delta) => {
|
|
672
1209
|
this.accumulatedText += delta;
|
|
1210
|
+
this.updateCurrentProgress({ outputChars: this.accumulatedText.length });
|
|
673
1211
|
this.broadcast({ type: "text_delta", id: turnId, delta });
|
|
674
1212
|
},
|
|
675
|
-
onToolStart: (toolName, toolCallId) =>
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
1213
|
+
onToolStart: (toolName, toolCallId) => {
|
|
1214
|
+
this.addCurrentTool(toolName);
|
|
1215
|
+
this.broadcast({ type: "tool_start", id: turnId, toolCallId, toolName });
|
|
1216
|
+
},
|
|
1217
|
+
onToolUpdate: (toolCallId, partialResult) => {
|
|
1218
|
+
this.updateCurrentProgress();
|
|
1219
|
+
this.broadcast({ type: "tool_update", id: turnId, toolCallId, partialResult });
|
|
1220
|
+
},
|
|
1221
|
+
onToolEnd: (toolCallId, isError) => {
|
|
1222
|
+
this.updateCurrentProgress({ currentTool: undefined });
|
|
1223
|
+
this.broadcast({ type: "tool_end", id: turnId, toolCallId, isError });
|
|
1224
|
+
},
|
|
1225
|
+
onTodoUpdate: (items) => {
|
|
1226
|
+
this.updateCurrentProgress({ detail: `Plan: ${items.filter((item) => item.completed).length}/${items.length} done` });
|
|
1227
|
+
this.broadcast({ type: "todo_update", id: turnId, items });
|
|
1228
|
+
},
|
|
679
1229
|
onTurnComplete: () => { },
|
|
680
1230
|
onAgentEnd: () => this.broadcast({ type: "turn_complete", id: turnId, at: new Date().toISOString() }),
|
|
681
1231
|
};
|
|
682
1232
|
try {
|
|
683
1233
|
await session.prompt(envelope.input, callbacks);
|
|
684
1234
|
this.updateSession(session);
|
|
1235
|
+
await this.persistWorkspaceArtifactsForTurn(session.getInfo().workspace, turnId, startedDate);
|
|
685
1236
|
if (this.accumulatedText.trim()) {
|
|
686
1237
|
this.chatStore.append({
|
|
687
1238
|
threadId: info.threadId ?? "pending",
|
|
@@ -701,6 +1252,16 @@ export class RelayRuntime {
|
|
|
701
1252
|
prompt: envelope.description,
|
|
702
1253
|
durationMs: Date.now() - this.currentTurnStartedAt,
|
|
703
1254
|
});
|
|
1255
|
+
this.appendAudit({
|
|
1256
|
+
action: "prompt_completed",
|
|
1257
|
+
status: "ok",
|
|
1258
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
1259
|
+
agentId: info.agentId,
|
|
1260
|
+
threadId: info.threadId,
|
|
1261
|
+
workspace: info.workspace,
|
|
1262
|
+
description: envelope.description,
|
|
1263
|
+
});
|
|
1264
|
+
this.updateCurrentProgress({ status: "completed" });
|
|
704
1265
|
this.broadcast({ type: "turn_complete", id: turnId, at: new Date().toISOString() });
|
|
705
1266
|
this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
|
|
706
1267
|
}
|
|
@@ -724,12 +1285,27 @@ export class RelayRuntime {
|
|
|
724
1285
|
detail: errorText,
|
|
725
1286
|
durationMs: Date.now() - this.currentTurnStartedAt,
|
|
726
1287
|
});
|
|
1288
|
+
this.appendAudit({
|
|
1289
|
+
action: "prompt_failed",
|
|
1290
|
+
status: "failed",
|
|
1291
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
1292
|
+
agentId: info.agentId,
|
|
1293
|
+
threadId: info.threadId,
|
|
1294
|
+
workspace: info.workspace,
|
|
1295
|
+
description: envelope.description,
|
|
1296
|
+
detail: errorText,
|
|
1297
|
+
});
|
|
1298
|
+
this.updateCurrentProgress({ status: "failed", detail: errorText });
|
|
727
1299
|
this.broadcast({ type: "turn_error", id: turnId, error: errorText, at: new Date().toISOString() });
|
|
728
1300
|
this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
|
|
729
1301
|
throw error;
|
|
730
1302
|
}
|
|
731
1303
|
finally {
|
|
732
1304
|
this.currentTurnId = null;
|
|
1305
|
+
if (this.currentProgress) {
|
|
1306
|
+
this.currentProgress.durationMs = Date.now() - this.currentTurnStartedAt;
|
|
1307
|
+
this.currentProgress.updatedAt = new Date().toISOString();
|
|
1308
|
+
}
|
|
733
1309
|
await this.drainQueue();
|
|
734
1310
|
}
|
|
735
1311
|
}
|
|
@@ -741,6 +1317,11 @@ export class RelayRuntime {
|
|
|
741
1317
|
try {
|
|
742
1318
|
const session = await this.getSession(false);
|
|
743
1319
|
while (!session.isProcessing()) {
|
|
1320
|
+
const external = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
|
|
1321
|
+
if (external?.activity.active) {
|
|
1322
|
+
this.broadcastStatus(`Waiting for ${external.agentLabel} CLI task... ${this.queue().length} queued.`, "info");
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
744
1325
|
const next = this.promptStore.dequeue(WEB_CONTEXT_KEY);
|
|
745
1326
|
this.broadcastQueue();
|
|
746
1327
|
if (!next) {
|
|
@@ -758,10 +1339,85 @@ export class RelayRuntime {
|
|
|
758
1339
|
this.broadcast({ type: "session_update", session: this.publicInfo(session) });
|
|
759
1340
|
}
|
|
760
1341
|
appendActivity(input) {
|
|
761
|
-
const event = this.activityStore.append(input);
|
|
1342
|
+
const event = this.activityStore.append(this.enrichActivityInput(input));
|
|
762
1343
|
this.broadcast({ type: "activity_update", events: this.activity({ limit: 50 }) });
|
|
763
1344
|
return event;
|
|
764
1345
|
}
|
|
1346
|
+
enrichActivityInput(input) {
|
|
1347
|
+
return this.enrichActivityFields(input);
|
|
1348
|
+
}
|
|
1349
|
+
enrichActivityEvent(event) {
|
|
1350
|
+
return this.enrichActivityFields(event);
|
|
1351
|
+
}
|
|
1352
|
+
enrichActivityFields(event) {
|
|
1353
|
+
const info = this.registry.get(WEB_CONTEXT_KEY)?.getInfo();
|
|
1354
|
+
if (!info) {
|
|
1355
|
+
return !event.threadId && !event.workspace ? { ...event, workspace: this.config.workspace } : event;
|
|
1356
|
+
}
|
|
1357
|
+
if (event.threadId && info.threadId && event.threadId === info.threadId) {
|
|
1358
|
+
return { ...event, workspace: event.workspace ?? info.workspace, agentId: event.agentId ?? info.agentId };
|
|
1359
|
+
}
|
|
1360
|
+
if (!event.threadId && !event.workspace) {
|
|
1361
|
+
return { ...event, workspace: this.config.workspace };
|
|
1362
|
+
}
|
|
1363
|
+
return event;
|
|
1364
|
+
}
|
|
1365
|
+
appendAudit(input) {
|
|
1366
|
+
return this.auditStore.append({ ...input, channelId: "web" });
|
|
1367
|
+
}
|
|
1368
|
+
updateCurrentProgress(patch = {}) {
|
|
1369
|
+
if (!this.currentProgress) {
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
if ("currentTool" in patch) {
|
|
1373
|
+
this.currentProgress.currentTool = patch.currentTool;
|
|
1374
|
+
const { currentTool: _currentTool, ...rest } = patch;
|
|
1375
|
+
Object.assign(this.currentProgress, rest);
|
|
1376
|
+
}
|
|
1377
|
+
else {
|
|
1378
|
+
Object.assign(this.currentProgress, patch);
|
|
1379
|
+
}
|
|
1380
|
+
this.currentProgress.durationMs = Date.now() - this.currentTurnStartedAt;
|
|
1381
|
+
this.currentProgress.updatedAt = new Date().toISOString();
|
|
1382
|
+
}
|
|
1383
|
+
addCurrentTool(toolName) {
|
|
1384
|
+
if (!this.currentProgress) {
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
const existing = this.currentProgress.tools.find((tool) => tool.name === toolName);
|
|
1388
|
+
if (existing) {
|
|
1389
|
+
existing.count += 1;
|
|
1390
|
+
}
|
|
1391
|
+
else {
|
|
1392
|
+
this.currentProgress.tools.push({ name: toolName, count: 1 });
|
|
1393
|
+
}
|
|
1394
|
+
this.updateCurrentProgress({ currentTool: toolName, lastTool: toolName });
|
|
1395
|
+
}
|
|
1396
|
+
externalTask() {
|
|
1397
|
+
if (!this.externalMirror) {
|
|
1398
|
+
return null;
|
|
1399
|
+
}
|
|
1400
|
+
const startedAt = this.externalMirror.startedAt ?? new Date().toISOString();
|
|
1401
|
+
const startedMs = new Date(startedAt).getTime();
|
|
1402
|
+
return {
|
|
1403
|
+
id: this.externalMirror.turnId ?? "cli",
|
|
1404
|
+
source: "cli",
|
|
1405
|
+
status: this.externalMirror.latestStatus?.includes("failed")
|
|
1406
|
+
? "failed"
|
|
1407
|
+
: this.externalMirror.latestStatus?.includes("aborted")
|
|
1408
|
+
? "aborted"
|
|
1409
|
+
: this.externalMirror.latestStatus?.includes("finished") || this.externalMirror.latestStatus?.includes("completed")
|
|
1410
|
+
? "completed"
|
|
1411
|
+
: "running",
|
|
1412
|
+
threadId: this.externalMirror.threadId,
|
|
1413
|
+
startedAt,
|
|
1414
|
+
updatedAt: new Date().toISOString(),
|
|
1415
|
+
durationMs: Number.isFinite(startedMs) ? Math.max(0, Date.now() - startedMs) : 0,
|
|
1416
|
+
outputChars: 0,
|
|
1417
|
+
tools: [],
|
|
1418
|
+
detail: this.externalMirror.latestStatus ?? this.externalMirror.rolloutPath,
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
765
1421
|
broadcastQueue() {
|
|
766
1422
|
this.broadcast({ type: "queue_update", queue: this.queue(), paused: this.queuePaused() });
|
|
767
1423
|
}
|
|
@@ -788,6 +1444,20 @@ export class RelayRuntime {
|
|
|
788
1444
|
capabilities: info.capabilities ?? CODEX_AGENT_CAPABILITIES,
|
|
789
1445
|
};
|
|
790
1446
|
}
|
|
1447
|
+
async persistWorkspaceArtifactsForTurn(workspace, turnId, startedAt) {
|
|
1448
|
+
const report = await collectRecentWorkspaceArtifacts(workspace, {
|
|
1449
|
+
since: startedAt,
|
|
1450
|
+
until: new Date(),
|
|
1451
|
+
maxFileSize: this.config.maxFileSize,
|
|
1452
|
+
limit: 20,
|
|
1453
|
+
ignoreDirs: this.config.artifactIgnoreDirs,
|
|
1454
|
+
ignoreGlobs: this.config.artifactIgnoreGlobs,
|
|
1455
|
+
});
|
|
1456
|
+
if (report.artifacts.length === 0 && report.skippedCount === 0 && !report.omittedCount) {
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
await persistWorkspaceArtifactReport(workspace, turnId, report);
|
|
1460
|
+
}
|
|
791
1461
|
}
|
|
792
1462
|
function queueItemDto(item) {
|
|
793
1463
|
return {
|
|
@@ -820,7 +1490,63 @@ function externalStatusLine(snapshot, queueLength) {
|
|
|
820
1490
|
? formatDuration((Date.now() - snapshot.activity.startedAt.getTime()) / 1000)
|
|
821
1491
|
: "-";
|
|
822
1492
|
const tool = snapshot.latestToolName ?? "-";
|
|
823
|
-
return
|
|
1493
|
+
return `${snapshot.agentLabel} CLI running · ${elapsed} · tool ${tool} · ${queueLength} queued`;
|
|
1494
|
+
}
|
|
1495
|
+
function cliHealthForAgent(agentId, health) {
|
|
1496
|
+
if (agentId === "pi") {
|
|
1497
|
+
return { path: health.piCliPath, label: health.piCli, version: health.piCliVersion };
|
|
1498
|
+
}
|
|
1499
|
+
if (agentId === "hermes") {
|
|
1500
|
+
return { path: health.hermesCliPath, label: health.hermesCli, version: health.hermesCliVersion };
|
|
1501
|
+
}
|
|
1502
|
+
if (agentId === "openclaw") {
|
|
1503
|
+
return { path: health.openClawCliPath, label: health.openClawCli, version: health.openClawCliVersion };
|
|
1504
|
+
}
|
|
1505
|
+
if (agentId === "claude-code") {
|
|
1506
|
+
return { path: health.claudeCodeCliPath, label: health.claudeCodeCli, version: health.claudeCodeCliVersion };
|
|
1507
|
+
}
|
|
1508
|
+
return { path: health.codexCliPath, label: health.codexCli, version: health.codexCliVersion };
|
|
1509
|
+
}
|
|
1510
|
+
function versionCheckForAgent(agentId, versions) {
|
|
1511
|
+
if (agentId === "pi")
|
|
1512
|
+
return versions.pi;
|
|
1513
|
+
if (agentId === "hermes")
|
|
1514
|
+
return versions.hermes;
|
|
1515
|
+
if (agentId === "openclaw")
|
|
1516
|
+
return versions.openclaw;
|
|
1517
|
+
if (agentId === "claude-code")
|
|
1518
|
+
return versions.claudeCode;
|
|
1519
|
+
return versions.codex;
|
|
1520
|
+
}
|
|
1521
|
+
function hostLoginCommand(info, config) {
|
|
1522
|
+
if (info.agentId === "hermes") {
|
|
1523
|
+
return `${config.hermesCliPath ?? "hermes"} login --no-browser`;
|
|
1524
|
+
}
|
|
1525
|
+
if (info.agentId === "claude-code") {
|
|
1526
|
+
return `${config.claudeCodeCliPath ?? "claude"} auth login`;
|
|
1527
|
+
}
|
|
1528
|
+
if (info.agentId === "pi") {
|
|
1529
|
+
return `${config.piCliPath ?? "pi"} auth login`;
|
|
1530
|
+
}
|
|
1531
|
+
if (info.agentId === "openclaw") {
|
|
1532
|
+
return `${config.openClawCliPath ?? "openclaw"} login`;
|
|
1533
|
+
}
|
|
1534
|
+
return "codex login --device-auth";
|
|
1535
|
+
}
|
|
1536
|
+
function hostLogoutCommand(info, config) {
|
|
1537
|
+
if (info.agentId === "hermes") {
|
|
1538
|
+
return `${config.hermesCliPath ?? "hermes"} logout`;
|
|
1539
|
+
}
|
|
1540
|
+
if (info.agentId === "claude-code") {
|
|
1541
|
+
return `${config.claudeCodeCliPath ?? "claude"} auth logout`;
|
|
1542
|
+
}
|
|
1543
|
+
if (info.agentId === "pi") {
|
|
1544
|
+
return `${config.piCliPath ?? "pi"} auth logout`;
|
|
1545
|
+
}
|
|
1546
|
+
if (info.agentId === "openclaw") {
|
|
1547
|
+
return `${config.openClawCliPath ?? "openclaw"} logout`;
|
|
1548
|
+
}
|
|
1549
|
+
return "codex logout";
|
|
824
1550
|
}
|
|
825
1551
|
function durationFromDates(start, end) {
|
|
826
1552
|
if (!start || !end) {
|