@nordbyte/nordrelay 0.5.0 → 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.
Files changed (38) hide show
  1. package/README.md +16 -10
  2. package/dist/access-control.js +2 -0
  3. package/dist/agent-updates.js +43 -8
  4. package/dist/bot-ui.js +1 -0
  5. package/dist/bot.js +108 -1063
  6. package/dist/channel-actions.js +8 -8
  7. package/dist/operations.js +63 -9
  8. package/dist/relay-artifact-service.js +126 -0
  9. package/dist/relay-external-activity-monitor.js +216 -0
  10. package/dist/relay-queue-service.js +66 -0
  11. package/dist/relay-runtime-types.js +1 -0
  12. package/dist/relay-runtime.js +77 -359
  13. package/dist/support-bundle.js +205 -0
  14. package/dist/telegram-agent-commands.js +212 -0
  15. package/dist/telegram-artifact-commands.js +139 -0
  16. package/dist/telegram-command-menu.js +1 -0
  17. package/dist/telegram-command-types.js +1 -0
  18. package/dist/telegram-diagnostics-command.js +102 -0
  19. package/dist/telegram-general-commands.js +52 -0
  20. package/dist/telegram-operational-commands.js +153 -0
  21. package/dist/telegram-preference-commands.js +198 -0
  22. package/dist/telegram-queue-commands.js +278 -0
  23. package/dist/telegram-support-command.js +53 -0
  24. package/dist/telegram-update-commands.js +6 -1
  25. package/dist/web-api-contract.js +79 -31
  26. package/dist/web-api-types.js +1 -0
  27. package/dist/web-dashboard-access-routes.js +163 -0
  28. package/dist/web-dashboard-artifact-routes.js +65 -0
  29. package/dist/web-dashboard-assets.js +2 -0
  30. package/dist/web-dashboard-http.js +143 -0
  31. package/dist/web-dashboard-pages.js +257 -0
  32. package/dist/web-dashboard-runtime-routes.js +92 -0
  33. package/dist/web-dashboard-session-routes.js +209 -0
  34. package/dist/web-dashboard.js +43 -882
  35. package/dist/webui-assets/dashboard.css +74 -4
  36. package/dist/webui-assets/dashboard.js +163 -24
  37. package/dist/zip-writer.js +83 -0
  38. package/package.json +10 -4
@@ -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 { createArtifactZipBundle, collectRecentWorkspaceArtifacts, getArtifactTurnReport, ensureOutDir, listRecentArtifactReports, persistWorkspaceArtifactReport, removeArtifactTurn, totalArtifactSize, } from "./artifacts.js";
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,15 +38,16 @@ 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;
42
- externalMonitorRunning = false;
43
47
  currentTurnId = null;
44
48
  accumulatedText = "";
45
49
  currentTurnStartedAt = 0;
46
50
  currentProgress = null;
47
- externalMirror = null;
48
51
  constructor(config) {
49
52
  this.config = config;
50
53
  this.registry = new SessionRegistry(config, {
@@ -56,12 +59,27 @@ export class RelayRuntime {
56
59
  this.activityStore = new WebActivityStore(config.workspace, config.stateBackend, config.auditMaxEvents);
57
60
  this.auditStore = new AuditLogStore(config.workspace, config.stateBackend, config.auditMaxEvents);
58
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);
59
64
  this.agentUpdates = new AgentUpdateManager({
60
65
  onUpdate: (job) => this.broadcast({ type: "agent_update", job }),
61
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
+ });
62
80
  if (config.codexExternalBusyCheckMs > 0) {
63
81
  this.externalMonitor = setInterval(() => {
64
- void this.monitorExternalActivitySafe();
82
+ void this.externalActivityMonitor.monitorSafe();
65
83
  }, config.codexExternalBusyCheckMs);
66
84
  this.externalMonitor.unref?.();
67
85
  }
@@ -132,18 +150,18 @@ export class RelayRuntime {
132
150
  agentUpdateJobs() {
133
151
  return this.agentUpdates.list();
134
152
  }
135
- startAgentUpdate(agentId) {
153
+ startAgentUpdate(agentId, operation = "update") {
136
154
  const job = this.agentUpdates.start(agentId, {
137
155
  piCliPath: this.config.piCliPath,
138
156
  hermesCliPath: this.config.hermesCliPath,
139
157
  openClawCliPath: this.config.openClawCliPath,
140
158
  claudeCodeCliPath: this.config.claudeCodeCliPath,
141
- });
142
- this.broadcastStatus(`${job.agentLabel} update started. Log: ${job.logPath}`, "warn");
159
+ }, operation);
160
+ this.broadcastStatus(`${job.agentLabel} ${operation} started. Log: ${job.logPath}`, "warn");
143
161
  this.appendActivity({
144
162
  source: "web",
145
163
  status: "info",
146
- type: "agent_update_started",
164
+ type: operation === "install" ? "agent_install_started" : "agent_update_started",
147
165
  agentId,
148
166
  threadId: null,
149
167
  workspace: this.config.workspace,
@@ -154,7 +172,7 @@ export class RelayRuntime {
154
172
  status: "ok",
155
173
  contextKey: WEB_CONTEXT_KEY,
156
174
  agentId,
157
- description: `update ${agentId}`,
175
+ description: `${operation} ${agentId}`,
158
176
  detail: job.summary,
159
177
  });
160
178
  return job;
@@ -188,8 +206,8 @@ export class RelayRuntime {
188
206
  runtime: {
189
207
  stateBackend: this.config.stateBackend,
190
208
  sourceWorkspace: this.config.workspace,
191
- queuePaused: this.promptStore.isPaused(WEB_CONTEXT_KEY),
192
- externalMirror: this.externalMirror ? { ...this.externalMirror } : null,
209
+ queuePaused: this.queueService.isPaused(),
210
+ externalMirror: this.externalActivityMonitor.snapshot(),
193
211
  agentDiagnostics: getAgentDiagnostics(await this.getSession(true), this.config),
194
212
  },
195
213
  };
@@ -244,7 +262,7 @@ export class RelayRuntime {
244
262
  tasks() {
245
263
  return {
246
264
  current: this.currentProgress ? { ...this.currentProgress, tools: [...this.currentProgress.tools] } : null,
247
- external: this.externalTask(),
265
+ external: this.externalActivityMonitor.task(),
248
266
  queue: this.queue(),
249
267
  queuePaused: this.queuePaused(),
250
268
  recent: this.activity({ limit: 20 }),
@@ -253,6 +271,32 @@ export class RelayRuntime {
253
271
  audit(limit = 50) {
254
272
  return this.auditStore.list(limit);
255
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
+ }
256
300
  locks() {
257
301
  return this.lockStore.list();
258
302
  }
@@ -463,7 +507,7 @@ export class RelayRuntime {
463
507
  return this.activityStore.list(options).map((event) => this.enrichActivityEvent(event, currentInfo));
464
508
  }
465
509
  async retry() {
466
- const cached = this.promptStore.getLastPrompt(WEB_CONTEXT_KEY);
510
+ const cached = this.queueService.getLastPrompt();
467
511
  if (!cached) {
468
512
  throw new Error("Nothing to retry. Send a message first.");
469
513
  }
@@ -758,7 +802,7 @@ export class RelayRuntime {
758
802
  const session = await this.getSession(false);
759
803
  const external = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
760
804
  if (session.isProcessing() || external?.activity.active) {
761
- const queued = this.promptStore.enqueue(WEB_CONTEXT_KEY, envelope);
805
+ const queued = this.queueService.enqueue(envelope);
762
806
  const info = this.publicInfo(session);
763
807
  this.appendActivity({
764
808
  source: "web",
@@ -770,7 +814,7 @@ export class RelayRuntime {
770
814
  prompt: envelope.description,
771
815
  detail: external?.activity.active
772
816
  ? `Queued because ${external.agentLabel} CLI is still processing another task.`
773
- : `Queued at position ${this.promptStore.list(WEB_CONTEXT_KEY).length}.`,
817
+ : `Queued at position ${this.queueService.length()}.`,
774
818
  });
775
819
  this.appendAudit({
776
820
  action: "prompt_queued",
@@ -783,7 +827,7 @@ export class RelayRuntime {
783
827
  description: envelope.description,
784
828
  });
785
829
  if (external?.activity.active) {
786
- this.broadcastStatus(`Waiting for ${external.agentLabel} CLI task... ${this.promptStore.list(WEB_CONTEXT_KEY).length} queued.`, "info");
830
+ this.broadcastStatus(`Waiting for ${external.agentLabel} CLI task... ${this.queueService.length()} queued.`, "info");
787
831
  }
788
832
  this.broadcastQueue();
789
833
  return { queued: true, queueId: queued.id };
@@ -794,30 +838,14 @@ export class RelayRuntime {
794
838
  return { queued: false };
795
839
  }
796
840
  queue() {
797
- return this.promptStore.list(WEB_CONTEXT_KEY).map(queueItemDto);
841
+ return this.queueService.list();
798
842
  }
799
843
  queuePaused() {
800
- return this.promptStore.isPaused(WEB_CONTEXT_KEY);
844
+ return this.queueService.isPaused();
801
845
  }
802
846
  queueAction(action, id) {
803
- if (action === "pause")
804
- this.promptStore.pause(WEB_CONTEXT_KEY);
805
- if (action === "resume")
806
- this.promptStore.resume(WEB_CONTEXT_KEY);
807
- if (action === "clear")
808
- this.promptStore.clear(WEB_CONTEXT_KEY);
809
- if (id && action === "cancel")
810
- this.promptStore.remove(WEB_CONTEXT_KEY, id);
811
- if (id && action === "top")
812
- this.promptStore.moveToTop(WEB_CONTEXT_KEY, id);
813
- if (id && action === "up")
814
- this.promptStore.moveUp(WEB_CONTEXT_KEY, id);
815
- if (id && action === "down")
816
- this.promptStore.moveDown(WEB_CONTEXT_KEY, id);
847
+ this.queueService.apply(action, id);
817
848
  if (id && action === "run") {
818
- const item = this.promptStore.remove(WEB_CONTEXT_KEY, id);
819
- if (item)
820
- this.promptStore.enqueueFront(WEB_CONTEXT_KEY, item);
821
849
  void this.drainQueue().catch((error) => this.broadcastStatus(friendlyErrorText(error), "error"));
822
850
  }
823
851
  this.appendActivity({
@@ -839,58 +867,23 @@ export class RelayRuntime {
839
867
  }
840
868
  async artifacts() {
841
869
  const session = await this.getSession(true);
842
- return (await listRecentArtifactReports(session.getInfo().workspace, 20, this.config.maxFileSize)).map(artifactDto);
870
+ return this.artifactService.list(session.getInfo().workspace, 20);
843
871
  }
844
872
  async artifact(turnId) {
845
873
  const session = await this.getSession(true);
846
- return getArtifactTurnReport(session.getInfo().workspace, turnId, this.config.maxFileSize);
874
+ return this.artifactService.get(session.getInfo().workspace, turnId);
847
875
  }
848
876
  async deleteArtifact(turnId) {
849
877
  const session = await this.getSession(true);
850
- return removeArtifactTurn(session.getInfo().workspace, turnId);
878
+ return this.artifactService.delete(session.getInfo().workspace, turnId);
851
879
  }
852
880
  async createArtifactZip(turnId) {
853
- const report = await this.artifact(turnId);
854
- if (!report) {
855
- return null;
856
- }
857
- const bundle = await createArtifactZipBundle(report.artifacts, report.outDir, {
858
- maxFileSize: this.config.maxFileSize,
859
- bundleName: `nordrelay-artifacts-${turnId}.zip`,
860
- });
861
- 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);
862
883
  }
863
884
  async artifactPreview(turnId, relativePath) {
864
- const report = await this.artifact(turnId);
865
- const artifact = report?.artifacts.find((candidate) => candidate.relativePath.split(path.sep).join("/") === relativePath);
866
- if (!artifact) {
867
- return null;
868
- }
869
- const extension = path.extname(artifact.name).toLowerCase();
870
- if ([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"].includes(extension)) {
871
- return {
872
- kind: "image",
873
- name: artifact.name,
874
- sizeBytes: artifact.sizeBytes,
875
- };
876
- }
877
- if (!isPreviewableTextFile(extension, artifact.sizeBytes)) {
878
- return {
879
- kind: "unsupported",
880
- name: artifact.name,
881
- sizeBytes: artifact.sizeBytes,
882
- detail: artifact.sizeBytes > MAX_TEXT_PREVIEW_BYTES ? "File is too large for inline preview." : "File type is not previewable.",
883
- };
884
- }
885
- const buffer = await readFile(artifact.localPath);
886
- const truncated = buffer.byteLength > MAX_TEXT_PREVIEW_BYTES;
887
- return {
888
- kind: "text",
889
- name: artifact.name,
890
- sizeBytes: artifact.sizeBytes,
891
- truncated,
892
- text: buffer.subarray(0, MAX_TEXT_PREVIEW_BYTES).toString("utf8"),
893
- };
885
+ const session = await this.getSession(true);
886
+ return this.artifactService.preview(session.getInfo().workspace, turnId, relativePath);
894
887
  }
895
888
  async logs(target = "connector", lines = 100) {
896
889
  if (target === "update") {
@@ -934,158 +927,6 @@ export class RelayRuntime {
934
927
  this.registry.disposeAll();
935
928
  this.subscribers.clear();
936
929
  }
937
- async monitorExternalActivity() {
938
- const session = await this.getSession(true);
939
- const info = this.publicInfo(session);
940
- if (!info.capabilities.externalActivity || !info.threadId || session.isProcessing()) {
941
- return;
942
- }
943
- const snapshot = getExternalSnapshotForSession(session, this.config, {
944
- afterLine: this.externalMirror?.threadId === info.threadId ? this.externalMirror.lastLine : Number.MAX_SAFE_INTEGER,
945
- }) ?? getExternalSnapshotForSession(session, this.config, {
946
- maxEvents: 0,
947
- });
948
- if (!snapshot) {
949
- return;
950
- }
951
- if (!this.externalMirror || this.externalMirror.threadId !== snapshot.threadId || this.externalMirror.rolloutPath !== snapshot.sourcePath) {
952
- this.externalMirror = {
953
- threadId: snapshot.threadId,
954
- rolloutPath: snapshot.sourcePath,
955
- lastLine: snapshot.lineCount,
956
- turnId: snapshot.activity.turnId,
957
- startedAt: snapshot.activity.startedAt?.toISOString() ?? null,
958
- };
959
- if (snapshot.activity.active) {
960
- this.startExternalTurn(snapshot);
961
- }
962
- return;
963
- }
964
- const mirror = this.externalMirror;
965
- if (snapshot.activity.active) {
966
- if (mirror.turnId !== snapshot.activity.turnId) {
967
- mirror.turnId = snapshot.activity.turnId;
968
- mirror.startedAt = snapshot.activity.startedAt?.toISOString() ?? null;
969
- mirror.latestAgentLine = undefined;
970
- this.startExternalTurn(snapshot);
971
- }
972
- this.broadcastExternalEvents(snapshot, snapshot.events.filter((event) => event.lineNumber > mirror.lastLine));
973
- mirror.lastLine = Math.max(mirror.lastLine, snapshot.lineCount);
974
- mirror.latestStatus = externalStatusLine(snapshot, this.queue().length);
975
- this.broadcastStatus(mirror.latestStatus, "info");
976
- return;
977
- }
978
- const terminalEvent = [...snapshot.events].reverse().find((event) => event.kind === "task" && event.status && event.status !== "started");
979
- if (terminalEvent && terminalEvent.lineNumber > mirror.lastLine) {
980
- const finalAgent = snapshot.events.filter((event) => event.kind === "agent" && event.text).at(-1);
981
- const finalText = finalAgent?.text ?? snapshot.latestAgentMessage;
982
- const finalLine = finalAgent?.lineNumber ?? snapshot.lineCount;
983
- if (finalText && finalLine !== mirror.latestAgentLine) {
984
- this.chatStore.append({
985
- threadId: snapshot.threadId,
986
- role: "agent",
987
- text: finalText,
988
- source: "cli",
989
- turnId: terminalEvent.turnId ?? undefined,
990
- });
991
- this.broadcast({ type: "text_delta", id: terminalEvent.turnId ?? "cli", delta: finalText });
992
- mirror.latestAgentLine = finalLine;
993
- }
994
- const externalStartedAt = mirror.startedAt ? new Date(mirror.startedAt) : snapshot.activity.startedAt;
995
- this.broadcast({
996
- type: "turn_complete",
997
- id: terminalEvent.turnId ?? "cli",
998
- at: terminalEvent.timestamp?.toISOString() ?? new Date().toISOString(),
999
- });
1000
- this.appendActivity({
1001
- source: "cli",
1002
- status: terminalEvent.status === "aborted" ? "aborted" : terminalEvent.status === "failed" ? "failed" : "completed",
1003
- type: "cli_turn_finished",
1004
- threadId: snapshot.threadId,
1005
- workspace: info.workspace,
1006
- agentId: info.agentId,
1007
- prompt: snapshot.latestUserMessage ?? undefined,
1008
- detail: `${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`,
1009
- durationMs: durationFromDates(externalStartedAt, terminalEvent.timestamp),
1010
- });
1011
- if (externalStartedAt && terminalEvent.turnId) {
1012
- await this.persistWorkspaceArtifactsForTurn(info.workspace, terminalEvent.turnId, externalStartedAt);
1013
- }
1014
- mirror.latestStatus = `${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`;
1015
- this.broadcastStatus(`${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`, terminalEvent.status === "failed" ? "error" : terminalEvent.status === "aborted" ? "warn" : "info");
1016
- this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
1017
- await this.drainQueue();
1018
- }
1019
- mirror.lastLine = Math.max(mirror.lastLine, snapshot.lineCount);
1020
- }
1021
- async monitorExternalActivitySafe() {
1022
- if (this.externalMonitorRunning) {
1023
- return;
1024
- }
1025
- this.externalMonitorRunning = true;
1026
- try {
1027
- await this.monitorExternalActivity();
1028
- }
1029
- catch (error) {
1030
- this.broadcastStatus(friendlyErrorText(error), "error");
1031
- }
1032
- finally {
1033
- this.externalMonitorRunning = false;
1034
- }
1035
- }
1036
- startExternalTurn(snapshot) {
1037
- const prompt = snapshot.latestUserMessage ?? `${snapshot.agentLabel} CLI task`;
1038
- this.chatStore.append({
1039
- threadId: snapshot.threadId,
1040
- role: "user",
1041
- text: prompt,
1042
- source: "cli",
1043
- turnId: snapshot.activity.turnId ?? undefined,
1044
- timestamp: snapshot.activity.startedAt?.toISOString(),
1045
- });
1046
- this.broadcast({
1047
- type: "turn_start",
1048
- id: snapshot.activity.turnId ?? "cli",
1049
- prompt,
1050
- at: snapshot.activity.startedAt?.toISOString() ?? new Date().toISOString(),
1051
- source: "cli",
1052
- });
1053
- this.appendActivity({
1054
- source: "cli",
1055
- status: "running",
1056
- type: "cli_turn_started",
1057
- threadId: snapshot.threadId,
1058
- prompt,
1059
- detail: `${snapshot.sourceLabel}: ${snapshot.sourcePath}`,
1060
- });
1061
- }
1062
- broadcastExternalEvents(snapshot, events) {
1063
- for (const event of events) {
1064
- if (event.kind === "tool" && event.status === "started") {
1065
- this.broadcast({
1066
- type: "tool_start",
1067
- id: snapshot.activity.turnId ?? "cli",
1068
- toolCallId: `cli-${event.lineNumber}`,
1069
- toolName: event.toolName ?? "tool",
1070
- });
1071
- this.appendActivity({
1072
- source: "cli",
1073
- status: "running",
1074
- type: "cli_tool_started",
1075
- threadId: snapshot.threadId,
1076
- detail: event.toolName ?? "tool",
1077
- });
1078
- }
1079
- if (event.kind === "tool" && event.status === "finished") {
1080
- this.broadcast({
1081
- type: "tool_end",
1082
- id: snapshot.activity.turnId ?? "cli",
1083
- toolCallId: `cli-${event.lineNumber}`,
1084
- isError: false,
1085
- });
1086
- }
1087
- }
1088
- }
1089
930
  async getSession(deferThreadStart) {
1090
931
  return this.registry.getOrCreate(WEB_CONTEXT_KEY, { deferThreadStart });
1091
932
  }
@@ -1199,7 +1040,7 @@ export class RelayRuntime {
1199
1040
  outputChars: 0,
1200
1041
  tools: [],
1201
1042
  };
1202
- this.promptStore.setLastPrompt(WEB_CONTEXT_KEY, envelope);
1043
+ this.queueService.setLastPrompt(envelope);
1203
1044
  const startedDate = new Date();
1204
1045
  const startedAt = startedDate.toISOString();
1205
1046
  this.chatStore.append({
@@ -1257,7 +1098,7 @@ export class RelayRuntime {
1257
1098
  try {
1258
1099
  await session.prompt(envelope.input, callbacks);
1259
1100
  this.updateSession(session);
1260
- await this.persistWorkspaceArtifactsForTurn(session.getInfo().workspace, turnId, startedDate);
1101
+ await this.artifactService.persistWorkspaceArtifactsForTurn(session.getInfo().workspace, turnId, startedDate);
1261
1102
  if (this.accumulatedText.trim()) {
1262
1103
  this.chatStore.append({
1263
1104
  threadId: info.threadId ?? "pending",
@@ -1335,7 +1176,7 @@ export class RelayRuntime {
1335
1176
  }
1336
1177
  }
1337
1178
  async drainQueue() {
1338
- if (this.draining || this.promptStore.isPaused(WEB_CONTEXT_KEY)) {
1179
+ if (this.draining || this.queueService.isPaused()) {
1339
1180
  return;
1340
1181
  }
1341
1182
  this.draining = true;
@@ -1344,10 +1185,10 @@ export class RelayRuntime {
1344
1185
  while (!session.isProcessing()) {
1345
1186
  const external = getExternalSnapshotForSession(session, this.config, { maxEvents: 0 });
1346
1187
  if (external?.activity.active) {
1347
- this.broadcastStatus(`Waiting for ${external.agentLabel} CLI task... ${this.queue().length} queued.`, "info");
1188
+ this.broadcastStatus(`Waiting for ${external.agentLabel} CLI task... ${this.queueService.length()} queued.`, "info");
1348
1189
  return;
1349
1190
  }
1350
- const next = this.promptStore.dequeue(WEB_CONTEXT_KEY);
1191
+ const next = this.queueService.dequeue();
1351
1192
  this.broadcastQueue();
1352
1193
  if (!next) {
1353
1194
  return;
@@ -1417,31 +1258,6 @@ export class RelayRuntime {
1417
1258
  }
1418
1259
  this.updateCurrentProgress({ currentTool: toolName, lastTool: toolName });
1419
1260
  }
1420
- externalTask() {
1421
- if (!this.externalMirror) {
1422
- return null;
1423
- }
1424
- const startedAt = this.externalMirror.startedAt ?? new Date().toISOString();
1425
- const startedMs = new Date(startedAt).getTime();
1426
- return {
1427
- id: this.externalMirror.turnId ?? "cli",
1428
- source: "cli",
1429
- status: this.externalMirror.latestStatus?.includes("failed")
1430
- ? "failed"
1431
- : this.externalMirror.latestStatus?.includes("aborted")
1432
- ? "aborted"
1433
- : this.externalMirror.latestStatus?.includes("finished") || this.externalMirror.latestStatus?.includes("completed")
1434
- ? "completed"
1435
- : "running",
1436
- threadId: this.externalMirror.threadId,
1437
- startedAt,
1438
- updatedAt: new Date().toISOString(),
1439
- durationMs: Number.isFinite(startedMs) ? Math.max(0, Date.now() - startedMs) : 0,
1440
- outputChars: 0,
1441
- tools: [],
1442
- detail: this.externalMirror.latestStatus ?? this.externalMirror.rolloutPath,
1443
- };
1444
- }
1445
1261
  broadcastQueue() {
1446
1262
  this.broadcast({ type: "queue_update", queue: this.queue(), paused: this.queuePaused() });
1447
1263
  }
@@ -1468,53 +1284,6 @@ export class RelayRuntime {
1468
1284
  capabilities: info.capabilities ?? CODEX_AGENT_CAPABILITIES,
1469
1285
  };
1470
1286
  }
1471
- async persistWorkspaceArtifactsForTurn(workspace, turnId, startedAt) {
1472
- const report = await collectRecentWorkspaceArtifacts(workspace, {
1473
- since: startedAt,
1474
- until: new Date(),
1475
- maxFileSize: this.config.maxFileSize,
1476
- limit: 20,
1477
- ignoreDirs: this.config.artifactIgnoreDirs,
1478
- ignoreGlobs: this.config.artifactIgnoreGlobs,
1479
- });
1480
- if (report.artifacts.length === 0 && report.skippedCount === 0 && !report.omittedCount) {
1481
- return;
1482
- }
1483
- await persistWorkspaceArtifactReport(workspace, turnId, report);
1484
- }
1485
- }
1486
- function queueItemDto(item) {
1487
- return {
1488
- id: item.id,
1489
- description: item.description,
1490
- createdAt: new Date(item.createdAt).toISOString(),
1491
- attempts: item.attempts ?? 0,
1492
- notBefore: item.notBefore ? new Date(item.notBefore).toISOString() : undefined,
1493
- lastError: item.lastError,
1494
- };
1495
- }
1496
- function artifactDto(report) {
1497
- return {
1498
- turnId: report.turnId,
1499
- updatedAt: report.updatedAt.toISOString(),
1500
- source: report.source,
1501
- fileCount: report.artifacts.length,
1502
- totalSizeBytes: totalArtifactSize(report.artifacts),
1503
- skippedCount: report.skippedCount,
1504
- omittedCount: report.omittedCount,
1505
- artifacts: report.artifacts.map((artifact) => ({
1506
- name: artifact.name,
1507
- relativePath: artifact.relativePath.split(path.sep).join("/"),
1508
- sizeBytes: artifact.sizeBytes,
1509
- })),
1510
- };
1511
- }
1512
- function externalStatusLine(snapshot, queueLength) {
1513
- const elapsed = snapshot.activity.startedAt
1514
- ? formatDuration((Date.now() - snapshot.activity.startedAt.getTime()) / 1000)
1515
- : "-";
1516
- const tool = snapshot.latestToolName ?? "-";
1517
- return `${snapshot.agentLabel} CLI running · ${elapsed} · tool ${tool} · ${queueLength} queued`;
1518
1287
  }
1519
1288
  function cliHealthForAgent(agentId, health) {
1520
1289
  if (agentId === "pi") {
@@ -1572,23 +1341,6 @@ function hostLogoutCommand(info, config) {
1572
1341
  }
1573
1342
  return "codex logout";
1574
1343
  }
1575
- function durationFromDates(start, end) {
1576
- if (!start || !end) {
1577
- return undefined;
1578
- }
1579
- return Math.max(0, end.getTime() - start.getTime());
1580
- }
1581
- function formatDuration(seconds) {
1582
- if (!Number.isFinite(seconds) || seconds < 0) {
1583
- return "-";
1584
- }
1585
- if (seconds < 60) {
1586
- return `${Math.round(seconds)}s`;
1587
- }
1588
- const minutes = Math.floor(seconds / 60);
1589
- const remainder = Math.round(seconds % 60);
1590
- return `${minutes}m ${remainder}s`;
1591
- }
1592
1344
  function normalizeMimeType(value, name) {
1593
1345
  const configured = value?.trim();
1594
1346
  if (configured) {
@@ -1622,37 +1374,3 @@ function uploadFileDtos(files) {
1622
1374
  sizeBytes: file.sizeBytes,
1623
1375
  }));
1624
1376
  }
1625
- function isPreviewableTextFile(extension, sizeBytes) {
1626
- if (sizeBytes > MAX_TEXT_PREVIEW_BYTES * 4) {
1627
- return false;
1628
- }
1629
- return [
1630
- "",
1631
- ".c",
1632
- ".conf",
1633
- ".cpp",
1634
- ".css",
1635
- ".csv",
1636
- ".env",
1637
- ".go",
1638
- ".html",
1639
- ".java",
1640
- ".js",
1641
- ".json",
1642
- ".jsx",
1643
- ".log",
1644
- ".md",
1645
- ".py",
1646
- ".rb",
1647
- ".rs",
1648
- ".sh",
1649
- ".sql",
1650
- ".toml",
1651
- ".ts",
1652
- ".tsx",
1653
- ".txt",
1654
- ".xml",
1655
- ".yaml",
1656
- ".yml",
1657
- ].includes(extension);
1658
- }