@nordbyte/nordrelay 0.6.0 → 0.8.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 +52 -0
- package/README.md +171 -50
- package/dist/access-control.js +6 -1
- package/dist/activity-events.js +2 -2
- package/dist/adapter-conformance.js +61 -0
- package/dist/bot-preferences.js +1 -0
- package/dist/bot.js +95 -37
- package/dist/channel-adapter.js +44 -11
- package/dist/channel-command-catalog.js +94 -0
- package/dist/channel-command-core.js +60 -0
- package/dist/channel-command-service.js +230 -1
- package/dist/channel-mirror-registry.js +84 -0
- package/dist/channel-peer-prompt.js +95 -0
- package/dist/channel-prompt-engine.js +177 -0
- package/dist/channel-runtime.js +12 -5
- package/dist/channel-turn-lifecycle.js +73 -0
- package/dist/codex-state.js +114 -78
- package/dist/config-metadata.js +82 -8
- package/dist/config.js +79 -7
- package/dist/context-key.js +42 -0
- package/dist/discord-bot.js +173 -342
- package/dist/discord-command-surface.js +11 -73
- package/dist/index.js +29 -0
- package/dist/metrics.js +48 -0
- package/dist/peer-auth.js +85 -0
- package/dist/peer-client.js +288 -0
- package/dist/peer-context.js +21 -0
- package/dist/peer-identity.js +127 -0
- package/dist/peer-readiness.js +77 -0
- package/dist/peer-runtime-service.js +658 -0
- package/dist/peer-server.js +220 -0
- package/dist/peer-store.js +307 -0
- package/dist/peer-types.js +52 -0
- package/dist/relay-runtime-helpers.js +210 -0
- package/dist/relay-runtime.js +79 -274
- package/dist/remote-prompt.js +98 -0
- package/dist/settings-wizard-test.js +216 -0
- package/dist/slack-artifacts.js +165 -0
- package/dist/slack-bot.js +1461 -0
- package/dist/slack-channel-runtime.js +147 -0
- package/dist/slack-command-surface.js +46 -0
- package/dist/slack-diagnostics.js +116 -0
- package/dist/slack-rate-limit.js +139 -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/user-management-crypto.js +38 -0
- package/dist/user-management-normalize.js +188 -0
- package/dist/user-management-types.js +1 -0
- package/dist/user-management.js +193 -196
- package/dist/web-api-contract.js +16 -0
- package/dist/web-dashboard-access-routes.js +62 -0
- package/dist/web-dashboard-assets.js +1 -0
- package/dist/web-dashboard-pages.js +26 -4
- package/dist/web-dashboard-peer-routes.js +225 -0
- package/dist/web-dashboard-ui.js +1 -0
- package/dist/web-dashboard.js +46 -0
- package/dist/web-state.js +2 -2
- package/dist/webui-assets/dashboard.css +193 -0
- package/dist/webui-assets/dashboard.js +870 -57
- package/package.json +5 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +468 -11
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,24 @@ 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";
|
|
32
|
+
import { collectSlackDiagnostics } from "./slack-diagnostics.js";
|
|
33
|
+
import { getSlackRateLimitMetrics } from "./slack-rate-limit.js";
|
|
31
34
|
import { createSupportBundle } from "./support-bundle.js";
|
|
32
35
|
import { transcribeAudio } from "./voice.js";
|
|
33
36
|
import { WebActivityStore, WebChatStore, } from "./web-state.js";
|
|
34
|
-
import { channelIdForContextKey } from "./context-key.js";
|
|
35
37
|
import { evaluateWorkspacePolicy, filterAllowedWorkspaces } from "./workspace-policy.js";
|
|
36
|
-
const WEB_CONTEXT_KEY = "web:dashboard";
|
|
38
|
+
export const WEB_CONTEXT_KEY = "web:dashboard";
|
|
37
39
|
const ACTIVE_CODEX_DISCOVERY_LIMIT = 200;
|
|
38
40
|
const ACTIVE_ACTIVITY_TTL_MS = 6 * 60 * 60 * 1000;
|
|
39
41
|
const MAX_WEB_SESSION_PAGE_SIZE = 50;
|
|
40
42
|
const MAX_CHAT_HISTORY = 250;
|
|
41
43
|
export class RelayRuntime {
|
|
42
44
|
config;
|
|
45
|
+
contextKey;
|
|
43
46
|
registry;
|
|
44
47
|
promptStore;
|
|
45
48
|
chatStore;
|
|
@@ -50,6 +53,7 @@ export class RelayRuntime {
|
|
|
50
53
|
queueService;
|
|
51
54
|
jobStore;
|
|
52
55
|
artifactService;
|
|
56
|
+
mirrorRegistry;
|
|
53
57
|
externalActivityMonitor;
|
|
54
58
|
cache = new RuntimeSnapshotCache();
|
|
55
59
|
turnService;
|
|
@@ -57,25 +61,29 @@ export class RelayRuntime {
|
|
|
57
61
|
agentUpdateActors = new Map();
|
|
58
62
|
agentUpdateStates = new Map();
|
|
59
63
|
externalMonitor;
|
|
64
|
+
activeSessionsBroadcastTimer = null;
|
|
65
|
+
activeSessionsLastBroadcastAt = 0;
|
|
60
66
|
draining = false;
|
|
61
67
|
currentTurnId = null;
|
|
62
68
|
accumulatedText = "";
|
|
63
69
|
currentTurnStartedAt = 0;
|
|
64
70
|
currentProgress = null;
|
|
65
|
-
constructor(config) {
|
|
71
|
+
constructor(config, options = {}) {
|
|
66
72
|
this.config = config;
|
|
73
|
+
this.contextKey = options.contextKey ?? WEB_CONTEXT_KEY;
|
|
67
74
|
this.registry = new SessionRegistry(config, {
|
|
68
|
-
fileName: "web-contexts.json",
|
|
69
|
-
sqliteKey: "web-contexts",
|
|
75
|
+
fileName: options.registryFileName ?? "web-contexts.json",
|
|
76
|
+
sqliteKey: options.registrySqliteKey ?? "web-contexts",
|
|
70
77
|
});
|
|
71
78
|
this.promptStore = new PromptStore(config.workspace, config.stateBackend);
|
|
72
79
|
this.chatStore = new WebChatStore(config.workspace, config.stateBackend, MAX_CHAT_HISTORY);
|
|
73
80
|
this.activityStore = new WebActivityStore(config.workspace, config.stateBackend, config.auditMaxEvents);
|
|
74
81
|
this.auditStore = new AuditLogStore(config.workspace, config.stateBackend, config.auditMaxEvents);
|
|
75
82
|
this.lockStore = new SessionLockStore(config.workspace, config.stateBackend);
|
|
76
|
-
this.queueService = new RelayQueueService(this.promptStore,
|
|
83
|
+
this.queueService = new RelayQueueService(this.promptStore, this.contextKey);
|
|
77
84
|
this.jobStore = new UnifiedJobStore(config.workspace, config.stateBackend, config.unifiedJobMaxItems);
|
|
78
85
|
this.artifactService = new RelayArtifactService(config);
|
|
86
|
+
this.mirrorRegistry = new ChannelMirrorRegistry(config, this.promptStore);
|
|
79
87
|
this.agentUpdates = new AgentUpdateManager({
|
|
80
88
|
onUpdate: (job) => {
|
|
81
89
|
this.broadcast({ type: "agent_update", job });
|
|
@@ -103,7 +111,7 @@ export class RelayRuntime {
|
|
|
103
111
|
}
|
|
104
112
|
this.turnService = new ChannelTurnService({
|
|
105
113
|
source: "web",
|
|
106
|
-
contextKey:
|
|
114
|
+
contextKey: this.contextKey,
|
|
107
115
|
chatStore: this.chatStore,
|
|
108
116
|
artifactService: this.artifactService,
|
|
109
117
|
checkAuth: (info) => this.checkAgentAuth(info),
|
|
@@ -136,6 +144,7 @@ export class RelayRuntime {
|
|
|
136
144
|
this.subscribers.add(callback);
|
|
137
145
|
void this.snapshot().then((data) => callback({ type: "snapshot", data })).catch(() => { });
|
|
138
146
|
void this.chatHistory().then((messages) => callback({ type: "chat_history", messages })).catch(() => { });
|
|
147
|
+
void this.activeSessions().then((active) => callback({ type: "active_sessions_update", active })).catch(() => { });
|
|
139
148
|
callback({ type: "activity_update", events: this.activity({ limit: 50 }) });
|
|
140
149
|
return () => this.subscribers.delete(callback);
|
|
141
150
|
}
|
|
@@ -205,7 +214,7 @@ export class RelayRuntime {
|
|
|
205
214
|
this.appendAudit({
|
|
206
215
|
action: "command",
|
|
207
216
|
status: "ok",
|
|
208
|
-
contextKey:
|
|
217
|
+
contextKey: this.contextKey,
|
|
209
218
|
actor,
|
|
210
219
|
description: "update",
|
|
211
220
|
detail: update.summary,
|
|
@@ -242,7 +251,7 @@ export class RelayRuntime {
|
|
|
242
251
|
this.appendAudit({
|
|
243
252
|
action: "command",
|
|
244
253
|
status: "ok",
|
|
245
|
-
contextKey:
|
|
254
|
+
contextKey: this.contextKey,
|
|
246
255
|
agentId,
|
|
247
256
|
actor,
|
|
248
257
|
description: `${operation} ${agentId}`,
|
|
@@ -268,7 +277,7 @@ export class RelayRuntime {
|
|
|
268
277
|
this.appendAudit({
|
|
269
278
|
action: "command",
|
|
270
279
|
status: "ok",
|
|
271
|
-
contextKey:
|
|
280
|
+
contextKey: this.contextKey,
|
|
272
281
|
agentId: job.agentId,
|
|
273
282
|
actor,
|
|
274
283
|
description: `delete update log ${id}`,
|
|
@@ -323,6 +332,11 @@ export class RelayRuntime {
|
|
|
323
332
|
queuePaused: this.queueService.isPaused(),
|
|
324
333
|
externalMirror: this.externalActivityMonitor.snapshot(),
|
|
325
334
|
agentDiagnostics: getAgentDiagnostics(session, this.config),
|
|
335
|
+
slackDiagnostics: await collectSlackDiagnostics({
|
|
336
|
+
config: this.config,
|
|
337
|
+
timeoutMs: 2_500,
|
|
338
|
+
rateLimit: getSlackRateLimitMetrics(),
|
|
339
|
+
}),
|
|
326
340
|
},
|
|
327
341
|
};
|
|
328
342
|
});
|
|
@@ -566,8 +580,8 @@ export class RelayRuntime {
|
|
|
566
580
|
if (this.currentProgress?.status === "running") {
|
|
567
581
|
addActiveSession({
|
|
568
582
|
...this.currentProgress,
|
|
569
|
-
contextKey:
|
|
570
|
-
sourceContextKey:
|
|
583
|
+
contextKey: this.contextKey,
|
|
584
|
+
sourceContextKey: this.contextKey,
|
|
571
585
|
source: "web",
|
|
572
586
|
status: "running",
|
|
573
587
|
queueLength: this.queueService.length(),
|
|
@@ -581,7 +595,7 @@ export class RelayRuntime {
|
|
|
581
595
|
addActiveSession(active);
|
|
582
596
|
}
|
|
583
597
|
for (const meta of knownContexts) {
|
|
584
|
-
if (meta.contextKey ===
|
|
598
|
+
if (meta.contextKey === this.contextKey && this.currentProgress?.status === "running") {
|
|
585
599
|
continue;
|
|
586
600
|
}
|
|
587
601
|
const active = this.externalActiveSession(meta, knownContexts, preferences);
|
|
@@ -631,7 +645,7 @@ export class RelayRuntime {
|
|
|
631
645
|
this.appendAudit({
|
|
632
646
|
action: "command",
|
|
633
647
|
status: "ok",
|
|
634
|
-
contextKey:
|
|
648
|
+
contextKey: this.contextKey,
|
|
635
649
|
actor,
|
|
636
650
|
description: "export diagnostics bundle",
|
|
637
651
|
detail: bundle.path,
|
|
@@ -643,7 +657,7 @@ export class RelayRuntime {
|
|
|
643
657
|
}
|
|
644
658
|
lockWebSession(ownerName = "Web dashboard", actor) {
|
|
645
659
|
const label = ownerName || actor?.label || "Web dashboard";
|
|
646
|
-
const lock = this.lockStore.set(
|
|
660
|
+
const lock = this.lockStore.set(this.contextKey, {
|
|
647
661
|
userId: actor?.id ?? "web",
|
|
648
662
|
label,
|
|
649
663
|
channel: "web",
|
|
@@ -660,7 +674,7 @@ export class RelayRuntime {
|
|
|
660
674
|
this.appendAudit({
|
|
661
675
|
action: "lock_updated",
|
|
662
676
|
status: "ok",
|
|
663
|
-
contextKey:
|
|
677
|
+
contextKey: this.contextKey,
|
|
664
678
|
actor,
|
|
665
679
|
description: "lock",
|
|
666
680
|
detail: `locked by ${label}`,
|
|
@@ -668,7 +682,7 @@ export class RelayRuntime {
|
|
|
668
682
|
return lock;
|
|
669
683
|
}
|
|
670
684
|
unlockWebSession(actor) {
|
|
671
|
-
const removed = this.lockStore.clear(
|
|
685
|
+
const removed = this.lockStore.clear(this.contextKey);
|
|
672
686
|
this.appendActivity({
|
|
673
687
|
source: "web",
|
|
674
688
|
status: "info",
|
|
@@ -681,7 +695,7 @@ export class RelayRuntime {
|
|
|
681
695
|
this.appendAudit({
|
|
682
696
|
action: "lock_updated",
|
|
683
697
|
status: "ok",
|
|
684
|
-
contextKey:
|
|
698
|
+
contextKey: this.contextKey,
|
|
685
699
|
actor,
|
|
686
700
|
description: "unlock",
|
|
687
701
|
detail: removed ? "unlocked" : "no lock",
|
|
@@ -788,7 +802,7 @@ export class RelayRuntime {
|
|
|
788
802
|
this.appendAudit({
|
|
789
803
|
action: "command",
|
|
790
804
|
status: result.success ? "ok" : "failed",
|
|
791
|
-
contextKey:
|
|
805
|
+
contextKey: this.contextKey,
|
|
792
806
|
agentId: info.agentId,
|
|
793
807
|
threadId: info.threadId,
|
|
794
808
|
workspace: info.workspace,
|
|
@@ -851,7 +865,7 @@ export class RelayRuntime {
|
|
|
851
865
|
this.appendAudit({
|
|
852
866
|
action: "command",
|
|
853
867
|
status: result.success ? "ok" : "failed",
|
|
854
|
-
contextKey:
|
|
868
|
+
contextKey: this.contextKey,
|
|
855
869
|
agentId: info.agentId,
|
|
856
870
|
threadId: info.threadId,
|
|
857
871
|
workspace: info.workspace,
|
|
@@ -902,7 +916,7 @@ export class RelayRuntime {
|
|
|
902
916
|
return { removed, messages };
|
|
903
917
|
}
|
|
904
918
|
activity(options = {}) {
|
|
905
|
-
const currentInfo = this.registry.get(
|
|
919
|
+
const currentInfo = this.registry.get(this.contextKey)?.getInfo();
|
|
906
920
|
return this.activityStore.list(options).map((event) => this.enrichActivityEvent(event, currentInfo));
|
|
907
921
|
}
|
|
908
922
|
async retry(actor) {
|
|
@@ -913,7 +927,7 @@ export class RelayRuntime {
|
|
|
913
927
|
this.appendAudit({
|
|
914
928
|
action: "command",
|
|
915
929
|
status: "ok",
|
|
916
|
-
contextKey:
|
|
930
|
+
contextKey: this.contextKey,
|
|
917
931
|
actor,
|
|
918
932
|
description: "retry",
|
|
919
933
|
detail: cached.description,
|
|
@@ -943,7 +957,7 @@ export class RelayRuntime {
|
|
|
943
957
|
this.appendAudit({
|
|
944
958
|
action: "command",
|
|
945
959
|
status: "ok",
|
|
946
|
-
contextKey:
|
|
960
|
+
contextKey: this.contextKey,
|
|
947
961
|
agentId: result.info.agentId,
|
|
948
962
|
threadId: result.info.threadId,
|
|
949
963
|
workspace: result.info.workspace,
|
|
@@ -1018,7 +1032,7 @@ export class RelayRuntime {
|
|
|
1018
1032
|
if (!enabledAgents(this.config).includes(agentId)) {
|
|
1019
1033
|
throw new Error(`Agent is not enabled: ${agentId}`);
|
|
1020
1034
|
}
|
|
1021
|
-
const session = await this.registry.switchAgent(
|
|
1035
|
+
const session = await this.registry.switchAgent(this.contextKey, agentId);
|
|
1022
1036
|
this.updateSession(session);
|
|
1023
1037
|
const info = this.publicInfo(session);
|
|
1024
1038
|
this.appendActivity({
|
|
@@ -1034,7 +1048,7 @@ export class RelayRuntime {
|
|
|
1034
1048
|
return this.publicInfo(session);
|
|
1035
1049
|
}
|
|
1036
1050
|
async newSession(options = {}, actor) {
|
|
1037
|
-
const session = options.agentId ? await this.registry.switchAgent(
|
|
1051
|
+
const session = options.agentId ? await this.registry.switchAgent(this.contextKey, options.agentId) : await this.getSession(true);
|
|
1038
1052
|
this.ensureIdle(session);
|
|
1039
1053
|
if (options.reasoningEffort) {
|
|
1040
1054
|
const reasoningOptions = agentReasoningOptions(session.getInfo().agentId);
|
|
@@ -1273,7 +1287,7 @@ export class RelayRuntime {
|
|
|
1273
1287
|
this.appendAudit({
|
|
1274
1288
|
action: "prompt_queued",
|
|
1275
1289
|
status: "ok",
|
|
1276
|
-
contextKey:
|
|
1290
|
+
contextKey: this.contextKey,
|
|
1277
1291
|
agentId: info.agentId,
|
|
1278
1292
|
threadId: info.threadId,
|
|
1279
1293
|
workspace: info.workspace,
|
|
@@ -1318,7 +1332,7 @@ export class RelayRuntime {
|
|
|
1318
1332
|
this.appendAudit({
|
|
1319
1333
|
action: "queue_updated",
|
|
1320
1334
|
status: "ok",
|
|
1321
|
-
contextKey:
|
|
1335
|
+
contextKey: this.contextKey,
|
|
1322
1336
|
actor,
|
|
1323
1337
|
description: id ? `${action}: ${id}` : action,
|
|
1324
1338
|
});
|
|
@@ -1394,7 +1408,7 @@ export class RelayRuntime {
|
|
|
1394
1408
|
this.appendAudit({
|
|
1395
1409
|
action: "command",
|
|
1396
1410
|
status: "ok",
|
|
1397
|
-
contextKey:
|
|
1411
|
+
contextKey: this.contextKey,
|
|
1398
1412
|
actor,
|
|
1399
1413
|
description: `clear ${target} log`,
|
|
1400
1414
|
detail: result.filePath,
|
|
@@ -1416,7 +1430,7 @@ export class RelayRuntime {
|
|
|
1416
1430
|
this.appendAudit({
|
|
1417
1431
|
action: "command",
|
|
1418
1432
|
status: "ok",
|
|
1419
|
-
contextKey:
|
|
1433
|
+
contextKey: this.contextKey,
|
|
1420
1434
|
actor,
|
|
1421
1435
|
description: "restart connector",
|
|
1422
1436
|
});
|
|
@@ -1431,7 +1445,7 @@ export class RelayRuntime {
|
|
|
1431
1445
|
this.subscribers.clear();
|
|
1432
1446
|
}
|
|
1433
1447
|
async getSession(deferThreadStart) {
|
|
1434
|
-
return this.registry.getOrCreate(
|
|
1448
|
+
return this.registry.getOrCreate(this.contextKey, { deferThreadStart });
|
|
1435
1449
|
}
|
|
1436
1450
|
async cached(key, producer) {
|
|
1437
1451
|
return (await this.cache.get(key, this.config.dashboardCacheTtlMs, producer)).value;
|
|
@@ -1455,10 +1469,10 @@ export class RelayRuntime {
|
|
|
1455
1469
|
finally {
|
|
1456
1470
|
sharedRegistry.disposeAll();
|
|
1457
1471
|
}
|
|
1458
|
-
const current = this.registry.get(
|
|
1472
|
+
const current = this.registry.get(this.contextKey)?.getInfo();
|
|
1459
1473
|
if (current) {
|
|
1460
1474
|
add({
|
|
1461
|
-
contextKey:
|
|
1475
|
+
contextKey: this.contextKey,
|
|
1462
1476
|
agentId: current.agentId,
|
|
1463
1477
|
threadId: current.threadId,
|
|
1464
1478
|
workspace: current.workspace,
|
|
@@ -1524,7 +1538,12 @@ export class RelayRuntime {
|
|
|
1524
1538
|
return [];
|
|
1525
1539
|
}
|
|
1526
1540
|
const active = [];
|
|
1541
|
+
const nowMs = Date.now();
|
|
1542
|
+
const staleAfterMs = this.config.codexExternalBusyStaleMs;
|
|
1527
1543
|
for (const thread of listCodexThreads(ACTIVE_CODEX_DISCOVERY_LIMIT)) {
|
|
1544
|
+
if (staleAfterMs > 0 && nowMs - thread.updatedAt.getTime() > staleAfterMs) {
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1528
1547
|
const meta = {
|
|
1529
1548
|
contextKey: `cli:codex:${thread.id}`,
|
|
1530
1549
|
agentId: "codex",
|
|
@@ -1553,6 +1572,12 @@ export class RelayRuntime {
|
|
|
1553
1572
|
if (!capabilities.externalActivity) {
|
|
1554
1573
|
return null;
|
|
1555
1574
|
}
|
|
1575
|
+
if (agentId === "codex" &&
|
|
1576
|
+
meta.updatedAt &&
|
|
1577
|
+
this.config.codexExternalBusyStaleMs > 0 &&
|
|
1578
|
+
Date.now() - meta.updatedAt > this.config.codexExternalBusyStaleMs) {
|
|
1579
|
+
return null;
|
|
1580
|
+
}
|
|
1556
1581
|
const snapshot = getExternalSnapshotForSession(this.sessionStubForMetadata(meta, agentId, capabilities), this.config, {
|
|
1557
1582
|
maxEvents: 8,
|
|
1558
1583
|
});
|
|
@@ -1563,8 +1588,8 @@ export class RelayRuntime {
|
|
|
1563
1588
|
const updatedAt = snapshot.activity.updatedAt?.toISOString() ?? new Date().toISOString();
|
|
1564
1589
|
const startedMs = Date.parse(startedAt);
|
|
1565
1590
|
const sourceContextKey = `cli:${snapshot.agentId}:${snapshot.threadId}`;
|
|
1566
|
-
const mirrorChannels = this.
|
|
1567
|
-
const queueLength =
|
|
1591
|
+
const mirrorChannels = this.mirrorRegistry.activeMirrorsForThread(snapshot.agentId, snapshot.threadId, knownContexts, preferences);
|
|
1592
|
+
const queueLength = this.mirrorRegistry.queueLengthForExternalSource(sourceContextKey, mirrorChannels);
|
|
1568
1593
|
const mirrorDetail = mirrorChannels.length > 0
|
|
1569
1594
|
? `Mirroring: ${mirrorChannels.map((mirror) => `${mirror.source} ${mirror.mode}`).join(", ")}`
|
|
1570
1595
|
: "Mirroring: none";
|
|
@@ -1585,42 +1610,11 @@ export class RelayRuntime {
|
|
|
1585
1610
|
updatedAt,
|
|
1586
1611
|
durationMs: Number.isFinite(startedMs) ? Math.max(0, Date.now() - startedMs) : 0,
|
|
1587
1612
|
queueLength,
|
|
1588
|
-
queuePaused:
|
|
1613
|
+
queuePaused: this.mirrorRegistry.queuePausedForExternalSource(sourceContextKey, mirrorChannels),
|
|
1589
1614
|
mirrorChannels,
|
|
1590
1615
|
detail: `${mirrorDetail} | ${snapshot.sourceLabel}: ${snapshot.sourcePath}`,
|
|
1591
1616
|
};
|
|
1592
1617
|
}
|
|
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
1618
|
sessionStubForMetadata(meta, agentId, capabilities) {
|
|
1625
1619
|
const info = {
|
|
1626
1620
|
agentId,
|
|
@@ -1785,7 +1779,7 @@ export class RelayRuntime {
|
|
|
1785
1779
|
}
|
|
1786
1780
|
}
|
|
1787
1781
|
updateSession(session) {
|
|
1788
|
-
this.registry.updateMetadata(
|
|
1782
|
+
this.registry.updateMetadata(this.contextKey, session);
|
|
1789
1783
|
this.broadcast({ type: "session_update", session: this.publicInfo(session) });
|
|
1790
1784
|
}
|
|
1791
1785
|
recordActivity(input) {
|
|
@@ -1891,6 +1885,23 @@ export class RelayRuntime {
|
|
|
1891
1885
|
this.subscribers.delete(subscriber);
|
|
1892
1886
|
}
|
|
1893
1887
|
}
|
|
1888
|
+
if (shouldRefreshActiveSessions(event)) {
|
|
1889
|
+
this.scheduleActiveSessionsBroadcast();
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
scheduleActiveSessionsBroadcast() {
|
|
1893
|
+
if (this.activeSessionsBroadcastTimer) {
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
const delayMs = Math.max(0, 1_000 - (Date.now() - this.activeSessionsLastBroadcastAt));
|
|
1897
|
+
this.activeSessionsBroadcastTimer = setTimeout(() => {
|
|
1898
|
+
this.activeSessionsBroadcastTimer = null;
|
|
1899
|
+
this.activeSessionsLastBroadcastAt = Date.now();
|
|
1900
|
+
void this.activeSessions()
|
|
1901
|
+
.then((active) => this.broadcast({ type: "active_sessions_update", active }))
|
|
1902
|
+
.catch(() => { });
|
|
1903
|
+
}, delayMs);
|
|
1904
|
+
this.activeSessionsBroadcastTimer.unref?.();
|
|
1894
1905
|
}
|
|
1895
1906
|
publicInfo(session) {
|
|
1896
1907
|
const info = session.getInfo();
|
|
@@ -1903,209 +1914,3 @@ export class RelayRuntime {
|
|
|
1903
1914
|
};
|
|
1904
1915
|
}
|
|
1905
1916
|
}
|
|
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
|
-
}
|