@nordbyte/nordrelay 0.8.2 → 0.8.3
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/README.md +4 -0
- package/dist/access/audit-log.js +30 -13
- package/dist/channels/discord/discord-bot.js +12 -27
- package/dist/channels/shared/channel-bridge-controller.js +1 -1
- package/dist/channels/shared/channel-prompt-queue.js +37 -0
- package/dist/channels/shared/channel-turn-service.js +23 -9
- package/dist/channels/slack/slack-bot.js +12 -15
- package/dist/channels/telegram/bot.js +18 -4
- package/dist/core/pagination.js +22 -0
- package/dist/peers/peer-store.js +16 -0
- package/dist/peers/peer-types.js +19 -0
- package/dist/peers/peer-web-proxy-contract.js +2 -0
- package/dist/runtime/relay-external-activity-monitor.js +15 -0
- package/dist/runtime/relay-queue-service.js +1 -0
- package/dist/runtime/relay-runtime-dashboard.js +3 -0
- package/dist/runtime/relay-runtime-helpers.js +3 -0
- package/dist/runtime/relay-runtime-prompt-queue-artifacts.js +14 -10
- package/dist/runtime/relay-runtime-sessions.js +8 -0
- package/dist/runtime/relay-runtime-trace.js +92 -0
- package/dist/runtime/relay-runtime-updates-jobs.js +11 -5
- package/dist/runtime/relay-runtime.js +16 -6
- package/dist/state/prompt-store.js +13 -1
- package/dist/web/web-api-contract.js +2 -0
- package/dist/web/web-dashboard-access-routes.js +15 -12
- package/dist/web/web-dashboard-artifact-routes.js +6 -2
- package/dist/web/web-dashboard-assets.js +1 -0
- package/dist/web/web-dashboard-pages.js +58 -20
- package/dist/web/web-dashboard-peer-routes.js +19 -0
- package/dist/web/web-dashboard-runtime-routes.js +8 -1
- package/dist/web/web-dashboard-session-routes.js +17 -12
- package/dist/web/web-dashboard-ui.js +46 -10
- package/dist/web/web-performance.js +2 -0
- package/dist/web/web-state.js +33 -4
- package/dist/webui-assets/dashboard.css +227 -39
- package/dist/webui-assets/dashboard.js +728 -58
- package/package.json +4 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +333 -8
- package/plugins/nordrelay/scripts/service-installer.mjs +183 -0
- package/scripts/postinstall.mjs +122 -0
package/README.md
CHANGED
|
@@ -22,6 +22,8 @@ nordrelay doctor
|
|
|
22
22
|
nordrelay start
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
If `nordrelay` is not found after a global npm install, the npm global bin directory is not in your shell `PATH`. New installs run a postinstall check and print the exact command to add the bin directory to your shell profile.
|
|
26
|
+
|
|
25
27
|
Open the dashboard:
|
|
26
28
|
|
|
27
29
|
```bash
|
|
@@ -78,6 +80,8 @@ nordrelay doctor
|
|
|
78
80
|
nordrelay web
|
|
79
81
|
nordrelay restart
|
|
80
82
|
nordrelay update
|
|
83
|
+
nordrelay service install
|
|
84
|
+
nordrelay service status
|
|
81
85
|
```
|
|
82
86
|
|
|
83
87
|
Chat adapters share the core command set:
|
package/dist/access/audit-log.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { auditCategoryForAction, } from "../core/activity-events.js";
|
|
3
|
+
import { cursorPage, normalizeCursorLimit } from "../core/pagination.js";
|
|
3
4
|
import { createDocumentStore } from "../state/state-backend.js";
|
|
4
5
|
export class AuditLogStore {
|
|
5
6
|
store;
|
|
@@ -31,20 +32,36 @@ export class AuditLogStore {
|
|
|
31
32
|
}
|
|
32
33
|
list(options = 20) {
|
|
33
34
|
const resolved = typeof options === "number" ? { limit: options } : options;
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
return this.listPage(resolved).items;
|
|
36
|
+
}
|
|
37
|
+
listPage(options = {}) {
|
|
38
|
+
const limit = normalizeCursorLimit(options.limit, 20, 500);
|
|
39
|
+
const events = this.filteredEvents(options)
|
|
40
|
+
.sort((left, right) => Date.parse(right.timestamp) - Date.parse(left.timestamp));
|
|
41
|
+
return cursorPage(events, options.cursor, limit, (event) => event.id);
|
|
42
|
+
}
|
|
43
|
+
findByCorrelationId(correlationId, limit = 100) {
|
|
44
|
+
const needle = correlationId.trim();
|
|
45
|
+
if (!needle) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
return this.readPayload().events
|
|
49
|
+
.filter((event) => event.correlationId === needle)
|
|
50
|
+
.sort((left, right) => Date.parse(left.timestamp) - Date.parse(right.timestamp))
|
|
51
|
+
.slice(-Math.max(1, Math.min(500, limit)));
|
|
52
|
+
}
|
|
53
|
+
filteredEvents(options) {
|
|
54
|
+
const since = normalizeSince(options.since);
|
|
36
55
|
return this.readPayload().events
|
|
37
|
-
.filter((event) => !
|
|
38
|
-
.filter((event) => !
|
|
39
|
-
.filter((event) => !
|
|
40
|
-
.filter((event) => !
|
|
41
|
-
.filter((event) => !
|
|
42
|
-
.filter((event) => !
|
|
43
|
-
.filter((event) => !
|
|
44
|
-
.filter((event) => !
|
|
45
|
-
.filter((event) => !since || Date.parse(event.timestamp) >= since)
|
|
46
|
-
.slice(-limit)
|
|
47
|
-
.reverse();
|
|
56
|
+
.filter((event) => !options.channelId || options.channelId === "all" || event.channelId === options.channelId)
|
|
57
|
+
.filter((event) => !options.status || options.status === "all" || event.status === options.status)
|
|
58
|
+
.filter((event) => !options.action || options.action === "all" || event.action === options.action)
|
|
59
|
+
.filter((event) => !options.category || options.category === "all" || (event.category ?? auditCategoryForAction(event.action)) === options.category)
|
|
60
|
+
.filter((event) => !options.agentId || options.agentId === "all" || event.agentId === options.agentId)
|
|
61
|
+
.filter((event) => !options.threadId || event.threadId === options.threadId)
|
|
62
|
+
.filter((event) => !options.workspace || event.workspace === options.workspace)
|
|
63
|
+
.filter((event) => !options.actor || auditActorMatches(event, options.actor))
|
|
64
|
+
.filter((event) => !since || Date.parse(event.timestamp) >= since);
|
|
48
65
|
}
|
|
49
66
|
readPayload() {
|
|
50
67
|
const payload = this.store.read();
|
|
@@ -18,6 +18,7 @@ import { createSharedChannelCommandDispatcher } from "../shared/channel-command-
|
|
|
18
18
|
import { ChannelCommandService } from "../shared/channel-command-service.js";
|
|
19
19
|
import { discordHelpCommandList } from "../shared/channel-command-catalog.js";
|
|
20
20
|
import { createChannelPromptEngine } from "../shared/channel-prompt-engine.js";
|
|
21
|
+
import { queueChannelPromptIfBusy } from "../shared/channel-prompt-queue.js";
|
|
21
22
|
import { runChannelPeerPrompt } from "../shared/channel-peer-prompt.js";
|
|
22
23
|
import { deliverChannelAction } from "../shared/channel-runtime.js";
|
|
23
24
|
import { deliverChannelCliArtifacts } from "../shared/channel-cli-artifacts.js";
|
|
@@ -304,30 +305,17 @@ export function createDiscordBridge(config, registry) {
|
|
|
304
305
|
if (!options.fromQueue && await denyIfLocked(request)) {
|
|
305
306
|
return;
|
|
306
307
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
});
|
|
319
|
-
appendActivity(request, {
|
|
320
|
-
status: "queued",
|
|
321
|
-
type: "prompt_queued",
|
|
322
|
-
prompt: item.description,
|
|
323
|
-
detail: text,
|
|
324
|
-
});
|
|
325
|
-
audit(request, {
|
|
326
|
-
action: "prompt_queued",
|
|
327
|
-
status: "ok",
|
|
328
|
-
promptId: item.id,
|
|
329
|
-
description: item.description,
|
|
330
|
-
});
|
|
308
|
+
if (await queueChannelPromptIfBusy({
|
|
309
|
+
request,
|
|
310
|
+
envelope,
|
|
311
|
+
fromQueue: options.fromQueue,
|
|
312
|
+
promptStore,
|
|
313
|
+
busy: getBusyReason(request.contextKey),
|
|
314
|
+
actionPrefix: "discord",
|
|
315
|
+
reply,
|
|
316
|
+
appendActivity,
|
|
317
|
+
audit,
|
|
318
|
+
})) {
|
|
331
319
|
return;
|
|
332
320
|
}
|
|
333
321
|
const busyState = getBusyState(request.contextKey);
|
|
@@ -1580,6 +1568,3 @@ function inferMimeType(name) {
|
|
|
1580
1568
|
return "audio/webm";
|
|
1581
1569
|
return "application/octet-stream";
|
|
1582
1570
|
}
|
|
1583
|
-
function isQueuedPrompt(value) {
|
|
1584
|
-
return Boolean(value && typeof value === "object" && "id" in value && "contextKey" in value);
|
|
1585
|
-
}
|
|
@@ -42,7 +42,7 @@ export function createChannelQueueStatusController(options) {
|
|
|
42
42
|
}
|
|
43
43
|
export function createChannelActivityRecorder(options) {
|
|
44
44
|
return (request, input) => {
|
|
45
|
-
options.activityStore.append({
|
|
45
|
+
return options.activityStore.append({
|
|
46
46
|
source: options.source,
|
|
47
47
|
contextKey: request.contextKey,
|
|
48
48
|
actor: input.actor ?? options.actorFor(request),
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export async function queueChannelPromptIfBusy(options) {
|
|
2
|
+
if (!options.busy.busy) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
const item = options.fromQueue && isQueuedPrompt(options.envelope)
|
|
6
|
+
? options.envelope
|
|
7
|
+
: options.promptStore.enqueue(options.request.contextKey, options.envelope);
|
|
8
|
+
const position = options.promptStore.list(options.request.contextKey).findIndex((queued) => queued.id === item.id) + 1;
|
|
9
|
+
const text = options.busy.kind === "external"
|
|
10
|
+
? `Queued prompt ${item.id} at position ${position}. The ${options.busy.agentLabel} session is still active and is processing a previous task.`
|
|
11
|
+
: `Queued prompt ${item.id} at position ${position}.`;
|
|
12
|
+
await options.reply(options.request, text, {
|
|
13
|
+
buttons: [[{ label: "Cancel queued message", action: `${options.actionPrefix}_queue_cancel:${options.request.contextKey}:${item.id}` }]],
|
|
14
|
+
});
|
|
15
|
+
options.appendActivity(options.request, {
|
|
16
|
+
status: "queued",
|
|
17
|
+
type: "prompt_queued",
|
|
18
|
+
prompt: item.description,
|
|
19
|
+
detail: text,
|
|
20
|
+
correlationId: item.correlationId,
|
|
21
|
+
});
|
|
22
|
+
options.audit(options.request, {
|
|
23
|
+
action: "prompt_queued",
|
|
24
|
+
status: "ok",
|
|
25
|
+
promptId: item.id,
|
|
26
|
+
correlationId: item.correlationId,
|
|
27
|
+
description: item.description,
|
|
28
|
+
});
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
function isQueuedPrompt(value) {
|
|
32
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const candidate = value;
|
|
36
|
+
return typeof candidate.id === "string" && typeof candidate.createdAt === "number" && typeof candidate.description === "string";
|
|
37
|
+
}
|
|
@@ -17,12 +17,14 @@ export class ChannelTurnService {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
const turnId = randomUUID().slice(0, 12);
|
|
20
|
+
const correlationId = envelope.correlationId ?? turnId;
|
|
20
21
|
const startedMs = Date.now();
|
|
21
22
|
this.options.setCurrentTurn(turnId, startedMs, "");
|
|
22
23
|
this.options.setCurrentProgress({
|
|
23
24
|
id: turnId,
|
|
24
25
|
source: this.options.source,
|
|
25
26
|
status: "running",
|
|
27
|
+
correlationId,
|
|
26
28
|
prompt: envelope.description,
|
|
27
29
|
agentId: info.agentId,
|
|
28
30
|
agentLabel: info.agentLabel,
|
|
@@ -42,6 +44,7 @@ export class ChannelTurnService {
|
|
|
42
44
|
role: "user",
|
|
43
45
|
text: envelope.description,
|
|
44
46
|
source: this.options.source,
|
|
47
|
+
correlationId,
|
|
45
48
|
turnId,
|
|
46
49
|
timestamp: startedAt,
|
|
47
50
|
});
|
|
@@ -53,6 +56,7 @@ export class ChannelTurnService {
|
|
|
53
56
|
workspace: info.workspace,
|
|
54
57
|
agentId: info.agentId,
|
|
55
58
|
actor,
|
|
59
|
+
correlationId,
|
|
56
60
|
prompt: envelope.description,
|
|
57
61
|
});
|
|
58
62
|
this.options.appendAudit({
|
|
@@ -63,9 +67,10 @@ export class ChannelTurnService {
|
|
|
63
67
|
threadId: info.threadId,
|
|
64
68
|
workspace: info.workspace,
|
|
65
69
|
actor,
|
|
70
|
+
correlationId,
|
|
66
71
|
description: envelope.description,
|
|
67
72
|
});
|
|
68
|
-
this.options.broadcast({ type: "turn_start", id: turnId, prompt: envelope.description, at: startedAt, source: this.options.source });
|
|
73
|
+
this.options.broadcast({ type: "turn_start", id: turnId, prompt: envelope.description, at: startedAt, source: this.options.source, correlationId });
|
|
69
74
|
try {
|
|
70
75
|
await session.prompt(envelope.input, this.callbacks(turnId, info, envelope, actor));
|
|
71
76
|
this.options.updateSession(session);
|
|
@@ -77,6 +82,7 @@ export class ChannelTurnService {
|
|
|
77
82
|
role: "agent",
|
|
78
83
|
text,
|
|
79
84
|
source: this.options.source,
|
|
85
|
+
correlationId,
|
|
80
86
|
turnId,
|
|
81
87
|
});
|
|
82
88
|
}
|
|
@@ -88,6 +94,7 @@ export class ChannelTurnService {
|
|
|
88
94
|
workspace: info.workspace,
|
|
89
95
|
agentId: info.agentId,
|
|
90
96
|
actor,
|
|
97
|
+
correlationId,
|
|
91
98
|
prompt: envelope.description,
|
|
92
99
|
durationMs: Date.now() - this.options.getCurrentTurnStartedAt(),
|
|
93
100
|
});
|
|
@@ -99,10 +106,11 @@ export class ChannelTurnService {
|
|
|
99
106
|
threadId: info.threadId,
|
|
100
107
|
workspace: info.workspace,
|
|
101
108
|
actor,
|
|
109
|
+
correlationId,
|
|
102
110
|
description: envelope.description,
|
|
103
111
|
});
|
|
104
112
|
this.updateCurrentProgress({ status: "completed" });
|
|
105
|
-
this.options.broadcast({ type: "turn_complete", id: turnId, at: new Date().toISOString() });
|
|
113
|
+
this.options.broadcast({ type: "turn_complete", id: turnId, at: new Date().toISOString(), correlationId });
|
|
106
114
|
this.options.broadcast({ type: "chat_history", messages: await this.options.chatHistory() });
|
|
107
115
|
}
|
|
108
116
|
catch (error) {
|
|
@@ -112,6 +120,7 @@ export class ChannelTurnService {
|
|
|
112
120
|
role: "system",
|
|
113
121
|
text: `Error: ${errorText}`,
|
|
114
122
|
source: this.options.source,
|
|
123
|
+
correlationId,
|
|
115
124
|
turnId,
|
|
116
125
|
});
|
|
117
126
|
this.options.appendActivity({
|
|
@@ -122,6 +131,7 @@ export class ChannelTurnService {
|
|
|
122
131
|
workspace: info.workspace,
|
|
123
132
|
agentId: info.agentId,
|
|
124
133
|
actor,
|
|
134
|
+
correlationId,
|
|
125
135
|
prompt: envelope.description,
|
|
126
136
|
detail: errorText,
|
|
127
137
|
durationMs: Date.now() - this.options.getCurrentTurnStartedAt(),
|
|
@@ -134,11 +144,12 @@ export class ChannelTurnService {
|
|
|
134
144
|
threadId: info.threadId,
|
|
135
145
|
workspace: info.workspace,
|
|
136
146
|
actor,
|
|
147
|
+
correlationId,
|
|
137
148
|
description: envelope.description,
|
|
138
149
|
detail: errorText,
|
|
139
150
|
});
|
|
140
151
|
this.updateCurrentProgress({ status: "failed", detail: errorText });
|
|
141
|
-
this.options.broadcast({ type: "turn_error", id: turnId, error: errorText, at: new Date().toISOString() });
|
|
152
|
+
this.options.broadcast({ type: "turn_error", id: turnId, error: errorText, at: new Date().toISOString(), correlationId });
|
|
142
153
|
this.options.broadcast({ type: "chat_history", messages: await this.options.chatHistory() });
|
|
143
154
|
throw error;
|
|
144
155
|
}
|
|
@@ -152,12 +163,13 @@ export class ChannelTurnService {
|
|
|
152
163
|
}
|
|
153
164
|
}
|
|
154
165
|
callbacks(turnId, info, envelope, actor) {
|
|
166
|
+
const correlationId = envelope.correlationId ?? turnId;
|
|
155
167
|
return {
|
|
156
168
|
onTextDelta: (delta) => {
|
|
157
169
|
const nextText = this.options.getAccumulatedText() + delta;
|
|
158
170
|
this.options.setAccumulatedText(nextText);
|
|
159
171
|
this.updateCurrentProgress({ outputChars: nextText.length });
|
|
160
|
-
this.options.broadcast({ type: "text_delta", id: turnId, delta });
|
|
172
|
+
this.options.broadcast({ type: "text_delta", id: turnId, delta, correlationId });
|
|
161
173
|
},
|
|
162
174
|
onToolStart: (toolName, toolCallId) => {
|
|
163
175
|
this.addCurrentTool(toolName);
|
|
@@ -169,14 +181,15 @@ export class ChannelTurnService {
|
|
|
169
181
|
workspace: info.workspace,
|
|
170
182
|
agentId: info.agentId,
|
|
171
183
|
actor,
|
|
184
|
+
correlationId,
|
|
172
185
|
prompt: envelope.description,
|
|
173
186
|
detail: toolName,
|
|
174
187
|
});
|
|
175
|
-
this.options.broadcast({ type: "tool_start", id: turnId, toolCallId, toolName });
|
|
188
|
+
this.options.broadcast({ type: "tool_start", id: turnId, toolCallId, toolName, correlationId });
|
|
176
189
|
},
|
|
177
190
|
onToolUpdate: (toolCallId, partialResult) => {
|
|
178
191
|
this.updateCurrentProgress();
|
|
179
|
-
this.options.broadcast({ type: "tool_update", id: turnId, toolCallId, partialResult });
|
|
192
|
+
this.options.broadcast({ type: "tool_update", id: turnId, toolCallId, partialResult, correlationId });
|
|
180
193
|
},
|
|
181
194
|
onToolEnd: (toolCallId, isError) => {
|
|
182
195
|
const progress = this.options.getCurrentProgress();
|
|
@@ -190,17 +203,18 @@ export class ChannelTurnService {
|
|
|
190
203
|
workspace: info.workspace,
|
|
191
204
|
agentId: info.agentId,
|
|
192
205
|
actor,
|
|
206
|
+
correlationId,
|
|
193
207
|
prompt: envelope.description,
|
|
194
208
|
detail: toolName,
|
|
195
209
|
});
|
|
196
|
-
this.options.broadcast({ type: "tool_end", id: turnId, toolCallId, isError });
|
|
210
|
+
this.options.broadcast({ type: "tool_end", id: turnId, toolCallId, isError, correlationId });
|
|
197
211
|
},
|
|
198
212
|
onTodoUpdate: (items) => {
|
|
199
213
|
this.updateCurrentProgress({ detail: `Plan: ${items.filter((item) => item.completed).length}/${items.length} done` });
|
|
200
|
-
this.options.broadcast({ type: "todo_update", id: turnId, items });
|
|
214
|
+
this.options.broadcast({ type: "todo_update", id: turnId, items, correlationId });
|
|
201
215
|
},
|
|
202
216
|
onTurnComplete: () => { },
|
|
203
|
-
onAgentEnd: () => this.options.broadcast({ type: "turn_complete", id: turnId, at: new Date().toISOString() }),
|
|
217
|
+
onAgentEnd: () => this.options.broadcast({ type: "turn_complete", id: turnId, at: new Date().toISOString(), correlationId }),
|
|
204
218
|
};
|
|
205
219
|
}
|
|
206
220
|
updateCurrentProgress(patch = {}) {
|
|
@@ -18,6 +18,7 @@ import { createSharedChannelCommandDispatcher } from "../shared/channel-command-
|
|
|
18
18
|
import { slackHelpCommandList } from "../shared/channel-command-catalog.js";
|
|
19
19
|
import { ChannelCommandService } from "../shared/channel-command-service.js";
|
|
20
20
|
import { createChannelPromptEngine } from "../shared/channel-prompt-engine.js";
|
|
21
|
+
import { queueChannelPromptIfBusy } from "../shared/channel-prompt-queue.js";
|
|
21
22
|
import { runChannelPeerPrompt } from "../shared/channel-peer-prompt.js";
|
|
22
23
|
import { deliverChannelAction } from "../shared/channel-runtime.js";
|
|
23
24
|
import { deliverChannelCliArtifacts } from "../shared/channel-cli-artifacts.js";
|
|
@@ -284,18 +285,17 @@ export function createSlackBridge(config, registry) {
|
|
|
284
285
|
if (!options.fromQueue && await denyIfLocked(request)) {
|
|
285
286
|
return;
|
|
286
287
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
audit(request, { action: "prompt_queued", status: "ok", promptId: item.id, description: item.description });
|
|
288
|
+
if (await queueChannelPromptIfBusy({
|
|
289
|
+
request,
|
|
290
|
+
envelope,
|
|
291
|
+
fromQueue: options.fromQueue,
|
|
292
|
+
promptStore,
|
|
293
|
+
busy: getBusyReason(request.contextKey),
|
|
294
|
+
actionPrefix: "slack",
|
|
295
|
+
reply,
|
|
296
|
+
appendActivity,
|
|
297
|
+
audit,
|
|
298
|
+
})) {
|
|
299
299
|
return;
|
|
300
300
|
}
|
|
301
301
|
const busyState = getBusyState(request.contextKey);
|
|
@@ -1321,6 +1321,3 @@ function inferMimeType(name) {
|
|
|
1321
1321
|
return "audio/webm";
|
|
1322
1322
|
return "application/octet-stream";
|
|
1323
1323
|
}
|
|
1324
|
-
function isQueuedPrompt(value) {
|
|
1325
|
-
return Boolean(value && typeof value === "object" && "id" in value && "contextKey" in value);
|
|
1326
|
-
}
|
|
@@ -11,7 +11,7 @@ import { AuditLogStore } from "../../access/audit-log.js";
|
|
|
11
11
|
import { formatSessionLabel } from "./bot-ui.js";
|
|
12
12
|
import { BotPreferencesStore, isQuietNow, } from "../../state/bot-preferences.js";
|
|
13
13
|
import { renderAgentUpdateJobAction } from "../shared/channel-actions.js";
|
|
14
|
-
import { createChannelBusyStore } from "../shared/channel-bridge-controller.js";
|
|
14
|
+
import { createChannelActivityRecorder, createChannelBusyStore } from "../shared/channel-bridge-controller.js";
|
|
15
15
|
import { ChannelCommandService } from "../shared/channel-command-service.js";
|
|
16
16
|
import { runChannelPeerPrompt } from "../shared/channel-peer-prompt.js";
|
|
17
17
|
import { deliverChannelAction } from "../shared/channel-runtime.js";
|
|
@@ -585,17 +585,31 @@ export function createBot(config, registry) {
|
|
|
585
585
|
function appendActivity(input) {
|
|
586
586
|
return activityStore.append(input);
|
|
587
587
|
}
|
|
588
|
+
const appendTelegramBridgeActivity = createChannelActivityRecorder({
|
|
589
|
+
source: "telegram",
|
|
590
|
+
workspace: config.workspace,
|
|
591
|
+
activityStore,
|
|
592
|
+
actorFor: (request) => telegramActivityActor(request.ctx),
|
|
593
|
+
});
|
|
588
594
|
function appendTelegramActivity(ctx, contextKey, session, input) {
|
|
589
595
|
const info = session.getInfo();
|
|
590
|
-
|
|
591
|
-
source: "telegram",
|
|
596
|
+
const event = appendTelegramBridgeActivity({
|
|
592
597
|
contextKey,
|
|
598
|
+
context: telegramChannelContextFromCtx(ctx) ?? {
|
|
599
|
+
channelId: "telegram",
|
|
600
|
+
chatId: String(ctx.chat?.id ?? contextKey),
|
|
601
|
+
userId: ctx.from?.id !== undefined ? String(ctx.from.id) : undefined,
|
|
602
|
+
username: ctx.from?.username,
|
|
603
|
+
},
|
|
604
|
+
authUser: getAuthenticatedUser(ctx) ?? undefined,
|
|
605
|
+
ctx,
|
|
606
|
+
}, {
|
|
593
607
|
...input,
|
|
594
608
|
threadId: input.threadId ?? info.threadId,
|
|
595
609
|
workspace: input.workspace ?? info.workspace,
|
|
596
610
|
agentId: input.agentId ?? idOf(info),
|
|
597
|
-
actor: input.actor ?? telegramActivityActor(ctx),
|
|
598
611
|
});
|
|
612
|
+
return event;
|
|
599
613
|
}
|
|
600
614
|
function recordTelegramAgentUpdateLifecycle(job) {
|
|
601
615
|
const previous = agentUpdateStates.get(job.id);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function normalizeCursorLimit(value, fallback = 100, max = 500) {
|
|
2
|
+
const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
|
|
3
|
+
const selected = Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
4
|
+
return Math.max(1, Math.min(max, selected));
|
|
5
|
+
}
|
|
6
|
+
export function cursorPage(items, cursor, limit, cursorOf) {
|
|
7
|
+
const normalizedLimit = normalizeCursorLimit(limit, limit);
|
|
8
|
+
const startIndex = cursor ? Math.max(0, items.findIndex((item) => cursorOf(item) === cursor) + 1) : 0;
|
|
9
|
+
const window = items.slice(startIndex, startIndex + normalizedLimit + 1);
|
|
10
|
+
const pageItems = window.slice(0, normalizedLimit);
|
|
11
|
+
const hasNext = window.length > normalizedLimit;
|
|
12
|
+
const last = pageItems.at(-1);
|
|
13
|
+
return {
|
|
14
|
+
items: pageItems,
|
|
15
|
+
pagination: {
|
|
16
|
+
limit: normalizedLimit,
|
|
17
|
+
nextCursor: hasNext && last ? cursorOf(last) ?? null : null,
|
|
18
|
+
hasNext,
|
|
19
|
+
total: items.length,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
package/dist/peers/peer-store.js
CHANGED
|
@@ -61,6 +61,22 @@ export class PeerStore {
|
|
|
61
61
|
});
|
|
62
62
|
return { invitation: publicInvitation(invitation), code };
|
|
63
63
|
}
|
|
64
|
+
createRotationInvitation(id, options = {}) {
|
|
65
|
+
const peer = this.get(id);
|
|
66
|
+
if (!peer) {
|
|
67
|
+
throw new Error("Peer not found.");
|
|
68
|
+
}
|
|
69
|
+
const created = this.createInvitation({
|
|
70
|
+
name: `${peer.name} rotation`,
|
|
71
|
+
group: peer.group,
|
|
72
|
+
expiresInMs: options.expiresInMs,
|
|
73
|
+
scopes: peer.scopes,
|
|
74
|
+
allowedAgents: peer.allowedAgents,
|
|
75
|
+
allowedWorkspaceRoots: peer.allowedWorkspaceRoots,
|
|
76
|
+
workspaceAliases: peer.workspaceAliases,
|
|
77
|
+
});
|
|
78
|
+
return { peer: publicPeer(peer), ...created };
|
|
79
|
+
}
|
|
64
80
|
consumeInvitation(code, usedByNodeId) {
|
|
65
81
|
const trimmed = code.trim();
|
|
66
82
|
if (!trimmed) {
|
package/dist/peers/peer-types.js
CHANGED
|
@@ -13,6 +13,7 @@ export const DEFAULT_PEER_SCOPES = [
|
|
|
13
13
|
"logs.read",
|
|
14
14
|
];
|
|
15
15
|
export function publicPeer(record) {
|
|
16
|
+
const trust = peerTrust(record);
|
|
16
17
|
return {
|
|
17
18
|
id: record.id,
|
|
18
19
|
name: record.name,
|
|
@@ -36,6 +37,8 @@ export function publicPeer(record) {
|
|
|
36
37
|
remoteStatus: record.remoteStatus,
|
|
37
38
|
lastError: record.lastError,
|
|
38
39
|
healthHistory: record.healthHistory?.map((sample) => ({ ...sample })),
|
|
40
|
+
trustStatus: trust.status,
|
|
41
|
+
trustWarnings: trust.warnings,
|
|
39
42
|
effectiveAccess: {
|
|
40
43
|
scopes: [...record.scopes],
|
|
41
44
|
allowedAgents: [...record.allowedAgents],
|
|
@@ -44,6 +47,22 @@ export function publicPeer(record) {
|
|
|
44
47
|
},
|
|
45
48
|
};
|
|
46
49
|
}
|
|
50
|
+
function peerTrust(record) {
|
|
51
|
+
const warnings = [];
|
|
52
|
+
if (!record.enabled) {
|
|
53
|
+
warnings.push("Peer is disabled.");
|
|
54
|
+
return { status: "disabled", warnings };
|
|
55
|
+
}
|
|
56
|
+
if (record.lastError) {
|
|
57
|
+
warnings.push(record.lastError);
|
|
58
|
+
return { status: "error", warnings };
|
|
59
|
+
}
|
|
60
|
+
if (record.url?.startsWith("https://") && !record.tlsFingerprint) {
|
|
61
|
+
warnings.push("TLS fingerprint is not pinned yet. Probe or re-pin this peer before using it over untrusted networks.");
|
|
62
|
+
return { status: "tls-unpinned", warnings };
|
|
63
|
+
}
|
|
64
|
+
return { status: "trusted", warnings };
|
|
65
|
+
}
|
|
47
66
|
export function publicInvitation(record) {
|
|
48
67
|
return {
|
|
49
68
|
id: record.id,
|
|
@@ -19,6 +19,7 @@ const LOCAL_ONLY_ROUTE_PATHS = new Set([
|
|
|
19
19
|
"/api/peers/invitations/:id",
|
|
20
20
|
"/api/peers/:id",
|
|
21
21
|
"/api/peers/:id/repin",
|
|
22
|
+
"/api/peers/:id/rotate",
|
|
22
23
|
"/api/peers/:id/health",
|
|
23
24
|
"/api/peers/:id/proxy",
|
|
24
25
|
"/api/peers/:id/events",
|
|
@@ -52,6 +53,7 @@ const IMPLEMENTED_ROUTE_PATHS = new Set([
|
|
|
52
53
|
"/api/progress",
|
|
53
54
|
"/api/metrics",
|
|
54
55
|
"/api/jobs",
|
|
56
|
+
"/api/trace",
|
|
55
57
|
"/api/jobs/:id/log",
|
|
56
58
|
"/api/jobs/:id/action",
|
|
57
59
|
"/api/active-sessions",
|
|
@@ -127,6 +127,7 @@ export class RelayExternalActivityMonitor {
|
|
|
127
127
|
role: "agent",
|
|
128
128
|
text: finalText,
|
|
129
129
|
source: "cli",
|
|
130
|
+
correlationId: externalCorrelationId(snapshot),
|
|
130
131
|
turnId: terminalEvent.turnId ?? undefined,
|
|
131
132
|
key: externalMessageKey("final", snapshot, terminalEvent.lineNumber),
|
|
132
133
|
});
|
|
@@ -138,6 +139,7 @@ export class RelayExternalActivityMonitor {
|
|
|
138
139
|
type: "turn_complete",
|
|
139
140
|
id: terminalEvent.turnId ?? "cli",
|
|
140
141
|
at: terminalEvent.timestamp?.toISOString() ?? new Date().toISOString(),
|
|
142
|
+
correlationId: externalCorrelationId(snapshot),
|
|
141
143
|
});
|
|
142
144
|
}
|
|
143
145
|
this.options.appendActivity({
|
|
@@ -148,6 +150,7 @@ export class RelayExternalActivityMonitor {
|
|
|
148
150
|
workspace: info.workspace,
|
|
149
151
|
agentId: info.agentId,
|
|
150
152
|
actor: CLI_ACTIVITY_ACTOR,
|
|
153
|
+
correlationId: externalCorrelationId(snapshot),
|
|
151
154
|
prompt: snapshot.latestUserMessage ?? undefined,
|
|
152
155
|
detail: `${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`,
|
|
153
156
|
durationMs: durationFromDates(externalStartedAt, terminalEvent.timestamp),
|
|
@@ -176,6 +179,7 @@ export class RelayExternalActivityMonitor {
|
|
|
176
179
|
role: "system",
|
|
177
180
|
text: `Working on ${trimLine(prompt, 500)}`,
|
|
178
181
|
source: "cli",
|
|
182
|
+
correlationId: externalCorrelationId(snapshot),
|
|
179
183
|
turnId: snapshot.activity.turnId ?? undefined,
|
|
180
184
|
timestamp: snapshot.activity.startedAt?.toISOString(),
|
|
181
185
|
key: externalMessageKey("working", snapshot),
|
|
@@ -193,6 +197,7 @@ export class RelayExternalActivityMonitor {
|
|
|
193
197
|
workspace: info.workspace,
|
|
194
198
|
agentId: info.agentId,
|
|
195
199
|
actor: CLI_ACTIVITY_ACTOR,
|
|
200
|
+
correlationId: externalCorrelationId(snapshot),
|
|
196
201
|
prompt,
|
|
197
202
|
detail: `${snapshot.sourceLabel}: ${snapshot.sourcePath}`,
|
|
198
203
|
});
|
|
@@ -206,6 +211,7 @@ export class RelayExternalActivityMonitor {
|
|
|
206
211
|
id: snapshot.activity.turnId ?? "cli",
|
|
207
212
|
toolCallId: `cli-${event.lineNumber}`,
|
|
208
213
|
toolName: event.toolName ?? "tool",
|
|
214
|
+
correlationId: externalCorrelationId(snapshot),
|
|
209
215
|
});
|
|
210
216
|
}
|
|
211
217
|
this.options.appendActivity({
|
|
@@ -216,6 +222,7 @@ export class RelayExternalActivityMonitor {
|
|
|
216
222
|
workspace: info.workspace,
|
|
217
223
|
agentId: info.agentId,
|
|
218
224
|
actor: CLI_ACTIVITY_ACTOR,
|
|
225
|
+
correlationId: externalCorrelationId(snapshot),
|
|
219
226
|
detail: event.toolName ?? "tool",
|
|
220
227
|
});
|
|
221
228
|
}
|
|
@@ -226,6 +233,7 @@ export class RelayExternalActivityMonitor {
|
|
|
226
233
|
id: snapshot.activity.turnId ?? "cli",
|
|
227
234
|
toolCallId: `cli-${event.lineNumber}`,
|
|
228
235
|
isError: false,
|
|
236
|
+
correlationId: externalCorrelationId(snapshot),
|
|
229
237
|
});
|
|
230
238
|
}
|
|
231
239
|
this.options.appendActivity({
|
|
@@ -236,6 +244,7 @@ export class RelayExternalActivityMonitor {
|
|
|
236
244
|
workspace: info.workspace,
|
|
237
245
|
agentId: info.agentId,
|
|
238
246
|
actor: CLI_ACTIVITY_ACTOR,
|
|
247
|
+
correlationId: externalCorrelationId(snapshot),
|
|
239
248
|
detail: event.toolName ?? "tool",
|
|
240
249
|
});
|
|
241
250
|
}
|
|
@@ -246,6 +255,7 @@ export class RelayExternalActivityMonitor {
|
|
|
246
255
|
id: snapshot.activity.turnId ?? "cli",
|
|
247
256
|
toolCallId: `cli-${event.lineNumber}`,
|
|
248
257
|
isError: true,
|
|
258
|
+
correlationId: externalCorrelationId(snapshot),
|
|
249
259
|
});
|
|
250
260
|
}
|
|
251
261
|
this.options.appendActivity({
|
|
@@ -276,6 +286,7 @@ export class RelayExternalActivityMonitor {
|
|
|
276
286
|
role: event.kind === "tool" ? "tool" : "system",
|
|
277
287
|
text: rendered.plain,
|
|
278
288
|
source: "cli",
|
|
289
|
+
correlationId: externalCorrelationId(snapshot),
|
|
279
290
|
turnId: event.turnId ?? snapshot.activity.turnId ?? undefined,
|
|
280
291
|
timestamp: event.timestamp?.toISOString(),
|
|
281
292
|
key: externalMessageKey("event", snapshot, event.lineNumber),
|
|
@@ -298,6 +309,7 @@ export class RelayExternalActivityMonitor {
|
|
|
298
309
|
role: "system",
|
|
299
310
|
text: text ?? renderExternalMirrorStatus(snapshot, this.options.queueLength()).plain,
|
|
300
311
|
source: "cli",
|
|
312
|
+
correlationId: externalCorrelationId(snapshot),
|
|
301
313
|
turnId: snapshot.activity.turnId ?? undefined,
|
|
302
314
|
key: externalMessageKey("status", snapshot),
|
|
303
315
|
});
|
|
@@ -318,6 +330,9 @@ function externalMessageKey(kind, snapshot, lineNumber) {
|
|
|
318
330
|
lineNumber ?? "",
|
|
319
331
|
].join(":");
|
|
320
332
|
}
|
|
333
|
+
function externalCorrelationId(snapshot) {
|
|
334
|
+
return `cli:${snapshot.agentId}:${snapshot.activity.turnId ?? snapshot.threadId}`;
|
|
335
|
+
}
|
|
321
336
|
function externalStatusLine(snapshot, queueLength) {
|
|
322
337
|
const elapsed = snapshot.activity.startedAt
|
|
323
338
|
? formatDuration((Date.now() - snapshot.activity.startedAt.getTime()) / 1000)
|
|
@@ -60,6 +60,7 @@ export function queueItemDto(item) {
|
|
|
60
60
|
description: item.description,
|
|
61
61
|
createdAt: new Date(item.createdAt).toISOString(),
|
|
62
62
|
attempts: item.attempts ?? 0,
|
|
63
|
+
correlationId: item.correlationId,
|
|
63
64
|
notBefore: item.notBefore ? new Date(item.notBefore).toISOString() : undefined,
|
|
64
65
|
lastError: item.lastError,
|
|
65
66
|
};
|
|
@@ -111,6 +111,9 @@ export async function relayRuntimeMetrics(runtime) {
|
|
|
111
111
|
export function relayRuntimeAudit(runtime, options = 50) {
|
|
112
112
|
return runtime.auditStore.list(options);
|
|
113
113
|
}
|
|
114
|
+
export function relayRuntimeAuditPage(runtime, options = {}) {
|
|
115
|
+
return runtime.auditStore.listPage(options);
|
|
116
|
+
}
|
|
114
117
|
export async function relayRuntimeSupportBundle(runtime, actor) {
|
|
115
118
|
const bundle = await createSupportBundle({
|
|
116
119
|
config: runtime.config,
|