@nordbyte/nordrelay 0.5.2 → 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 +80 -11
- package/README.md +154 -22
- package/dist/access-control.js +7 -1
- package/dist/activity-events.js +44 -0
- package/dist/audit-log.js +40 -2
- package/dist/bot-preferences.js +1 -0
- package/dist/bot-rendering.js +10 -7
- package/dist/bot.js +535 -11
- package/dist/channel-actions.js +7 -2
- package/dist/channel-adapter.js +40 -7
- package/dist/channel-command-catalog.js +88 -0
- package/dist/channel-command-service.js +369 -0
- 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/channel-turn-service.js +237 -0
- package/dist/codex-state.js +114 -78
- package/dist/config-metadata.js +93 -13
- package/dist/config.js +103 -8
- package/dist/context-key.js +87 -5
- package/dist/discord-artifacts.js +165 -0
- package/dist/discord-bot.js +2073 -0
- package/dist/discord-channel-runtime.js +133 -0
- package/dist/discord-command-surface.js +57 -0
- package/dist/discord-rate-limit.js +141 -0
- package/dist/index.js +36 -5
- package/dist/job-store.js +127 -0
- package/dist/metrics.js +87 -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-external-activity-monitor.js +47 -6
- package/dist/relay-runtime-helpers.js +208 -0
- package/dist/relay-runtime.js +897 -394
- package/dist/remote-prompt.js +98 -0
- package/dist/runtime-cache.js +57 -0
- package/dist/session-locks.js +10 -7
- package/dist/support-bundle.js +1 -0
- package/dist/telegram-access-commands.js +15 -2
- package/dist/telegram-access-middleware.js +16 -3
- package/dist/telegram-agent-commands.js +25 -0
- package/dist/telegram-artifact-commands.js +46 -0
- package/dist/telegram-command-menu.js +3 -53
- package/dist/telegram-diagnostics-command.js +5 -50
- package/dist/telegram-general-commands.js +16 -6
- package/dist/telegram-operational-commands.js +14 -6
- package/dist/telegram-preference-commands.js +23 -127
- package/dist/telegram-queue-commands.js +74 -4
- package/dist/telegram-support-command.js +7 -0
- package/dist/telegram-update-commands.js +27 -0
- package/dist/user-management.js +208 -0
- package/dist/web-api-contract.js +17 -0
- package/dist/web-dashboard-access-routes.js +74 -1
- package/dist/web-dashboard-artifact-routes.js +3 -3
- package/dist/web-dashboard-assets.js +2 -0
- package/dist/web-dashboard-pages.js +109 -13
- package/dist/web-dashboard-peer-routes.js +204 -0
- package/dist/web-dashboard-runtime-routes.js +53 -8
- package/dist/web-dashboard-session-routes.js +27 -20
- package/dist/web-dashboard-ui.js +2 -0
- package/dist/web-dashboard.js +160 -6
- package/dist/web-state.js +33 -2
- package/dist/webui-assets/dashboard.css +75 -1
- package/dist/webui-assets/dashboard.js +779 -55
- package/package.json +5 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +578 -19
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { friendlyErrorText } from "./error-messages.js";
|
|
2
|
+
import { RemoteRelayClient } from "./peer-client.js";
|
|
3
|
+
import { peerPromptProxyPayload } from "./remote-prompt.js";
|
|
4
|
+
export async function runChannelPeerPrompt(options) {
|
|
5
|
+
if (!options.targetPeerId) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
const client = options.remoteClient ?? new RemoteRelayClient();
|
|
9
|
+
let responseMessageId;
|
|
10
|
+
let accumulated = "";
|
|
11
|
+
let lastEditAt = 0;
|
|
12
|
+
let completed = false;
|
|
13
|
+
let closeSubscription = () => { };
|
|
14
|
+
const typing = setInterval(() => {
|
|
15
|
+
void options.sendTyping().catch(() => { });
|
|
16
|
+
}, options.typingIntervalMs);
|
|
17
|
+
typing.unref?.();
|
|
18
|
+
void options.sendTyping().catch(() => { });
|
|
19
|
+
const flush = async (force = false) => {
|
|
20
|
+
if (!accumulated.trim()) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
if (!force && now - lastEditAt < options.editMinIntervalMs) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (responseMessageId === undefined) {
|
|
28
|
+
responseMessageId = await options.sendResponse(accumulated);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
await options.editResponse(responseMessageId, accumulated);
|
|
32
|
+
}
|
|
33
|
+
lastEditAt = now;
|
|
34
|
+
};
|
|
35
|
+
const done = new Promise((resolve) => {
|
|
36
|
+
const timeout = setTimeout(resolve, 30 * 60 * 1000);
|
|
37
|
+
timeout.unref?.();
|
|
38
|
+
let subscription;
|
|
39
|
+
const finish = () => {
|
|
40
|
+
clearTimeout(timeout);
|
|
41
|
+
subscription?.close();
|
|
42
|
+
resolve();
|
|
43
|
+
};
|
|
44
|
+
subscription = client.subscribe(options.targetPeerId, (event) => {
|
|
45
|
+
if (event.type === "turn_start") {
|
|
46
|
+
void options.sendTurnStart(event.prompt).catch(() => { });
|
|
47
|
+
}
|
|
48
|
+
else if (event.type === "text_delta") {
|
|
49
|
+
accumulated += event.delta;
|
|
50
|
+
void flush(false).catch(() => { });
|
|
51
|
+
}
|
|
52
|
+
else if (event.type === "tool_start") {
|
|
53
|
+
void options.sendToolStart(event.toolName).catch(() => { });
|
|
54
|
+
}
|
|
55
|
+
else if (event.type === "turn_complete") {
|
|
56
|
+
completed = true;
|
|
57
|
+
finish();
|
|
58
|
+
}
|
|
59
|
+
else if (event.type === "turn_error") {
|
|
60
|
+
accumulated += `\n\nError: ${event.error}`;
|
|
61
|
+
completed = true;
|
|
62
|
+
finish();
|
|
63
|
+
}
|
|
64
|
+
}, (error) => {
|
|
65
|
+
accumulated += `\n\nRemote event stream failed: ${error.message}`;
|
|
66
|
+
finish();
|
|
67
|
+
}, options.contextKey);
|
|
68
|
+
closeSubscription = () => subscription?.close();
|
|
69
|
+
});
|
|
70
|
+
try {
|
|
71
|
+
const result = await client.webProxy(options.targetPeerId, await peerPromptProxyPayload(options.prompt), options.prompt.activityActor, options.contextKey);
|
|
72
|
+
if (isQueuedRemoteResult(result)) {
|
|
73
|
+
closeSubscription();
|
|
74
|
+
await options.sendQueued(String(result.queueId ?? ""));
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
await done;
|
|
78
|
+
await flush(true);
|
|
79
|
+
if (!accumulated.trim() && completed) {
|
|
80
|
+
await options.sendCompleted();
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
await options.sendFailure(friendlyErrorText(error));
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
clearInterval(typing);
|
|
90
|
+
closeSubscription();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function isQueuedRemoteResult(value) {
|
|
94
|
+
return Boolean(value && typeof value === "object" && "queued" in value && value.queued);
|
|
95
|
+
}
|
package/dist/channel-runtime.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
export class ChannelCommandRouter {
|
|
2
2
|
handlers = new Map();
|
|
3
3
|
command(name, handler) {
|
|
4
|
-
const normalized =
|
|
4
|
+
const normalized = normalizeChannelCommandName(name);
|
|
5
5
|
if (!normalized) {
|
|
6
6
|
throw new Error("Channel command name is required.");
|
|
7
7
|
}
|
|
8
8
|
this.handlers.set(normalized, handler);
|
|
9
9
|
return this;
|
|
10
10
|
}
|
|
11
|
+
commands(names, handler) {
|
|
12
|
+
for (const name of names) {
|
|
13
|
+
this.command(name, handler);
|
|
14
|
+
}
|
|
15
|
+
return this;
|
|
16
|
+
}
|
|
11
17
|
async dispatch(message) {
|
|
12
18
|
const parsed = parseChannelCommand(message.text ?? "");
|
|
13
19
|
if (!parsed) {
|
|
@@ -36,13 +42,14 @@ export async function deliverChannelAction(runtime, context, response) {
|
|
|
36
42
|
buttons: response.buttons,
|
|
37
43
|
});
|
|
38
44
|
}
|
|
39
|
-
export function parseChannelCommand(text) {
|
|
40
|
-
const
|
|
45
|
+
export function parseChannelCommand(text, options = {}) {
|
|
46
|
+
const mention = options.allowBotMention === false ? "" : "(?:@\\w+)?";
|
|
47
|
+
const match = text.trimStart().match(new RegExp(`^/([a-zA-Z0-9_-]+)${mention}(?:\\s+([\\s\\S]*))?$`));
|
|
41
48
|
if (!match?.[1]) {
|
|
42
49
|
return null;
|
|
43
50
|
}
|
|
44
51
|
return {
|
|
45
|
-
command:
|
|
52
|
+
command: normalizeChannelCommandName(match[1]),
|
|
46
53
|
argument: match[2]?.trim() ?? "",
|
|
47
54
|
};
|
|
48
55
|
}
|
|
@@ -84,6 +91,6 @@ export class InMemoryChannelRuntime {
|
|
|
84
91
|
return { messageId };
|
|
85
92
|
}
|
|
86
93
|
}
|
|
87
|
-
function
|
|
94
|
+
export function normalizeChannelCommandName(name) {
|
|
88
95
|
return name.trim().replace(/^\//, "").toLowerCase();
|
|
89
96
|
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { CODEX_AGENT_CAPABILITIES, agentLabel, } from "./agent.js";
|
|
3
|
+
import { friendlyErrorText } from "./error-messages.js";
|
|
4
|
+
export class ChannelTurnService {
|
|
5
|
+
options;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.options = options;
|
|
8
|
+
}
|
|
9
|
+
async run(session, envelope) {
|
|
10
|
+
const actor = envelope.activityActor;
|
|
11
|
+
await this.options.ensureActiveThread(session);
|
|
12
|
+
const info = session.getInfo();
|
|
13
|
+
if ((info.capabilities ?? CODEX_AGENT_CAPABILITIES).auth) {
|
|
14
|
+
const auth = await this.options.checkAuth(info);
|
|
15
|
+
if (!auth.authenticated) {
|
|
16
|
+
throw new Error(`${agentLabel(info.agentId)} is not authenticated: ${auth.detail}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const turnId = randomUUID().slice(0, 12);
|
|
20
|
+
const startedMs = Date.now();
|
|
21
|
+
this.options.setCurrentTurn(turnId, startedMs, "");
|
|
22
|
+
this.options.setCurrentProgress({
|
|
23
|
+
id: turnId,
|
|
24
|
+
source: this.options.source,
|
|
25
|
+
status: "running",
|
|
26
|
+
prompt: envelope.description,
|
|
27
|
+
agentId: info.agentId,
|
|
28
|
+
agentLabel: info.agentLabel,
|
|
29
|
+
threadId: info.threadId,
|
|
30
|
+
workspace: info.workspace,
|
|
31
|
+
startedAt: new Date(startedMs).toISOString(),
|
|
32
|
+
updatedAt: new Date(startedMs).toISOString(),
|
|
33
|
+
durationMs: 0,
|
|
34
|
+
outputChars: 0,
|
|
35
|
+
tools: [],
|
|
36
|
+
});
|
|
37
|
+
this.options.setLastPrompt(envelope);
|
|
38
|
+
const startedDate = new Date();
|
|
39
|
+
const startedAt = startedDate.toISOString();
|
|
40
|
+
this.options.chatStore.append({
|
|
41
|
+
threadId: info.threadId ?? "pending",
|
|
42
|
+
role: "user",
|
|
43
|
+
text: envelope.description,
|
|
44
|
+
source: this.options.source,
|
|
45
|
+
turnId,
|
|
46
|
+
timestamp: startedAt,
|
|
47
|
+
});
|
|
48
|
+
this.options.appendActivity({
|
|
49
|
+
source: this.options.source,
|
|
50
|
+
status: "running",
|
|
51
|
+
type: "prompt_started",
|
|
52
|
+
threadId: info.threadId,
|
|
53
|
+
workspace: info.workspace,
|
|
54
|
+
agentId: info.agentId,
|
|
55
|
+
actor,
|
|
56
|
+
prompt: envelope.description,
|
|
57
|
+
});
|
|
58
|
+
this.options.appendAudit({
|
|
59
|
+
action: "prompt_started",
|
|
60
|
+
status: "ok",
|
|
61
|
+
contextKey: this.options.contextKey,
|
|
62
|
+
agentId: info.agentId,
|
|
63
|
+
threadId: info.threadId,
|
|
64
|
+
workspace: info.workspace,
|
|
65
|
+
actor,
|
|
66
|
+
description: envelope.description,
|
|
67
|
+
});
|
|
68
|
+
this.options.broadcast({ type: "turn_start", id: turnId, prompt: envelope.description, at: startedAt, source: this.options.source });
|
|
69
|
+
try {
|
|
70
|
+
await session.prompt(envelope.input, this.callbacks(turnId, info, envelope, actor));
|
|
71
|
+
this.options.updateSession(session);
|
|
72
|
+
await this.options.artifactService.persistWorkspaceArtifactsForTurn(session.getInfo().workspace, turnId, startedDate);
|
|
73
|
+
const text = this.options.getAccumulatedText();
|
|
74
|
+
if (text.trim()) {
|
|
75
|
+
this.options.chatStore.append({
|
|
76
|
+
threadId: info.threadId ?? "pending",
|
|
77
|
+
role: "agent",
|
|
78
|
+
text,
|
|
79
|
+
source: this.options.source,
|
|
80
|
+
turnId,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
this.options.appendActivity({
|
|
84
|
+
source: this.options.source,
|
|
85
|
+
status: "completed",
|
|
86
|
+
type: "prompt_completed",
|
|
87
|
+
threadId: info.threadId,
|
|
88
|
+
workspace: info.workspace,
|
|
89
|
+
agentId: info.agentId,
|
|
90
|
+
actor,
|
|
91
|
+
prompt: envelope.description,
|
|
92
|
+
durationMs: Date.now() - this.options.getCurrentTurnStartedAt(),
|
|
93
|
+
});
|
|
94
|
+
this.options.appendAudit({
|
|
95
|
+
action: "prompt_completed",
|
|
96
|
+
status: "ok",
|
|
97
|
+
contextKey: this.options.contextKey,
|
|
98
|
+
agentId: info.agentId,
|
|
99
|
+
threadId: info.threadId,
|
|
100
|
+
workspace: info.workspace,
|
|
101
|
+
actor,
|
|
102
|
+
description: envelope.description,
|
|
103
|
+
});
|
|
104
|
+
this.updateCurrentProgress({ status: "completed" });
|
|
105
|
+
this.options.broadcast({ type: "turn_complete", id: turnId, at: new Date().toISOString() });
|
|
106
|
+
this.options.broadcast({ type: "chat_history", messages: await this.options.chatHistory() });
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
const errorText = friendlyErrorText(error);
|
|
110
|
+
this.options.chatStore.append({
|
|
111
|
+
threadId: info.threadId ?? "pending",
|
|
112
|
+
role: "system",
|
|
113
|
+
text: `Error: ${errorText}`,
|
|
114
|
+
source: this.options.source,
|
|
115
|
+
turnId,
|
|
116
|
+
});
|
|
117
|
+
this.options.appendActivity({
|
|
118
|
+
source: this.options.source,
|
|
119
|
+
status: "failed",
|
|
120
|
+
type: "prompt_failed",
|
|
121
|
+
threadId: info.threadId,
|
|
122
|
+
workspace: info.workspace,
|
|
123
|
+
agentId: info.agentId,
|
|
124
|
+
actor,
|
|
125
|
+
prompt: envelope.description,
|
|
126
|
+
detail: errorText,
|
|
127
|
+
durationMs: Date.now() - this.options.getCurrentTurnStartedAt(),
|
|
128
|
+
});
|
|
129
|
+
this.options.appendAudit({
|
|
130
|
+
action: "prompt_failed",
|
|
131
|
+
status: "failed",
|
|
132
|
+
contextKey: this.options.contextKey,
|
|
133
|
+
agentId: info.agentId,
|
|
134
|
+
threadId: info.threadId,
|
|
135
|
+
workspace: info.workspace,
|
|
136
|
+
actor,
|
|
137
|
+
description: envelope.description,
|
|
138
|
+
detail: errorText,
|
|
139
|
+
});
|
|
140
|
+
this.updateCurrentProgress({ status: "failed", detail: errorText });
|
|
141
|
+
this.options.broadcast({ type: "turn_error", id: turnId, error: errorText, at: new Date().toISOString() });
|
|
142
|
+
this.options.broadcast({ type: "chat_history", messages: await this.options.chatHistory() });
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
const progress = this.options.getCurrentProgress();
|
|
147
|
+
if (progress) {
|
|
148
|
+
progress.durationMs = Date.now() - this.options.getCurrentTurnStartedAt();
|
|
149
|
+
progress.updatedAt = new Date().toISOString();
|
|
150
|
+
}
|
|
151
|
+
this.options.setCurrentTurn(null);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
callbacks(turnId, info, envelope, actor) {
|
|
155
|
+
return {
|
|
156
|
+
onTextDelta: (delta) => {
|
|
157
|
+
const nextText = this.options.getAccumulatedText() + delta;
|
|
158
|
+
this.options.setAccumulatedText(nextText);
|
|
159
|
+
this.updateCurrentProgress({ outputChars: nextText.length });
|
|
160
|
+
this.options.broadcast({ type: "text_delta", id: turnId, delta });
|
|
161
|
+
},
|
|
162
|
+
onToolStart: (toolName, toolCallId) => {
|
|
163
|
+
this.addCurrentTool(toolName);
|
|
164
|
+
this.options.appendActivity({
|
|
165
|
+
source: this.options.source,
|
|
166
|
+
status: "running",
|
|
167
|
+
type: "tool_started",
|
|
168
|
+
threadId: info.threadId,
|
|
169
|
+
workspace: info.workspace,
|
|
170
|
+
agentId: info.agentId,
|
|
171
|
+
actor,
|
|
172
|
+
prompt: envelope.description,
|
|
173
|
+
detail: toolName,
|
|
174
|
+
});
|
|
175
|
+
this.options.broadcast({ type: "tool_start", id: turnId, toolCallId, toolName });
|
|
176
|
+
},
|
|
177
|
+
onToolUpdate: (toolCallId, partialResult) => {
|
|
178
|
+
this.updateCurrentProgress();
|
|
179
|
+
this.options.broadcast({ type: "tool_update", id: turnId, toolCallId, partialResult });
|
|
180
|
+
},
|
|
181
|
+
onToolEnd: (toolCallId, isError) => {
|
|
182
|
+
const progress = this.options.getCurrentProgress();
|
|
183
|
+
const toolName = progress?.currentTool ?? progress?.lastTool ?? toolCallId;
|
|
184
|
+
this.updateCurrentProgress({ currentTool: undefined });
|
|
185
|
+
this.options.appendActivity({
|
|
186
|
+
source: this.options.source,
|
|
187
|
+
status: isError ? "failed" : "completed",
|
|
188
|
+
type: isError ? "tool_failed" : "tool_completed",
|
|
189
|
+
threadId: info.threadId,
|
|
190
|
+
workspace: info.workspace,
|
|
191
|
+
agentId: info.agentId,
|
|
192
|
+
actor,
|
|
193
|
+
prompt: envelope.description,
|
|
194
|
+
detail: toolName,
|
|
195
|
+
});
|
|
196
|
+
this.options.broadcast({ type: "tool_end", id: turnId, toolCallId, isError });
|
|
197
|
+
},
|
|
198
|
+
onTodoUpdate: (items) => {
|
|
199
|
+
this.updateCurrentProgress({ detail: `Plan: ${items.filter((item) => item.completed).length}/${items.length} done` });
|
|
200
|
+
this.options.broadcast({ type: "todo_update", id: turnId, items });
|
|
201
|
+
},
|
|
202
|
+
onTurnComplete: () => { },
|
|
203
|
+
onAgentEnd: () => this.options.broadcast({ type: "turn_complete", id: turnId, at: new Date().toISOString() }),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
updateCurrentProgress(patch = {}) {
|
|
207
|
+
const progress = this.options.getCurrentProgress();
|
|
208
|
+
if (!progress) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if ("currentTool" in patch) {
|
|
212
|
+
progress.currentTool = patch.currentTool;
|
|
213
|
+
const { currentTool: _currentTool, ...rest } = patch;
|
|
214
|
+
Object.assign(progress, rest);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
Object.assign(progress, patch);
|
|
218
|
+
}
|
|
219
|
+
progress.durationMs = Date.now() - this.options.getCurrentTurnStartedAt();
|
|
220
|
+
progress.updatedAt = new Date().toISOString();
|
|
221
|
+
this.options.setCurrentProgress(progress);
|
|
222
|
+
}
|
|
223
|
+
addCurrentTool(toolName) {
|
|
224
|
+
const progress = this.options.getCurrentProgress();
|
|
225
|
+
if (!progress) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const existing = progress.tools.find((tool) => tool.name === toolName);
|
|
229
|
+
if (existing) {
|
|
230
|
+
existing.count += 1;
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
progress.tools.push({ name: toolName, count: 1 });
|
|
234
|
+
}
|
|
235
|
+
this.updateCurrentProgress({ currentTool: toolName, lastTool: toolName });
|
|
236
|
+
}
|
|
237
|
+
}
|
package/dist/codex-state.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
1
|
+
import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { isCodexApprovalPolicy, isCodexSandboxMode, } from "./codex-launch.js";
|
|
4
|
+
const ROLLOUT_CACHE_MAX_EVENTS = 200;
|
|
5
|
+
const rolloutSnapshotCache = new Map();
|
|
4
6
|
export const FALLBACK_MODELS = [
|
|
5
7
|
{ slug: "gpt-5.5", displayName: "GPT-5.5" },
|
|
6
8
|
{ slug: "gpt-5.4", displayName: "GPT-5.4" },
|
|
@@ -74,31 +76,7 @@ export function getThreadUsage(id) {
|
|
|
74
76
|
}
|
|
75
77
|
}
|
|
76
78
|
export function getThreadActivity(id, options = {}) {
|
|
77
|
-
|
|
78
|
-
if (!rolloutPath || !existsSync(rolloutPath)) {
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
try {
|
|
82
|
-
const parsed = parseActivityFromRollout(id, rolloutPath, readFileSync(rolloutPath, "utf8"));
|
|
83
|
-
const fileModifiedAtMs = statSync(rolloutPath).mtimeMs;
|
|
84
|
-
const updatedAtMs = Math.max(parsed.updatedAt?.getTime() ?? 0, fileModifiedAtMs);
|
|
85
|
-
const updatedAt = updatedAtMs > 0 ? new Date(updatedAtMs) : parsed.updatedAt;
|
|
86
|
-
const staleAfterMs = options.staleAfterMs ?? 5 * 60 * 1000;
|
|
87
|
-
const nowMs = options.nowMs ?? Date.now();
|
|
88
|
-
const stale = Boolean(parsed.active &&
|
|
89
|
-
updatedAt &&
|
|
90
|
-
staleAfterMs > 0 &&
|
|
91
|
-
nowMs - updatedAt.getTime() > staleAfterMs);
|
|
92
|
-
return {
|
|
93
|
-
...parsed,
|
|
94
|
-
updatedAt,
|
|
95
|
-
stale,
|
|
96
|
-
active: parsed.active && !stale,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
79
|
+
return getThreadRolloutSnapshot(id, { ...options, maxEvents: 0 })?.activity ?? null;
|
|
102
80
|
}
|
|
103
81
|
export function getThreadRolloutSnapshot(id, options = {}) {
|
|
104
82
|
const rolloutPath = getThreadRolloutPath(id);
|
|
@@ -106,8 +84,9 @@ export function getThreadRolloutSnapshot(id, options = {}) {
|
|
|
106
84
|
return null;
|
|
107
85
|
}
|
|
108
86
|
try {
|
|
109
|
-
const
|
|
110
|
-
|
|
87
|
+
const fileModifiedAtMs = statSync(rolloutPath).mtimeMs;
|
|
88
|
+
const parsed = readCachedRolloutSnapshot(id, rolloutPath);
|
|
89
|
+
return finalizeRolloutSnapshot(parsed, fileModifiedAtMs, options);
|
|
111
90
|
}
|
|
112
91
|
catch {
|
|
113
92
|
return null;
|
|
@@ -167,13 +146,7 @@ export function getThreadRolloutPath(id) {
|
|
|
167
146
|
}) ?? null);
|
|
168
147
|
}
|
|
169
148
|
function parseUsageFromRollout(contents) {
|
|
170
|
-
|
|
171
|
-
let contextUsedPercent = null;
|
|
172
|
-
let lastTokenUsage = null;
|
|
173
|
-
let totalTokenUsage = null;
|
|
174
|
-
let rateLimits = null;
|
|
175
|
-
let updatedAt = null;
|
|
176
|
-
for (const line of contents.split(/\r?\n/)) {
|
|
149
|
+
for (const line of iterateLinesReverse(contents)) {
|
|
177
150
|
if (!line.includes('"token_count"')) {
|
|
178
151
|
continue;
|
|
179
152
|
}
|
|
@@ -188,6 +161,7 @@ function parseUsageFromRollout(contents) {
|
|
|
188
161
|
if (payload?.type !== "token_count") {
|
|
189
162
|
continue;
|
|
190
163
|
}
|
|
164
|
+
let updatedAt = null;
|
|
191
165
|
const timestamp = readString(readObject(event)?.timestamp);
|
|
192
166
|
if (timestamp) {
|
|
193
167
|
const parsedTimestamp = new Date(timestamp);
|
|
@@ -196,54 +170,116 @@ function parseUsageFromRollout(contents) {
|
|
|
196
170
|
}
|
|
197
171
|
}
|
|
198
172
|
const info = readObject(payload.info);
|
|
199
|
-
const
|
|
200
|
-
const
|
|
173
|
+
const totalTokenUsage = parseTokenUsage(readObject(info?.total_token_usage));
|
|
174
|
+
const lastTokenUsage = parseTokenUsage(readObject(info?.last_token_usage));
|
|
201
175
|
const parsedContextWindow = readNumber(info?.model_context_window);
|
|
202
|
-
|
|
203
|
-
|
|
176
|
+
const contextWindow = parsedContextWindow !== null && parsedContextWindow > 0
|
|
177
|
+
? parsedContextWindow
|
|
178
|
+
: null;
|
|
179
|
+
const contextUsedPercent = lastTokenUsage && contextWindow
|
|
180
|
+
? Math.min(100, (lastTokenUsage.totalTokens / contextWindow) * 100)
|
|
181
|
+
: null;
|
|
182
|
+
const rateLimits = parseRateLimits(readObject(payload.rate_limits));
|
|
183
|
+
if (!lastTokenUsage && !totalTokenUsage && !rateLimits) {
|
|
184
|
+
continue;
|
|
204
185
|
}
|
|
205
|
-
|
|
206
|
-
|
|
186
|
+
return {
|
|
187
|
+
contextWindow,
|
|
188
|
+
contextUsedPercent,
|
|
189
|
+
lastTokenUsage,
|
|
190
|
+
totalTokenUsage,
|
|
191
|
+
rateLimits,
|
|
192
|
+
updatedAt,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
function readCachedRolloutSnapshot(threadId, rolloutPath) {
|
|
198
|
+
const size = statSync(rolloutPath).size;
|
|
199
|
+
const cached = rolloutSnapshotCache.get(rolloutPath);
|
|
200
|
+
if (cached && size >= cached.byteOffset) {
|
|
201
|
+
const suffix = size > cached.byteOffset
|
|
202
|
+
? readFileRangeUtf8(rolloutPath, cached.byteOffset, size - cached.byteOffset)
|
|
203
|
+
: "";
|
|
204
|
+
if (!suffix.trim()) {
|
|
205
|
+
return cached.parsed;
|
|
207
206
|
}
|
|
208
|
-
|
|
209
|
-
|
|
207
|
+
const parsed = parseRolloutSnapshot(threadId, rolloutPath, suffix, {
|
|
208
|
+
base: cached.parsed,
|
|
209
|
+
maxEvents: ROLLOUT_CACHE_MAX_EVENTS,
|
|
210
|
+
});
|
|
211
|
+
rolloutSnapshotCache.set(rolloutPath, { byteOffset: size, parsed });
|
|
212
|
+
return parsed;
|
|
213
|
+
}
|
|
214
|
+
const contents = readFileSync(rolloutPath, "utf8");
|
|
215
|
+
const parsed = parseRolloutSnapshot(threadId, rolloutPath, contents, {
|
|
216
|
+
maxEvents: ROLLOUT_CACHE_MAX_EVENTS,
|
|
217
|
+
});
|
|
218
|
+
rolloutSnapshotCache.set(rolloutPath, {
|
|
219
|
+
byteOffset: Buffer.byteLength(contents),
|
|
220
|
+
parsed,
|
|
221
|
+
});
|
|
222
|
+
return parsed;
|
|
223
|
+
}
|
|
224
|
+
function readFileRangeUtf8(filePath, position, length) {
|
|
225
|
+
if (length <= 0) {
|
|
226
|
+
return "";
|
|
227
|
+
}
|
|
228
|
+
const fd = openSync(filePath, "r");
|
|
229
|
+
try {
|
|
230
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
231
|
+
const bytesRead = readSync(fd, buffer, 0, length, position);
|
|
232
|
+
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
closeSync(fd);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function* iterateLinesReverse(contents) {
|
|
239
|
+
let end = contents.length;
|
|
240
|
+
while (end > 0) {
|
|
241
|
+
let start = contents.lastIndexOf("\n", end - 1);
|
|
242
|
+
const lineStart = start === -1 ? 0 : start + 1;
|
|
243
|
+
let line = contents.slice(lineStart, end);
|
|
244
|
+
if (line.endsWith("\r")) {
|
|
245
|
+
line = line.slice(0, -1);
|
|
210
246
|
}
|
|
211
|
-
if (
|
|
212
|
-
|
|
247
|
+
if (line.trim()) {
|
|
248
|
+
yield line;
|
|
213
249
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
rateLimits = parsedRateLimits;
|
|
250
|
+
if (start === -1) {
|
|
251
|
+
break;
|
|
217
252
|
}
|
|
253
|
+
end = start;
|
|
218
254
|
}
|
|
219
|
-
if (!lastTokenUsage && !totalTokenUsage && !rateLimits) {
|
|
220
|
-
return null;
|
|
221
|
-
}
|
|
222
|
-
return {
|
|
223
|
-
contextWindow,
|
|
224
|
-
contextUsedPercent,
|
|
225
|
-
lastTokenUsage,
|
|
226
|
-
totalTokenUsage,
|
|
227
|
-
rateLimits,
|
|
228
|
-
updatedAt,
|
|
229
|
-
};
|
|
230
255
|
}
|
|
231
|
-
function
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
let
|
|
236
|
-
let
|
|
237
|
-
let
|
|
238
|
-
|
|
239
|
-
let latestUserMessage = null;
|
|
240
|
-
let latestToolName = null;
|
|
241
|
-
const events = [];
|
|
256
|
+
function parseRolloutSnapshot(threadId, rolloutPath, contents, options = {}) {
|
|
257
|
+
let activeTurnId = options.base?.activity.active ? options.base.activity.turnId : null;
|
|
258
|
+
let startedAt = options.base?.activity.active ? options.base.activity.startedAt : null;
|
|
259
|
+
let updatedAt = options.base?.activity.updatedAt ?? null;
|
|
260
|
+
let latestAgentMessage = options.base?.latestAgentMessage ?? null;
|
|
261
|
+
let latestUserMessage = options.base?.latestUserMessage ?? null;
|
|
262
|
+
let latestToolName = options.base?.latestToolName ?? null;
|
|
263
|
+
const events = [...(options.base?.events ?? [])];
|
|
242
264
|
const lines = contents.split(/\r?\n/);
|
|
265
|
+
const lineNumberOffset = options.base?.lineCount ?? 0;
|
|
266
|
+
let lineCount = lineNumberOffset;
|
|
267
|
+
const afterLine = options.afterLine ?? 0;
|
|
268
|
+
const maxEvents = options.maxEvents ?? Number.POSITIVE_INFINITY;
|
|
269
|
+
const pushEvent = (event) => {
|
|
270
|
+
if (maxEvents <= 0 || event.lineNumber <= afterLine) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
events.push(event);
|
|
274
|
+
if (Number.isFinite(maxEvents) && events.length > maxEvents) {
|
|
275
|
+
events.splice(0, events.length - maxEvents);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
243
278
|
for (const [index, line] of lines.entries()) {
|
|
244
279
|
if (!line.trim()) {
|
|
245
280
|
continue;
|
|
246
281
|
}
|
|
282
|
+
lineCount += 1;
|
|
247
283
|
if (!line.includes('"task_') &&
|
|
248
284
|
!line.includes('"turn_') &&
|
|
249
285
|
!line.includes('"user_message"') &&
|
|
@@ -262,7 +298,7 @@ function parseRolloutSnapshot(threadId, rolloutPath, contents) {
|
|
|
262
298
|
const eventObject = readObject(event);
|
|
263
299
|
const payload = readObject(eventObject?.payload);
|
|
264
300
|
const eventTimestamp = parseTimestamp(readString(eventObject?.timestamp));
|
|
265
|
-
const lineNumber = index + 1;
|
|
301
|
+
const lineNumber = lineNumberOffset + index + 1;
|
|
266
302
|
if (activeTurnId && eventTimestamp) {
|
|
267
303
|
updatedAt = eventTimestamp;
|
|
268
304
|
}
|
|
@@ -274,7 +310,7 @@ function parseRolloutSnapshot(threadId, rolloutPath, contents) {
|
|
|
274
310
|
activeTurnId = readString(payload?.turn_id);
|
|
275
311
|
startedAt = parseUnixSeconds(readNumber(payload?.started_at)) ?? eventTimestamp;
|
|
276
312
|
updatedAt = eventTimestamp ?? startedAt;
|
|
277
|
-
|
|
313
|
+
pushEvent({
|
|
278
314
|
lineNumber,
|
|
279
315
|
kind: "task",
|
|
280
316
|
timestamp: eventTimestamp,
|
|
@@ -289,7 +325,7 @@ function parseRolloutSnapshot(threadId, rolloutPath, contents) {
|
|
|
289
325
|
}
|
|
290
326
|
if (isTaskTerminalEvent(type)) {
|
|
291
327
|
const turnId = readString(payload?.turn_id);
|
|
292
|
-
|
|
328
|
+
pushEvent({
|
|
293
329
|
lineNumber,
|
|
294
330
|
kind: "task",
|
|
295
331
|
timestamp: eventTimestamp,
|
|
@@ -309,7 +345,7 @@ function parseRolloutSnapshot(threadId, rolloutPath, contents) {
|
|
|
309
345
|
}
|
|
310
346
|
if (type === "user_message") {
|
|
311
347
|
latestUserMessage = readString(payload?.message);
|
|
312
|
-
|
|
348
|
+
pushEvent({
|
|
313
349
|
lineNumber,
|
|
314
350
|
kind: "user",
|
|
315
351
|
timestamp: eventTimestamp,
|
|
@@ -324,7 +360,7 @@ function parseRolloutSnapshot(threadId, rolloutPath, contents) {
|
|
|
324
360
|
}
|
|
325
361
|
if (type === "agent_message") {
|
|
326
362
|
latestAgentMessage = readString(payload?.message);
|
|
327
|
-
|
|
363
|
+
pushEvent({
|
|
328
364
|
lineNumber,
|
|
329
365
|
kind: "agent",
|
|
330
366
|
timestamp: eventTimestamp,
|
|
@@ -339,7 +375,7 @@ function parseRolloutSnapshot(threadId, rolloutPath, contents) {
|
|
|
339
375
|
}
|
|
340
376
|
if (type === "function_call") {
|
|
341
377
|
latestToolName = readString(payload?.name);
|
|
342
|
-
|
|
378
|
+
pushEvent({
|
|
343
379
|
lineNumber,
|
|
344
380
|
kind: "tool",
|
|
345
381
|
timestamp: eventTimestamp,
|
|
@@ -353,7 +389,7 @@ function parseRolloutSnapshot(threadId, rolloutPath, contents) {
|
|
|
353
389
|
continue;
|
|
354
390
|
}
|
|
355
391
|
if (type === "function_call_output") {
|
|
356
|
-
|
|
392
|
+
pushEvent({
|
|
357
393
|
lineNumber,
|
|
358
394
|
kind: "tool",
|
|
359
395
|
timestamp: eventTimestamp,
|
|
@@ -369,7 +405,7 @@ function parseRolloutSnapshot(threadId, rolloutPath, contents) {
|
|
|
369
405
|
return {
|
|
370
406
|
threadId,
|
|
371
407
|
rolloutPath,
|
|
372
|
-
lineCount
|
|
408
|
+
lineCount,
|
|
373
409
|
activity: {
|
|
374
410
|
threadId,
|
|
375
411
|
rolloutPath,
|