@nordbyte/nordrelay 0.4.1 → 0.5.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 +155 -64
- package/README.md +81 -65
- package/dist/access-control.js +126 -115
- package/dist/agent-updates.js +62 -9
- package/dist/bot-rendering.js +838 -0
- package/dist/bot-ui.js +1 -0
- package/dist/bot.js +342 -2498
- package/dist/channel-actions.js +8 -8
- package/dist/channel-runtime.js +89 -0
- package/dist/config-metadata.js +238 -0
- package/dist/config.js +0 -58
- package/dist/index.js +8 -0
- package/dist/operations.js +63 -9
- package/dist/relay-artifact-service.js +126 -0
- package/dist/relay-external-activity-monitor.js +216 -0
- package/dist/relay-queue-service.js +66 -0
- package/dist/relay-runtime-types.js +1 -0
- package/dist/relay-runtime.js +96 -354
- package/dist/settings-service.js +2 -117
- package/dist/support-bundle.js +205 -0
- package/dist/telegram-access-commands.js +123 -0
- package/dist/telegram-access-middleware.js +129 -0
- package/dist/telegram-agent-commands.js +212 -0
- package/dist/telegram-artifact-commands.js +139 -0
- package/dist/telegram-channel-runtime.js +132 -0
- package/dist/telegram-command-menu.js +55 -0
- package/dist/telegram-command-types.js +1 -0
- package/dist/telegram-diagnostics-command.js +102 -0
- package/dist/telegram-general-commands.js +52 -0
- package/dist/telegram-operational-commands.js +153 -0
- package/dist/telegram-output.js +216 -0
- package/dist/telegram-preference-commands.js +198 -0
- package/dist/telegram-queue-commands.js +278 -0
- package/dist/telegram-support-command.js +53 -0
- package/dist/telegram-update-commands.js +93 -0
- package/dist/user-management.js +708 -0
- package/dist/web-api-contract.js +104 -0
- package/dist/web-api-types.js +1 -0
- package/dist/web-dashboard-access-routes.js +163 -0
- package/dist/web-dashboard-artifact-routes.js +65 -0
- package/dist/web-dashboard-assets.js +35 -2
- package/dist/web-dashboard-http.js +143 -0
- package/dist/web-dashboard-pages.js +257 -0
- package/dist/web-dashboard-runtime-routes.js +92 -0
- package/dist/web-dashboard-session-routes.js +209 -0
- package/dist/web-dashboard-ui.js +14 -14
- package/dist/web-dashboard.js +330 -707
- package/dist/webui-assets/dashboard.css +989 -0
- package/dist/webui-assets/dashboard.js +1750 -0
- package/dist/zip-writer.js +83 -0
- package/package.json +13 -4
- package/plugins/nordrelay/.codex-plugin/plugin.json +1 -1
- package/plugins/nordrelay/commands/remote.md +1 -1
- package/plugins/nordrelay/scripts/nordrelay.mjs +227 -78
- package/plugins/nordrelay/skills/telegram-remote/SKILL.md +1 -1
- package/dist/web-dashboard-client.js +0 -275
- package/dist/web-dashboard-style.js +0 -9
package/dist/relay-runtime.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import {
|
|
3
|
+
import { ensureOutDir } from "./artifacts.js";
|
|
5
4
|
import { buildFileInstructions, outboxPath, stageFile, } from "./attachments.js";
|
|
6
5
|
import { CODEX_AGENT_CAPABILITIES, agentLabel, agentReasoningLabel, agentReasoningOptions, } from "./agent.js";
|
|
7
6
|
import { getAgentDiagnostics, getExternalSnapshotForSession, } from "./agent-activity.js";
|
|
@@ -17,16 +16,19 @@ import { checkOpenClawAuthStatus } from "./openclaw-auth.js";
|
|
|
17
16
|
import { clearLogFile, getAgentUpdateLogPath, getConnectorHealth, getConnectorLogPath, getPackageVersion, getUpdateLogPath, getVersionChecks, readConnectorState, readFormattedLogTail, spawnConnectorRestart, spawnSelfUpdate } from "./operations.js";
|
|
18
17
|
import { checkPiAuthStatus } from "./pi-auth.js";
|
|
19
18
|
import { PromptStore, toPromptEnvelope } from "./prompt-store.js";
|
|
19
|
+
import { RelayArtifactService } from "./relay-artifact-service.js";
|
|
20
|
+
import { RelayExternalActivityMonitor } from "./relay-external-activity-monitor.js";
|
|
21
|
+
import { RelayQueueService } from "./relay-queue-service.js";
|
|
20
22
|
import { renderSessionInfoPlain, renderSessionUsageRows } from "./session-format.js";
|
|
21
23
|
import { SessionLockStore } from "./session-locks.js";
|
|
22
24
|
import { SessionRegistry } from "./session-registry.js";
|
|
25
|
+
import { createSupportBundle } from "./support-bundle.js";
|
|
23
26
|
import { transcribeAudio } from "./voice.js";
|
|
24
27
|
import { WebActivityStore, WebChatStore, } from "./web-state.js";
|
|
25
28
|
import { evaluateWorkspacePolicy, filterAllowedWorkspaces } from "./workspace-policy.js";
|
|
26
29
|
const WEB_CONTEXT_KEY = "web:dashboard";
|
|
27
30
|
const MAX_WEB_SESSION_PAGE_SIZE = 50;
|
|
28
31
|
const MAX_CHAT_HISTORY = 250;
|
|
29
|
-
const MAX_TEXT_PREVIEW_BYTES = 256 * 1024;
|
|
30
32
|
export class RelayRuntime {
|
|
31
33
|
config;
|
|
32
34
|
registry;
|
|
@@ -36,6 +38,9 @@ export class RelayRuntime {
|
|
|
36
38
|
auditStore;
|
|
37
39
|
lockStore;
|
|
38
40
|
agentUpdates;
|
|
41
|
+
queueService;
|
|
42
|
+
artifactService;
|
|
43
|
+
externalActivityMonitor;
|
|
39
44
|
subscribers = new Set();
|
|
40
45
|
externalMonitor;
|
|
41
46
|
draining = false;
|
|
@@ -43,7 +48,6 @@ export class RelayRuntime {
|
|
|
43
48
|
accumulatedText = "";
|
|
44
49
|
currentTurnStartedAt = 0;
|
|
45
50
|
currentProgress = null;
|
|
46
|
-
externalMirror = null;
|
|
47
51
|
constructor(config) {
|
|
48
52
|
this.config = config;
|
|
49
53
|
this.registry = new SessionRegistry(config, {
|
|
@@ -55,12 +59,27 @@ export class RelayRuntime {
|
|
|
55
59
|
this.activityStore = new WebActivityStore(config.workspace, config.stateBackend, config.auditMaxEvents);
|
|
56
60
|
this.auditStore = new AuditLogStore(config.workspace, config.stateBackend, config.auditMaxEvents);
|
|
57
61
|
this.lockStore = new SessionLockStore(config.workspace, config.stateBackend);
|
|
62
|
+
this.queueService = new RelayQueueService(this.promptStore, WEB_CONTEXT_KEY);
|
|
63
|
+
this.artifactService = new RelayArtifactService(config);
|
|
58
64
|
this.agentUpdates = new AgentUpdateManager({
|
|
59
65
|
onUpdate: (job) => this.broadcast({ type: "agent_update", job }),
|
|
60
66
|
});
|
|
67
|
+
this.externalActivityMonitor = new RelayExternalActivityMonitor({
|
|
68
|
+
config,
|
|
69
|
+
getSession: () => this.getSession(true),
|
|
70
|
+
publicInfo: (session) => this.publicInfo(session),
|
|
71
|
+
queueLength: () => this.queueService.length(),
|
|
72
|
+
chatStore: this.chatStore,
|
|
73
|
+
chatHistory: () => this.chatHistory(),
|
|
74
|
+
persistWorkspaceArtifactsForTurn: (workspace, turnId, startedAt) => this.artifactService.persistWorkspaceArtifactsForTurn(workspace, turnId, startedAt),
|
|
75
|
+
drainQueue: () => this.drainQueue(),
|
|
76
|
+
appendActivity: (input) => this.appendActivity(input),
|
|
77
|
+
broadcast: (event) => this.broadcast(event),
|
|
78
|
+
broadcastStatus: (message, level) => this.broadcastStatus(message, level),
|
|
79
|
+
});
|
|
61
80
|
if (config.codexExternalBusyCheckMs > 0) {
|
|
62
81
|
this.externalMonitor = setInterval(() => {
|
|
63
|
-
void this.
|
|
82
|
+
void this.externalActivityMonitor.monitorSafe();
|
|
64
83
|
}, config.codexExternalBusyCheckMs);
|
|
65
84
|
this.externalMonitor.unref?.();
|
|
66
85
|
}
|
|
@@ -131,18 +150,18 @@ export class RelayRuntime {
|
|
|
131
150
|
agentUpdateJobs() {
|
|
132
151
|
return this.agentUpdates.list();
|
|
133
152
|
}
|
|
134
|
-
startAgentUpdate(agentId) {
|
|
153
|
+
startAgentUpdate(agentId, operation = "update") {
|
|
135
154
|
const job = this.agentUpdates.start(agentId, {
|
|
136
155
|
piCliPath: this.config.piCliPath,
|
|
137
156
|
hermesCliPath: this.config.hermesCliPath,
|
|
138
157
|
openClawCliPath: this.config.openClawCliPath,
|
|
139
158
|
claudeCodeCliPath: this.config.claudeCodeCliPath,
|
|
140
|
-
});
|
|
141
|
-
this.broadcastStatus(`${job.agentLabel}
|
|
159
|
+
}, operation);
|
|
160
|
+
this.broadcastStatus(`${job.agentLabel} ${operation} started. Log: ${job.logPath}`, "warn");
|
|
142
161
|
this.appendActivity({
|
|
143
162
|
source: "web",
|
|
144
163
|
status: "info",
|
|
145
|
-
type: "agent_update_started",
|
|
164
|
+
type: operation === "install" ? "agent_install_started" : "agent_update_started",
|
|
146
165
|
agentId,
|
|
147
166
|
threadId: null,
|
|
148
167
|
workspace: this.config.workspace,
|
|
@@ -153,7 +172,7 @@ export class RelayRuntime {
|
|
|
153
172
|
status: "ok",
|
|
154
173
|
contextKey: WEB_CONTEXT_KEY,
|
|
155
174
|
agentId,
|
|
156
|
-
description:
|
|
175
|
+
description: `${operation} ${agentId}`,
|
|
157
176
|
detail: job.summary,
|
|
158
177
|
});
|
|
159
178
|
return job;
|
|
@@ -161,6 +180,18 @@ export class RelayRuntime {
|
|
|
161
180
|
agentUpdateLog(id) {
|
|
162
181
|
return this.agentUpdates.readLog(id);
|
|
163
182
|
}
|
|
183
|
+
deleteAgentUpdateLog(id) {
|
|
184
|
+
const job = this.agentUpdates.deleteLog(id);
|
|
185
|
+
this.appendAudit({
|
|
186
|
+
action: "command",
|
|
187
|
+
status: "ok",
|
|
188
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
189
|
+
agentId: job.agentId,
|
|
190
|
+
description: `delete update log ${id}`,
|
|
191
|
+
detail: job.logPath,
|
|
192
|
+
});
|
|
193
|
+
return job;
|
|
194
|
+
}
|
|
164
195
|
sendAgentUpdateInput(id, input) {
|
|
165
196
|
return this.agentUpdates.sendInput(id, input);
|
|
166
197
|
}
|
|
@@ -175,8 +206,8 @@ export class RelayRuntime {
|
|
|
175
206
|
runtime: {
|
|
176
207
|
stateBackend: this.config.stateBackend,
|
|
177
208
|
sourceWorkspace: this.config.workspace,
|
|
178
|
-
queuePaused: this.
|
|
179
|
-
externalMirror: this.
|
|
209
|
+
queuePaused: this.queueService.isPaused(),
|
|
210
|
+
externalMirror: this.externalActivityMonitor.snapshot(),
|
|
180
211
|
agentDiagnostics: getAgentDiagnostics(await this.getSession(true), this.config),
|
|
181
212
|
},
|
|
182
213
|
};
|
|
@@ -224,18 +255,14 @@ export class RelayRuntime {
|
|
|
224
255
|
}
|
|
225
256
|
permissions() {
|
|
226
257
|
return {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
telegramAllowedUserIds: this.config.telegramAllowedUserIds,
|
|
230
|
-
telegramReadOnlyUserIds: this.config.telegramReadOnlyUserIds,
|
|
231
|
-
telegramAllowedChatIds: this.config.telegramAllowedChatIds,
|
|
232
|
-
telegramRolePolicies: this.config.telegramRolePolicies,
|
|
258
|
+
mode: "users",
|
|
259
|
+
message: "Access is managed by NordRelay users, groups, Telegram identities, and Telegram chat access records.",
|
|
233
260
|
};
|
|
234
261
|
}
|
|
235
262
|
tasks() {
|
|
236
263
|
return {
|
|
237
264
|
current: this.currentProgress ? { ...this.currentProgress, tools: [...this.currentProgress.tools] } : null,
|
|
238
|
-
external: this.
|
|
265
|
+
external: this.externalActivityMonitor.task(),
|
|
239
266
|
queue: this.queue(),
|
|
240
267
|
queuePaused: this.queuePaused(),
|
|
241
268
|
recent: this.activity({ limit: 20 }),
|
|
@@ -244,6 +271,32 @@ export class RelayRuntime {
|
|
|
244
271
|
audit(limit = 50) {
|
|
245
272
|
return this.auditStore.list(limit);
|
|
246
273
|
}
|
|
274
|
+
async supportBundle() {
|
|
275
|
+
const bundle = await createSupportBundle({
|
|
276
|
+
config: this.config,
|
|
277
|
+
diagnostics: await this.diagnostics(),
|
|
278
|
+
adapterHealth: await this.adapterHealth(),
|
|
279
|
+
auditEvents: this.auditStore.list(100),
|
|
280
|
+
agentUpdateJobs: this.agentUpdates.list(),
|
|
281
|
+
source: "web",
|
|
282
|
+
});
|
|
283
|
+
this.appendActivity({
|
|
284
|
+
source: "web",
|
|
285
|
+
status: "info",
|
|
286
|
+
type: "diagnostics_bundle_exported",
|
|
287
|
+
threadId: null,
|
|
288
|
+
workspace: this.config.workspace,
|
|
289
|
+
detail: bundle.path,
|
|
290
|
+
});
|
|
291
|
+
this.appendAudit({
|
|
292
|
+
action: "command",
|
|
293
|
+
status: "ok",
|
|
294
|
+
contextKey: WEB_CONTEXT_KEY,
|
|
295
|
+
description: "export diagnostics bundle",
|
|
296
|
+
detail: bundle.path,
|
|
297
|
+
});
|
|
298
|
+
return bundle;
|
|
299
|
+
}
|
|
247
300
|
locks() {
|
|
248
301
|
return this.lockStore.list();
|
|
249
302
|
}
|
|
@@ -450,10 +503,11 @@ export class RelayRuntime {
|
|
|
450
503
|
return { removed, messages };
|
|
451
504
|
}
|
|
452
505
|
activity(options = {}) {
|
|
453
|
-
|
|
506
|
+
const currentInfo = this.registry.get(WEB_CONTEXT_KEY)?.getInfo();
|
|
507
|
+
return this.activityStore.list(options).map((event) => this.enrichActivityEvent(event, currentInfo));
|
|
454
508
|
}
|
|
455
509
|
async retry() {
|
|
456
|
-
const cached = this.
|
|
510
|
+
const cached = this.queueService.getLastPrompt();
|
|
457
511
|
if (!cached) {
|
|
458
512
|
throw new Error("Nothing to retry. Send a message first.");
|
|
459
513
|
}
|
|
@@ -748,7 +802,7 @@ export class RelayRuntime {
|
|
|
748
802
|
const session = await this.getSession(false);
|
|
749
803
|
const external = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
|
|
750
804
|
if (session.isProcessing() || external?.activity.active) {
|
|
751
|
-
const queued = this.
|
|
805
|
+
const queued = this.queueService.enqueue(envelope);
|
|
752
806
|
const info = this.publicInfo(session);
|
|
753
807
|
this.appendActivity({
|
|
754
808
|
source: "web",
|
|
@@ -760,7 +814,7 @@ export class RelayRuntime {
|
|
|
760
814
|
prompt: envelope.description,
|
|
761
815
|
detail: external?.activity.active
|
|
762
816
|
? `Queued because ${external.agentLabel} CLI is still processing another task.`
|
|
763
|
-
: `Queued at position ${this.
|
|
817
|
+
: `Queued at position ${this.queueService.length()}.`,
|
|
764
818
|
});
|
|
765
819
|
this.appendAudit({
|
|
766
820
|
action: "prompt_queued",
|
|
@@ -773,7 +827,7 @@ export class RelayRuntime {
|
|
|
773
827
|
description: envelope.description,
|
|
774
828
|
});
|
|
775
829
|
if (external?.activity.active) {
|
|
776
|
-
this.broadcastStatus(`Waiting for ${external.agentLabel} CLI task... ${this.
|
|
830
|
+
this.broadcastStatus(`Waiting for ${external.agentLabel} CLI task... ${this.queueService.length()} queued.`, "info");
|
|
777
831
|
}
|
|
778
832
|
this.broadcastQueue();
|
|
779
833
|
return { queued: true, queueId: queued.id };
|
|
@@ -784,30 +838,14 @@ export class RelayRuntime {
|
|
|
784
838
|
return { queued: false };
|
|
785
839
|
}
|
|
786
840
|
queue() {
|
|
787
|
-
return this.
|
|
841
|
+
return this.queueService.list();
|
|
788
842
|
}
|
|
789
843
|
queuePaused() {
|
|
790
|
-
return this.
|
|
844
|
+
return this.queueService.isPaused();
|
|
791
845
|
}
|
|
792
846
|
queueAction(action, id) {
|
|
793
|
-
|
|
794
|
-
this.promptStore.pause(WEB_CONTEXT_KEY);
|
|
795
|
-
if (action === "resume")
|
|
796
|
-
this.promptStore.resume(WEB_CONTEXT_KEY);
|
|
797
|
-
if (action === "clear")
|
|
798
|
-
this.promptStore.clear(WEB_CONTEXT_KEY);
|
|
799
|
-
if (id && action === "cancel")
|
|
800
|
-
this.promptStore.remove(WEB_CONTEXT_KEY, id);
|
|
801
|
-
if (id && action === "top")
|
|
802
|
-
this.promptStore.moveToTop(WEB_CONTEXT_KEY, id);
|
|
803
|
-
if (id && action === "up")
|
|
804
|
-
this.promptStore.moveUp(WEB_CONTEXT_KEY, id);
|
|
805
|
-
if (id && action === "down")
|
|
806
|
-
this.promptStore.moveDown(WEB_CONTEXT_KEY, id);
|
|
847
|
+
this.queueService.apply(action, id);
|
|
807
848
|
if (id && action === "run") {
|
|
808
|
-
const item = this.promptStore.remove(WEB_CONTEXT_KEY, id);
|
|
809
|
-
if (item)
|
|
810
|
-
this.promptStore.enqueueFront(WEB_CONTEXT_KEY, item);
|
|
811
849
|
void this.drainQueue().catch((error) => this.broadcastStatus(friendlyErrorText(error), "error"));
|
|
812
850
|
}
|
|
813
851
|
this.appendActivity({
|
|
@@ -829,58 +867,23 @@ export class RelayRuntime {
|
|
|
829
867
|
}
|
|
830
868
|
async artifacts() {
|
|
831
869
|
const session = await this.getSession(true);
|
|
832
|
-
return (
|
|
870
|
+
return this.artifactService.list(session.getInfo().workspace, 20);
|
|
833
871
|
}
|
|
834
872
|
async artifact(turnId) {
|
|
835
873
|
const session = await this.getSession(true);
|
|
836
|
-
return
|
|
874
|
+
return this.artifactService.get(session.getInfo().workspace, turnId);
|
|
837
875
|
}
|
|
838
876
|
async deleteArtifact(turnId) {
|
|
839
877
|
const session = await this.getSession(true);
|
|
840
|
-
return
|
|
878
|
+
return this.artifactService.delete(session.getInfo().workspace, turnId);
|
|
841
879
|
}
|
|
842
880
|
async createArtifactZip(turnId) {
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
return null;
|
|
846
|
-
}
|
|
847
|
-
const bundle = await createArtifactZipBundle(report.artifacts, report.outDir, {
|
|
848
|
-
maxFileSize: this.config.maxFileSize,
|
|
849
|
-
bundleName: `nordrelay-artifacts-${turnId}.zip`,
|
|
850
|
-
});
|
|
851
|
-
return bundle ? { path: bundle.localPath, name: bundle.name } : null;
|
|
881
|
+
const session = await this.getSession(true);
|
|
882
|
+
return this.artifactService.createZip(session.getInfo().workspace, turnId);
|
|
852
883
|
}
|
|
853
884
|
async artifactPreview(turnId, relativePath) {
|
|
854
|
-
const
|
|
855
|
-
|
|
856
|
-
if (!artifact) {
|
|
857
|
-
return null;
|
|
858
|
-
}
|
|
859
|
-
const extension = path.extname(artifact.name).toLowerCase();
|
|
860
|
-
if ([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"].includes(extension)) {
|
|
861
|
-
return {
|
|
862
|
-
kind: "image",
|
|
863
|
-
name: artifact.name,
|
|
864
|
-
sizeBytes: artifact.sizeBytes,
|
|
865
|
-
};
|
|
866
|
-
}
|
|
867
|
-
if (!isPreviewableTextFile(extension, artifact.sizeBytes)) {
|
|
868
|
-
return {
|
|
869
|
-
kind: "unsupported",
|
|
870
|
-
name: artifact.name,
|
|
871
|
-
sizeBytes: artifact.sizeBytes,
|
|
872
|
-
detail: artifact.sizeBytes > MAX_TEXT_PREVIEW_BYTES ? "File is too large for inline preview." : "File type is not previewable.",
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
const buffer = await readFile(artifact.localPath);
|
|
876
|
-
const truncated = buffer.byteLength > MAX_TEXT_PREVIEW_BYTES;
|
|
877
|
-
return {
|
|
878
|
-
kind: "text",
|
|
879
|
-
name: artifact.name,
|
|
880
|
-
sizeBytes: artifact.sizeBytes,
|
|
881
|
-
truncated,
|
|
882
|
-
text: buffer.subarray(0, MAX_TEXT_PREVIEW_BYTES).toString("utf8"),
|
|
883
|
-
};
|
|
885
|
+
const session = await this.getSession(true);
|
|
886
|
+
return this.artifactService.preview(session.getInfo().workspace, turnId, relativePath);
|
|
884
887
|
}
|
|
885
888
|
async logs(target = "connector", lines = 100) {
|
|
886
889
|
if (target === "update") {
|
|
@@ -924,143 +927,6 @@ export class RelayRuntime {
|
|
|
924
927
|
this.registry.disposeAll();
|
|
925
928
|
this.subscribers.clear();
|
|
926
929
|
}
|
|
927
|
-
async monitorExternalActivity() {
|
|
928
|
-
const session = await this.getSession(true);
|
|
929
|
-
const info = this.publicInfo(session);
|
|
930
|
-
if (!info.capabilities.externalActivity || !info.threadId || session.isProcessing()) {
|
|
931
|
-
return;
|
|
932
|
-
}
|
|
933
|
-
const snapshot = getExternalSnapshotForSession(session, this.config, {
|
|
934
|
-
afterLine: this.externalMirror?.threadId === info.threadId ? this.externalMirror.lastLine : Number.MAX_SAFE_INTEGER,
|
|
935
|
-
}) ?? getExternalSnapshotForSession(session, this.config, {
|
|
936
|
-
maxEvents: 0,
|
|
937
|
-
});
|
|
938
|
-
if (!snapshot) {
|
|
939
|
-
return;
|
|
940
|
-
}
|
|
941
|
-
if (!this.externalMirror || this.externalMirror.threadId !== snapshot.threadId || this.externalMirror.rolloutPath !== snapshot.sourcePath) {
|
|
942
|
-
this.externalMirror = {
|
|
943
|
-
threadId: snapshot.threadId,
|
|
944
|
-
rolloutPath: snapshot.sourcePath,
|
|
945
|
-
lastLine: snapshot.lineCount,
|
|
946
|
-
turnId: snapshot.activity.turnId,
|
|
947
|
-
startedAt: snapshot.activity.startedAt?.toISOString() ?? null,
|
|
948
|
-
};
|
|
949
|
-
if (snapshot.activity.active) {
|
|
950
|
-
this.startExternalTurn(snapshot);
|
|
951
|
-
}
|
|
952
|
-
return;
|
|
953
|
-
}
|
|
954
|
-
const mirror = this.externalMirror;
|
|
955
|
-
if (snapshot.activity.active) {
|
|
956
|
-
if (mirror.turnId !== snapshot.activity.turnId) {
|
|
957
|
-
mirror.turnId = snapshot.activity.turnId;
|
|
958
|
-
mirror.startedAt = snapshot.activity.startedAt?.toISOString() ?? null;
|
|
959
|
-
mirror.latestAgentLine = undefined;
|
|
960
|
-
this.startExternalTurn(snapshot);
|
|
961
|
-
}
|
|
962
|
-
this.broadcastExternalEvents(snapshot, snapshot.events.filter((event) => event.lineNumber > mirror.lastLine));
|
|
963
|
-
mirror.lastLine = Math.max(mirror.lastLine, snapshot.lineCount);
|
|
964
|
-
mirror.latestStatus = externalStatusLine(snapshot, this.queue().length);
|
|
965
|
-
this.broadcastStatus(mirror.latestStatus, "info");
|
|
966
|
-
return;
|
|
967
|
-
}
|
|
968
|
-
const terminalEvent = [...snapshot.events].reverse().find((event) => event.kind === "task" && event.status && event.status !== "started");
|
|
969
|
-
if (terminalEvent && terminalEvent.lineNumber > mirror.lastLine) {
|
|
970
|
-
const finalAgent = snapshot.events.filter((event) => event.kind === "agent" && event.text).at(-1);
|
|
971
|
-
const finalText = finalAgent?.text ?? snapshot.latestAgentMessage;
|
|
972
|
-
const finalLine = finalAgent?.lineNumber ?? snapshot.lineCount;
|
|
973
|
-
if (finalText && finalLine !== mirror.latestAgentLine) {
|
|
974
|
-
this.chatStore.append({
|
|
975
|
-
threadId: snapshot.threadId,
|
|
976
|
-
role: "agent",
|
|
977
|
-
text: finalText,
|
|
978
|
-
source: "cli",
|
|
979
|
-
turnId: terminalEvent.turnId ?? undefined,
|
|
980
|
-
});
|
|
981
|
-
this.broadcast({ type: "text_delta", id: terminalEvent.turnId ?? "cli", delta: finalText });
|
|
982
|
-
mirror.latestAgentLine = finalLine;
|
|
983
|
-
}
|
|
984
|
-
const externalStartedAt = mirror.startedAt ? new Date(mirror.startedAt) : snapshot.activity.startedAt;
|
|
985
|
-
this.broadcast({
|
|
986
|
-
type: "turn_complete",
|
|
987
|
-
id: terminalEvent.turnId ?? "cli",
|
|
988
|
-
at: terminalEvent.timestamp?.toISOString() ?? new Date().toISOString(),
|
|
989
|
-
});
|
|
990
|
-
this.appendActivity({
|
|
991
|
-
source: "cli",
|
|
992
|
-
status: terminalEvent.status === "aborted" ? "aborted" : terminalEvent.status === "failed" ? "failed" : "completed",
|
|
993
|
-
type: "cli_turn_finished",
|
|
994
|
-
threadId: snapshot.threadId,
|
|
995
|
-
workspace: info.workspace,
|
|
996
|
-
agentId: info.agentId,
|
|
997
|
-
prompt: snapshot.latestUserMessage ?? undefined,
|
|
998
|
-
detail: `${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`,
|
|
999
|
-
durationMs: durationFromDates(externalStartedAt, terminalEvent.timestamp),
|
|
1000
|
-
});
|
|
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");
|
|
1006
|
-
this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
|
|
1007
|
-
await this.drainQueue();
|
|
1008
|
-
}
|
|
1009
|
-
mirror.lastLine = Math.max(mirror.lastLine, snapshot.lineCount);
|
|
1010
|
-
}
|
|
1011
|
-
startExternalTurn(snapshot) {
|
|
1012
|
-
const prompt = snapshot.latestUserMessage ?? `${snapshot.agentLabel} CLI task`;
|
|
1013
|
-
this.chatStore.append({
|
|
1014
|
-
threadId: snapshot.threadId,
|
|
1015
|
-
role: "user",
|
|
1016
|
-
text: prompt,
|
|
1017
|
-
source: "cli",
|
|
1018
|
-
turnId: snapshot.activity.turnId ?? undefined,
|
|
1019
|
-
timestamp: snapshot.activity.startedAt?.toISOString(),
|
|
1020
|
-
});
|
|
1021
|
-
this.broadcast({
|
|
1022
|
-
type: "turn_start",
|
|
1023
|
-
id: snapshot.activity.turnId ?? "cli",
|
|
1024
|
-
prompt,
|
|
1025
|
-
at: snapshot.activity.startedAt?.toISOString() ?? new Date().toISOString(),
|
|
1026
|
-
source: "cli",
|
|
1027
|
-
});
|
|
1028
|
-
this.appendActivity({
|
|
1029
|
-
source: "cli",
|
|
1030
|
-
status: "running",
|
|
1031
|
-
type: "cli_turn_started",
|
|
1032
|
-
threadId: snapshot.threadId,
|
|
1033
|
-
prompt,
|
|
1034
|
-
detail: `${snapshot.sourceLabel}: ${snapshot.sourcePath}`,
|
|
1035
|
-
});
|
|
1036
|
-
}
|
|
1037
|
-
broadcastExternalEvents(snapshot, events) {
|
|
1038
|
-
for (const event of events) {
|
|
1039
|
-
if (event.kind === "tool" && event.status === "started") {
|
|
1040
|
-
this.broadcast({
|
|
1041
|
-
type: "tool_start",
|
|
1042
|
-
id: snapshot.activity.turnId ?? "cli",
|
|
1043
|
-
toolCallId: `cli-${event.lineNumber}`,
|
|
1044
|
-
toolName: event.toolName ?? "tool",
|
|
1045
|
-
});
|
|
1046
|
-
this.appendActivity({
|
|
1047
|
-
source: "cli",
|
|
1048
|
-
status: "running",
|
|
1049
|
-
type: "cli_tool_started",
|
|
1050
|
-
threadId: snapshot.threadId,
|
|
1051
|
-
detail: event.toolName ?? "tool",
|
|
1052
|
-
});
|
|
1053
|
-
}
|
|
1054
|
-
if (event.kind === "tool" && event.status === "finished") {
|
|
1055
|
-
this.broadcast({
|
|
1056
|
-
type: "tool_end",
|
|
1057
|
-
id: snapshot.activity.turnId ?? "cli",
|
|
1058
|
-
toolCallId: `cli-${event.lineNumber}`,
|
|
1059
|
-
isError: false,
|
|
1060
|
-
});
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
930
|
async getSession(deferThreadStart) {
|
|
1065
931
|
return this.registry.getOrCreate(WEB_CONTEXT_KEY, { deferThreadStart });
|
|
1066
932
|
}
|
|
@@ -1174,7 +1040,7 @@ export class RelayRuntime {
|
|
|
1174
1040
|
outputChars: 0,
|
|
1175
1041
|
tools: [],
|
|
1176
1042
|
};
|
|
1177
|
-
this.
|
|
1043
|
+
this.queueService.setLastPrompt(envelope);
|
|
1178
1044
|
const startedDate = new Date();
|
|
1179
1045
|
const startedAt = startedDate.toISOString();
|
|
1180
1046
|
this.chatStore.append({
|
|
@@ -1232,7 +1098,7 @@ export class RelayRuntime {
|
|
|
1232
1098
|
try {
|
|
1233
1099
|
await session.prompt(envelope.input, callbacks);
|
|
1234
1100
|
this.updateSession(session);
|
|
1235
|
-
await this.persistWorkspaceArtifactsForTurn(session.getInfo().workspace, turnId, startedDate);
|
|
1101
|
+
await this.artifactService.persistWorkspaceArtifactsForTurn(session.getInfo().workspace, turnId, startedDate);
|
|
1236
1102
|
if (this.accumulatedText.trim()) {
|
|
1237
1103
|
this.chatStore.append({
|
|
1238
1104
|
threadId: info.threadId ?? "pending",
|
|
@@ -1310,7 +1176,7 @@ export class RelayRuntime {
|
|
|
1310
1176
|
}
|
|
1311
1177
|
}
|
|
1312
1178
|
async drainQueue() {
|
|
1313
|
-
if (this.draining || this.
|
|
1179
|
+
if (this.draining || this.queueService.isPaused()) {
|
|
1314
1180
|
return;
|
|
1315
1181
|
}
|
|
1316
1182
|
this.draining = true;
|
|
@@ -1319,10 +1185,10 @@ export class RelayRuntime {
|
|
|
1319
1185
|
while (!session.isProcessing()) {
|
|
1320
1186
|
const external = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
|
|
1321
1187
|
if (external?.activity.active) {
|
|
1322
|
-
this.broadcastStatus(`Waiting for ${external.agentLabel} CLI task... ${this.
|
|
1188
|
+
this.broadcastStatus(`Waiting for ${external.agentLabel} CLI task... ${this.queueService.length()} queued.`, "info");
|
|
1323
1189
|
return;
|
|
1324
1190
|
}
|
|
1325
|
-
const next = this.
|
|
1191
|
+
const next = this.queueService.dequeue();
|
|
1326
1192
|
this.broadcastQueue();
|
|
1327
1193
|
if (!next) {
|
|
1328
1194
|
return;
|
|
@@ -1346,11 +1212,10 @@ export class RelayRuntime {
|
|
|
1346
1212
|
enrichActivityInput(input) {
|
|
1347
1213
|
return this.enrichActivityFields(input);
|
|
1348
1214
|
}
|
|
1349
|
-
enrichActivityEvent(event) {
|
|
1350
|
-
return this.enrichActivityFields(event);
|
|
1215
|
+
enrichActivityEvent(event, info) {
|
|
1216
|
+
return this.enrichActivityFields(event, info);
|
|
1351
1217
|
}
|
|
1352
|
-
enrichActivityFields(event) {
|
|
1353
|
-
const info = this.registry.get(WEB_CONTEXT_KEY)?.getInfo();
|
|
1218
|
+
enrichActivityFields(event, info) {
|
|
1354
1219
|
if (!info) {
|
|
1355
1220
|
return !event.threadId && !event.workspace ? { ...event, workspace: this.config.workspace } : event;
|
|
1356
1221
|
}
|
|
@@ -1393,31 +1258,6 @@ export class RelayRuntime {
|
|
|
1393
1258
|
}
|
|
1394
1259
|
this.updateCurrentProgress({ currentTool: toolName, lastTool: toolName });
|
|
1395
1260
|
}
|
|
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
|
-
}
|
|
1421
1261
|
broadcastQueue() {
|
|
1422
1262
|
this.broadcast({ type: "queue_update", queue: this.queue(), paused: this.queuePaused() });
|
|
1423
1263
|
}
|
|
@@ -1444,53 +1284,6 @@ export class RelayRuntime {
|
|
|
1444
1284
|
capabilities: info.capabilities ?? CODEX_AGENT_CAPABILITIES,
|
|
1445
1285
|
};
|
|
1446
1286
|
}
|
|
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
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
function queueItemDto(item) {
|
|
1463
|
-
return {
|
|
1464
|
-
id: item.id,
|
|
1465
|
-
description: item.description,
|
|
1466
|
-
createdAt: new Date(item.createdAt).toISOString(),
|
|
1467
|
-
attempts: item.attempts ?? 0,
|
|
1468
|
-
notBefore: item.notBefore ? new Date(item.notBefore).toISOString() : undefined,
|
|
1469
|
-
lastError: item.lastError,
|
|
1470
|
-
};
|
|
1471
|
-
}
|
|
1472
|
-
function artifactDto(report) {
|
|
1473
|
-
return {
|
|
1474
|
-
turnId: report.turnId,
|
|
1475
|
-
updatedAt: report.updatedAt.toISOString(),
|
|
1476
|
-
source: report.source,
|
|
1477
|
-
fileCount: report.artifacts.length,
|
|
1478
|
-
totalSizeBytes: totalArtifactSize(report.artifacts),
|
|
1479
|
-
skippedCount: report.skippedCount,
|
|
1480
|
-
omittedCount: report.omittedCount,
|
|
1481
|
-
artifacts: report.artifacts.map((artifact) => ({
|
|
1482
|
-
name: artifact.name,
|
|
1483
|
-
relativePath: artifact.relativePath.split(path.sep).join("/"),
|
|
1484
|
-
sizeBytes: artifact.sizeBytes,
|
|
1485
|
-
})),
|
|
1486
|
-
};
|
|
1487
|
-
}
|
|
1488
|
-
function externalStatusLine(snapshot, queueLength) {
|
|
1489
|
-
const elapsed = snapshot.activity.startedAt
|
|
1490
|
-
? formatDuration((Date.now() - snapshot.activity.startedAt.getTime()) / 1000)
|
|
1491
|
-
: "-";
|
|
1492
|
-
const tool = snapshot.latestToolName ?? "-";
|
|
1493
|
-
return `${snapshot.agentLabel} CLI running · ${elapsed} · tool ${tool} · ${queueLength} queued`;
|
|
1494
1287
|
}
|
|
1495
1288
|
function cliHealthForAgent(agentId, health) {
|
|
1496
1289
|
if (agentId === "pi") {
|
|
@@ -1548,23 +1341,6 @@ function hostLogoutCommand(info, config) {
|
|
|
1548
1341
|
}
|
|
1549
1342
|
return "codex logout";
|
|
1550
1343
|
}
|
|
1551
|
-
function durationFromDates(start, end) {
|
|
1552
|
-
if (!start || !end) {
|
|
1553
|
-
return undefined;
|
|
1554
|
-
}
|
|
1555
|
-
return Math.max(0, end.getTime() - start.getTime());
|
|
1556
|
-
}
|
|
1557
|
-
function formatDuration(seconds) {
|
|
1558
|
-
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
1559
|
-
return "-";
|
|
1560
|
-
}
|
|
1561
|
-
if (seconds < 60) {
|
|
1562
|
-
return `${Math.round(seconds)}s`;
|
|
1563
|
-
}
|
|
1564
|
-
const minutes = Math.floor(seconds / 60);
|
|
1565
|
-
const remainder = Math.round(seconds % 60);
|
|
1566
|
-
return `${minutes}m ${remainder}s`;
|
|
1567
|
-
}
|
|
1568
1344
|
function normalizeMimeType(value, name) {
|
|
1569
1345
|
const configured = value?.trim();
|
|
1570
1346
|
if (configured) {
|
|
@@ -1598,37 +1374,3 @@ function uploadFileDtos(files) {
|
|
|
1598
1374
|
sizeBytes: file.sizeBytes,
|
|
1599
1375
|
}));
|
|
1600
1376
|
}
|
|
1601
|
-
function isPreviewableTextFile(extension, sizeBytes) {
|
|
1602
|
-
if (sizeBytes > MAX_TEXT_PREVIEW_BYTES * 4) {
|
|
1603
|
-
return false;
|
|
1604
|
-
}
|
|
1605
|
-
return [
|
|
1606
|
-
"",
|
|
1607
|
-
".c",
|
|
1608
|
-
".conf",
|
|
1609
|
-
".cpp",
|
|
1610
|
-
".css",
|
|
1611
|
-
".csv",
|
|
1612
|
-
".env",
|
|
1613
|
-
".go",
|
|
1614
|
-
".html",
|
|
1615
|
-
".java",
|
|
1616
|
-
".js",
|
|
1617
|
-
".json",
|
|
1618
|
-
".jsx",
|
|
1619
|
-
".log",
|
|
1620
|
-
".md",
|
|
1621
|
-
".py",
|
|
1622
|
-
".rb",
|
|
1623
|
-
".rs",
|
|
1624
|
-
".sh",
|
|
1625
|
-
".sql",
|
|
1626
|
-
".toml",
|
|
1627
|
-
".ts",
|
|
1628
|
-
".tsx",
|
|
1629
|
-
".txt",
|
|
1630
|
-
".xml",
|
|
1631
|
-
".yaml",
|
|
1632
|
-
".yml",
|
|
1633
|
-
].includes(extension);
|
|
1634
|
-
}
|