@nordbyte/nordrelay 0.3.0 → 0.3.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/CHANGELOG.md +9 -0
- package/README.md +24 -18
- package/dist/bot.js +5 -2
- package/dist/codex-session.js +3 -1
- package/dist/context-key.js +23 -0
- package/dist/operations.js +1 -1
- package/dist/relay-runtime.js +436 -7
- package/dist/session-registry.js +3 -3
- package/dist/settings-service.js +46 -23
- package/dist/state-backend.js +17 -8
- package/dist/web-dashboard.js +159 -33
- package/dist/web-state.js +131 -0
- package/docker-compose.yml +1 -1
- package/package.json +1 -1
- package/plugins/nordrelay/scripts/nordrelay.mjs +42 -17
package/dist/relay-runtime.js
CHANGED
|
@@ -1,35 +1,58 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { createArtifactZipBundle, getArtifactTurnReport, ensureOutDir, listRecentArtifactReports, removeArtifactTurn, totalArtifactSize, } from "./artifacts.js";
|
|
4
5
|
import { buildFileInstructions, outboxPath, stageFile, } from "./attachments.js";
|
|
5
6
|
import { CODEX_AGENT_CAPABILITIES, CODEX_REASONING_EFFORTS, PI_THINKING_LEVELS, agentLabel, agentReasoningLabel, } from "./agent.js";
|
|
6
7
|
import { enabledAgents } from "./agent-factory.js";
|
|
7
8
|
import { checkAuthStatus } from "./codex-auth.js";
|
|
9
|
+
import { getThreadRolloutSnapshot, } from "./codex-state.js";
|
|
8
10
|
import { friendlyErrorText } from "./error-messages.js";
|
|
9
|
-
import { getConnectorHealth, getVersionChecks, readFormattedLogTail } from "./operations.js";
|
|
11
|
+
import { getConnectorHealth, getVersionChecks, readFormattedLogTail, spawnConnectorRestart } from "./operations.js";
|
|
10
12
|
import { PromptStore, toPromptEnvelope } from "./prompt-store.js";
|
|
11
13
|
import { renderSessionInfoPlain } from "./session-format.js";
|
|
12
14
|
import { SessionRegistry } from "./session-registry.js";
|
|
13
15
|
import { transcribeAudio } from "./voice.js";
|
|
16
|
+
import { WebActivityStore, WebChatStore, } from "./web-state.js";
|
|
14
17
|
import { evaluateWorkspacePolicy, filterAllowedWorkspaces } from "./workspace-policy.js";
|
|
15
|
-
const WEB_CONTEXT_KEY = "
|
|
18
|
+
const WEB_CONTEXT_KEY = "web:dashboard";
|
|
16
19
|
const MAX_WEB_SESSION_PAGE_SIZE = 50;
|
|
20
|
+
const MAX_CHAT_HISTORY = 250;
|
|
21
|
+
const MAX_TEXT_PREVIEW_BYTES = 256 * 1024;
|
|
17
22
|
export class RelayRuntime {
|
|
18
23
|
config;
|
|
19
24
|
registry;
|
|
20
25
|
promptStore;
|
|
26
|
+
chatStore;
|
|
27
|
+
activityStore;
|
|
21
28
|
subscribers = new Set();
|
|
29
|
+
externalMonitor;
|
|
22
30
|
draining = false;
|
|
23
31
|
currentTurnId = null;
|
|
24
32
|
accumulatedText = "";
|
|
33
|
+
currentTurnStartedAt = 0;
|
|
34
|
+
externalMirror = null;
|
|
25
35
|
constructor(config) {
|
|
26
36
|
this.config = config;
|
|
27
|
-
this.registry = new SessionRegistry(config
|
|
37
|
+
this.registry = new SessionRegistry(config, {
|
|
38
|
+
fileName: "web-contexts.json",
|
|
39
|
+
sqliteKey: "web-contexts",
|
|
40
|
+
});
|
|
28
41
|
this.promptStore = new PromptStore(config.workspace, config.stateBackend);
|
|
42
|
+
this.chatStore = new WebChatStore(config.workspace, config.stateBackend, MAX_CHAT_HISTORY);
|
|
43
|
+
this.activityStore = new WebActivityStore(config.workspace, config.stateBackend, config.auditMaxEvents);
|
|
44
|
+
if (config.codexExternalBusyCheckMs > 0) {
|
|
45
|
+
this.externalMonitor = setInterval(() => {
|
|
46
|
+
void this.monitorExternalActivity().catch((error) => this.broadcastStatus(friendlyErrorText(error), "error"));
|
|
47
|
+
}, config.codexExternalBusyCheckMs);
|
|
48
|
+
this.externalMonitor.unref?.();
|
|
49
|
+
}
|
|
29
50
|
}
|
|
30
51
|
subscribe(callback) {
|
|
31
52
|
this.subscribers.add(callback);
|
|
32
53
|
void this.snapshot().then((data) => callback({ type: "snapshot", data })).catch(() => { });
|
|
54
|
+
void this.chatHistory().then((messages) => callback({ type: "chat_history", messages })).catch(() => { });
|
|
55
|
+
callback({ type: "activity_update", events: this.activity({ limit: 50 }) });
|
|
33
56
|
return () => this.subscribers.delete(callback);
|
|
34
57
|
}
|
|
35
58
|
async snapshot() {
|
|
@@ -39,6 +62,7 @@ export class RelayRuntime {
|
|
|
39
62
|
session: info,
|
|
40
63
|
sessionText: renderSessionInfoPlain(info),
|
|
41
64
|
queue: this.queue(),
|
|
65
|
+
queuePaused: this.queuePaused(),
|
|
42
66
|
processing: session.isProcessing(),
|
|
43
67
|
enabledAgents: enabledAgents(this.config),
|
|
44
68
|
workspaces: filterAllowedWorkspaces(session.listWorkspaces(), this.config),
|
|
@@ -51,6 +75,53 @@ export class RelayRuntime {
|
|
|
51
75
|
snapshot: await this.snapshot(),
|
|
52
76
|
};
|
|
53
77
|
}
|
|
78
|
+
async diagnostics() {
|
|
79
|
+
return {
|
|
80
|
+
health: await getConnectorHealth(),
|
|
81
|
+
versionChecks: await getVersionChecks({ piCliPath: this.config.piCliPath }),
|
|
82
|
+
snapshot: await this.snapshot(),
|
|
83
|
+
runtime: {
|
|
84
|
+
stateBackend: this.config.stateBackend,
|
|
85
|
+
sourceWorkspace: this.config.workspace,
|
|
86
|
+
queuePaused: this.promptStore.isPaused(WEB_CONTEXT_KEY),
|
|
87
|
+
externalMirror: this.externalMirror ? { ...this.externalMirror } : null,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
async controlOptions() {
|
|
92
|
+
const session = await this.getSession(true);
|
|
93
|
+
const info = this.publicInfo(session);
|
|
94
|
+
const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
|
|
95
|
+
return {
|
|
96
|
+
models: capabilities.modelSelection ? session.listModels() : [],
|
|
97
|
+
reasoningLabel: agentReasoningLabel(info.agentId),
|
|
98
|
+
reasoningOptions: info.agentId === "pi" ? PI_THINKING_LEVELS : CODEX_REASONING_EFFORTS,
|
|
99
|
+
launchProfiles: capabilities.launchProfiles
|
|
100
|
+
? this.config.launchProfiles.map((profile) => ({
|
|
101
|
+
id: profile.id,
|
|
102
|
+
label: profile.label,
|
|
103
|
+
behavior: `${profile.sandboxMode} / ${profile.approvalPolicy}`,
|
|
104
|
+
unsafe: profile.unsafe,
|
|
105
|
+
}))
|
|
106
|
+
: [],
|
|
107
|
+
workspaces: filterAllowedWorkspaces(session.listWorkspaces(), this.config),
|
|
108
|
+
capabilities,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async chatHistory(limit = 200) {
|
|
112
|
+
const session = await this.getSession(true);
|
|
113
|
+
return this.chatStore.list(this.publicInfo(session).threadId, limit);
|
|
114
|
+
}
|
|
115
|
+
async clearChatHistory() {
|
|
116
|
+
const session = await this.getSession(true);
|
|
117
|
+
const removed = this.chatStore.clear(this.publicInfo(session).threadId);
|
|
118
|
+
const messages = await this.chatHistory();
|
|
119
|
+
this.broadcast({ type: "chat_history", messages });
|
|
120
|
+
return { removed, messages };
|
|
121
|
+
}
|
|
122
|
+
activity(options = {}) {
|
|
123
|
+
return this.activityStore.list(options);
|
|
124
|
+
}
|
|
54
125
|
async listSessions(limit = 80, query = "") {
|
|
55
126
|
return this.filteredSessions(await this.getSession(true), query, Math.max(1, limit * 3)).slice(0, limit);
|
|
56
127
|
}
|
|
@@ -101,10 +172,32 @@ export class RelayRuntime {
|
|
|
101
172
|
return this.publicInfo(session);
|
|
102
173
|
}
|
|
103
174
|
async newSession(options = {}) {
|
|
104
|
-
const session = await this.getSession(true);
|
|
175
|
+
const session = options.agentId ? await this.registry.switchAgent(WEB_CONTEXT_KEY, options.agentId) : await this.getSession(true);
|
|
105
176
|
this.ensureIdle(session);
|
|
177
|
+
if (options.reasoningEffort) {
|
|
178
|
+
const reasoningOptions = session.getInfo().agentId === "pi" ? PI_THINKING_LEVELS : CODEX_REASONING_EFFORTS;
|
|
179
|
+
if (!reasoningOptions.includes(options.reasoningEffort)) {
|
|
180
|
+
throw new Error(`Invalid ${agentReasoningLabel(session.getInfo().agentId)} value: ${options.reasoningEffort}`);
|
|
181
|
+
}
|
|
182
|
+
session.setReasoningEffort(options.reasoningEffort);
|
|
183
|
+
}
|
|
184
|
+
if (options.launchProfileId && (session.getInfo().capabilities ?? CODEX_AGENT_CAPABILITIES).launchProfiles) {
|
|
185
|
+
session.setLaunchProfile(options.launchProfileId);
|
|
186
|
+
}
|
|
187
|
+
if (typeof options.fastMode === "boolean" && (session.getInfo().capabilities ?? CODEX_AGENT_CAPABILITIES).fastMode) {
|
|
188
|
+
session.setFastMode(options.fastMode);
|
|
189
|
+
}
|
|
106
190
|
const info = await session.newThread(options.workspace, options.model);
|
|
107
191
|
this.updateSession(session);
|
|
192
|
+
this.appendActivity({
|
|
193
|
+
source: "web",
|
|
194
|
+
status: "info",
|
|
195
|
+
type: "session_new",
|
|
196
|
+
threadId: info.threadId,
|
|
197
|
+
workspace: info.workspace,
|
|
198
|
+
agentId: info.agentId,
|
|
199
|
+
detail: "New dashboard session created.",
|
|
200
|
+
});
|
|
108
201
|
return this.publicInfo(session);
|
|
109
202
|
}
|
|
110
203
|
async switchSession(threadId) {
|
|
@@ -112,6 +205,16 @@ export class RelayRuntime {
|
|
|
112
205
|
this.ensureIdle(session);
|
|
113
206
|
const info = await session.switchSession(threadId);
|
|
114
207
|
this.updateSession(session);
|
|
208
|
+
this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
|
|
209
|
+
this.appendActivity({
|
|
210
|
+
source: "web",
|
|
211
|
+
status: "info",
|
|
212
|
+
type: "session_switch",
|
|
213
|
+
threadId: info.threadId,
|
|
214
|
+
workspace: info.workspace,
|
|
215
|
+
agentId: info.agentId,
|
|
216
|
+
detail: "Dashboard switched session.",
|
|
217
|
+
});
|
|
115
218
|
return this.publicInfo(session);
|
|
116
219
|
}
|
|
117
220
|
async attachSession(threadId) {
|
|
@@ -240,6 +343,17 @@ export class RelayRuntime {
|
|
|
240
343
|
const session = await this.getSession(false);
|
|
241
344
|
if (session.isProcessing()) {
|
|
242
345
|
const queued = this.promptStore.enqueue(WEB_CONTEXT_KEY, envelope);
|
|
346
|
+
const info = this.publicInfo(session);
|
|
347
|
+
this.appendActivity({
|
|
348
|
+
source: "web",
|
|
349
|
+
status: "queued",
|
|
350
|
+
type: "prompt_queued",
|
|
351
|
+
threadId: info.threadId,
|
|
352
|
+
workspace: info.workspace,
|
|
353
|
+
agentId: info.agentId,
|
|
354
|
+
prompt: envelope.description,
|
|
355
|
+
detail: `Queued at position ${this.promptStore.list(WEB_CONTEXT_KEY).length}.`,
|
|
356
|
+
});
|
|
243
357
|
this.broadcastQueue();
|
|
244
358
|
return { queued: true, queueId: queued.id };
|
|
245
359
|
}
|
|
@@ -251,6 +365,9 @@ export class RelayRuntime {
|
|
|
251
365
|
queue() {
|
|
252
366
|
return this.promptStore.list(WEB_CONTEXT_KEY).map(queueItemDto);
|
|
253
367
|
}
|
|
368
|
+
queuePaused() {
|
|
369
|
+
return this.promptStore.isPaused(WEB_CONTEXT_KEY);
|
|
370
|
+
}
|
|
254
371
|
queueAction(action, id) {
|
|
255
372
|
if (action === "pause")
|
|
256
373
|
this.promptStore.pause(WEB_CONTEXT_KEY);
|
|
@@ -272,6 +389,14 @@ export class RelayRuntime {
|
|
|
272
389
|
this.promptStore.enqueueFront(WEB_CONTEXT_KEY, item);
|
|
273
390
|
void this.drainQueue().catch((error) => this.broadcastStatus(friendlyErrorText(error), "error"));
|
|
274
391
|
}
|
|
392
|
+
this.appendActivity({
|
|
393
|
+
source: "web",
|
|
394
|
+
status: "info",
|
|
395
|
+
type: "queue_updated",
|
|
396
|
+
threadId: null,
|
|
397
|
+
workspace: this.config.workspace,
|
|
398
|
+
detail: id ? `${action}: ${id}` : action,
|
|
399
|
+
});
|
|
275
400
|
this.broadcastQueue();
|
|
276
401
|
return this.queue();
|
|
277
402
|
}
|
|
@@ -298,6 +423,38 @@ export class RelayRuntime {
|
|
|
298
423
|
});
|
|
299
424
|
return bundle ? { path: bundle.localPath, name: bundle.name } : null;
|
|
300
425
|
}
|
|
426
|
+
async artifactPreview(turnId, relativePath) {
|
|
427
|
+
const report = await this.artifact(turnId);
|
|
428
|
+
const artifact = report?.artifacts.find((candidate) => candidate.relativePath.split(path.sep).join("/") === relativePath);
|
|
429
|
+
if (!artifact) {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
const extension = path.extname(artifact.name).toLowerCase();
|
|
433
|
+
if ([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"].includes(extension)) {
|
|
434
|
+
return {
|
|
435
|
+
kind: "image",
|
|
436
|
+
name: artifact.name,
|
|
437
|
+
sizeBytes: artifact.sizeBytes,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
if (!isPreviewableTextFile(extension, artifact.sizeBytes)) {
|
|
441
|
+
return {
|
|
442
|
+
kind: "unsupported",
|
|
443
|
+
name: artifact.name,
|
|
444
|
+
sizeBytes: artifact.sizeBytes,
|
|
445
|
+
detail: artifact.sizeBytes > MAX_TEXT_PREVIEW_BYTES ? "File is too large for inline preview." : "File type is not previewable.",
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
const buffer = await readFile(artifact.localPath);
|
|
449
|
+
const truncated = buffer.byteLength > MAX_TEXT_PREVIEW_BYTES;
|
|
450
|
+
return {
|
|
451
|
+
kind: "text",
|
|
452
|
+
name: artifact.name,
|
|
453
|
+
sizeBytes: artifact.sizeBytes,
|
|
454
|
+
truncated,
|
|
455
|
+
text: buffer.subarray(0, MAX_TEXT_PREVIEW_BYTES).toString("utf8"),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
301
458
|
async logs(target = "connector", lines = 100) {
|
|
302
459
|
if (target === "update") {
|
|
303
460
|
const { getUpdateLogPath } = await import("./operations.js");
|
|
@@ -305,10 +462,160 @@ export class RelayRuntime {
|
|
|
305
462
|
}
|
|
306
463
|
return readFormattedLogTail(lines);
|
|
307
464
|
}
|
|
465
|
+
restartConnector() {
|
|
466
|
+
spawnConnectorRestart();
|
|
467
|
+
this.broadcastStatus("Restart requested. The dashboard may disconnect briefly.", "warn");
|
|
468
|
+
this.appendActivity({
|
|
469
|
+
source: "web",
|
|
470
|
+
status: "info",
|
|
471
|
+
type: "restart_requested",
|
|
472
|
+
threadId: null,
|
|
473
|
+
workspace: this.config.workspace,
|
|
474
|
+
detail: "Dashboard requested a connector restart.",
|
|
475
|
+
});
|
|
476
|
+
return { ok: true, message: "Restart requested." };
|
|
477
|
+
}
|
|
308
478
|
dispose() {
|
|
479
|
+
if (this.externalMonitor) {
|
|
480
|
+
clearInterval(this.externalMonitor);
|
|
481
|
+
}
|
|
309
482
|
this.registry.disposeAll();
|
|
310
483
|
this.subscribers.clear();
|
|
311
484
|
}
|
|
485
|
+
async monitorExternalActivity() {
|
|
486
|
+
const session = await this.getSession(true);
|
|
487
|
+
const info = this.publicInfo(session);
|
|
488
|
+
if (!info.capabilities.externalActivity || info.agentId !== "codex" || !info.threadId || session.isProcessing()) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const snapshot = getThreadRolloutSnapshot(info.threadId, {
|
|
492
|
+
afterLine: this.externalMirror?.threadId === info.threadId ? this.externalMirror.lastLine : Number.MAX_SAFE_INTEGER,
|
|
493
|
+
staleAfterMs: this.config.codexExternalBusyStaleMs,
|
|
494
|
+
}) ?? getThreadRolloutSnapshot(info.threadId, {
|
|
495
|
+
staleAfterMs: this.config.codexExternalBusyStaleMs,
|
|
496
|
+
maxEvents: 0,
|
|
497
|
+
});
|
|
498
|
+
if (!snapshot) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (!this.externalMirror || this.externalMirror.threadId !== snapshot.threadId || this.externalMirror.rolloutPath !== snapshot.rolloutPath) {
|
|
502
|
+
this.externalMirror = {
|
|
503
|
+
threadId: snapshot.threadId,
|
|
504
|
+
rolloutPath: snapshot.rolloutPath,
|
|
505
|
+
lastLine: snapshot.lineCount,
|
|
506
|
+
turnId: snapshot.activity.turnId,
|
|
507
|
+
startedAt: snapshot.activity.startedAt?.toISOString() ?? null,
|
|
508
|
+
};
|
|
509
|
+
if (snapshot.activity.active) {
|
|
510
|
+
this.startExternalTurn(snapshot);
|
|
511
|
+
}
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const mirror = this.externalMirror;
|
|
515
|
+
if (snapshot.activity.active) {
|
|
516
|
+
if (mirror.turnId !== snapshot.activity.turnId) {
|
|
517
|
+
mirror.turnId = snapshot.activity.turnId;
|
|
518
|
+
mirror.startedAt = snapshot.activity.startedAt?.toISOString() ?? null;
|
|
519
|
+
mirror.latestAgentLine = undefined;
|
|
520
|
+
this.startExternalTurn(snapshot);
|
|
521
|
+
}
|
|
522
|
+
this.broadcastExternalEvents(snapshot, snapshot.events.filter((event) => event.lineNumber > mirror.lastLine));
|
|
523
|
+
mirror.lastLine = Math.max(mirror.lastLine, snapshot.lineCount);
|
|
524
|
+
mirror.latestStatus = externalStatusLine(snapshot, this.queue().length);
|
|
525
|
+
this.broadcastStatus(mirror.latestStatus, "info");
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const terminalEvent = [...snapshot.events].reverse().find((event) => event.kind === "task" && event.status && event.status !== "started");
|
|
529
|
+
if (terminalEvent && terminalEvent.lineNumber > mirror.lastLine) {
|
|
530
|
+
const finalAgent = snapshot.events.filter((event) => event.kind === "agent" && event.text).at(-1);
|
|
531
|
+
const finalText = finalAgent?.text ?? snapshot.latestAgentMessage;
|
|
532
|
+
const finalLine = finalAgent?.lineNumber ?? snapshot.lineCount;
|
|
533
|
+
if (finalText && finalLine !== mirror.latestAgentLine) {
|
|
534
|
+
this.chatStore.append({
|
|
535
|
+
threadId: snapshot.threadId,
|
|
536
|
+
role: "agent",
|
|
537
|
+
text: finalText,
|
|
538
|
+
source: "cli",
|
|
539
|
+
turnId: terminalEvent.turnId ?? undefined,
|
|
540
|
+
});
|
|
541
|
+
this.broadcast({ type: "text_delta", id: terminalEvent.turnId ?? "cli", delta: finalText });
|
|
542
|
+
mirror.latestAgentLine = finalLine;
|
|
543
|
+
}
|
|
544
|
+
const externalStartedAt = mirror.startedAt ? new Date(mirror.startedAt) : snapshot.activity.startedAt;
|
|
545
|
+
this.broadcast({
|
|
546
|
+
type: "turn_complete",
|
|
547
|
+
id: terminalEvent.turnId ?? "cli",
|
|
548
|
+
at: terminalEvent.timestamp?.toISOString() ?? new Date().toISOString(),
|
|
549
|
+
});
|
|
550
|
+
this.appendActivity({
|
|
551
|
+
source: "cli",
|
|
552
|
+
status: terminalEvent.status === "aborted" ? "aborted" : terminalEvent.status === "failed" ? "failed" : "completed",
|
|
553
|
+
type: "cli_turn_finished",
|
|
554
|
+
threadId: snapshot.threadId,
|
|
555
|
+
workspace: info.workspace,
|
|
556
|
+
agentId: info.agentId,
|
|
557
|
+
prompt: snapshot.latestUserMessage ?? undefined,
|
|
558
|
+
detail: `Codex CLI task ${terminalEvent.status ?? "finished"}.`,
|
|
559
|
+
durationMs: durationFromDates(externalStartedAt, terminalEvent.timestamp),
|
|
560
|
+
});
|
|
561
|
+
this.broadcastStatus(`Codex CLI task ${terminalEvent.status ?? "finished"}.`, terminalEvent.status === "failed" ? "error" : terminalEvent.status === "aborted" ? "warn" : "info");
|
|
562
|
+
this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
|
|
563
|
+
}
|
|
564
|
+
mirror.lastLine = Math.max(mirror.lastLine, snapshot.lineCount);
|
|
565
|
+
}
|
|
566
|
+
startExternalTurn(snapshot) {
|
|
567
|
+
const prompt = snapshot.latestUserMessage ?? "Codex CLI task";
|
|
568
|
+
this.chatStore.append({
|
|
569
|
+
threadId: snapshot.threadId,
|
|
570
|
+
role: "user",
|
|
571
|
+
text: prompt,
|
|
572
|
+
source: "cli",
|
|
573
|
+
turnId: snapshot.activity.turnId ?? undefined,
|
|
574
|
+
timestamp: snapshot.activity.startedAt?.toISOString(),
|
|
575
|
+
});
|
|
576
|
+
this.broadcast({
|
|
577
|
+
type: "turn_start",
|
|
578
|
+
id: snapshot.activity.turnId ?? "cli",
|
|
579
|
+
prompt,
|
|
580
|
+
at: snapshot.activity.startedAt?.toISOString() ?? new Date().toISOString(),
|
|
581
|
+
source: "cli",
|
|
582
|
+
});
|
|
583
|
+
this.appendActivity({
|
|
584
|
+
source: "cli",
|
|
585
|
+
status: "running",
|
|
586
|
+
type: "cli_turn_started",
|
|
587
|
+
threadId: snapshot.threadId,
|
|
588
|
+
prompt,
|
|
589
|
+
detail: `Rollout: ${snapshot.rolloutPath}`,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
broadcastExternalEvents(snapshot, events) {
|
|
593
|
+
for (const event of events) {
|
|
594
|
+
if (event.kind === "tool" && event.status === "started") {
|
|
595
|
+
this.broadcast({
|
|
596
|
+
type: "tool_start",
|
|
597
|
+
id: snapshot.activity.turnId ?? "cli",
|
|
598
|
+
toolCallId: `cli-${event.lineNumber}`,
|
|
599
|
+
toolName: event.toolName ?? "tool",
|
|
600
|
+
});
|
|
601
|
+
this.appendActivity({
|
|
602
|
+
source: "cli",
|
|
603
|
+
status: "running",
|
|
604
|
+
type: "cli_tool_started",
|
|
605
|
+
threadId: snapshot.threadId,
|
|
606
|
+
detail: event.toolName ?? "tool",
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
if (event.kind === "tool" && event.status === "finished") {
|
|
610
|
+
this.broadcast({
|
|
611
|
+
type: "tool_end",
|
|
612
|
+
id: snapshot.activity.turnId ?? "cli",
|
|
613
|
+
toolCallId: `cli-${event.lineNumber}`,
|
|
614
|
+
isError: false,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
312
619
|
async getSession(deferThreadStart) {
|
|
313
620
|
return this.registry.getOrCreate(WEB_CONTEXT_KEY, { deferThreadStart });
|
|
314
621
|
}
|
|
@@ -338,9 +645,28 @@ export class RelayRuntime {
|
|
|
338
645
|
}
|
|
339
646
|
const turnId = randomUUID().slice(0, 12);
|
|
340
647
|
this.currentTurnId = turnId;
|
|
648
|
+
this.currentTurnStartedAt = Date.now();
|
|
341
649
|
this.accumulatedText = "";
|
|
342
650
|
this.promptStore.setLastPrompt(WEB_CONTEXT_KEY, envelope);
|
|
343
|
-
|
|
651
|
+
const startedAt = new Date().toISOString();
|
|
652
|
+
this.chatStore.append({
|
|
653
|
+
threadId: info.threadId ?? "pending",
|
|
654
|
+
role: "user",
|
|
655
|
+
text: envelope.description,
|
|
656
|
+
source: "web",
|
|
657
|
+
turnId,
|
|
658
|
+
timestamp: startedAt,
|
|
659
|
+
});
|
|
660
|
+
this.appendActivity({
|
|
661
|
+
source: "web",
|
|
662
|
+
status: "running",
|
|
663
|
+
type: "prompt_started",
|
|
664
|
+
threadId: info.threadId,
|
|
665
|
+
workspace: info.workspace,
|
|
666
|
+
agentId: info.agentId,
|
|
667
|
+
prompt: envelope.description,
|
|
668
|
+
});
|
|
669
|
+
this.broadcast({ type: "turn_start", id: turnId, prompt: envelope.description, at: startedAt, source: "web" });
|
|
344
670
|
const callbacks = {
|
|
345
671
|
onTextDelta: (delta) => {
|
|
346
672
|
this.accumulatedText += delta;
|
|
@@ -356,10 +682,50 @@ export class RelayRuntime {
|
|
|
356
682
|
try {
|
|
357
683
|
await session.prompt(envelope.input, callbacks);
|
|
358
684
|
this.updateSession(session);
|
|
685
|
+
if (this.accumulatedText.trim()) {
|
|
686
|
+
this.chatStore.append({
|
|
687
|
+
threadId: info.threadId ?? "pending",
|
|
688
|
+
role: "agent",
|
|
689
|
+
text: this.accumulatedText,
|
|
690
|
+
source: "web",
|
|
691
|
+
turnId,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
this.appendActivity({
|
|
695
|
+
source: "web",
|
|
696
|
+
status: "completed",
|
|
697
|
+
type: "prompt_completed",
|
|
698
|
+
threadId: info.threadId,
|
|
699
|
+
workspace: info.workspace,
|
|
700
|
+
agentId: info.agentId,
|
|
701
|
+
prompt: envelope.description,
|
|
702
|
+
durationMs: Date.now() - this.currentTurnStartedAt,
|
|
703
|
+
});
|
|
359
704
|
this.broadcast({ type: "turn_complete", id: turnId, at: new Date().toISOString() });
|
|
705
|
+
this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
|
|
360
706
|
}
|
|
361
707
|
catch (error) {
|
|
362
|
-
|
|
708
|
+
const errorText = friendlyErrorText(error);
|
|
709
|
+
this.chatStore.append({
|
|
710
|
+
threadId: info.threadId ?? "pending",
|
|
711
|
+
role: "system",
|
|
712
|
+
text: `Error: ${errorText}`,
|
|
713
|
+
source: "web",
|
|
714
|
+
turnId,
|
|
715
|
+
});
|
|
716
|
+
this.appendActivity({
|
|
717
|
+
source: "web",
|
|
718
|
+
status: "failed",
|
|
719
|
+
type: "prompt_failed",
|
|
720
|
+
threadId: info.threadId,
|
|
721
|
+
workspace: info.workspace,
|
|
722
|
+
agentId: info.agentId,
|
|
723
|
+
prompt: envelope.description,
|
|
724
|
+
detail: errorText,
|
|
725
|
+
durationMs: Date.now() - this.currentTurnStartedAt,
|
|
726
|
+
});
|
|
727
|
+
this.broadcast({ type: "turn_error", id: turnId, error: errorText, at: new Date().toISOString() });
|
|
728
|
+
this.broadcast({ type: "chat_history", messages: await this.chatHistory() });
|
|
363
729
|
throw error;
|
|
364
730
|
}
|
|
365
731
|
finally {
|
|
@@ -391,8 +757,13 @@ export class RelayRuntime {
|
|
|
391
757
|
this.registry.updateMetadata(WEB_CONTEXT_KEY, session);
|
|
392
758
|
this.broadcast({ type: "session_update", session: this.publicInfo(session) });
|
|
393
759
|
}
|
|
760
|
+
appendActivity(input) {
|
|
761
|
+
const event = this.activityStore.append(input);
|
|
762
|
+
this.broadcast({ type: "activity_update", events: this.activity({ limit: 50 }) });
|
|
763
|
+
return event;
|
|
764
|
+
}
|
|
394
765
|
broadcastQueue() {
|
|
395
|
-
this.broadcast({ type: "queue_update", queue: this.queue() });
|
|
766
|
+
this.broadcast({ type: "queue_update", queue: this.queue(), paused: this.queuePaused() });
|
|
396
767
|
}
|
|
397
768
|
broadcastStatus(message, level = "info") {
|
|
398
769
|
this.broadcast({ type: "status", message, level, at: new Date().toISOString() });
|
|
@@ -444,6 +815,30 @@ function artifactDto(report) {
|
|
|
444
815
|
})),
|
|
445
816
|
};
|
|
446
817
|
}
|
|
818
|
+
function externalStatusLine(snapshot, queueLength) {
|
|
819
|
+
const elapsed = snapshot.activity.startedAt
|
|
820
|
+
? formatDuration((Date.now() - snapshot.activity.startedAt.getTime()) / 1000)
|
|
821
|
+
: "-";
|
|
822
|
+
const tool = snapshot.latestToolName ?? "-";
|
|
823
|
+
return `Codex CLI running · ${elapsed} · tool ${tool} · ${queueLength} queued`;
|
|
824
|
+
}
|
|
825
|
+
function durationFromDates(start, end) {
|
|
826
|
+
if (!start || !end) {
|
|
827
|
+
return undefined;
|
|
828
|
+
}
|
|
829
|
+
return Math.max(0, end.getTime() - start.getTime());
|
|
830
|
+
}
|
|
831
|
+
function formatDuration(seconds) {
|
|
832
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
833
|
+
return "-";
|
|
834
|
+
}
|
|
835
|
+
if (seconds < 60) {
|
|
836
|
+
return `${Math.round(seconds)}s`;
|
|
837
|
+
}
|
|
838
|
+
const minutes = Math.floor(seconds / 60);
|
|
839
|
+
const remainder = Math.round(seconds % 60);
|
|
840
|
+
return `${minutes}m ${remainder}s`;
|
|
841
|
+
}
|
|
447
842
|
function normalizeMimeType(value, name) {
|
|
448
843
|
const configured = value?.trim();
|
|
449
844
|
if (configured) {
|
|
@@ -477,3 +872,37 @@ function uploadFileDtos(files) {
|
|
|
477
872
|
sizeBytes: file.sizeBytes,
|
|
478
873
|
}));
|
|
479
874
|
}
|
|
875
|
+
function isPreviewableTextFile(extension, sizeBytes) {
|
|
876
|
+
if (sizeBytes > MAX_TEXT_PREVIEW_BYTES * 4) {
|
|
877
|
+
return false;
|
|
878
|
+
}
|
|
879
|
+
return [
|
|
880
|
+
"",
|
|
881
|
+
".c",
|
|
882
|
+
".conf",
|
|
883
|
+
".cpp",
|
|
884
|
+
".css",
|
|
885
|
+
".csv",
|
|
886
|
+
".env",
|
|
887
|
+
".go",
|
|
888
|
+
".html",
|
|
889
|
+
".java",
|
|
890
|
+
".js",
|
|
891
|
+
".json",
|
|
892
|
+
".jsx",
|
|
893
|
+
".log",
|
|
894
|
+
".md",
|
|
895
|
+
".py",
|
|
896
|
+
".rb",
|
|
897
|
+
".rs",
|
|
898
|
+
".sh",
|
|
899
|
+
".sql",
|
|
900
|
+
".toml",
|
|
901
|
+
".ts",
|
|
902
|
+
".tsx",
|
|
903
|
+
".txt",
|
|
904
|
+
".xml",
|
|
905
|
+
".yaml",
|
|
906
|
+
".yml",
|
|
907
|
+
].includes(extension);
|
|
908
|
+
}
|
package/dist/session-registry.js
CHANGED
|
@@ -8,12 +8,12 @@ export class SessionRegistry {
|
|
|
8
8
|
metadata = new Map();
|
|
9
9
|
store;
|
|
10
10
|
onRemoveCallback;
|
|
11
|
-
constructor(config) {
|
|
11
|
+
constructor(config, options = {}) {
|
|
12
12
|
this.config = config;
|
|
13
13
|
this.store = createDocumentStore({
|
|
14
14
|
workspace: config.workspace,
|
|
15
|
-
fileName: "contexts.json",
|
|
16
|
-
sqliteKey: "contexts",
|
|
15
|
+
fileName: options.fileName ?? "contexts.json",
|
|
16
|
+
sqliteKey: options.sqliteKey ?? "contexts",
|
|
17
17
|
backend: config.stateBackend,
|
|
18
18
|
});
|
|
19
19
|
this.loadPersistedMetadata();
|