letagents 0.12.12 → 0.12.13
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/dist/mcp/git-remote.js +7 -7
- package/dist/mcp/local-state/agent-sessions.js +63 -5
- package/dist/mcp/local-state/local-chat.js +189 -48
- package/dist/mcp/local-state/storage.js +16 -9
- package/dist/mcp/server/daemon-tool-executor.js +92 -0
- package/dist/mcp/server/register-tools.js +4 -2
- package/dist/mcp/server/runtime/agent-sessions.js +14 -5
- package/dist/mcp/server/runtime/api.js +11 -3
- package/dist/mcp/server/runtime/daemon-tool-context.js +11 -0
- package/dist/mcp/server/runtime/messages.js +55 -0
- package/dist/mcp/server/runtime/room-state.js +13 -3
- package/dist/mcp/server/runtime/rooms.js +51 -30
- package/dist/mcp/server/runtime/supervisor-bridge.js +49 -8
- package/dist/mcp/server/runtime/worker-bearer.js +5 -1
- package/dist/mcp/server/runtime.js +3 -3
- package/dist/mcp/server/supervised-tool-facade.js +34 -5
- package/dist/mcp/server/tools/messages/read-tool.js +54 -97
- package/dist/mcp/server/tools/messages/reasoning-tool.js +2 -0
- package/dist/mcp/server/tools/messages/send-tool.js +2 -0
- package/dist/mcp/server/tools/messages/status-tool.js +2 -0
- package/dist/mcp/server/tools/messages/wait-tool.js +290 -68
- package/dist/mcp/server/tools/onboarding/status-tool.js +4 -4
- package/dist/mcp/server/tools/rooms/inspection-tools.js +15 -10
- package/dist/mcp/server/tools/rooms/repo-initialization-tool.js +2 -1
- package/dist/mcp/server/tools/tasks/board-tools.js +34 -2
- package/dist/mcp/sse-client.js +163 -20
- package/dist/shared/activation-routing.js +146 -23
- package/dist/shared/agent-presence.js +6 -0
- package/dist/shared/desktop-release-manifest.js +63 -0
- package/dist/shared/desktop-release.js +60 -0
- package/dist/shared/scoped-ids.js +6 -0
- package/package.json +6 -2
- package/shared/message-contracts.d.mts +32 -0
- package/shared/message-contracts.mjs +109 -0
- package/shared/routing-aliases.d.mts +18 -0
- package/shared/routing-aliases.mjs +66 -0
- package/shared/sqlite-thread-routing.d.mts +72 -0
- package/shared/sqlite-thread-routing.mjs +1038 -0
|
@@ -4,6 +4,34 @@ import { createTask, listTasks } from "./api.js";
|
|
|
4
4
|
import { resolveTaskToolIdentity, resolveTaskToolTarget, taskActorPayload, } from "./context.js";
|
|
5
5
|
import { jsonToolResponse, taskToolError } from "./response.js";
|
|
6
6
|
import { boardIntentApprovalSchema, TASK_STATUSES, workerTaskIdentitySchema } from "./schemas.js";
|
|
7
|
+
export const MAX_BOARD_WORKFLOW_ARTIFACTS_PER_TASK = 4;
|
|
8
|
+
export const MAX_BOARD_WORKFLOW_REFS_PER_TASK = 4;
|
|
9
|
+
function compactBoardWorkflowArtifact(value) {
|
|
10
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
11
|
+
return value;
|
|
12
|
+
const artifact = value;
|
|
13
|
+
// A change-summary detail may itself contain hundreds of files. The board is
|
|
14
|
+
// an index; callers that need complete artifact detail use get_room_artifacts.
|
|
15
|
+
return Object.fromEntries(Object.entries(artifact).filter(([key]) => key !== "detail"));
|
|
16
|
+
}
|
|
17
|
+
export function compactTaskForBoard(value) {
|
|
18
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
19
|
+
return value;
|
|
20
|
+
const task = value;
|
|
21
|
+
const artifacts = Array.isArray(task.workflow_artifacts) ? task.workflow_artifacts : [];
|
|
22
|
+
const refs = Array.isArray(task.workflow_refs) ? task.workflow_refs : [];
|
|
23
|
+
return {
|
|
24
|
+
...task,
|
|
25
|
+
workflow_artifacts: artifacts
|
|
26
|
+
.slice(-MAX_BOARD_WORKFLOW_ARTIFACTS_PER_TASK)
|
|
27
|
+
.map(compactBoardWorkflowArtifact),
|
|
28
|
+
workflow_refs: refs.slice(-MAX_BOARD_WORKFLOW_REFS_PER_TASK),
|
|
29
|
+
workflow_artifact_count: artifacts.length,
|
|
30
|
+
workflow_ref_count: refs.length,
|
|
31
|
+
workflow_artifacts_truncated: artifacts.length > MAX_BOARD_WORKFLOW_ARTIFACTS_PER_TASK,
|
|
32
|
+
workflow_refs_truncated: refs.length > MAX_BOARD_WORKFLOW_REFS_PER_TASK,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
7
35
|
export function registerTaskBoardTools(server) {
|
|
8
36
|
server.tool("add_task", "Add a new task to the room board. Tasks normally start as 'proposed' and must be " +
|
|
9
37
|
"accepted before an agent can claim them. Agent-created tasks require coordinator " +
|
|
@@ -59,7 +87,7 @@ export function registerTaskBoardTools(server) {
|
|
|
59
87
|
const qs = pageParams.toString();
|
|
60
88
|
const result = await listTasks(target, qs);
|
|
61
89
|
const tasks = result.tasks ?? [];
|
|
62
|
-
allTasks.push(...tasks);
|
|
90
|
+
allTasks.push(...tasks.map(compactTaskForBoard));
|
|
63
91
|
if (!result.has_more || tasks.length === 0)
|
|
64
92
|
break;
|
|
65
93
|
const lastTask = tasks[tasks.length - 1];
|
|
@@ -68,6 +96,10 @@ export function registerTaskBoardTools(server) {
|
|
|
68
96
|
afterCursor = lastTask.id;
|
|
69
97
|
}
|
|
70
98
|
await heartbeatRoomPresence(target.effectiveRoomId, await ensureAgentIdentity());
|
|
71
|
-
return jsonToolResponse({
|
|
99
|
+
return jsonToolResponse({
|
|
100
|
+
success: true,
|
|
101
|
+
tasks: allTasks,
|
|
102
|
+
artifact_detail_instruction: "Board tasks contain bounded artifact summaries. Use get_room_artifacts for complete workflow artifact detail.",
|
|
103
|
+
}, 2);
|
|
72
104
|
});
|
|
73
105
|
}
|
package/dist/mcp/sse-client.js
CHANGED
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
import { encodeRoomIdPath } from "./room-id.js";
|
|
2
|
+
const MAX_SSE_FRAME_BYTES = 1024 * 1024;
|
|
3
|
+
class SseFrameOverflowError extends Error {
|
|
4
|
+
constructor() {
|
|
5
|
+
super("SSE frame exceeded bounded size");
|
|
6
|
+
this.name = "SseFrameOverflowError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
class SseFrameMalformedError extends Error {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
super("SSE frame was malformed", options);
|
|
12
|
+
this.name = "SseFrameMalformedError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
2
15
|
export class SseClient {
|
|
3
16
|
apiUrl;
|
|
4
17
|
getAccessToken;
|
|
5
18
|
subscriptions = new Map();
|
|
19
|
+
eventCursors = new Map();
|
|
6
20
|
constructor(apiUrl, getAccessToken) {
|
|
7
21
|
this.apiUrl = apiUrl.replace(/\/$/, "");
|
|
8
22
|
this.getAccessToken = getAccessToken;
|
|
9
23
|
}
|
|
10
|
-
subscribe(target, onMessage) {
|
|
24
|
+
subscribe(target, onMessage, onGap) {
|
|
11
25
|
const subscriptionKey = target.roomId;
|
|
12
26
|
if (this.subscriptions.has(subscriptionKey)) {
|
|
13
27
|
return;
|
|
14
28
|
}
|
|
15
29
|
const controller = new AbortController();
|
|
16
|
-
const promise = this.consumeStream(target, controller.signal, onMessage)
|
|
30
|
+
const promise = this.consumeStream(target, controller.signal, onMessage, onGap)
|
|
17
31
|
.catch((error) => {
|
|
18
32
|
if (controller.signal.aborted) {
|
|
19
33
|
return;
|
|
@@ -51,17 +65,41 @@ export class SseClient {
|
|
|
51
65
|
Authorization: `Bearer ${token}`,
|
|
52
66
|
};
|
|
53
67
|
}
|
|
54
|
-
async consumeStream(target, signal, onMessage) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
async consumeStream(target, signal, onMessage, onGap) {
|
|
69
|
+
let lastEventId = this.eventCursors.get(target.roomId) ?? null;
|
|
70
|
+
let useLegacyRoute = false;
|
|
71
|
+
let retryMs = 1_000;
|
|
72
|
+
while (!signal.aborted) {
|
|
73
|
+
const baseUrl = useLegacyRoute && target.projectId
|
|
74
|
+
? `${this.apiUrl}/projects/${encodeURIComponent(target.projectId)}/messages/stream`
|
|
75
|
+
: `${this.apiUrl}/rooms/${encodeRoomIdPath(target.roomId)}/messages/stream`;
|
|
76
|
+
const url = this.withIncludePromptOnly(this.withAgentIdentityQuery(baseUrl, target));
|
|
77
|
+
try {
|
|
78
|
+
lastEventId = await this.openStream(url, signal, target.roomId, lastEventId, onMessage, onGap);
|
|
79
|
+
retryMs = 1_000;
|
|
62
80
|
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (signal.aborted)
|
|
83
|
+
return;
|
|
84
|
+
if (error instanceof SseFrameOverflowError || error instanceof SseFrameMalformedError) {
|
|
85
|
+
lastEventId = null;
|
|
86
|
+
}
|
|
87
|
+
if (this.isMissingRouteError(error)) {
|
|
88
|
+
if (!useLegacyRoute && target.projectId) {
|
|
89
|
+
useLegacyRoute = true;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
// A missing room/route is terminal. Retrying it forever creates
|
|
93
|
+
// background traffic and can never heal without a new room join.
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
console.error(`SSE stream disconnected for room ${target.roomId}:`, error);
|
|
97
|
+
}
|
|
98
|
+
if (signal.aborted)
|
|
99
|
+
return;
|
|
100
|
+
await waitForRetry(signal, retryMs);
|
|
101
|
+
retryMs = Math.min(Math.ceil(retryMs * 1.5), 30_000);
|
|
63
102
|
}
|
|
64
|
-
await this.openStream(this.withIncludePromptOnly(this.withAgentIdentityQuery(`${this.apiUrl}/projects/${encodeURIComponent(target.projectId)}/messages/stream`, target)), signal, onMessage);
|
|
65
103
|
}
|
|
66
104
|
withIncludePromptOnly(url) {
|
|
67
105
|
return `${url}${url.includes("?") ? "&" : "?"}include_prompt_only=1`;
|
|
@@ -80,9 +118,14 @@ export class SseClient {
|
|
|
80
118
|
}
|
|
81
119
|
return `${url}${url.includes("?") ? "&" : "?"}${params.toString()}`;
|
|
82
120
|
}
|
|
83
|
-
async openStream(url, signal, onMessage) {
|
|
121
|
+
async openStream(url, signal, roomId, lastEventId, onMessage, onGap) {
|
|
122
|
+
const headers = await this.getHeaders();
|
|
123
|
+
if (signal.aborted)
|
|
124
|
+
return lastEventId;
|
|
125
|
+
if (lastEventId)
|
|
126
|
+
headers["Last-Event-ID"] = lastEventId;
|
|
84
127
|
const response = await fetch(url, {
|
|
85
|
-
headers
|
|
128
|
+
headers,
|
|
86
129
|
signal,
|
|
87
130
|
});
|
|
88
131
|
if (!response.ok) {
|
|
@@ -94,43 +137,143 @@ export class SseClient {
|
|
|
94
137
|
const reader = response.body.getReader();
|
|
95
138
|
const decoder = new TextDecoder();
|
|
96
139
|
let buffer = "";
|
|
140
|
+
let bufferBytes = 0;
|
|
97
141
|
while (!signal.aborted) {
|
|
98
142
|
const { value, done } = await reader.read();
|
|
99
143
|
if (done) {
|
|
100
144
|
break;
|
|
101
145
|
}
|
|
102
146
|
buffer += decoder.decode(value, { stream: true });
|
|
147
|
+
bufferBytes += value.byteLength;
|
|
148
|
+
if (bufferBytes > MAX_SSE_FRAME_BYTES) {
|
|
149
|
+
// The cursor is no longer safe: an unterminated/oversize frame may
|
|
150
|
+
// contain an event we could not parse. Force pull-state repair before
|
|
151
|
+
// reconnecting and never retain attacker-controlled remainder bytes.
|
|
152
|
+
this.eventCursors.delete(roomId);
|
|
153
|
+
try {
|
|
154
|
+
onGap?.({ room_id: roomId, event_cursor: null, gap: true });
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
console.error("SSE gap callback failed:", error);
|
|
158
|
+
}
|
|
159
|
+
await reader.cancel("SSE frame exceeded bounded size").catch(() => undefined);
|
|
160
|
+
throw new SseFrameOverflowError();
|
|
161
|
+
}
|
|
103
162
|
let boundaryIndex = buffer.indexOf("\n\n");
|
|
163
|
+
let consumedFrame = false;
|
|
104
164
|
while (boundaryIndex !== -1) {
|
|
105
165
|
const rawEvent = buffer.slice(0, boundaryIndex);
|
|
106
166
|
buffer = buffer.slice(boundaryIndex + 2);
|
|
107
|
-
|
|
167
|
+
try {
|
|
168
|
+
lastEventId = this.handleEvent(rawEvent, lastEventId, onMessage, onGap);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
// A complete but malformed typed frame is the same lost-boundary
|
|
172
|
+
// condition as an oversized frame. Never reconnect from its cursor
|
|
173
|
+
// and replay it forever; force the authorized pull-state repair.
|
|
174
|
+
this.eventCursors.delete(roomId);
|
|
175
|
+
try {
|
|
176
|
+
onGap?.({ room_id: roomId, event_cursor: null, gap: true });
|
|
177
|
+
}
|
|
178
|
+
catch (gapError) {
|
|
179
|
+
console.error("SSE gap callback failed:", gapError);
|
|
180
|
+
}
|
|
181
|
+
await reader.cancel("SSE frame was malformed").catch(() => undefined);
|
|
182
|
+
throw new SseFrameMalformedError({ cause: error });
|
|
183
|
+
}
|
|
184
|
+
this.rememberEventCursor(roomId, lastEventId);
|
|
185
|
+
consumedFrame = true;
|
|
108
186
|
boundaryIndex = buffer.indexOf("\n\n");
|
|
109
187
|
}
|
|
188
|
+
if (consumedFrame)
|
|
189
|
+
bufferBytes = new TextEncoder().encode(buffer).byteLength;
|
|
110
190
|
}
|
|
111
191
|
const trailing = buffer + decoder.decode();
|
|
112
192
|
if (trailing.trim()) {
|
|
113
|
-
|
|
193
|
+
try {
|
|
194
|
+
lastEventId = this.handleEvent(trailing, lastEventId, onMessage, onGap);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
this.eventCursors.delete(roomId);
|
|
198
|
+
try {
|
|
199
|
+
onGap?.({ room_id: roomId, event_cursor: null, gap: true });
|
|
200
|
+
}
|
|
201
|
+
catch (gapError) {
|
|
202
|
+
console.error("SSE gap callback failed:", gapError);
|
|
203
|
+
}
|
|
204
|
+
throw new SseFrameMalformedError({ cause: error });
|
|
205
|
+
}
|
|
206
|
+
this.rememberEventCursor(roomId, lastEventId);
|
|
114
207
|
}
|
|
208
|
+
return lastEventId;
|
|
115
209
|
}
|
|
116
|
-
handleEvent(rawEvent, onMessage) {
|
|
210
|
+
handleEvent(rawEvent, lastEventId, onMessage, onGap) {
|
|
117
211
|
const normalizedEvent = rawEvent.replace(/\r/g, "");
|
|
118
|
-
const
|
|
119
|
-
|
|
212
|
+
const lines = normalizedEvent.split("\n");
|
|
213
|
+
const eventName = lines.find((line) => line.startsWith("event:"))?.slice(6).trim() || "message";
|
|
214
|
+
const frameId = lines.find((line) => line.startsWith("id:"))?.slice(3).trim() || null;
|
|
215
|
+
const dataLines = lines
|
|
120
216
|
.filter((line) => line.startsWith("data:"))
|
|
121
217
|
.map((line) => line.slice(5).trimStart());
|
|
122
218
|
if (dataLines.length === 0) {
|
|
123
|
-
return;
|
|
219
|
+
return frameId ?? lastEventId;
|
|
124
220
|
}
|
|
125
221
|
// No prompt enrichment here: the only SSE subscriber (room-state.ts) discards
|
|
126
222
|
// the message body, and expanding the room-agent prompt on this background
|
|
127
223
|
// path would consume the once-per-session full-prompt delivery before the
|
|
128
224
|
// agent ever sees it. Prompt expansion happens only on visible tool
|
|
129
225
|
// responses (runtime/messages.ts).
|
|
130
|
-
|
|
226
|
+
const payload = JSON.parse(dataLines.join("\n"));
|
|
227
|
+
if (eventName === "room_sync") {
|
|
228
|
+
if (payload.gap === true) {
|
|
229
|
+
try {
|
|
230
|
+
onGap?.(payload);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
console.error("SSE gap callback failed:", error);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (Object.prototype.hasOwnProperty.call(payload, "event_cursor")) {
|
|
237
|
+
return typeof payload.event_cursor === "string" ? payload.event_cursor : null;
|
|
238
|
+
}
|
|
239
|
+
return frameId ?? lastEventId;
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
onMessage(payload);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
console.error("SSE message callback failed:", error);
|
|
246
|
+
}
|
|
247
|
+
return frameId ?? lastEventId;
|
|
131
248
|
}
|
|
132
249
|
isMissingRouteError(error) {
|
|
133
250
|
return (error instanceof Error &&
|
|
134
251
|
/status 404|status 405|Cannot (GET|POST|PATCH)/.test(error.message));
|
|
135
252
|
}
|
|
253
|
+
rememberEventCursor(roomId, cursor) {
|
|
254
|
+
this.eventCursors.delete(roomId);
|
|
255
|
+
if (!cursor)
|
|
256
|
+
return;
|
|
257
|
+
this.eventCursors.set(roomId, cursor);
|
|
258
|
+
while (this.eventCursors.size > 32) {
|
|
259
|
+
const oldestRoomId = this.eventCursors.keys().next().value;
|
|
260
|
+
if (!oldestRoomId)
|
|
261
|
+
break;
|
|
262
|
+
this.eventCursors.delete(oldestRoomId);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function waitForRetry(signal, delayMs) {
|
|
267
|
+
if (signal.aborted)
|
|
268
|
+
return Promise.resolve();
|
|
269
|
+
return new Promise((resolve) => {
|
|
270
|
+
const timer = setTimeout(finish, delayMs);
|
|
271
|
+
timer.unref?.();
|
|
272
|
+
function finish() {
|
|
273
|
+
signal.removeEventListener("abort", finish);
|
|
274
|
+
clearTimeout(timer);
|
|
275
|
+
resolve();
|
|
276
|
+
}
|
|
277
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
278
|
+
});
|
|
136
279
|
}
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import { normalizeRoutingHandle, normalizeRoutingSender, routingIdentityAliases, routingSenderAliasRows, routingSenderAliases, } from "../../shared/routing-aliases.mjs";
|
|
2
|
+
import { parsePositivePgIntegerScopedId } from "./scoped-ids.js";
|
|
3
|
+
/**
|
|
4
|
+
* Repository events can contain text written by any external contributor.
|
|
5
|
+
* They remain visible room activity, but their text is never an instruction
|
|
6
|
+
* channel for a managed local worker.
|
|
7
|
+
*/
|
|
8
|
+
export function isUntrustedExternalActivationSource(source) {
|
|
9
|
+
return normalizeSender(source) === "github";
|
|
10
|
+
}
|
|
1
11
|
const NON_AGENT_AT_HANDLES = new Set([
|
|
2
12
|
"charset",
|
|
3
13
|
"container",
|
|
@@ -47,8 +57,8 @@ export function attachAgentMessageActivationsFromReceipts(messages, identity, re
|
|
|
47
57
|
}
|
|
48
58
|
return messages.map((message) => {
|
|
49
59
|
const msgIdStr = String(message.id ?? "");
|
|
50
|
-
const msgNum =
|
|
51
|
-
const receipt =
|
|
60
|
+
const msgNum = parsePositivePgIntegerScopedId(msgIdStr, "msg");
|
|
61
|
+
const receipt = msgNum !== null ? receiptsMap.get(msgNum) || receiptsMap.get(msgIdStr) : null;
|
|
52
62
|
if (receipt) {
|
|
53
63
|
const reason = receipt.activation_reason;
|
|
54
64
|
return {
|
|
@@ -62,7 +72,11 @@ export function attachAgentMessageActivationsFromReceipts(messages, identity, re
|
|
|
62
72
|
},
|
|
63
73
|
};
|
|
64
74
|
}
|
|
65
|
-
|
|
75
|
+
// System failure rows are canonical silent control events. A routing
|
|
76
|
+
// snapshot with no receipt must not erase their diagnostic reason.
|
|
77
|
+
if (msgNum !== null
|
|
78
|
+
&& snapshotNumbers.has(msgNum)
|
|
79
|
+
&& normalizeSender(message.source) !== "managed_agent_failure") {
|
|
66
80
|
return {
|
|
67
81
|
...message,
|
|
68
82
|
activation: {
|
|
@@ -84,17 +98,27 @@ export function attachAgentMessageActivations(messages, identity, context = {})
|
|
|
84
98
|
return messages.map((message) => attachAgentMessageActivation(message, identity, context));
|
|
85
99
|
}
|
|
86
100
|
export function decideAgentMessageActivation(message, identity, context = {}) {
|
|
87
|
-
if (normalizeSender(message.source) === "managed_agent_failure"
|
|
101
|
+
if (normalizeSender(message.source) === "managed_agent_failure"
|
|
102
|
+
|| isUntrustedExternalActivationSource(message.source)) {
|
|
88
103
|
return decision("silent", "system_event");
|
|
89
104
|
}
|
|
90
|
-
|
|
105
|
+
const messageId = normalizedString(message.id);
|
|
106
|
+
const authoritativeLegacyDecision = context.authoritativeLegacyDecisions?.get(messageId);
|
|
107
|
+
if (authoritativeLegacyDecision)
|
|
108
|
+
return authoritativeLegacyDecision;
|
|
109
|
+
if (context.selfMessageIds !== undefined
|
|
110
|
+
? context.selfMessageIds.has(messageId)
|
|
111
|
+
: senderMatchesIdentity(message.sender, identity)) {
|
|
91
112
|
return decision("silent", "self_message");
|
|
92
113
|
}
|
|
93
114
|
const mentions = extractMentionHandles(message.text);
|
|
94
115
|
if (mentions.some(isBroadcastHandle)) {
|
|
95
116
|
return decision("activate", "broadcast");
|
|
96
117
|
}
|
|
97
|
-
|
|
118
|
+
const authoritativeExplicitMentions = context.explicitMentionMessageIds;
|
|
119
|
+
if (authoritativeExplicitMentions !== undefined
|
|
120
|
+
? authoritativeExplicitMentions.has(messageId)
|
|
121
|
+
: mentions.some((mention) => activationIdentityAliases(identity).has(normalizeMentionIdentityHandle(mention)))) {
|
|
98
122
|
return decision("activate", "explicit_mention");
|
|
99
123
|
}
|
|
100
124
|
if (hasBroadcastAddress(message.text)) {
|
|
@@ -103,13 +127,22 @@ export function decideAgentMessageActivation(message, identity, context = {}) {
|
|
|
103
127
|
if (mentions.some(isLikelyAgentMentionHandle)) {
|
|
104
128
|
return decision("silent", "explicit_other_mention");
|
|
105
129
|
}
|
|
106
|
-
|
|
130
|
+
const authoritativeThreadParticipantRootIds = context.threadParticipantRootIds;
|
|
131
|
+
const authoritativeReplyTargets = context.replyTargetMessageIds;
|
|
132
|
+
const hasAuthoritativeThreadMembership = isThreadReply(message)
|
|
133
|
+
&& authoritativeThreadParticipantRootIds !== undefined;
|
|
134
|
+
if (authoritativeReplyTargets !== undefined
|
|
135
|
+
? authoritativeReplyTargets.has(messageId)
|
|
136
|
+
: !hasAuthoritativeThreadMembership && senderMatchesIdentity(message.reply_to?.sender, identity)) {
|
|
107
137
|
return decision("activate", "reply_target");
|
|
108
138
|
}
|
|
109
139
|
if (isAgentReplyTarget(message.reply_to) && !isThreadReply(message)) {
|
|
110
140
|
return decision("silent", "other_reply_target");
|
|
111
141
|
}
|
|
112
|
-
if (isThreadReply(message)
|
|
142
|
+
if (isThreadReply(message)
|
|
143
|
+
&& (hasAuthoritativeThreadMembership
|
|
144
|
+
? authoritativeThreadParticipantRootIds.has(threadRootId(message))
|
|
145
|
+
: threadParticipantsIncludeIdentity(message, identity))) {
|
|
113
146
|
return decision("activate", "thread_participant");
|
|
114
147
|
}
|
|
115
148
|
const taskOwnerDecision = decideTaskOwnerActivation(message, identity, context);
|
|
@@ -127,9 +160,12 @@ function decision(decisionValue, reason) {
|
|
|
127
160
|
}
|
|
128
161
|
function isThreadReply(message) {
|
|
129
162
|
const ownId = normalizedString(message.id);
|
|
130
|
-
const rootId =
|
|
163
|
+
const rootId = threadRootId(message);
|
|
131
164
|
return Boolean(ownId && rootId && ownId !== rootId);
|
|
132
165
|
}
|
|
166
|
+
function threadRootId(message) {
|
|
167
|
+
return normalizedString(message.thread_root_id) || normalizedString(message.thread?.root_message_id);
|
|
168
|
+
}
|
|
133
169
|
function isAgentReplyTarget(replyTo) {
|
|
134
170
|
return normalizeSender(replyTo?.source) === "agent";
|
|
135
171
|
}
|
|
@@ -248,24 +284,111 @@ function senderMatchesIdentity(sender, identity) {
|
|
|
248
284
|
const normalizedSender = normalizeSender(sender);
|
|
249
285
|
if (!normalizedSender)
|
|
250
286
|
return false;
|
|
251
|
-
const aliases =
|
|
287
|
+
const aliases = activationIdentityAliases(identity);
|
|
252
288
|
if (aliases.has(normalizedSender))
|
|
253
289
|
return true;
|
|
254
290
|
return String(sender || "")
|
|
255
291
|
.split("|")
|
|
256
292
|
.some((part) => aliases.has(normalizeSender(part)));
|
|
257
293
|
}
|
|
258
|
-
function
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
294
|
+
export function activationIdentityAliases(identity) {
|
|
295
|
+
return routingIdentityAliases(identity);
|
|
296
|
+
}
|
|
297
|
+
/** Canonical aliases materialized from a historical message sender. */
|
|
298
|
+
export function activationSenderAliases(sender, segmentLimit = 16) {
|
|
299
|
+
return routingSenderAliases(sender, segmentLimit);
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Resolve identity-bearing addresses against the complete active room
|
|
303
|
+
* population. A display alias is authority only when it names one durable
|
|
304
|
+
* agent key globally; account/provider filtering happens after this step.
|
|
305
|
+
* Full historical sender labels take precedence over their pipe-delimited
|
|
306
|
+
* compatibility segments.
|
|
307
|
+
*/
|
|
308
|
+
export function resolveGloballyAddressedAgentKeys(message, identities) {
|
|
309
|
+
return createGlobalAgentAddressResolver(identities)(message);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Build the room-wide alias authority once, then resolve a page of legacy
|
|
313
|
+
* messages without rebuilding every active worker alias set per message.
|
|
314
|
+
*/
|
|
315
|
+
export function createGlobalAgentAddressResolver(identities) {
|
|
316
|
+
const keysByAlias = new Map();
|
|
317
|
+
for (const identity of identities) {
|
|
318
|
+
const key = normalizedString(identity.agent_key);
|
|
319
|
+
if (!key)
|
|
320
|
+
continue;
|
|
321
|
+
for (const alias of activationIdentityAliases(identity)) {
|
|
322
|
+
const keys = keysByAlias.get(alias) ?? new Set();
|
|
323
|
+
keys.add(key);
|
|
324
|
+
keysByAlias.set(alias, keys);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return (message) => {
|
|
328
|
+
const mentions = extractMentionHandles(message.text);
|
|
329
|
+
const broadcast = mentions.some(isBroadcastHandle) || hasBroadcastAddress(message.text);
|
|
330
|
+
const hasMention = mentions.some((mention) => !isBroadcastHandle(mention));
|
|
331
|
+
const hasAgentMention = mentions.some(isLikelyAgentMentionHandle);
|
|
332
|
+
const explicitMentionKeys = new Set();
|
|
333
|
+
for (const mention of mentions) {
|
|
334
|
+
if (isBroadcastHandle(mention))
|
|
335
|
+
continue;
|
|
336
|
+
const alias = normalizeMentionIdentityHandle(mention);
|
|
337
|
+
if (!alias)
|
|
338
|
+
continue;
|
|
339
|
+
const keys = keysByAlias.get(alias);
|
|
340
|
+
if (keys?.size === 1)
|
|
341
|
+
explicitMentionKeys.add(keys.values().next().value);
|
|
342
|
+
}
|
|
343
|
+
const replyTargetKeys = new Set();
|
|
344
|
+
const replyAliases = normalizedString(message.reply_to?.source) === "agent"
|
|
345
|
+
? routingSenderAliasRows(message.reply_to?.sender)
|
|
346
|
+
: [];
|
|
347
|
+
const matchingKeys = (full) => {
|
|
348
|
+
const keys = new Set();
|
|
349
|
+
for (const row of replyAliases) {
|
|
350
|
+
if (row.isFull !== full)
|
|
351
|
+
continue;
|
|
352
|
+
for (const key of keysByAlias.get(row.alias) ?? [])
|
|
353
|
+
keys.add(key);
|
|
354
|
+
}
|
|
355
|
+
return keys;
|
|
356
|
+
};
|
|
357
|
+
const fullMatches = matchingKeys(true);
|
|
358
|
+
const replyMatches = fullMatches.size > 0 ? fullMatches : matchingKeys(false);
|
|
359
|
+
if (replyMatches.size === 1)
|
|
360
|
+
replyTargetKeys.add(replyMatches.values().next().value);
|
|
361
|
+
const senderKeys = new Set();
|
|
362
|
+
const senderAliases = routingSenderAliasRows(message.sender);
|
|
363
|
+
const senderMatchingKeys = (full) => {
|
|
364
|
+
const keys = new Set();
|
|
365
|
+
for (const row of senderAliases) {
|
|
366
|
+
if (row.isFull !== full)
|
|
367
|
+
continue;
|
|
368
|
+
for (const key of keysByAlias.get(row.alias) ?? [])
|
|
369
|
+
keys.add(key);
|
|
370
|
+
}
|
|
371
|
+
return keys;
|
|
372
|
+
};
|
|
373
|
+
const senderFullMatches = senderMatchingKeys(true);
|
|
374
|
+
const senderMatches = senderFullMatches.size > 0
|
|
375
|
+
? senderFullMatches
|
|
376
|
+
: senderMatchingKeys(false);
|
|
377
|
+
if (senderMatches.size === 1)
|
|
378
|
+
senderKeys.add(senderMatches.values().next().value);
|
|
379
|
+
return {
|
|
380
|
+
broadcast,
|
|
381
|
+
hasMention,
|
|
382
|
+
hasAgentMention,
|
|
383
|
+
explicitMentionKeys,
|
|
384
|
+
replyTargetKeys,
|
|
385
|
+
senderKeys,
|
|
386
|
+
};
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
/** Shared legacy task-follow-up classifier used by API and desktop overlays. */
|
|
390
|
+
export function isTaskOwnerFollowUpMessageText(text) {
|
|
391
|
+
return isTaskOwnerFollowUp(text);
|
|
269
392
|
}
|
|
270
393
|
function extractMentionHandles(text) {
|
|
271
394
|
const raw = typeof text === "string" ? text : "";
|
|
@@ -305,10 +428,10 @@ function normalizedString(value) {
|
|
|
305
428
|
return typeof value === "string" ? value.trim() : "";
|
|
306
429
|
}
|
|
307
430
|
function normalizeSender(value) {
|
|
308
|
-
return
|
|
431
|
+
return normalizeRoutingSender(value);
|
|
309
432
|
}
|
|
310
433
|
function normalizeHandle(value) {
|
|
311
|
-
return
|
|
434
|
+
return normalizeRoutingHandle(value);
|
|
312
435
|
}
|
|
313
436
|
function normalizeMentionIdentityHandle(value) {
|
|
314
437
|
const normalized = normalizeHandle(value);
|
|
@@ -21,6 +21,12 @@ export const ROOM_AGENT_SESSION_KINDS = [
|
|
|
21
21
|
"controller",
|
|
22
22
|
"worker",
|
|
23
23
|
];
|
|
24
|
+
export function isRoomAgentDeliveryCredentialExpired(fence, now = Date.now()) {
|
|
25
|
+
if (fence?.kind !== "bearer" || !fence.expires_at)
|
|
26
|
+
return false;
|
|
27
|
+
const expiresAt = Date.parse(fence.expires_at);
|
|
28
|
+
return Number.isFinite(expiresAt) && expiresAt <= now;
|
|
29
|
+
}
|
|
24
30
|
export function normalizeRoomAgentSessionKind(value) {
|
|
25
31
|
return String(value || "").trim().toLowerCase() === "worker" ? "worker" : "controller";
|
|
26
32
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export const MAC_DESKTOP_PUBLIC_BASE_URL = "https://downloads.letagents.chat";
|
|
2
|
+
function record(value, label) {
|
|
3
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4
|
+
throw new Error(`${label} must be an object.`);
|
|
5
|
+
}
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
function numericVersion(value) {
|
|
9
|
+
if (!/^\d+\.\d+\.\d+$/.test(value)) {
|
|
10
|
+
throw new Error("Desktop release manifest version must be numeric x.y.z.");
|
|
11
|
+
}
|
|
12
|
+
return value.split(".").map(Number);
|
|
13
|
+
}
|
|
14
|
+
function compareVersions(left, right) {
|
|
15
|
+
const leftParts = numericVersion(left);
|
|
16
|
+
const rightParts = numericVersion(right);
|
|
17
|
+
for (let index = 0; index < leftParts.length; index += 1) {
|
|
18
|
+
if (leftParts[index] !== rightParts[index])
|
|
19
|
+
return leftParts[index] - rightParts[index];
|
|
20
|
+
}
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
export function parseMacDesktopPublicReleaseManifest(value, minimumVersion) {
|
|
24
|
+
const manifest = record(value, "Desktop release manifest");
|
|
25
|
+
const version = manifest.version;
|
|
26
|
+
if (manifest.schemaVersion !== 1 || manifest.channel !== "beta") {
|
|
27
|
+
throw new Error("Desktop release manifest has an unsupported contract.");
|
|
28
|
+
}
|
|
29
|
+
if (typeof version !== "string") {
|
|
30
|
+
throw new Error("Desktop release manifest version must be numeric x.y.z.");
|
|
31
|
+
}
|
|
32
|
+
numericVersion(version);
|
|
33
|
+
if (compareVersions(version, minimumVersion) < 0) {
|
|
34
|
+
throw new Error(`Desktop release manifest ${version} is older than ${minimumVersion}.`);
|
|
35
|
+
}
|
|
36
|
+
const rawAssets = record(manifest.assets, "Desktop release assets");
|
|
37
|
+
const checksumsUrl = `${MAC_DESKTOP_PUBLIC_BASE_URL}/desktop/v${version}/checksums.txt`;
|
|
38
|
+
if (manifest.checksumsUrl !== checksumsUrl) {
|
|
39
|
+
throw new Error("Desktop release manifest does not use the immutable public checksum URL.");
|
|
40
|
+
}
|
|
41
|
+
const assets = {};
|
|
42
|
+
for (const architecture of ["arm64", "x64"]) {
|
|
43
|
+
const rawAsset = record(rawAssets[architecture], `${architecture} desktop release asset`);
|
|
44
|
+
const fileName = `LetAgents-${version}-darwin-${architecture}.dmg`;
|
|
45
|
+
const publicUrl = `${MAC_DESKTOP_PUBLIC_BASE_URL}/desktop/v${version}/${fileName}`;
|
|
46
|
+
if (rawAsset.fileName !== fileName || rawAsset.publicUrl !== publicUrl) {
|
|
47
|
+
throw new Error(`${architecture} desktop release asset does not use the immutable public URL.`);
|
|
48
|
+
}
|
|
49
|
+
if (!Number.isSafeInteger(rawAsset.bytes) || Number(rawAsset.bytes) <= 0) {
|
|
50
|
+
throw new Error(`${architecture} desktop release asset must have a positive byte size.`);
|
|
51
|
+
}
|
|
52
|
+
if (typeof rawAsset.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(rawAsset.sha256)) {
|
|
53
|
+
throw new Error(`${architecture} desktop release asset must have a lowercase SHA-256 digest.`);
|
|
54
|
+
}
|
|
55
|
+
assets[architecture] = {
|
|
56
|
+
fileName,
|
|
57
|
+
publicUrl,
|
|
58
|
+
bytes: Number(rawAsset.bytes),
|
|
59
|
+
sha256: rawAsset.sha256,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return { schemaVersion: 1, channel: "beta", version, checksumsUrl, assets };
|
|
63
|
+
}
|