@nordbyte/nordrelay 0.3.1 → 0.4.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 +45 -2
- package/README.md +204 -30
- package/dist/agent-activity.js +300 -0
- package/dist/agent-adapter.js +17 -30
- package/dist/agent-factory.js +27 -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 +328 -159
- 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 +83 -2
- 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 +673 -51
- package/dist/session-format.js +28 -18
- package/dist/session-registry.js +40 -15
- package/dist/settings-service.js +35 -4
- package/dist/web-dashboard-ui.js +18 -0
- package/dist/web-dashboard.js +329 -47
- 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 +131 -3
- package/plugins/nordrelay/skills/telegram-remote/SKILL.md +2 -2
- package/CHANGELOG.md +0 -26
package/dist/relay-runtime.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
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 { createAgentSessionService, enabledAgents } from "./agent-factory.js";
|
|
10
|
+
import { AuditLogStore } from "./audit-log.js";
|
|
11
|
+
import { checkAuthStatus, startLogin as startCodexLogin, startLogout as startCodexLogout } from "./codex-auth.js";
|
|
12
|
+
import { checkClaudeCodeAuthStatus, startClaudeCodeLogin, startClaudeCodeLogout } from "./claude-code-auth.js";
|
|
10
13
|
import { friendlyErrorText } from "./error-messages.js";
|
|
11
|
-
import {
|
|
14
|
+
import { checkHermesAuthStatus, startHermesLogin, startHermesLogout } from "./hermes-auth.js";
|
|
15
|
+
import { checkOpenClawAuthStatus } from "./openclaw-auth.js";
|
|
16
|
+
import { getConnectorHealth, getVersionChecks, readConnectorState, readFormattedLogTail, spawnConnectorRestart, spawnSelfUpdate } from "./operations.js";
|
|
17
|
+
import { checkPiAuthStatus } from "./pi-auth.js";
|
|
12
18
|
import { PromptStore, toPromptEnvelope } from "./prompt-store.js";
|
|
13
19
|
import { renderSessionInfoPlain } from "./session-format.js";
|
|
20
|
+
import { SessionLockStore } from "./session-locks.js";
|
|
14
21
|
import { SessionRegistry } from "./session-registry.js";
|
|
15
22
|
import { transcribeAudio } from "./voice.js";
|
|
16
23
|
import { WebActivityStore, WebChatStore, } from "./web-state.js";
|
|
@@ -25,12 +32,15 @@ export class RelayRuntime {
|
|
|
25
32
|
promptStore;
|
|
26
33
|
chatStore;
|
|
27
34
|
activityStore;
|
|
35
|
+
auditStore;
|
|
36
|
+
lockStore;
|
|
28
37
|
subscribers = new Set();
|
|
29
38
|
externalMonitor;
|
|
30
39
|
draining = false;
|
|
31
40
|
currentTurnId = null;
|
|
32
41
|
accumulatedText = "";
|
|
33
42
|
currentTurnStartedAt = 0;
|
|
43
|
+
currentProgress = null;
|
|
34
44
|
externalMirror = null;
|
|
35
45
|
constructor(config) {
|
|
36
46
|
this.config = config;
|
|
@@ -41,6 +51,8 @@ export class RelayRuntime {
|
|
|
41
51
|
this.promptStore = new PromptStore(config.workspace, config.stateBackend);
|
|
42
52
|
this.chatStore = new WebChatStore(config.workspace, config.stateBackend, MAX_CHAT_HISTORY);
|
|
43
53
|
this.activityStore = new WebActivityStore(config.workspace, config.stateBackend, config.auditMaxEvents);
|
|
54
|
+
this.auditStore = new AuditLogStore(config.workspace, config.stateBackend, config.auditMaxEvents);
|
|
55
|
+
this.lockStore = new SessionLockStore(config.workspace, config.stateBackend);
|
|
44
56
|
if (config.codexExternalBusyCheckMs > 0) {
|
|
45
57
|
this.externalMonitor = setInterval(() => {
|
|
46
58
|
void this.monitorExternalActivity().catch((error) => this.broadcastStatus(friendlyErrorText(error), "error"));
|
|
@@ -70,48 +82,311 @@ export class RelayRuntime {
|
|
|
70
82
|
}
|
|
71
83
|
async status() {
|
|
72
84
|
return {
|
|
73
|
-
health: await getConnectorHealth(),
|
|
74
|
-
versionChecks: await getVersionChecks({ piCliPath: this.config.piCliPath }),
|
|
85
|
+
health: await getConnectorHealth({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath }),
|
|
86
|
+
versionChecks: await getVersionChecks({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath }),
|
|
75
87
|
snapshot: await this.snapshot(),
|
|
76
88
|
};
|
|
77
89
|
}
|
|
90
|
+
async version() {
|
|
91
|
+
return {
|
|
92
|
+
health: await getConnectorHealth({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath }),
|
|
93
|
+
state: await readConnectorState(),
|
|
94
|
+
versionChecks: await getVersionChecks({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath }),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
updateConnector() {
|
|
98
|
+
const update = spawnSelfUpdate();
|
|
99
|
+
this.broadcastStatus(`Update started with ${update.method}. Log: ${update.logPath}`, "warn");
|
|
100
|
+
this.appendActivity({
|
|
101
|
+
source: "web",
|
|
102
|
+
status: "info",
|
|
103
|
+
type: "update_started",
|
|
104
|
+
threadId: null,
|
|
105
|
+
workspace: this.config.workspace,
|
|
106
|
+
detail: `${update.method}: ${update.summary}`,
|
|
107
|
+
});
|
|
108
|
+
this.appendAudit({
|
|
109
|
+
action: "command",
|
|
110
|
+
status: "ok",
|
|
111
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
112
|
+
description: "update",
|
|
113
|
+
detail: update.summary,
|
|
114
|
+
});
|
|
115
|
+
return update;
|
|
116
|
+
}
|
|
78
117
|
async diagnostics() {
|
|
79
118
|
return {
|
|
80
|
-
health: await getConnectorHealth(),
|
|
81
|
-
versionChecks: await getVersionChecks({ piCliPath: this.config.piCliPath }),
|
|
119
|
+
health: await getConnectorHealth({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath }),
|
|
120
|
+
versionChecks: await getVersionChecks({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath }),
|
|
82
121
|
snapshot: await this.snapshot(),
|
|
83
122
|
runtime: {
|
|
84
123
|
stateBackend: this.config.stateBackend,
|
|
85
124
|
sourceWorkspace: this.config.workspace,
|
|
86
125
|
queuePaused: this.promptStore.isPaused(WEB_CONTEXT_KEY),
|
|
87
126
|
externalMirror: this.externalMirror ? { ...this.externalMirror } : null,
|
|
127
|
+
agentDiagnostics: getAgentDiagnostics(await this.getSession(true), this.config),
|
|
88
128
|
},
|
|
89
129
|
};
|
|
90
130
|
}
|
|
91
|
-
async
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
131
|
+
async adapterHealth() {
|
|
132
|
+
const health = await getConnectorHealth({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath });
|
|
133
|
+
const versions = await getVersionChecks({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath });
|
|
134
|
+
return Promise.all(listAgentAdapterDescriptors().map(async (descriptor) => {
|
|
135
|
+
const enabled = enabledAgents(this.config).includes(descriptor.id);
|
|
136
|
+
const auth = descriptor.capabilities.auth && enabled
|
|
137
|
+
? await this.authStatus(descriptor.id).catch((error) => ({
|
|
138
|
+
agentId: descriptor.id,
|
|
139
|
+
agentLabel: descriptor.label,
|
|
140
|
+
supported: descriptor.capabilities.auth,
|
|
141
|
+
authenticated: false,
|
|
142
|
+
detail: friendlyErrorText(error),
|
|
143
|
+
loginSupported: descriptor.capabilities.login,
|
|
144
|
+
logoutSupported: descriptor.capabilities.logout,
|
|
105
145
|
}))
|
|
106
|
-
:
|
|
107
|
-
|
|
108
|
-
|
|
146
|
+
: null;
|
|
147
|
+
const cli = cliHealthForAgent(descriptor.id, health);
|
|
148
|
+
const version = versionCheckForAgent(descriptor.id, versions);
|
|
149
|
+
return {
|
|
150
|
+
id: descriptor.id,
|
|
151
|
+
label: descriptor.label,
|
|
152
|
+
enabled,
|
|
153
|
+
status: descriptor.status === "available" ? (enabled ? "enabled" : "disabled") : "planned",
|
|
154
|
+
auth: {
|
|
155
|
+
supported: descriptor.capabilities.auth,
|
|
156
|
+
authenticated: auth ? auth.authenticated : null,
|
|
157
|
+
method: auth?.method,
|
|
158
|
+
detail: auth?.detail,
|
|
159
|
+
},
|
|
160
|
+
cli,
|
|
161
|
+
version: {
|
|
162
|
+
installed: version.installedLabel,
|
|
163
|
+
latest: version.latestVersion,
|
|
164
|
+
status: version.status,
|
|
165
|
+
detail: version.detail,
|
|
166
|
+
},
|
|
167
|
+
capabilities: descriptor.capabilities,
|
|
168
|
+
notes: descriptor.notes,
|
|
169
|
+
};
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
permissions() {
|
|
173
|
+
return {
|
|
174
|
+
telegramAllowAnyChat: this.config.telegramAllowAnyChat,
|
|
175
|
+
telegramAdminUserIds: this.config.telegramAdminUserIds,
|
|
176
|
+
telegramAllowedUserIds: this.config.telegramAllowedUserIds,
|
|
177
|
+
telegramReadOnlyUserIds: this.config.telegramReadOnlyUserIds,
|
|
178
|
+
telegramAllowedChatIds: this.config.telegramAllowedChatIds,
|
|
179
|
+
telegramRolePolicies: this.config.telegramRolePolicies,
|
|
109
180
|
};
|
|
110
181
|
}
|
|
182
|
+
tasks() {
|
|
183
|
+
return {
|
|
184
|
+
current: this.currentProgress ? { ...this.currentProgress, tools: [...this.currentProgress.tools] } : null,
|
|
185
|
+
external: this.externalTask(),
|
|
186
|
+
queue: this.queue(),
|
|
187
|
+
queuePaused: this.queuePaused(),
|
|
188
|
+
recent: this.activity({ limit: 20 }),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
audit(limit = 50) {
|
|
192
|
+
return this.auditStore.list(limit);
|
|
193
|
+
}
|
|
194
|
+
locks() {
|
|
195
|
+
return this.lockStore.list();
|
|
196
|
+
}
|
|
197
|
+
lockWebSession(ownerName = "Web dashboard") {
|
|
198
|
+
const lock = this.lockStore.set(WEB_CONTEXT_KEY, 0, ownerName, this.config.sessionLockTtlMs);
|
|
199
|
+
this.appendAudit({
|
|
200
|
+
action: "lock_updated",
|
|
201
|
+
status: "ok",
|
|
202
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
203
|
+
description: "lock",
|
|
204
|
+
detail: `locked by ${ownerName}`,
|
|
205
|
+
});
|
|
206
|
+
return lock;
|
|
207
|
+
}
|
|
208
|
+
unlockWebSession() {
|
|
209
|
+
const removed = this.lockStore.clear(WEB_CONTEXT_KEY);
|
|
210
|
+
this.appendAudit({
|
|
211
|
+
action: "lock_updated",
|
|
212
|
+
status: "ok",
|
|
213
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
214
|
+
description: "unlock",
|
|
215
|
+
detail: removed ? "unlocked" : "no lock",
|
|
216
|
+
});
|
|
217
|
+
return { removed, locks: this.locks() };
|
|
218
|
+
}
|
|
219
|
+
async controlOptions(agentId) {
|
|
220
|
+
const { session, dispose } = await this.getControlSession(agentId);
|
|
221
|
+
try {
|
|
222
|
+
const info = this.publicInfo(session);
|
|
223
|
+
const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
|
|
224
|
+
if (capabilities.modelSelection) {
|
|
225
|
+
await session.refreshModels().catch((error) => {
|
|
226
|
+
console.warn(`Failed to refresh ${agentLabel(info.agentId)} models: ${error instanceof Error ? error.message : String(error)}`);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
models: capabilities.modelSelection ? session.listModels() : [],
|
|
231
|
+
reasoningLabel: agentReasoningLabel(info.agentId),
|
|
232
|
+
reasoningOptions: agentReasoningOptions(info.agentId),
|
|
233
|
+
launchProfiles: capabilities.launchProfiles ? session.listLaunchProfiles() : [],
|
|
234
|
+
workspaces: filterAllowedWorkspaces(session.listWorkspaces(), this.config),
|
|
235
|
+
capabilities,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
if (dispose) {
|
|
240
|
+
session.dispose();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async authStatus(agentId) {
|
|
245
|
+
const { session, dispose } = await this.getControlSession(agentId);
|
|
246
|
+
try {
|
|
247
|
+
const info = this.publicInfo(session);
|
|
248
|
+
const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
|
|
249
|
+
if (!capabilities.auth) {
|
|
250
|
+
return {
|
|
251
|
+
agentId: info.agentId,
|
|
252
|
+
agentLabel: info.agentLabel,
|
|
253
|
+
supported: false,
|
|
254
|
+
authenticated: null,
|
|
255
|
+
detail: `${info.agentLabel} authentication is managed outside NordRelay.`,
|
|
256
|
+
loginSupported: false,
|
|
257
|
+
logoutSupported: false,
|
|
258
|
+
hostLoginCommand: hostLoginCommand(info, this.config),
|
|
259
|
+
hostLogoutCommand: hostLogoutCommand(info, this.config),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const status = await this.checkAgentAuth(info);
|
|
263
|
+
return {
|
|
264
|
+
agentId: info.agentId,
|
|
265
|
+
agentLabel: info.agentLabel,
|
|
266
|
+
supported: true,
|
|
267
|
+
authenticated: status.authenticated,
|
|
268
|
+
method: status.method,
|
|
269
|
+
detail: status.detail,
|
|
270
|
+
loginSupported: capabilities.login,
|
|
271
|
+
logoutSupported: capabilities.logout,
|
|
272
|
+
hostLoginCommand: hostLoginCommand(info, this.config),
|
|
273
|
+
hostLogoutCommand: hostLogoutCommand(info, this.config),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
finally {
|
|
277
|
+
if (dispose) {
|
|
278
|
+
session.dispose();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async login(agentId) {
|
|
283
|
+
const { session, dispose } = await this.getControlSession(agentId);
|
|
284
|
+
try {
|
|
285
|
+
const info = this.publicInfo(session);
|
|
286
|
+
const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
|
|
287
|
+
if (!capabilities.login) {
|
|
288
|
+
return {
|
|
289
|
+
...(await this.authStatus(info.agentId)),
|
|
290
|
+
result: {
|
|
291
|
+
success: false,
|
|
292
|
+
message: `${info.agentLabel} login is not managed by NordRelay. Run ${hostLoginCommand(info, this.config)} on the host.`,
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
if (!this.config.enableTelegramLogin) {
|
|
297
|
+
return {
|
|
298
|
+
...(await this.authStatus(info.agentId)),
|
|
299
|
+
result: {
|
|
300
|
+
success: false,
|
|
301
|
+
message: `Remote login is disabled. Run ${hostLoginCommand(info, this.config)} on the host.`,
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const result = await this.startAgentLogin(info);
|
|
306
|
+
this.appendAudit({
|
|
307
|
+
action: "command",
|
|
308
|
+
status: result.success ? "ok" : "failed",
|
|
309
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
310
|
+
agentId: info.agentId,
|
|
311
|
+
threadId: info.threadId,
|
|
312
|
+
workspace: info.workspace,
|
|
313
|
+
description: "login",
|
|
314
|
+
detail: result.message,
|
|
315
|
+
});
|
|
316
|
+
return { ...(await this.authStatus(info.agentId)), result };
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
if (dispose) {
|
|
320
|
+
session.dispose();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async logout(agentId) {
|
|
325
|
+
const { session, dispose } = await this.getControlSession(agentId);
|
|
326
|
+
try {
|
|
327
|
+
const info = this.publicInfo(session);
|
|
328
|
+
const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
|
|
329
|
+
if (!capabilities.logout) {
|
|
330
|
+
return {
|
|
331
|
+
...(await this.authStatus(info.agentId)),
|
|
332
|
+
result: {
|
|
333
|
+
success: false,
|
|
334
|
+
message: `${info.agentLabel} logout is not managed by NordRelay. Run ${hostLogoutCommand(info, this.config)} on the host.`,
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
if (!this.config.enableTelegramLogin) {
|
|
339
|
+
return {
|
|
340
|
+
...(await this.authStatus(info.agentId)),
|
|
341
|
+
result: {
|
|
342
|
+
success: false,
|
|
343
|
+
message: `Remote auth management is disabled. Run ${hostLogoutCommand(info, this.config)} on the host.`,
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
const current = await this.checkAgentAuth(info);
|
|
348
|
+
if (current.method === "api-key") {
|
|
349
|
+
return {
|
|
350
|
+
...(await this.authStatus(info.agentId)),
|
|
351
|
+
result: {
|
|
352
|
+
success: false,
|
|
353
|
+
message: "Cannot logout while API-key authentication is configured. Remove the API key from .env to use CLI auth.",
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const result = await this.startAgentLogout(info);
|
|
358
|
+
this.appendAudit({
|
|
359
|
+
action: "command",
|
|
360
|
+
status: result.success ? "ok" : "failed",
|
|
361
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
362
|
+
agentId: info.agentId,
|
|
363
|
+
threadId: info.threadId,
|
|
364
|
+
workspace: info.workspace,
|
|
365
|
+
description: "logout",
|
|
366
|
+
detail: result.message,
|
|
367
|
+
});
|
|
368
|
+
return { ...(await this.authStatus(info.agentId)), result };
|
|
369
|
+
}
|
|
370
|
+
finally {
|
|
371
|
+
if (dispose) {
|
|
372
|
+
session.dispose();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
111
376
|
async chatHistory(limit = 200) {
|
|
112
377
|
const session = await this.getSession(true);
|
|
113
378
|
return this.chatStore.list(this.publicInfo(session).threadId, limit);
|
|
114
379
|
}
|
|
380
|
+
async sessionDetail(threadId) {
|
|
381
|
+
const session = await this.getSession(true);
|
|
382
|
+
const record = session.getSessionRecord(threadId);
|
|
383
|
+
return {
|
|
384
|
+
record,
|
|
385
|
+
active: this.publicInfo(session),
|
|
386
|
+
messages: this.chatStore.list(threadId, 100),
|
|
387
|
+
activity: this.activity({ limit: 100 }).filter((event) => event.threadId === threadId),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
115
390
|
async clearChatHistory() {
|
|
116
391
|
const session = await this.getSession(true);
|
|
117
392
|
const removed = this.chatStore.clear(this.publicInfo(session).threadId);
|
|
@@ -122,6 +397,51 @@ export class RelayRuntime {
|
|
|
122
397
|
activity(options = {}) {
|
|
123
398
|
return this.activityStore.list(options);
|
|
124
399
|
}
|
|
400
|
+
async retry() {
|
|
401
|
+
const cached = this.promptStore.getLastPrompt(WEB_CONTEXT_KEY);
|
|
402
|
+
if (!cached) {
|
|
403
|
+
throw new Error("Nothing to retry. Send a message first.");
|
|
404
|
+
}
|
|
405
|
+
this.appendAudit({
|
|
406
|
+
action: "command",
|
|
407
|
+
status: "ok",
|
|
408
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
409
|
+
description: "retry",
|
|
410
|
+
detail: cached.description,
|
|
411
|
+
});
|
|
412
|
+
return this.sendEnvelope(cached);
|
|
413
|
+
}
|
|
414
|
+
async sync() {
|
|
415
|
+
const session = await this.getSession(true);
|
|
416
|
+
const info = this.publicInfo(session);
|
|
417
|
+
if (!(info.capabilities ?? CODEX_AGENT_CAPABILITIES).externalActivity) {
|
|
418
|
+
throw new Error(`${info.agentLabel} has no external state watcher to sync.`);
|
|
419
|
+
}
|
|
420
|
+
const result = session.syncFromAgentState({ reattach: true });
|
|
421
|
+
if (result.changed) {
|
|
422
|
+
this.updateSession(session);
|
|
423
|
+
}
|
|
424
|
+
this.appendActivity({
|
|
425
|
+
source: "web",
|
|
426
|
+
status: "info",
|
|
427
|
+
type: "session_sync",
|
|
428
|
+
threadId: result.info.threadId,
|
|
429
|
+
workspace: result.info.workspace,
|
|
430
|
+
agentId: result.info.agentId,
|
|
431
|
+
detail: result.changedFields.length > 0 ? result.changedFields.join(", ") : "already in sync",
|
|
432
|
+
});
|
|
433
|
+
this.appendAudit({
|
|
434
|
+
action: "command",
|
|
435
|
+
status: "ok",
|
|
436
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
437
|
+
agentId: result.info.agentId,
|
|
438
|
+
threadId: result.info.threadId,
|
|
439
|
+
workspace: result.info.workspace,
|
|
440
|
+
description: "sync",
|
|
441
|
+
detail: result.changedFields.join(", ") || "none",
|
|
442
|
+
});
|
|
443
|
+
return result;
|
|
444
|
+
}
|
|
125
445
|
async listSessions(limit = 80, query = "") {
|
|
126
446
|
return this.filteredSessions(await this.getSession(true), query, Math.max(1, limit * 3)).slice(0, limit);
|
|
127
447
|
}
|
|
@@ -161,7 +481,12 @@ export class RelayRuntime {
|
|
|
161
481
|
});
|
|
162
482
|
}
|
|
163
483
|
async listModels() {
|
|
164
|
-
|
|
484
|
+
const session = await this.getSession(true);
|
|
485
|
+
const info = this.publicInfo(session);
|
|
486
|
+
await session.refreshModels({ force: true }).catch((error) => {
|
|
487
|
+
console.warn(`Failed to refresh ${agentLabel(info.agentId)} models: ${error instanceof Error ? error.message : String(error)}`);
|
|
488
|
+
});
|
|
489
|
+
return session.listModels();
|
|
165
490
|
}
|
|
166
491
|
async setAgent(agentId) {
|
|
167
492
|
if (!enabledAgents(this.config).includes(agentId)) {
|
|
@@ -175,7 +500,7 @@ export class RelayRuntime {
|
|
|
175
500
|
const session = options.agentId ? await this.registry.switchAgent(WEB_CONTEXT_KEY, options.agentId) : await this.getSession(true);
|
|
176
501
|
this.ensureIdle(session);
|
|
177
502
|
if (options.reasoningEffort) {
|
|
178
|
-
const reasoningOptions = session.getInfo().agentId
|
|
503
|
+
const reasoningOptions = agentReasoningOptions(session.getInfo().agentId);
|
|
179
504
|
if (!reasoningOptions.includes(options.reasoningEffort)) {
|
|
180
505
|
throw new Error(`Invalid ${agentReasoningLabel(session.getInfo().agentId)} value: ${options.reasoningEffort}`);
|
|
181
506
|
}
|
|
@@ -230,7 +555,7 @@ export class RelayRuntime {
|
|
|
230
555
|
async setReasoningEffort(effort) {
|
|
231
556
|
const session = await this.getSession(true);
|
|
232
557
|
this.ensureIdle(session);
|
|
233
|
-
const options = session.getInfo().agentId
|
|
558
|
+
const options = agentReasoningOptions(session.getInfo().agentId);
|
|
234
559
|
if (!options.includes(effort)) {
|
|
235
560
|
throw new Error(`Invalid ${agentReasoningLabel(session.getInfo().agentId)} value: ${effort}`);
|
|
236
561
|
}
|
|
@@ -264,6 +589,16 @@ export class RelayRuntime {
|
|
|
264
589
|
}
|
|
265
590
|
async abort() {
|
|
266
591
|
const session = await this.getSession(true);
|
|
592
|
+
const snapshot = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
|
|
593
|
+
if (snapshot?.activity.active && !session.isProcessing()) {
|
|
594
|
+
this.broadcast({
|
|
595
|
+
type: "status",
|
|
596
|
+
level: "warn",
|
|
597
|
+
message: `Cannot abort the external ${snapshot.agentLabel} CLI task from NordRelay. Stop it in the terminal where it is running.`,
|
|
598
|
+
at: new Date().toISOString(),
|
|
599
|
+
});
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
267
602
|
await session.abort();
|
|
268
603
|
this.broadcast({ type: "status", level: "warn", message: "Current operation aborted.", at: new Date().toISOString() });
|
|
269
604
|
}
|
|
@@ -341,7 +676,8 @@ export class RelayRuntime {
|
|
|
341
676
|
}
|
|
342
677
|
async sendEnvelope(envelope) {
|
|
343
678
|
const session = await this.getSession(false);
|
|
344
|
-
|
|
679
|
+
const external = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
|
|
680
|
+
if (session.isProcessing() || external?.activity.active) {
|
|
345
681
|
const queued = this.promptStore.enqueue(WEB_CONTEXT_KEY, envelope);
|
|
346
682
|
const info = this.publicInfo(session);
|
|
347
683
|
this.appendActivity({
|
|
@@ -352,8 +688,23 @@ export class RelayRuntime {
|
|
|
352
688
|
workspace: info.workspace,
|
|
353
689
|
agentId: info.agentId,
|
|
354
690
|
prompt: envelope.description,
|
|
355
|
-
detail:
|
|
691
|
+
detail: external?.activity.active
|
|
692
|
+
? `Queued because ${external.agentLabel} CLI is still processing another task.`
|
|
693
|
+
: `Queued at position ${this.promptStore.list(WEB_CONTEXT_KEY).length}.`,
|
|
694
|
+
});
|
|
695
|
+
this.appendAudit({
|
|
696
|
+
action: "prompt_queued",
|
|
697
|
+
status: "ok",
|
|
698
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
699
|
+
agentId: info.agentId,
|
|
700
|
+
threadId: info.threadId,
|
|
701
|
+
workspace: info.workspace,
|
|
702
|
+
promptId: queued.id,
|
|
703
|
+
description: envelope.description,
|
|
356
704
|
});
|
|
705
|
+
if (external?.activity.active) {
|
|
706
|
+
this.broadcastStatus(`Waiting for ${external.agentLabel} CLI task... ${this.promptStore.list(WEB_CONTEXT_KEY).length} queued.`, "info");
|
|
707
|
+
}
|
|
357
708
|
this.broadcastQueue();
|
|
358
709
|
return { queued: true, queueId: queued.id };
|
|
359
710
|
}
|
|
@@ -397,6 +748,12 @@ export class RelayRuntime {
|
|
|
397
748
|
workspace: this.config.workspace,
|
|
398
749
|
detail: id ? `${action}: ${id}` : action,
|
|
399
750
|
});
|
|
751
|
+
this.appendAudit({
|
|
752
|
+
action: "queue_updated",
|
|
753
|
+
status: "ok",
|
|
754
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
755
|
+
description: id ? `${action}: ${id}` : action,
|
|
756
|
+
});
|
|
400
757
|
this.broadcastQueue();
|
|
401
758
|
return this.queue();
|
|
402
759
|
}
|
|
@@ -485,23 +842,21 @@ export class RelayRuntime {
|
|
|
485
842
|
async monitorExternalActivity() {
|
|
486
843
|
const session = await this.getSession(true);
|
|
487
844
|
const info = this.publicInfo(session);
|
|
488
|
-
if (!info.capabilities.externalActivity ||
|
|
845
|
+
if (!info.capabilities.externalActivity || !info.threadId || session.isProcessing()) {
|
|
489
846
|
return;
|
|
490
847
|
}
|
|
491
|
-
const snapshot =
|
|
848
|
+
const snapshot = getExternalSnapshotForSession(session, this.config, {
|
|
492
849
|
afterLine: this.externalMirror?.threadId === info.threadId ? this.externalMirror.lastLine : Number.MAX_SAFE_INTEGER,
|
|
493
|
-
|
|
494
|
-
}) ?? getThreadRolloutSnapshot(info.threadId, {
|
|
495
|
-
staleAfterMs: this.config.codexExternalBusyStaleMs,
|
|
850
|
+
}) ?? getExternalSnapshotForSession(session, this.config, {
|
|
496
851
|
maxEvents: 0,
|
|
497
852
|
});
|
|
498
853
|
if (!snapshot) {
|
|
499
854
|
return;
|
|
500
855
|
}
|
|
501
|
-
if (!this.externalMirror || this.externalMirror.threadId !== snapshot.threadId || this.externalMirror.rolloutPath !== snapshot.
|
|
856
|
+
if (!this.externalMirror || this.externalMirror.threadId !== snapshot.threadId || this.externalMirror.rolloutPath !== snapshot.sourcePath) {
|
|
502
857
|
this.externalMirror = {
|
|
503
858
|
threadId: snapshot.threadId,
|
|
504
|
-
rolloutPath: snapshot.
|
|
859
|
+
rolloutPath: snapshot.sourcePath,
|
|
505
860
|
lastLine: snapshot.lineCount,
|
|
506
861
|
turnId: snapshot.activity.turnId,
|
|
507
862
|
startedAt: snapshot.activity.startedAt?.toISOString() ?? null,
|
|
@@ -555,16 +910,21 @@ export class RelayRuntime {
|
|
|
555
910
|
workspace: info.workspace,
|
|
556
911
|
agentId: info.agentId,
|
|
557
912
|
prompt: snapshot.latestUserMessage ?? undefined,
|
|
558
|
-
detail:
|
|
913
|
+
detail: `${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`,
|
|
559
914
|
durationMs: durationFromDates(externalStartedAt, terminalEvent.timestamp),
|
|
560
915
|
});
|
|
561
|
-
|
|
916
|
+
if (externalStartedAt && terminalEvent.turnId) {
|
|
917
|
+
await this.persistWorkspaceArtifactsForTurn(info.workspace, terminalEvent.turnId, externalStartedAt);
|
|
918
|
+
}
|
|
919
|
+
mirror.latestStatus = `${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`;
|
|
920
|
+
this.broadcastStatus(`${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`, terminalEvent.status === "failed" ? "error" : terminalEvent.status === "aborted" ? "warn" : "info");
|
|
562
921
|
this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
|
|
922
|
+
await this.drainQueue();
|
|
563
923
|
}
|
|
564
924
|
mirror.lastLine = Math.max(mirror.lastLine, snapshot.lineCount);
|
|
565
925
|
}
|
|
566
926
|
startExternalTurn(snapshot) {
|
|
567
|
-
const prompt = snapshot.latestUserMessage ??
|
|
927
|
+
const prompt = snapshot.latestUserMessage ?? `${snapshot.agentLabel} CLI task`;
|
|
568
928
|
this.chatStore.append({
|
|
569
929
|
threadId: snapshot.threadId,
|
|
570
930
|
role: "user",
|
|
@@ -586,7 +946,7 @@ export class RelayRuntime {
|
|
|
586
946
|
type: "cli_turn_started",
|
|
587
947
|
threadId: snapshot.threadId,
|
|
588
948
|
prompt,
|
|
589
|
-
detail:
|
|
949
|
+
detail: `${snapshot.sourceLabel}: ${snapshot.sourcePath}`,
|
|
590
950
|
});
|
|
591
951
|
}
|
|
592
952
|
broadcastExternalEvents(snapshot, events) {
|
|
@@ -619,12 +979,79 @@ export class RelayRuntime {
|
|
|
619
979
|
async getSession(deferThreadStart) {
|
|
620
980
|
return this.registry.getOrCreate(WEB_CONTEXT_KEY, { deferThreadStart });
|
|
621
981
|
}
|
|
982
|
+
async getControlSession(agentId) {
|
|
983
|
+
const active = await this.getSession(true);
|
|
984
|
+
const activeInfo = this.publicInfo(active);
|
|
985
|
+
if (!agentId || agentId === activeInfo.agentId) {
|
|
986
|
+
return { session: active, dispose: false };
|
|
987
|
+
}
|
|
988
|
+
if (!enabledAgents(this.config).includes(agentId)) {
|
|
989
|
+
throw new Error(`Agent is not enabled: ${agentId}`);
|
|
990
|
+
}
|
|
991
|
+
const session = await createAgentSessionService(this.config, agentId, {
|
|
992
|
+
deferThreadStart: true,
|
|
993
|
+
workspace: activeInfo.workspace,
|
|
994
|
+
});
|
|
995
|
+
return { session, dispose: true };
|
|
996
|
+
}
|
|
622
997
|
async ensureActiveThread(session) {
|
|
623
998
|
if (!session.hasActiveThread()) {
|
|
624
999
|
await session.newThread();
|
|
625
1000
|
this.updateSession(session);
|
|
626
1001
|
}
|
|
627
1002
|
}
|
|
1003
|
+
async checkAgentAuth(info) {
|
|
1004
|
+
if (info.agentId === "pi") {
|
|
1005
|
+
return checkPiAuthStatus(info.model);
|
|
1006
|
+
}
|
|
1007
|
+
if (info.agentId === "hermes") {
|
|
1008
|
+
return checkHermesAuthStatus({
|
|
1009
|
+
baseUrl: this.config.hermesApiBaseUrl,
|
|
1010
|
+
apiKey: this.config.hermesApiKey,
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
if (info.agentId === "openclaw") {
|
|
1014
|
+
return checkOpenClawAuthStatus({
|
|
1015
|
+
gatewayUrl: this.config.openClawGatewayUrl,
|
|
1016
|
+
token: this.config.openClawGatewayToken,
|
|
1017
|
+
password: this.config.openClawGatewayPassword,
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
if (info.agentId === "claude-code") {
|
|
1021
|
+
return checkClaudeCodeAuthStatus(this.config.claudeCodeCliPath);
|
|
1022
|
+
}
|
|
1023
|
+
return checkAuthStatus(this.config.codexApiKey);
|
|
1024
|
+
}
|
|
1025
|
+
async startAgentLogin(info) {
|
|
1026
|
+
if (info.agentId === "hermes") {
|
|
1027
|
+
return startHermesLogin(this.config.hermesCliPath);
|
|
1028
|
+
}
|
|
1029
|
+
if (info.agentId === "claude-code") {
|
|
1030
|
+
return startClaudeCodeLogin(this.config.claudeCodeCliPath);
|
|
1031
|
+
}
|
|
1032
|
+
if (info.agentId === "codex") {
|
|
1033
|
+
return startCodexLogin();
|
|
1034
|
+
}
|
|
1035
|
+
return {
|
|
1036
|
+
success: false,
|
|
1037
|
+
message: `${info.agentLabel} login is not managed by NordRelay. Run the agent login flow on the host.`,
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
async startAgentLogout(info) {
|
|
1041
|
+
if (info.agentId === "hermes") {
|
|
1042
|
+
return startHermesLogout(this.config.hermesCliPath);
|
|
1043
|
+
}
|
|
1044
|
+
if (info.agentId === "claude-code") {
|
|
1045
|
+
return startClaudeCodeLogout(this.config.claudeCodeCliPath);
|
|
1046
|
+
}
|
|
1047
|
+
if (info.agentId === "codex") {
|
|
1048
|
+
return startCodexLogout();
|
|
1049
|
+
}
|
|
1050
|
+
return {
|
|
1051
|
+
success: false,
|
|
1052
|
+
message: `${info.agentLabel} logout is not managed by NordRelay. Run the agent logout flow on the host.`,
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
628
1055
|
ensureIdle(session) {
|
|
629
1056
|
if (session.isProcessing()) {
|
|
630
1057
|
throw new Error("The active session is still processing a turn.");
|
|
@@ -634,9 +1061,9 @@ export class RelayRuntime {
|
|
|
634
1061
|
await this.ensureActiveThread(session);
|
|
635
1062
|
const info = session.getInfo();
|
|
636
1063
|
if ((info.capabilities ?? CODEX_AGENT_CAPABILITIES).auth) {
|
|
637
|
-
const auth = await
|
|
1064
|
+
const auth = await this.checkAgentAuth(info);
|
|
638
1065
|
if (!auth.authenticated) {
|
|
639
|
-
throw new Error(
|
|
1066
|
+
throw new Error(`${agentLabel(info.agentId)} is not authenticated: ${auth.detail}`);
|
|
640
1067
|
}
|
|
641
1068
|
}
|
|
642
1069
|
const workspacePolicy = evaluateWorkspacePolicy(session.getInfo().workspace, this.config);
|
|
@@ -647,8 +1074,24 @@ export class RelayRuntime {
|
|
|
647
1074
|
this.currentTurnId = turnId;
|
|
648
1075
|
this.currentTurnStartedAt = Date.now();
|
|
649
1076
|
this.accumulatedText = "";
|
|
1077
|
+
this.currentProgress = {
|
|
1078
|
+
id: turnId,
|
|
1079
|
+
source: "web",
|
|
1080
|
+
status: "running",
|
|
1081
|
+
prompt: envelope.description,
|
|
1082
|
+
agentId: info.agentId,
|
|
1083
|
+
agentLabel: info.agentLabel,
|
|
1084
|
+
threadId: info.threadId,
|
|
1085
|
+
workspace: info.workspace,
|
|
1086
|
+
startedAt: new Date(this.currentTurnStartedAt).toISOString(),
|
|
1087
|
+
updatedAt: new Date(this.currentTurnStartedAt).toISOString(),
|
|
1088
|
+
durationMs: 0,
|
|
1089
|
+
outputChars: 0,
|
|
1090
|
+
tools: [],
|
|
1091
|
+
};
|
|
650
1092
|
this.promptStore.setLastPrompt(WEB_CONTEXT_KEY, envelope);
|
|
651
|
-
const
|
|
1093
|
+
const startedDate = new Date();
|
|
1094
|
+
const startedAt = startedDate.toISOString();
|
|
652
1095
|
this.chatStore.append({
|
|
653
1096
|
threadId: info.threadId ?? "pending",
|
|
654
1097
|
role: "user",
|
|
@@ -666,22 +1109,45 @@ export class RelayRuntime {
|
|
|
666
1109
|
agentId: info.agentId,
|
|
667
1110
|
prompt: envelope.description,
|
|
668
1111
|
});
|
|
1112
|
+
this.appendAudit({
|
|
1113
|
+
action: "prompt_started",
|
|
1114
|
+
status: "ok",
|
|
1115
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
1116
|
+
agentId: info.agentId,
|
|
1117
|
+
threadId: info.threadId,
|
|
1118
|
+
workspace: info.workspace,
|
|
1119
|
+
description: envelope.description,
|
|
1120
|
+
});
|
|
669
1121
|
this.broadcast({ type: "turn_start", id: turnId, prompt: envelope.description, at: startedAt, source: "web" });
|
|
670
1122
|
const callbacks = {
|
|
671
1123
|
onTextDelta: (delta) => {
|
|
672
1124
|
this.accumulatedText += delta;
|
|
1125
|
+
this.updateCurrentProgress({ outputChars: this.accumulatedText.length });
|
|
673
1126
|
this.broadcast({ type: "text_delta", id: turnId, delta });
|
|
674
1127
|
},
|
|
675
|
-
onToolStart: (toolName, toolCallId) =>
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
1128
|
+
onToolStart: (toolName, toolCallId) => {
|
|
1129
|
+
this.addCurrentTool(toolName);
|
|
1130
|
+
this.broadcast({ type: "tool_start", id: turnId, toolCallId, toolName });
|
|
1131
|
+
},
|
|
1132
|
+
onToolUpdate: (toolCallId, partialResult) => {
|
|
1133
|
+
this.updateCurrentProgress();
|
|
1134
|
+
this.broadcast({ type: "tool_update", id: turnId, toolCallId, partialResult });
|
|
1135
|
+
},
|
|
1136
|
+
onToolEnd: (toolCallId, isError) => {
|
|
1137
|
+
this.updateCurrentProgress({ currentTool: undefined });
|
|
1138
|
+
this.broadcast({ type: "tool_end", id: turnId, toolCallId, isError });
|
|
1139
|
+
},
|
|
1140
|
+
onTodoUpdate: (items) => {
|
|
1141
|
+
this.updateCurrentProgress({ detail: `Plan: ${items.filter((item) => item.completed).length}/${items.length} done` });
|
|
1142
|
+
this.broadcast({ type: "todo_update", id: turnId, items });
|
|
1143
|
+
},
|
|
679
1144
|
onTurnComplete: () => { },
|
|
680
1145
|
onAgentEnd: () => this.broadcast({ type: "turn_complete", id: turnId, at: new Date().toISOString() }),
|
|
681
1146
|
};
|
|
682
1147
|
try {
|
|
683
1148
|
await session.prompt(envelope.input, callbacks);
|
|
684
1149
|
this.updateSession(session);
|
|
1150
|
+
await this.persistWorkspaceArtifactsForTurn(session.getInfo().workspace, turnId, startedDate);
|
|
685
1151
|
if (this.accumulatedText.trim()) {
|
|
686
1152
|
this.chatStore.append({
|
|
687
1153
|
threadId: info.threadId ?? "pending",
|
|
@@ -701,6 +1167,16 @@ export class RelayRuntime {
|
|
|
701
1167
|
prompt: envelope.description,
|
|
702
1168
|
durationMs: Date.now() - this.currentTurnStartedAt,
|
|
703
1169
|
});
|
|
1170
|
+
this.appendAudit({
|
|
1171
|
+
action: "prompt_completed",
|
|
1172
|
+
status: "ok",
|
|
1173
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
1174
|
+
agentId: info.agentId,
|
|
1175
|
+
threadId: info.threadId,
|
|
1176
|
+
workspace: info.workspace,
|
|
1177
|
+
description: envelope.description,
|
|
1178
|
+
});
|
|
1179
|
+
this.updateCurrentProgress({ status: "completed" });
|
|
704
1180
|
this.broadcast({ type: "turn_complete", id: turnId, at: new Date().toISOString() });
|
|
705
1181
|
this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
|
|
706
1182
|
}
|
|
@@ -724,12 +1200,27 @@ export class RelayRuntime {
|
|
|
724
1200
|
detail: errorText,
|
|
725
1201
|
durationMs: Date.now() - this.currentTurnStartedAt,
|
|
726
1202
|
});
|
|
1203
|
+
this.appendAudit({
|
|
1204
|
+
action: "prompt_failed",
|
|
1205
|
+
status: "failed",
|
|
1206
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
1207
|
+
agentId: info.agentId,
|
|
1208
|
+
threadId: info.threadId,
|
|
1209
|
+
workspace: info.workspace,
|
|
1210
|
+
description: envelope.description,
|
|
1211
|
+
detail: errorText,
|
|
1212
|
+
});
|
|
1213
|
+
this.updateCurrentProgress({ status: "failed", detail: errorText });
|
|
727
1214
|
this.broadcast({ type: "turn_error", id: turnId, error: errorText, at: new Date().toISOString() });
|
|
728
1215
|
this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
|
|
729
1216
|
throw error;
|
|
730
1217
|
}
|
|
731
1218
|
finally {
|
|
732
1219
|
this.currentTurnId = null;
|
|
1220
|
+
if (this.currentProgress) {
|
|
1221
|
+
this.currentProgress.durationMs = Date.now() - this.currentTurnStartedAt;
|
|
1222
|
+
this.currentProgress.updatedAt = new Date().toISOString();
|
|
1223
|
+
}
|
|
733
1224
|
await this.drainQueue();
|
|
734
1225
|
}
|
|
735
1226
|
}
|
|
@@ -741,6 +1232,11 @@ export class RelayRuntime {
|
|
|
741
1232
|
try {
|
|
742
1233
|
const session = await this.getSession(false);
|
|
743
1234
|
while (!session.isProcessing()) {
|
|
1235
|
+
const external = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
|
|
1236
|
+
if (external?.activity.active) {
|
|
1237
|
+
this.broadcastStatus(`Waiting for ${external.agentLabel} CLI task... ${this.queue().length} queued.`, "info");
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
744
1240
|
const next = this.promptStore.dequeue(WEB_CONTEXT_KEY);
|
|
745
1241
|
this.broadcastQueue();
|
|
746
1242
|
if (!next) {
|
|
@@ -762,6 +1258,62 @@ export class RelayRuntime {
|
|
|
762
1258
|
this.broadcast({ type: "activity_update", events: this.activity({ limit: 50 }) });
|
|
763
1259
|
return event;
|
|
764
1260
|
}
|
|
1261
|
+
appendAudit(input) {
|
|
1262
|
+
return this.auditStore.append({ ...input, channelId: "web" });
|
|
1263
|
+
}
|
|
1264
|
+
updateCurrentProgress(patch = {}) {
|
|
1265
|
+
if (!this.currentProgress) {
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
if ("currentTool" in patch) {
|
|
1269
|
+
this.currentProgress.currentTool = patch.currentTool;
|
|
1270
|
+
const { currentTool: _currentTool, ...rest } = patch;
|
|
1271
|
+
Object.assign(this.currentProgress, rest);
|
|
1272
|
+
}
|
|
1273
|
+
else {
|
|
1274
|
+
Object.assign(this.currentProgress, patch);
|
|
1275
|
+
}
|
|
1276
|
+
this.currentProgress.durationMs = Date.now() - this.currentTurnStartedAt;
|
|
1277
|
+
this.currentProgress.updatedAt = new Date().toISOString();
|
|
1278
|
+
}
|
|
1279
|
+
addCurrentTool(toolName) {
|
|
1280
|
+
if (!this.currentProgress) {
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
const existing = this.currentProgress.tools.find((tool) => tool.name === toolName);
|
|
1284
|
+
if (existing) {
|
|
1285
|
+
existing.count += 1;
|
|
1286
|
+
}
|
|
1287
|
+
else {
|
|
1288
|
+
this.currentProgress.tools.push({ name: toolName, count: 1 });
|
|
1289
|
+
}
|
|
1290
|
+
this.updateCurrentProgress({ currentTool: toolName, lastTool: toolName });
|
|
1291
|
+
}
|
|
1292
|
+
externalTask() {
|
|
1293
|
+
if (!this.externalMirror) {
|
|
1294
|
+
return null;
|
|
1295
|
+
}
|
|
1296
|
+
const startedAt = this.externalMirror.startedAt ?? new Date().toISOString();
|
|
1297
|
+
const startedMs = new Date(startedAt).getTime();
|
|
1298
|
+
return {
|
|
1299
|
+
id: this.externalMirror.turnId ?? "cli",
|
|
1300
|
+
source: "cli",
|
|
1301
|
+
status: this.externalMirror.latestStatus?.includes("failed")
|
|
1302
|
+
? "failed"
|
|
1303
|
+
: this.externalMirror.latestStatus?.includes("aborted")
|
|
1304
|
+
? "aborted"
|
|
1305
|
+
: this.externalMirror.latestStatus?.includes("finished") || this.externalMirror.latestStatus?.includes("completed")
|
|
1306
|
+
? "completed"
|
|
1307
|
+
: "running",
|
|
1308
|
+
threadId: this.externalMirror.threadId,
|
|
1309
|
+
startedAt,
|
|
1310
|
+
updatedAt: new Date().toISOString(),
|
|
1311
|
+
durationMs: Number.isFinite(startedMs) ? Math.max(0, Date.now() - startedMs) : 0,
|
|
1312
|
+
outputChars: 0,
|
|
1313
|
+
tools: [],
|
|
1314
|
+
detail: this.externalMirror.latestStatus ?? this.externalMirror.rolloutPath,
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
765
1317
|
broadcastQueue() {
|
|
766
1318
|
this.broadcast({ type: "queue_update", queue: this.queue(), paused: this.queuePaused() });
|
|
767
1319
|
}
|
|
@@ -788,6 +1340,20 @@ export class RelayRuntime {
|
|
|
788
1340
|
capabilities: info.capabilities ?? CODEX_AGENT_CAPABILITIES,
|
|
789
1341
|
};
|
|
790
1342
|
}
|
|
1343
|
+
async persistWorkspaceArtifactsForTurn(workspace, turnId, startedAt) {
|
|
1344
|
+
const report = await collectRecentWorkspaceArtifacts(workspace, {
|
|
1345
|
+
since: startedAt,
|
|
1346
|
+
until: new Date(),
|
|
1347
|
+
maxFileSize: this.config.maxFileSize,
|
|
1348
|
+
limit: 20,
|
|
1349
|
+
ignoreDirs: this.config.artifactIgnoreDirs,
|
|
1350
|
+
ignoreGlobs: this.config.artifactIgnoreGlobs,
|
|
1351
|
+
});
|
|
1352
|
+
if (report.artifacts.length === 0 && report.skippedCount === 0 && !report.omittedCount) {
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
await persistWorkspaceArtifactReport(workspace, turnId, report);
|
|
1356
|
+
}
|
|
791
1357
|
}
|
|
792
1358
|
function queueItemDto(item) {
|
|
793
1359
|
return {
|
|
@@ -820,7 +1386,63 @@ function externalStatusLine(snapshot, queueLength) {
|
|
|
820
1386
|
? formatDuration((Date.now() - snapshot.activity.startedAt.getTime()) / 1000)
|
|
821
1387
|
: "-";
|
|
822
1388
|
const tool = snapshot.latestToolName ?? "-";
|
|
823
|
-
return
|
|
1389
|
+
return `${snapshot.agentLabel} CLI running · ${elapsed} · tool ${tool} · ${queueLength} queued`;
|
|
1390
|
+
}
|
|
1391
|
+
function cliHealthForAgent(agentId, health) {
|
|
1392
|
+
if (agentId === "pi") {
|
|
1393
|
+
return { path: health.piCliPath, label: health.piCli, version: health.piCliVersion };
|
|
1394
|
+
}
|
|
1395
|
+
if (agentId === "hermes") {
|
|
1396
|
+
return { path: health.hermesCliPath, label: health.hermesCli, version: health.hermesCliVersion };
|
|
1397
|
+
}
|
|
1398
|
+
if (agentId === "openclaw") {
|
|
1399
|
+
return { path: health.openClawCliPath, label: health.openClawCli, version: health.openClawCliVersion };
|
|
1400
|
+
}
|
|
1401
|
+
if (agentId === "claude-code") {
|
|
1402
|
+
return { path: health.claudeCodeCliPath, label: health.claudeCodeCli, version: health.claudeCodeCliVersion };
|
|
1403
|
+
}
|
|
1404
|
+
return { path: health.codexCliPath, label: health.codexCli, version: health.codexCliVersion };
|
|
1405
|
+
}
|
|
1406
|
+
function versionCheckForAgent(agentId, versions) {
|
|
1407
|
+
if (agentId === "pi")
|
|
1408
|
+
return versions.pi;
|
|
1409
|
+
if (agentId === "hermes")
|
|
1410
|
+
return versions.hermes;
|
|
1411
|
+
if (agentId === "openclaw")
|
|
1412
|
+
return versions.openclaw;
|
|
1413
|
+
if (agentId === "claude-code")
|
|
1414
|
+
return versions.claudeCode;
|
|
1415
|
+
return versions.codex;
|
|
1416
|
+
}
|
|
1417
|
+
function hostLoginCommand(info, config) {
|
|
1418
|
+
if (info.agentId === "hermes") {
|
|
1419
|
+
return `${config.hermesCliPath ?? "hermes"} login --no-browser`;
|
|
1420
|
+
}
|
|
1421
|
+
if (info.agentId === "claude-code") {
|
|
1422
|
+
return `${config.claudeCodeCliPath ?? "claude"} auth login`;
|
|
1423
|
+
}
|
|
1424
|
+
if (info.agentId === "pi") {
|
|
1425
|
+
return `${config.piCliPath ?? "pi"} auth login`;
|
|
1426
|
+
}
|
|
1427
|
+
if (info.agentId === "openclaw") {
|
|
1428
|
+
return `${config.openClawCliPath ?? "openclaw"} login`;
|
|
1429
|
+
}
|
|
1430
|
+
return "codex login --device-auth";
|
|
1431
|
+
}
|
|
1432
|
+
function hostLogoutCommand(info, config) {
|
|
1433
|
+
if (info.agentId === "hermes") {
|
|
1434
|
+
return `${config.hermesCliPath ?? "hermes"} logout`;
|
|
1435
|
+
}
|
|
1436
|
+
if (info.agentId === "claude-code") {
|
|
1437
|
+
return `${config.claudeCodeCliPath ?? "claude"} auth logout`;
|
|
1438
|
+
}
|
|
1439
|
+
if (info.agentId === "pi") {
|
|
1440
|
+
return `${config.piCliPath ?? "pi"} auth logout`;
|
|
1441
|
+
}
|
|
1442
|
+
if (info.agentId === "openclaw") {
|
|
1443
|
+
return `${config.openClawCliPath ?? "openclaw"} logout`;
|
|
1444
|
+
}
|
|
1445
|
+
return "codex logout";
|
|
824
1446
|
}
|
|
825
1447
|
function durationFromDates(start, end) {
|
|
826
1448
|
if (!start || !end) {
|