@nordbyte/nordrelay 0.6.0 → 0.7.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 +17 -0
- package/README.md +67 -6
- package/dist/access-control.js +6 -1
- package/dist/activity-events.js +2 -2
- package/dist/bot-preferences.js +1 -0
- package/dist/bot.js +77 -6
- package/dist/channel-adapter.js +11 -5
- package/dist/channel-command-catalog.js +88 -0
- package/dist/channel-command-service.js +214 -1
- package/dist/channel-mirror-registry.js +77 -0
- package/dist/channel-peer-prompt.js +95 -0
- package/dist/channel-runtime.js +12 -5
- package/dist/codex-state.js +114 -78
- package/dist/config-metadata.js +15 -0
- package/dist/config.js +31 -6
- package/dist/context-key.js +10 -0
- package/dist/discord-bot.js +85 -26
- package/dist/discord-command-surface.js +11 -73
- package/dist/index.js +20 -0
- package/dist/metrics.js +46 -0
- package/dist/peer-auth.js +85 -0
- package/dist/peer-client.js +256 -0
- package/dist/peer-context.js +21 -0
- package/dist/peer-identity.js +127 -0
- package/dist/peer-runtime-service.js +636 -0
- package/dist/peer-server.js +220 -0
- package/dist/peer-store.js +294 -0
- package/dist/peer-types.js +52 -0
- package/dist/relay-runtime-helpers.js +208 -0
- package/dist/relay-runtime.js +72 -274
- package/dist/remote-prompt.js +98 -0
- package/dist/telegram-command-menu.js +3 -53
- package/dist/telegram-general-commands.js +14 -0
- package/dist/telegram-preference-commands.js +23 -127
- package/dist/web-api-contract.js +8 -0
- package/dist/web-dashboard-pages.js +12 -0
- package/dist/web-dashboard-peer-routes.js +204 -0
- package/dist/web-dashboard-ui.js +1 -0
- package/dist/web-dashboard.js +12 -0
- package/dist/webui-assets/dashboard.js +427 -14
- package/package.json +3 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +373 -7
package/dist/relay-runtime.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import path from "node:path";
|
|
3
2
|
import { ensureOutDir } from "./artifacts.js";
|
|
4
3
|
import { buildFileInstructions, outboxPath, stageFile, } from "./attachments.js";
|
|
5
4
|
import { CODEX_AGENT_CAPABILITIES, agentLabel, agentReasoningLabel, agentReasoningOptions, isAgentId, } from "./agent.js";
|
|
@@ -10,6 +9,7 @@ import { createAgentSessionService, enabledAgents } from "./agent-factory.js";
|
|
|
10
9
|
import { AuditLogStore } from "./audit-log.js";
|
|
11
10
|
import { BotPreferencesStore } from "./bot-preferences.js";
|
|
12
11
|
import { ChannelTurnService } from "./channel-turn-service.js";
|
|
12
|
+
import { activeSessionSourceForContextKey, ChannelMirrorRegistry } from "./channel-mirror-registry.js";
|
|
13
13
|
import { checkAuthStatus, startLogin as startCodexLogin, startLogout as startCodexLogout } from "./codex-auth.js";
|
|
14
14
|
import { checkClaudeCodeAuthStatus, startClaudeCodeLogin, startClaudeCodeLogout } from "./claude-code-auth.js";
|
|
15
15
|
import { listThreads as listCodexThreads } from "./codex-state.js";
|
|
@@ -25,21 +25,22 @@ import { RelayArtifactService } from "./relay-artifact-service.js";
|
|
|
25
25
|
import { RelayExternalActivityMonitor } from "./relay-external-activity-monitor.js";
|
|
26
26
|
import { RelayQueueService } from "./relay-queue-service.js";
|
|
27
27
|
import { RuntimeSnapshotCache } from "./runtime-cache.js";
|
|
28
|
+
import { activeSessionPriority, activityToUnifiedJob, agentUpdateStatusToUnified, cliHealthForAgent, dedupeJobs, hostLoginCommand, hostLogoutCommand, isPromptTerminalActivity, normalizeMimeType, promptActivityToUnifiedJob, shouldRefreshActiveSessions, taskToUnifiedJob, uploadFileDtos, versionCheckForAgent, } from "./relay-runtime-helpers.js";
|
|
28
29
|
import { renderSessionInfoPlain, renderSessionUsageRows } from "./session-format.js";
|
|
29
30
|
import { SessionLockStore } from "./session-locks.js";
|
|
30
31
|
import { SessionRegistry } from "./session-registry.js";
|
|
31
32
|
import { createSupportBundle } from "./support-bundle.js";
|
|
32
33
|
import { transcribeAudio } from "./voice.js";
|
|
33
34
|
import { WebActivityStore, WebChatStore, } from "./web-state.js";
|
|
34
|
-
import { channelIdForContextKey } from "./context-key.js";
|
|
35
35
|
import { evaluateWorkspacePolicy, filterAllowedWorkspaces } from "./workspace-policy.js";
|
|
36
|
-
const WEB_CONTEXT_KEY = "web:dashboard";
|
|
36
|
+
export const WEB_CONTEXT_KEY = "web:dashboard";
|
|
37
37
|
const ACTIVE_CODEX_DISCOVERY_LIMIT = 200;
|
|
38
38
|
const ACTIVE_ACTIVITY_TTL_MS = 6 * 60 * 60 * 1000;
|
|
39
39
|
const MAX_WEB_SESSION_PAGE_SIZE = 50;
|
|
40
40
|
const MAX_CHAT_HISTORY = 250;
|
|
41
41
|
export class RelayRuntime {
|
|
42
42
|
config;
|
|
43
|
+
contextKey;
|
|
43
44
|
registry;
|
|
44
45
|
promptStore;
|
|
45
46
|
chatStore;
|
|
@@ -50,6 +51,7 @@ export class RelayRuntime {
|
|
|
50
51
|
queueService;
|
|
51
52
|
jobStore;
|
|
52
53
|
artifactService;
|
|
54
|
+
mirrorRegistry;
|
|
53
55
|
externalActivityMonitor;
|
|
54
56
|
cache = new RuntimeSnapshotCache();
|
|
55
57
|
turnService;
|
|
@@ -57,25 +59,29 @@ export class RelayRuntime {
|
|
|
57
59
|
agentUpdateActors = new Map();
|
|
58
60
|
agentUpdateStates = new Map();
|
|
59
61
|
externalMonitor;
|
|
62
|
+
activeSessionsBroadcastTimer = null;
|
|
63
|
+
activeSessionsLastBroadcastAt = 0;
|
|
60
64
|
draining = false;
|
|
61
65
|
currentTurnId = null;
|
|
62
66
|
accumulatedText = "";
|
|
63
67
|
currentTurnStartedAt = 0;
|
|
64
68
|
currentProgress = null;
|
|
65
|
-
constructor(config) {
|
|
69
|
+
constructor(config, options = {}) {
|
|
66
70
|
this.config = config;
|
|
71
|
+
this.contextKey = options.contextKey ?? WEB_CONTEXT_KEY;
|
|
67
72
|
this.registry = new SessionRegistry(config, {
|
|
68
|
-
fileName: "web-contexts.json",
|
|
69
|
-
sqliteKey: "web-contexts",
|
|
73
|
+
fileName: options.registryFileName ?? "web-contexts.json",
|
|
74
|
+
sqliteKey: options.registrySqliteKey ?? "web-contexts",
|
|
70
75
|
});
|
|
71
76
|
this.promptStore = new PromptStore(config.workspace, config.stateBackend);
|
|
72
77
|
this.chatStore = new WebChatStore(config.workspace, config.stateBackend, MAX_CHAT_HISTORY);
|
|
73
78
|
this.activityStore = new WebActivityStore(config.workspace, config.stateBackend, config.auditMaxEvents);
|
|
74
79
|
this.auditStore = new AuditLogStore(config.workspace, config.stateBackend, config.auditMaxEvents);
|
|
75
80
|
this.lockStore = new SessionLockStore(config.workspace, config.stateBackend);
|
|
76
|
-
this.queueService = new RelayQueueService(this.promptStore,
|
|
81
|
+
this.queueService = new RelayQueueService(this.promptStore, this.contextKey);
|
|
77
82
|
this.jobStore = new UnifiedJobStore(config.workspace, config.stateBackend, config.unifiedJobMaxItems);
|
|
78
83
|
this.artifactService = new RelayArtifactService(config);
|
|
84
|
+
this.mirrorRegistry = new ChannelMirrorRegistry(config, this.promptStore);
|
|
79
85
|
this.agentUpdates = new AgentUpdateManager({
|
|
80
86
|
onUpdate: (job) => {
|
|
81
87
|
this.broadcast({ type: "agent_update", job });
|
|
@@ -103,7 +109,7 @@ export class RelayRuntime {
|
|
|
103
109
|
}
|
|
104
110
|
this.turnService = new ChannelTurnService({
|
|
105
111
|
source: "web",
|
|
106
|
-
contextKey:
|
|
112
|
+
contextKey: this.contextKey,
|
|
107
113
|
chatStore: this.chatStore,
|
|
108
114
|
artifactService: this.artifactService,
|
|
109
115
|
checkAuth: (info) => this.checkAgentAuth(info),
|
|
@@ -136,6 +142,7 @@ export class RelayRuntime {
|
|
|
136
142
|
this.subscribers.add(callback);
|
|
137
143
|
void this.snapshot().then((data) => callback({ type: "snapshot", data })).catch(() => { });
|
|
138
144
|
void this.chatHistory().then((messages) => callback({ type: "chat_history", messages })).catch(() => { });
|
|
145
|
+
void this.activeSessions().then((active) => callback({ type: "active_sessions_update", active })).catch(() => { });
|
|
139
146
|
callback({ type: "activity_update", events: this.activity({ limit: 50 }) });
|
|
140
147
|
return () => this.subscribers.delete(callback);
|
|
141
148
|
}
|
|
@@ -205,7 +212,7 @@ export class RelayRuntime {
|
|
|
205
212
|
this.appendAudit({
|
|
206
213
|
action: "command",
|
|
207
214
|
status: "ok",
|
|
208
|
-
contextKey:
|
|
215
|
+
contextKey: this.contextKey,
|
|
209
216
|
actor,
|
|
210
217
|
description: "update",
|
|
211
218
|
detail: update.summary,
|
|
@@ -242,7 +249,7 @@ export class RelayRuntime {
|
|
|
242
249
|
this.appendAudit({
|
|
243
250
|
action: "command",
|
|
244
251
|
status: "ok",
|
|
245
|
-
contextKey:
|
|
252
|
+
contextKey: this.contextKey,
|
|
246
253
|
agentId,
|
|
247
254
|
actor,
|
|
248
255
|
description: `${operation} ${agentId}`,
|
|
@@ -268,7 +275,7 @@ export class RelayRuntime {
|
|
|
268
275
|
this.appendAudit({
|
|
269
276
|
action: "command",
|
|
270
277
|
status: "ok",
|
|
271
|
-
contextKey:
|
|
278
|
+
contextKey: this.contextKey,
|
|
272
279
|
agentId: job.agentId,
|
|
273
280
|
actor,
|
|
274
281
|
description: `delete update log ${id}`,
|
|
@@ -566,8 +573,8 @@ export class RelayRuntime {
|
|
|
566
573
|
if (this.currentProgress?.status === "running") {
|
|
567
574
|
addActiveSession({
|
|
568
575
|
...this.currentProgress,
|
|
569
|
-
contextKey:
|
|
570
|
-
sourceContextKey:
|
|
576
|
+
contextKey: this.contextKey,
|
|
577
|
+
sourceContextKey: this.contextKey,
|
|
571
578
|
source: "web",
|
|
572
579
|
status: "running",
|
|
573
580
|
queueLength: this.queueService.length(),
|
|
@@ -581,7 +588,7 @@ export class RelayRuntime {
|
|
|
581
588
|
addActiveSession(active);
|
|
582
589
|
}
|
|
583
590
|
for (const meta of knownContexts) {
|
|
584
|
-
if (meta.contextKey ===
|
|
591
|
+
if (meta.contextKey === this.contextKey && this.currentProgress?.status === "running") {
|
|
585
592
|
continue;
|
|
586
593
|
}
|
|
587
594
|
const active = this.externalActiveSession(meta, knownContexts, preferences);
|
|
@@ -631,7 +638,7 @@ export class RelayRuntime {
|
|
|
631
638
|
this.appendAudit({
|
|
632
639
|
action: "command",
|
|
633
640
|
status: "ok",
|
|
634
|
-
contextKey:
|
|
641
|
+
contextKey: this.contextKey,
|
|
635
642
|
actor,
|
|
636
643
|
description: "export diagnostics bundle",
|
|
637
644
|
detail: bundle.path,
|
|
@@ -643,7 +650,7 @@ export class RelayRuntime {
|
|
|
643
650
|
}
|
|
644
651
|
lockWebSession(ownerName = "Web dashboard", actor) {
|
|
645
652
|
const label = ownerName || actor?.label || "Web dashboard";
|
|
646
|
-
const lock = this.lockStore.set(
|
|
653
|
+
const lock = this.lockStore.set(this.contextKey, {
|
|
647
654
|
userId: actor?.id ?? "web",
|
|
648
655
|
label,
|
|
649
656
|
channel: "web",
|
|
@@ -660,7 +667,7 @@ export class RelayRuntime {
|
|
|
660
667
|
this.appendAudit({
|
|
661
668
|
action: "lock_updated",
|
|
662
669
|
status: "ok",
|
|
663
|
-
contextKey:
|
|
670
|
+
contextKey: this.contextKey,
|
|
664
671
|
actor,
|
|
665
672
|
description: "lock",
|
|
666
673
|
detail: `locked by ${label}`,
|
|
@@ -668,7 +675,7 @@ export class RelayRuntime {
|
|
|
668
675
|
return lock;
|
|
669
676
|
}
|
|
670
677
|
unlockWebSession(actor) {
|
|
671
|
-
const removed = this.lockStore.clear(
|
|
678
|
+
const removed = this.lockStore.clear(this.contextKey);
|
|
672
679
|
this.appendActivity({
|
|
673
680
|
source: "web",
|
|
674
681
|
status: "info",
|
|
@@ -681,7 +688,7 @@ export class RelayRuntime {
|
|
|
681
688
|
this.appendAudit({
|
|
682
689
|
action: "lock_updated",
|
|
683
690
|
status: "ok",
|
|
684
|
-
contextKey:
|
|
691
|
+
contextKey: this.contextKey,
|
|
685
692
|
actor,
|
|
686
693
|
description: "unlock",
|
|
687
694
|
detail: removed ? "unlocked" : "no lock",
|
|
@@ -788,7 +795,7 @@ export class RelayRuntime {
|
|
|
788
795
|
this.appendAudit({
|
|
789
796
|
action: "command",
|
|
790
797
|
status: result.success ? "ok" : "failed",
|
|
791
|
-
contextKey:
|
|
798
|
+
contextKey: this.contextKey,
|
|
792
799
|
agentId: info.agentId,
|
|
793
800
|
threadId: info.threadId,
|
|
794
801
|
workspace: info.workspace,
|
|
@@ -851,7 +858,7 @@ export class RelayRuntime {
|
|
|
851
858
|
this.appendAudit({
|
|
852
859
|
action: "command",
|
|
853
860
|
status: result.success ? "ok" : "failed",
|
|
854
|
-
contextKey:
|
|
861
|
+
contextKey: this.contextKey,
|
|
855
862
|
agentId: info.agentId,
|
|
856
863
|
threadId: info.threadId,
|
|
857
864
|
workspace: info.workspace,
|
|
@@ -902,7 +909,7 @@ export class RelayRuntime {
|
|
|
902
909
|
return { removed, messages };
|
|
903
910
|
}
|
|
904
911
|
activity(options = {}) {
|
|
905
|
-
const currentInfo = this.registry.get(
|
|
912
|
+
const currentInfo = this.registry.get(this.contextKey)?.getInfo();
|
|
906
913
|
return this.activityStore.list(options).map((event) => this.enrichActivityEvent(event, currentInfo));
|
|
907
914
|
}
|
|
908
915
|
async retry(actor) {
|
|
@@ -913,7 +920,7 @@ export class RelayRuntime {
|
|
|
913
920
|
this.appendAudit({
|
|
914
921
|
action: "command",
|
|
915
922
|
status: "ok",
|
|
916
|
-
contextKey:
|
|
923
|
+
contextKey: this.contextKey,
|
|
917
924
|
actor,
|
|
918
925
|
description: "retry",
|
|
919
926
|
detail: cached.description,
|
|
@@ -943,7 +950,7 @@ export class RelayRuntime {
|
|
|
943
950
|
this.appendAudit({
|
|
944
951
|
action: "command",
|
|
945
952
|
status: "ok",
|
|
946
|
-
contextKey:
|
|
953
|
+
contextKey: this.contextKey,
|
|
947
954
|
agentId: result.info.agentId,
|
|
948
955
|
threadId: result.info.threadId,
|
|
949
956
|
workspace: result.info.workspace,
|
|
@@ -1018,7 +1025,7 @@ export class RelayRuntime {
|
|
|
1018
1025
|
if (!enabledAgents(this.config).includes(agentId)) {
|
|
1019
1026
|
throw new Error(`Agent is not enabled: ${agentId}`);
|
|
1020
1027
|
}
|
|
1021
|
-
const session = await this.registry.switchAgent(
|
|
1028
|
+
const session = await this.registry.switchAgent(this.contextKey, agentId);
|
|
1022
1029
|
this.updateSession(session);
|
|
1023
1030
|
const info = this.publicInfo(session);
|
|
1024
1031
|
this.appendActivity({
|
|
@@ -1034,7 +1041,7 @@ export class RelayRuntime {
|
|
|
1034
1041
|
return this.publicInfo(session);
|
|
1035
1042
|
}
|
|
1036
1043
|
async newSession(options = {}, actor) {
|
|
1037
|
-
const session = options.agentId ? await this.registry.switchAgent(
|
|
1044
|
+
const session = options.agentId ? await this.registry.switchAgent(this.contextKey, options.agentId) : await this.getSession(true);
|
|
1038
1045
|
this.ensureIdle(session);
|
|
1039
1046
|
if (options.reasoningEffort) {
|
|
1040
1047
|
const reasoningOptions = agentReasoningOptions(session.getInfo().agentId);
|
|
@@ -1273,7 +1280,7 @@ export class RelayRuntime {
|
|
|
1273
1280
|
this.appendAudit({
|
|
1274
1281
|
action: "prompt_queued",
|
|
1275
1282
|
status: "ok",
|
|
1276
|
-
contextKey:
|
|
1283
|
+
contextKey: this.contextKey,
|
|
1277
1284
|
agentId: info.agentId,
|
|
1278
1285
|
threadId: info.threadId,
|
|
1279
1286
|
workspace: info.workspace,
|
|
@@ -1318,7 +1325,7 @@ export class RelayRuntime {
|
|
|
1318
1325
|
this.appendAudit({
|
|
1319
1326
|
action: "queue_updated",
|
|
1320
1327
|
status: "ok",
|
|
1321
|
-
contextKey:
|
|
1328
|
+
contextKey: this.contextKey,
|
|
1322
1329
|
actor,
|
|
1323
1330
|
description: id ? `${action}: ${id}` : action,
|
|
1324
1331
|
});
|
|
@@ -1394,7 +1401,7 @@ export class RelayRuntime {
|
|
|
1394
1401
|
this.appendAudit({
|
|
1395
1402
|
action: "command",
|
|
1396
1403
|
status: "ok",
|
|
1397
|
-
contextKey:
|
|
1404
|
+
contextKey: this.contextKey,
|
|
1398
1405
|
actor,
|
|
1399
1406
|
description: `clear ${target} log`,
|
|
1400
1407
|
detail: result.filePath,
|
|
@@ -1416,7 +1423,7 @@ export class RelayRuntime {
|
|
|
1416
1423
|
this.appendAudit({
|
|
1417
1424
|
action: "command",
|
|
1418
1425
|
status: "ok",
|
|
1419
|
-
contextKey:
|
|
1426
|
+
contextKey: this.contextKey,
|
|
1420
1427
|
actor,
|
|
1421
1428
|
description: "restart connector",
|
|
1422
1429
|
});
|
|
@@ -1431,7 +1438,7 @@ export class RelayRuntime {
|
|
|
1431
1438
|
this.subscribers.clear();
|
|
1432
1439
|
}
|
|
1433
1440
|
async getSession(deferThreadStart) {
|
|
1434
|
-
return this.registry.getOrCreate(
|
|
1441
|
+
return this.registry.getOrCreate(this.contextKey, { deferThreadStart });
|
|
1435
1442
|
}
|
|
1436
1443
|
async cached(key, producer) {
|
|
1437
1444
|
return (await this.cache.get(key, this.config.dashboardCacheTtlMs, producer)).value;
|
|
@@ -1455,10 +1462,10 @@ export class RelayRuntime {
|
|
|
1455
1462
|
finally {
|
|
1456
1463
|
sharedRegistry.disposeAll();
|
|
1457
1464
|
}
|
|
1458
|
-
const current = this.registry.get(
|
|
1465
|
+
const current = this.registry.get(this.contextKey)?.getInfo();
|
|
1459
1466
|
if (current) {
|
|
1460
1467
|
add({
|
|
1461
|
-
contextKey:
|
|
1468
|
+
contextKey: this.contextKey,
|
|
1462
1469
|
agentId: current.agentId,
|
|
1463
1470
|
threadId: current.threadId,
|
|
1464
1471
|
workspace: current.workspace,
|
|
@@ -1524,7 +1531,12 @@ export class RelayRuntime {
|
|
|
1524
1531
|
return [];
|
|
1525
1532
|
}
|
|
1526
1533
|
const active = [];
|
|
1534
|
+
const nowMs = Date.now();
|
|
1535
|
+
const staleAfterMs = this.config.codexExternalBusyStaleMs;
|
|
1527
1536
|
for (const thread of listCodexThreads(ACTIVE_CODEX_DISCOVERY_LIMIT)) {
|
|
1537
|
+
if (staleAfterMs > 0 && nowMs - thread.updatedAt.getTime() > staleAfterMs) {
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1528
1540
|
const meta = {
|
|
1529
1541
|
contextKey: `cli:codex:${thread.id}`,
|
|
1530
1542
|
agentId: "codex",
|
|
@@ -1553,6 +1565,12 @@ export class RelayRuntime {
|
|
|
1553
1565
|
if (!capabilities.externalActivity) {
|
|
1554
1566
|
return null;
|
|
1555
1567
|
}
|
|
1568
|
+
if (agentId === "codex" &&
|
|
1569
|
+
meta.updatedAt &&
|
|
1570
|
+
this.config.codexExternalBusyStaleMs > 0 &&
|
|
1571
|
+
Date.now() - meta.updatedAt > this.config.codexExternalBusyStaleMs) {
|
|
1572
|
+
return null;
|
|
1573
|
+
}
|
|
1556
1574
|
const snapshot = getExternalSnapshotForSession(this.sessionStubForMetadata(meta, agentId, capabilities), this.config, {
|
|
1557
1575
|
maxEvents: 8,
|
|
1558
1576
|
});
|
|
@@ -1563,8 +1581,8 @@ export class RelayRuntime {
|
|
|
1563
1581
|
const updatedAt = snapshot.activity.updatedAt?.toISOString() ?? new Date().toISOString();
|
|
1564
1582
|
const startedMs = Date.parse(startedAt);
|
|
1565
1583
|
const sourceContextKey = `cli:${snapshot.agentId}:${snapshot.threadId}`;
|
|
1566
|
-
const mirrorChannels = this.
|
|
1567
|
-
const queueLength =
|
|
1584
|
+
const mirrorChannels = this.mirrorRegistry.activeMirrorsForThread(snapshot.agentId, snapshot.threadId, knownContexts, preferences);
|
|
1585
|
+
const queueLength = this.mirrorRegistry.queueLengthForExternalSource(sourceContextKey, mirrorChannels);
|
|
1568
1586
|
const mirrorDetail = mirrorChannels.length > 0
|
|
1569
1587
|
? `Mirroring: ${mirrorChannels.map((mirror) => `${mirror.source} ${mirror.mode}`).join(", ")}`
|
|
1570
1588
|
: "Mirroring: none";
|
|
@@ -1585,42 +1603,11 @@ export class RelayRuntime {
|
|
|
1585
1603
|
updatedAt,
|
|
1586
1604
|
durationMs: Number.isFinite(startedMs) ? Math.max(0, Date.now() - startedMs) : 0,
|
|
1587
1605
|
queueLength,
|
|
1588
|
-
queuePaused:
|
|
1606
|
+
queuePaused: this.mirrorRegistry.queuePausedForExternalSource(sourceContextKey, mirrorChannels),
|
|
1589
1607
|
mirrorChannels,
|
|
1590
1608
|
detail: `${mirrorDetail} | ${snapshot.sourceLabel}: ${snapshot.sourcePath}`,
|
|
1591
1609
|
};
|
|
1592
1610
|
}
|
|
1593
|
-
activeMirrorChannels(agentId, threadId, knownContexts, preferences) {
|
|
1594
|
-
const mirrors = [];
|
|
1595
|
-
const seen = new Set();
|
|
1596
|
-
for (const meta of knownContexts) {
|
|
1597
|
-
const metaAgentId = meta.agentId ?? this.config.defaultAgent ?? "codex";
|
|
1598
|
-
if (meta.threadId !== threadId || metaAgentId !== agentId) {
|
|
1599
|
-
continue;
|
|
1600
|
-
}
|
|
1601
|
-
const source = activeSessionSourceForContext(meta.contextKey);
|
|
1602
|
-
if (source !== "telegram" && source !== "discord") {
|
|
1603
|
-
continue;
|
|
1604
|
-
}
|
|
1605
|
-
const mode = this.effectiveMirrorMode(meta.contextKey, source, preferences);
|
|
1606
|
-
if (mode === "off" || seen.has(meta.contextKey)) {
|
|
1607
|
-
continue;
|
|
1608
|
-
}
|
|
1609
|
-
seen.add(meta.contextKey);
|
|
1610
|
-
mirrors.push({
|
|
1611
|
-
source,
|
|
1612
|
-
contextKey: meta.contextKey,
|
|
1613
|
-
mode,
|
|
1614
|
-
queueLength: this.promptStore.list(meta.contextKey).length,
|
|
1615
|
-
queuePaused: this.promptStore.isPaused(meta.contextKey),
|
|
1616
|
-
});
|
|
1617
|
-
}
|
|
1618
|
-
return mirrors;
|
|
1619
|
-
}
|
|
1620
|
-
effectiveMirrorMode(contextKey, source, preferences) {
|
|
1621
|
-
const configured = source === "telegram" ? this.config.telegramMirrorMode : this.config.discordMirrorMode;
|
|
1622
|
-
return preferences.get(contextKey).mirrorMode ?? configured;
|
|
1623
|
-
}
|
|
1624
1611
|
sessionStubForMetadata(meta, agentId, capabilities) {
|
|
1625
1612
|
const info = {
|
|
1626
1613
|
agentId,
|
|
@@ -1785,7 +1772,7 @@ export class RelayRuntime {
|
|
|
1785
1772
|
}
|
|
1786
1773
|
}
|
|
1787
1774
|
updateSession(session) {
|
|
1788
|
-
this.registry.updateMetadata(
|
|
1775
|
+
this.registry.updateMetadata(this.contextKey, session);
|
|
1789
1776
|
this.broadcast({ type: "session_update", session: this.publicInfo(session) });
|
|
1790
1777
|
}
|
|
1791
1778
|
recordActivity(input) {
|
|
@@ -1891,6 +1878,23 @@ export class RelayRuntime {
|
|
|
1891
1878
|
this.subscribers.delete(subscriber);
|
|
1892
1879
|
}
|
|
1893
1880
|
}
|
|
1881
|
+
if (shouldRefreshActiveSessions(event)) {
|
|
1882
|
+
this.scheduleActiveSessionsBroadcast();
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
scheduleActiveSessionsBroadcast() {
|
|
1886
|
+
if (this.activeSessionsBroadcastTimer) {
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
const delayMs = Math.max(0, 1_000 - (Date.now() - this.activeSessionsLastBroadcastAt));
|
|
1890
|
+
this.activeSessionsBroadcastTimer = setTimeout(() => {
|
|
1891
|
+
this.activeSessionsBroadcastTimer = null;
|
|
1892
|
+
this.activeSessionsLastBroadcastAt = Date.now();
|
|
1893
|
+
void this.activeSessions()
|
|
1894
|
+
.then((active) => this.broadcast({ type: "active_sessions_update", active }))
|
|
1895
|
+
.catch(() => { });
|
|
1896
|
+
}, delayMs);
|
|
1897
|
+
this.activeSessionsBroadcastTimer.unref?.();
|
|
1894
1898
|
}
|
|
1895
1899
|
publicInfo(session) {
|
|
1896
1900
|
const info = session.getInfo();
|
|
@@ -1903,209 +1907,3 @@ export class RelayRuntime {
|
|
|
1903
1907
|
};
|
|
1904
1908
|
}
|
|
1905
1909
|
}
|
|
1906
|
-
function cliHealthForAgent(agentId, health) {
|
|
1907
|
-
if (agentId === "pi") {
|
|
1908
|
-
return { path: health.piCliPath, label: health.piCli, version: health.piCliVersion };
|
|
1909
|
-
}
|
|
1910
|
-
if (agentId === "hermes") {
|
|
1911
|
-
return { path: health.hermesCliPath, label: health.hermesCli, version: health.hermesCliVersion };
|
|
1912
|
-
}
|
|
1913
|
-
if (agentId === "openclaw") {
|
|
1914
|
-
return { path: health.openClawCliPath, label: health.openClawCli, version: health.openClawCliVersion };
|
|
1915
|
-
}
|
|
1916
|
-
if (agentId === "claude-code") {
|
|
1917
|
-
return { path: health.claudeCodeCliPath, label: health.claudeCodeCli, version: health.claudeCodeCliVersion };
|
|
1918
|
-
}
|
|
1919
|
-
return { path: health.codexCliPath, label: health.codexCli, version: health.codexCliVersion };
|
|
1920
|
-
}
|
|
1921
|
-
function versionCheckForAgent(agentId, versions) {
|
|
1922
|
-
if (agentId === "pi")
|
|
1923
|
-
return versions.pi;
|
|
1924
|
-
if (agentId === "hermes")
|
|
1925
|
-
return versions.hermes;
|
|
1926
|
-
if (agentId === "openclaw")
|
|
1927
|
-
return versions.openclaw;
|
|
1928
|
-
if (agentId === "claude-code")
|
|
1929
|
-
return versions.claudeCode;
|
|
1930
|
-
return versions.codex;
|
|
1931
|
-
}
|
|
1932
|
-
function hostLoginCommand(info, config) {
|
|
1933
|
-
if (info.agentId === "hermes") {
|
|
1934
|
-
return `${config.hermesCliPath ?? "hermes"} login --no-browser`;
|
|
1935
|
-
}
|
|
1936
|
-
if (info.agentId === "claude-code") {
|
|
1937
|
-
return `${config.claudeCodeCliPath ?? "claude"} auth login`;
|
|
1938
|
-
}
|
|
1939
|
-
if (info.agentId === "pi") {
|
|
1940
|
-
return `${config.piCliPath ?? "pi"} auth login`;
|
|
1941
|
-
}
|
|
1942
|
-
if (info.agentId === "openclaw") {
|
|
1943
|
-
return `${config.openClawCliPath ?? "openclaw"} login`;
|
|
1944
|
-
}
|
|
1945
|
-
return "codex login --device-auth";
|
|
1946
|
-
}
|
|
1947
|
-
function hostLogoutCommand(info, config) {
|
|
1948
|
-
if (info.agentId === "hermes") {
|
|
1949
|
-
return `${config.hermesCliPath ?? "hermes"} logout`;
|
|
1950
|
-
}
|
|
1951
|
-
if (info.agentId === "claude-code") {
|
|
1952
|
-
return `${config.claudeCodeCliPath ?? "claude"} auth logout`;
|
|
1953
|
-
}
|
|
1954
|
-
if (info.agentId === "pi") {
|
|
1955
|
-
return `${config.piCliPath ?? "pi"} auth logout`;
|
|
1956
|
-
}
|
|
1957
|
-
if (info.agentId === "openclaw") {
|
|
1958
|
-
return `${config.openClawCliPath ?? "openclaw"} logout`;
|
|
1959
|
-
}
|
|
1960
|
-
return "codex logout";
|
|
1961
|
-
}
|
|
1962
|
-
function activeSessionSourceForContext(contextKey) {
|
|
1963
|
-
const channelId = channelIdForContextKey(contextKey);
|
|
1964
|
-
if (channelId === "telegram") {
|
|
1965
|
-
return "telegram";
|
|
1966
|
-
}
|
|
1967
|
-
if (channelId === "discord") {
|
|
1968
|
-
return "discord";
|
|
1969
|
-
}
|
|
1970
|
-
if (channelId === "web") {
|
|
1971
|
-
return "web";
|
|
1972
|
-
}
|
|
1973
|
-
return "cli";
|
|
1974
|
-
}
|
|
1975
|
-
function activeSessionPriority(session) {
|
|
1976
|
-
if (session.status === "running") {
|
|
1977
|
-
return 3;
|
|
1978
|
-
}
|
|
1979
|
-
return session.contextKey.startsWith("cli:") ? 1 : 2;
|
|
1980
|
-
}
|
|
1981
|
-
function isPromptTerminalActivity(event) {
|
|
1982
|
-
return event.status === "completed" ||
|
|
1983
|
-
event.status === "failed" ||
|
|
1984
|
-
event.status === "aborted" ||
|
|
1985
|
-
event.type === "prompt_completed" ||
|
|
1986
|
-
event.type === "prompt_failed" ||
|
|
1987
|
-
event.type === "prompt_aborted";
|
|
1988
|
-
}
|
|
1989
|
-
function taskToUnifiedJob(id, kind, title, task, options) {
|
|
1990
|
-
return {
|
|
1991
|
-
id,
|
|
1992
|
-
kind,
|
|
1993
|
-
title,
|
|
1994
|
-
status: task.status,
|
|
1995
|
-
source: task.source,
|
|
1996
|
-
agentId: task.agentId,
|
|
1997
|
-
agentLabel: task.agentLabel,
|
|
1998
|
-
threadId: task.threadId,
|
|
1999
|
-
workspace: task.workspace,
|
|
2000
|
-
startedAt: task.startedAt,
|
|
2001
|
-
updatedAt: task.updatedAt,
|
|
2002
|
-
durationMs: task.durationMs,
|
|
2003
|
-
summary: task.prompt || task.detail,
|
|
2004
|
-
logTail: task.currentTool || task.lastTool ? `Current tool: ${task.currentTool ?? "-"}\nLast tool: ${task.lastTool ?? "-"}` : undefined,
|
|
2005
|
-
...options,
|
|
2006
|
-
};
|
|
2007
|
-
}
|
|
2008
|
-
function activityToUnifiedJob(event, kind, title, options) {
|
|
2009
|
-
return {
|
|
2010
|
-
id: `${kind}:${event.id}`,
|
|
2011
|
-
kind,
|
|
2012
|
-
title,
|
|
2013
|
-
status: event.status,
|
|
2014
|
-
source: event.source,
|
|
2015
|
-
agentId: event.agentId,
|
|
2016
|
-
threadId: event.threadId,
|
|
2017
|
-
workspace: event.workspace,
|
|
2018
|
-
owner: event.actor,
|
|
2019
|
-
startedAt: event.timestamp,
|
|
2020
|
-
updatedAt: event.timestamp,
|
|
2021
|
-
finishedAt: event.timestamp,
|
|
2022
|
-
durationMs: event.durationMs,
|
|
2023
|
-
summary: event.prompt || event.detail,
|
|
2024
|
-
logPath: event.detail,
|
|
2025
|
-
logTail: event.detail,
|
|
2026
|
-
...options,
|
|
2027
|
-
};
|
|
2028
|
-
}
|
|
2029
|
-
function promptActivityToUnifiedJob(event) {
|
|
2030
|
-
const status = event.status === "info" ? "completed" : event.status;
|
|
2031
|
-
const sourceLabel = event.source === "web"
|
|
2032
|
-
? "WebUI"
|
|
2033
|
-
: event.source === "telegram"
|
|
2034
|
-
? "Telegram"
|
|
2035
|
-
: event.source === "discord"
|
|
2036
|
-
? "Discord"
|
|
2037
|
-
: "CLI";
|
|
2038
|
-
const promptKey = event.threadId ?? event.contextKey ?? event.id;
|
|
2039
|
-
return {
|
|
2040
|
-
id: `prompt:${event.source}:${promptKey}:${event.id}`,
|
|
2041
|
-
kind: event.source === "cli" ? "external-turn" : "web-turn",
|
|
2042
|
-
title: `${sourceLabel} prompt`,
|
|
2043
|
-
status,
|
|
2044
|
-
source: event.source,
|
|
2045
|
-
agentId: event.agentId,
|
|
2046
|
-
threadId: event.threadId,
|
|
2047
|
-
workspace: event.workspace,
|
|
2048
|
-
owner: event.actor,
|
|
2049
|
-
startedAt: event.timestamp,
|
|
2050
|
-
updatedAt: event.timestamp,
|
|
2051
|
-
finishedAt: status === "running" || status === "queued" ? undefined : event.timestamp,
|
|
2052
|
-
durationMs: event.durationMs,
|
|
2053
|
-
summary: event.prompt || event.detail,
|
|
2054
|
-
logTail: event.detail,
|
|
2055
|
-
canCancel: status === "running" && event.source === "web",
|
|
2056
|
-
canRetry: status !== "running",
|
|
2057
|
-
canReadLog: Boolean(event.detail || event.prompt),
|
|
2058
|
-
};
|
|
2059
|
-
}
|
|
2060
|
-
function agentUpdateStatusToUnified(status) {
|
|
2061
|
-
if (status === "cancelled")
|
|
2062
|
-
return "aborted";
|
|
2063
|
-
if (status === "running")
|
|
2064
|
-
return "running";
|
|
2065
|
-
if (status === "completed")
|
|
2066
|
-
return "completed";
|
|
2067
|
-
return "failed";
|
|
2068
|
-
}
|
|
2069
|
-
function dedupeJobs(jobs) {
|
|
2070
|
-
const seen = new Set();
|
|
2071
|
-
return jobs.filter((job) => {
|
|
2072
|
-
if (seen.has(job.id)) {
|
|
2073
|
-
return false;
|
|
2074
|
-
}
|
|
2075
|
-
seen.add(job.id);
|
|
2076
|
-
return true;
|
|
2077
|
-
});
|
|
2078
|
-
}
|
|
2079
|
-
function normalizeMimeType(value, name) {
|
|
2080
|
-
const configured = value?.trim();
|
|
2081
|
-
if (configured) {
|
|
2082
|
-
return configured;
|
|
2083
|
-
}
|
|
2084
|
-
const extension = path.extname(name).toLowerCase();
|
|
2085
|
-
if ([".jpg", ".jpeg"].includes(extension))
|
|
2086
|
-
return "image/jpeg";
|
|
2087
|
-
if (extension === ".png")
|
|
2088
|
-
return "image/png";
|
|
2089
|
-
if (extension === ".gif")
|
|
2090
|
-
return "image/gif";
|
|
2091
|
-
if (extension === ".webp")
|
|
2092
|
-
return "image/webp";
|
|
2093
|
-
if (extension === ".mp3")
|
|
2094
|
-
return "audio/mpeg";
|
|
2095
|
-
if (extension === ".wav")
|
|
2096
|
-
return "audio/wav";
|
|
2097
|
-
if (extension === ".ogg" || extension === ".oga")
|
|
2098
|
-
return "audio/ogg";
|
|
2099
|
-
if (extension === ".m4a")
|
|
2100
|
-
return "audio/mp4";
|
|
2101
|
-
if (extension === ".webm")
|
|
2102
|
-
return "audio/webm";
|
|
2103
|
-
return "application/octet-stream";
|
|
2104
|
-
}
|
|
2105
|
-
function uploadFileDtos(files) {
|
|
2106
|
-
return files.map((file) => ({
|
|
2107
|
-
name: file.safeName,
|
|
2108
|
-
mimeType: file.mimeType,
|
|
2109
|
-
sizeBytes: file.sizeBytes,
|
|
2110
|
-
}));
|
|
2111
|
-
}
|