@nordbyte/nordrelay 0.4.0 → 0.4.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.
@@ -6,6 +6,7 @@ import { buildFileInstructions, outboxPath, stageFile, } from "./attachments.js"
6
6
  import { CODEX_AGENT_CAPABILITIES, agentLabel, agentReasoningLabel, agentReasoningOptions, } from "./agent.js";
7
7
  import { getAgentDiagnostics, getExternalSnapshotForSession, } from "./agent-activity.js";
8
8
  import { listAgentAdapterDescriptors } from "./agent-adapter.js";
9
+ import { AgentUpdateManager } from "./agent-updates.js";
9
10
  import { createAgentSessionService, enabledAgents } from "./agent-factory.js";
10
11
  import { AuditLogStore } from "./audit-log.js";
11
12
  import { checkAuthStatus, startLogin as startCodexLogin, startLogout as startCodexLogout } from "./codex-auth.js";
@@ -13,10 +14,10 @@ import { checkClaudeCodeAuthStatus, startClaudeCodeLogin, startClaudeCodeLogout
13
14
  import { friendlyErrorText } from "./error-messages.js";
14
15
  import { checkHermesAuthStatus, startHermesLogin, startHermesLogout } from "./hermes-auth.js";
15
16
  import { checkOpenClawAuthStatus } from "./openclaw-auth.js";
16
- import { getConnectorHealth, getVersionChecks, readConnectorState, readFormattedLogTail, spawnConnectorRestart, spawnSelfUpdate } from "./operations.js";
17
+ import { clearLogFile, getAgentUpdateLogPath, getConnectorHealth, getConnectorLogPath, getPackageVersion, getUpdateLogPath, getVersionChecks, readConnectorState, readFormattedLogTail, spawnConnectorRestart, spawnSelfUpdate } from "./operations.js";
17
18
  import { checkPiAuthStatus } from "./pi-auth.js";
18
19
  import { PromptStore, toPromptEnvelope } from "./prompt-store.js";
19
- import { renderSessionInfoPlain } from "./session-format.js";
20
+ import { renderSessionInfoPlain, renderSessionUsageRows } from "./session-format.js";
20
21
  import { SessionLockStore } from "./session-locks.js";
21
22
  import { SessionRegistry } from "./session-registry.js";
22
23
  import { transcribeAudio } from "./voice.js";
@@ -34,6 +35,7 @@ export class RelayRuntime {
34
35
  activityStore;
35
36
  auditStore;
36
37
  lockStore;
38
+ agentUpdates;
37
39
  subscribers = new Set();
38
40
  externalMonitor;
39
41
  draining = false;
@@ -53,6 +55,9 @@ export class RelayRuntime {
53
55
  this.activityStore = new WebActivityStore(config.workspace, config.stateBackend, config.auditMaxEvents);
54
56
  this.auditStore = new AuditLogStore(config.workspace, config.stateBackend, config.auditMaxEvents);
55
57
  this.lockStore = new SessionLockStore(config.workspace, config.stateBackend);
58
+ this.agentUpdates = new AgentUpdateManager({
59
+ onUpdate: (job) => this.broadcast({ type: "agent_update", job }),
60
+ });
56
61
  if (config.codexExternalBusyCheckMs > 0) {
57
62
  this.externalMonitor = setInterval(() => {
58
63
  void this.monitorExternalActivity().catch((error) => this.broadcastStatus(friendlyErrorText(error), "error"));
@@ -87,6 +92,15 @@ export class RelayRuntime {
87
92
  snapshot: await this.snapshot(),
88
93
  };
89
94
  }
95
+ async bootstrapStatus() {
96
+ return {
97
+ health: {
98
+ version: await getPackageVersion(),
99
+ state: await readConnectorState(),
100
+ },
101
+ snapshot: await this.snapshot(),
102
+ };
103
+ }
90
104
  async version() {
91
105
  return {
92
106
  health: await getConnectorHealth({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath }),
@@ -114,6 +128,45 @@ export class RelayRuntime {
114
128
  });
115
129
  return update;
116
130
  }
131
+ agentUpdateJobs() {
132
+ return this.agentUpdates.list();
133
+ }
134
+ startAgentUpdate(agentId) {
135
+ const job = this.agentUpdates.start(agentId, {
136
+ piCliPath: this.config.piCliPath,
137
+ hermesCliPath: this.config.hermesCliPath,
138
+ openClawCliPath: this.config.openClawCliPath,
139
+ claudeCodeCliPath: this.config.claudeCodeCliPath,
140
+ });
141
+ this.broadcastStatus(`${job.agentLabel} update started. Log: ${job.logPath}`, "warn");
142
+ this.appendActivity({
143
+ source: "web",
144
+ status: "info",
145
+ type: "agent_update_started",
146
+ agentId,
147
+ threadId: null,
148
+ workspace: this.config.workspace,
149
+ detail: `${job.method}: ${job.summary}`,
150
+ });
151
+ this.appendAudit({
152
+ action: "command",
153
+ status: "ok",
154
+ contextKey: WEB_CONTEXT_KEY,
155
+ agentId,
156
+ description: `update ${agentId}`,
157
+ detail: job.summary,
158
+ });
159
+ return job;
160
+ }
161
+ agentUpdateLog(id) {
162
+ return this.agentUpdates.readLog(id);
163
+ }
164
+ sendAgentUpdateInput(id, input) {
165
+ return this.agentUpdates.sendInput(id, input);
166
+ }
167
+ cancelAgentUpdate(id) {
168
+ return this.agentUpdates.cancel(id);
169
+ }
117
170
  async diagnostics() {
118
171
  return {
119
172
  health: await getConnectorHealth({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath }),
@@ -380,9 +433,11 @@ export class RelayRuntime {
380
433
  async sessionDetail(threadId) {
381
434
  const session = await this.getSession(true);
382
435
  const record = session.getSessionRecord(threadId);
436
+ const active = this.publicInfo(session);
383
437
  return {
384
438
  record,
385
- active: this.publicInfo(session),
439
+ active,
440
+ usageRows: active.threadId === threadId ? renderSessionUsageRows(active) : [],
386
441
  messages: this.chatStore.list(threadId, 100),
387
442
  activity: this.activity({ limit: 100 }).filter((event) => event.threadId === threadId),
388
443
  };
@@ -395,7 +450,7 @@ export class RelayRuntime {
395
450
  return { removed, messages };
396
451
  }
397
452
  activity(options = {}) {
398
- return this.activityStore.list(options);
453
+ return this.activityStore.list(options).map((event) => this.enrichActivityEvent(event));
399
454
  }
400
455
  async retry() {
401
456
  const cached = this.promptStore.getLastPrompt(WEB_CONTEXT_KEY);
@@ -442,25 +497,40 @@ export class RelayRuntime {
442
497
  });
443
498
  return result;
444
499
  }
445
- async listSessions(limit = 80, query = "") {
446
- return this.filteredSessions(await this.getSession(true), query, Math.max(1, limit * 3)).slice(0, limit);
500
+ async listSessions(limit = 80, query = "", agentId) {
501
+ const { session, dispose } = await this.getControlSession(agentId);
502
+ try {
503
+ return this.filteredSessions(session, query, Math.max(1, limit * 3)).slice(0, limit);
504
+ }
505
+ finally {
506
+ if (dispose) {
507
+ session.dispose();
508
+ }
509
+ }
447
510
  }
448
- async listSessionsPage(page = 1, pageSize = MAX_WEB_SESSION_PAGE_SIZE, query = "") {
449
- const session = await this.getSession(true);
450
- const effectivePage = Math.max(1, Math.floor(page));
451
- const effectivePageSize = Math.min(MAX_WEB_SESSION_PAGE_SIZE, Math.max(1, Math.floor(pageSize)));
452
- const offset = (effectivePage - 1) * effectivePageSize;
453
- const requested = Math.min(5_000, Math.max(100, (offset + effectivePageSize + 1) * 3));
454
- const records = this.filteredSessions(session, query, requested);
455
- return {
456
- sessions: records.slice(offset, offset + effectivePageSize),
457
- pagination: {
458
- page: effectivePage,
459
- pageSize: effectivePageSize,
460
- hasPrevious: effectivePage > 1,
461
- hasNext: records.length > offset + effectivePageSize,
462
- },
463
- };
511
+ async listSessionsPage(page = 1, pageSize = MAX_WEB_SESSION_PAGE_SIZE, query = "", agentId) {
512
+ const { session, dispose } = await this.getControlSession(agentId);
513
+ try {
514
+ const effectivePage = Math.max(1, Math.floor(page));
515
+ const effectivePageSize = Math.min(MAX_WEB_SESSION_PAGE_SIZE, Math.max(1, Math.floor(pageSize)));
516
+ const offset = (effectivePage - 1) * effectivePageSize;
517
+ const requested = Math.min(5_000, Math.max(100, (offset + effectivePageSize + 1) * 3));
518
+ const records = this.filteredSessions(session, query, requested);
519
+ return {
520
+ sessions: records.slice(offset, offset + effectivePageSize),
521
+ pagination: {
522
+ page: effectivePage,
523
+ pageSize: effectivePageSize,
524
+ hasPrevious: effectivePage > 1,
525
+ hasNext: records.length > offset + effectivePageSize,
526
+ },
527
+ };
528
+ }
529
+ finally {
530
+ if (dispose) {
531
+ session.dispose();
532
+ }
533
+ }
464
534
  }
465
535
  filteredSessions(session, query, limit) {
466
536
  const normalized = query.trim().toLowerCase();
@@ -814,11 +884,25 @@ export class RelayRuntime {
814
884
  }
815
885
  async logs(target = "connector", lines = 100) {
816
886
  if (target === "update") {
817
- const { getUpdateLogPath } = await import("./operations.js");
818
887
  return readFormattedLogTail(lines, getUpdateLogPath());
819
888
  }
889
+ if (target === "agent-updates") {
890
+ return readFormattedLogTail(lines, getAgentUpdateLogPath());
891
+ }
820
892
  return readFormattedLogTail(lines);
821
893
  }
894
+ clearLogs(target = "connector") {
895
+ const result = clearLogFile(target === "update" ? getUpdateLogPath() : target === "agent-updates" ? getAgentUpdateLogPath() : getConnectorLogPath());
896
+ this.appendActivity({
897
+ source: "web",
898
+ status: "info",
899
+ type: "logs_cleared",
900
+ threadId: null,
901
+ workspace: this.config.workspace,
902
+ detail: `Cleared ${target} log.`,
903
+ });
904
+ return { ok: true, filePath: result.filePath, clearedAt: result.clearedAt.toISOString() };
905
+ }
822
906
  restartConnector() {
823
907
  spawnConnectorRestart();
824
908
  this.broadcastStatus("Restart requested. The dashboard may disconnect briefly.", "warn");
@@ -836,6 +920,7 @@ export class RelayRuntime {
836
920
  if (this.externalMonitor) {
837
921
  clearInterval(this.externalMonitor);
838
922
  }
923
+ this.agentUpdates.cancelAll();
839
924
  this.registry.disposeAll();
840
925
  this.subscribers.clear();
841
926
  }
@@ -1254,10 +1339,29 @@ export class RelayRuntime {
1254
1339
  this.broadcast({ type: "session_update", session: this.publicInfo(session) });
1255
1340
  }
1256
1341
  appendActivity(input) {
1257
- const event = this.activityStore.append(input);
1342
+ const event = this.activityStore.append(this.enrichActivityInput(input));
1258
1343
  this.broadcast({ type: "activity_update", events: this.activity({ limit: 50 }) });
1259
1344
  return event;
1260
1345
  }
1346
+ enrichActivityInput(input) {
1347
+ return this.enrichActivityFields(input);
1348
+ }
1349
+ enrichActivityEvent(event) {
1350
+ return this.enrichActivityFields(event);
1351
+ }
1352
+ enrichActivityFields(event) {
1353
+ const info = this.registry.get(WEB_CONTEXT_KEY)?.getInfo();
1354
+ if (!info) {
1355
+ return !event.threadId && !event.workspace ? { ...event, workspace: this.config.workspace } : event;
1356
+ }
1357
+ if (event.threadId && info.threadId && event.threadId === info.threadId) {
1358
+ return { ...event, workspace: event.workspace ?? info.workspace, agentId: event.agentId ?? info.agentId };
1359
+ }
1360
+ if (!event.threadId && !event.workspace) {
1361
+ return { ...event, workspace: this.config.workspace };
1362
+ }
1363
+ return event;
1364
+ }
1261
1365
  appendAudit(input) {
1262
1366
  return this.auditStore.append({ ...input, channelId: "web" });
1263
1367
  }
@@ -17,13 +17,27 @@ export function renderSessionInfoPlain(info) {
17
17
  capabilities.fastMode
18
18
  ? `Reasoning/Fast: ${info.reasoningEffort ?? "(model default)"} / ${info.fastMode ? "on" : "off"}`
19
19
  : `${agentReasoningLabel(agentId)}: ${info.reasoningEffort ?? "(model default)"}`,
20
- ...renderCodexUsagePlain(info, capabilities),
21
- ...renderAgentUsagePlain(info, capabilities),
22
- info.sessionTokens ? formatSessionTokensPlain(info.sessionTokens) : undefined,
20
+ ...renderSessionUsageRowsPlain(info),
23
21
  ]
24
22
  .filter((line) => Boolean(line))
25
23
  .join("\n");
26
24
  }
25
+ export function renderSessionUsageRowsPlain(info) {
26
+ const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
27
+ return [
28
+ ...renderCodexUsagePlain(info, capabilities),
29
+ ...renderAgentUsagePlain(info, capabilities),
30
+ info.sessionTokens ? formatSessionTokensPlain(info.sessionTokens) : undefined,
31
+ ].filter((line) => Boolean(line));
32
+ }
33
+ export function renderSessionUsageRows(info) {
34
+ const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
35
+ return [
36
+ ...renderCodexUsageRows(info, capabilities),
37
+ ...renderAgentUsageRows(info, capabilities),
38
+ info.sessionTokens ? ["Session tokens", formatSessionTokensValue(info.sessionTokens)] : undefined,
39
+ ].filter((row) => Boolean(row));
40
+ }
27
41
  export function renderSessionInfoHTML(info) {
28
42
  const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
29
43
  const agentId = info.agentId ?? "codex";
@@ -117,6 +131,37 @@ function renderCodexUsageHTML(info, capabilities) {
117
131
  }
118
132
  return lines;
119
133
  }
134
+ function renderCodexUsageRows(info, capabilities) {
135
+ const usage = info.codexUsage;
136
+ if (!usage) {
137
+ return [];
138
+ }
139
+ const rows = [];
140
+ if (capabilities.usageStats && usage.contextUsedPercent !== null && usage.contextWindow !== null && usage.lastTokenUsage) {
141
+ rows.push([
142
+ "Context used",
143
+ `${formatPercent(usage.contextUsedPercent)} (${formatCompactTokenCount(usage.lastTokenUsage.totalTokens)} / ${formatCompactTokenCount(usage.contextWindow)})`,
144
+ ]);
145
+ }
146
+ if (capabilities.usageStats && usage.totalTokenUsage) {
147
+ rows.push([
148
+ "Tokens",
149
+ [
150
+ `in ${formatCompactTokenCount(usage.totalTokenUsage.inputTokens)}`,
151
+ `cached ${formatCompactTokenCount(usage.totalTokenUsage.cachedInputTokens)}`,
152
+ `out ${formatCompactTokenCount(usage.totalTokenUsage.outputTokens)}`,
153
+ `reasoning out ${formatCompactTokenCount(usage.totalTokenUsage.reasoningOutputTokens)}`,
154
+ ].join(" · "),
155
+ ]);
156
+ }
157
+ if (capabilities.subscriptionLimits) {
158
+ const limits = formatLimitsLeft(usage);
159
+ if (limits) {
160
+ rows.push(["Limits left", limits]);
161
+ }
162
+ }
163
+ return rows;
164
+ }
120
165
  function renderAgentUsagePlain(info, capabilities) {
121
166
  if (!capabilities.usageStats) {
122
167
  return [];
@@ -159,6 +204,30 @@ function renderAgentUsageHTML(info, capabilities) {
159
204
  }
160
205
  return lines;
161
206
  }
207
+ function renderAgentUsageRows(info, capabilities) {
208
+ if (!capabilities.usageStats) {
209
+ return [];
210
+ }
211
+ const rows = [];
212
+ if (info.contextUsage?.percent !== undefined && info.contextUsage.percent !== null) {
213
+ const contextWindow = info.contextUsage.contextWindow !== null && info.contextUsage.contextWindow !== undefined
214
+ ? ` (${formatCompactTokenCount(info.contextUsage.tokens ?? 0)} / ${formatCompactTokenCount(info.contextUsage.contextWindow)})`
215
+ : "";
216
+ rows.push(["Context used", `${formatPercent(info.contextUsage.percent)}${contextWindow}`]);
217
+ }
218
+ if (info.sessionUsage) {
219
+ rows.push([
220
+ "Tokens",
221
+ [
222
+ `in ${formatCompactTokenCount(info.sessionUsage.input)}`,
223
+ `cache read ${formatCompactTokenCount(info.sessionUsage.cacheRead)}`,
224
+ `cache write ${formatCompactTokenCount(info.sessionUsage.cacheWrite)}`,
225
+ `out ${formatCompactTokenCount(info.sessionUsage.output)}`,
226
+ ].join(" · "),
227
+ ]);
228
+ }
229
+ return rows;
230
+ }
162
231
  function formatLimitsLeft(usage) {
163
232
  const parts = [];
164
233
  if (usage.rateLimits?.primary) {
@@ -0,0 +1,2 @@
1
+ export { dashboardJs } from "./web-dashboard-client.js";
2
+ export { dashboardCss } from "./web-dashboard-style.js";