letagents 0.12.13 → 0.12.15
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 +32 -0
- package/dist/mcp/local-state/agent-sessions.js +18 -3
- package/dist/mcp/local-state/local-chat.js +11 -27
- package/dist/mcp/local-state/storage.js +5 -1
- package/dist/mcp/server/register-tools.js +4 -1
- package/dist/mcp/server/runtime/agent-sessions.js +2 -2
- package/dist/mcp/server/runtime/api.js +5 -1
- package/dist/mcp/server/runtime/execution-profile.js +1 -0
- package/dist/mcp/server/runtime/identity.js +5 -0
- package/dist/mcp/server/runtime/presence.js +3 -3
- package/dist/mcp/server/runtime/room-api.js +5 -4
- package/dist/mcp/server/runtime/room-state.js +6 -6
- package/dist/mcp/server/runtime/rooms.js +3 -3
- package/dist/mcp/server/runtime/supervisor-bridge.js +80 -3
- package/dist/mcp/server/runtime/tool-surface-policy.js +7 -0
- package/dist/mcp/server/runtime/worker-bearer.js +12 -1
- package/dist/mcp/server/runtime/worker-handles.js +190 -0
- package/dist/mcp/server/runtime-contract.js +1 -0
- package/dist/mcp/server/runtime.js +3 -3
- package/dist/mcp/server/supervised-tool-facade.js +74 -2
- package/dist/mcp/server/tools/agent-sessions.js +22 -5
- package/dist/mcp/server/tools/messages/read-tool.js +6 -2
- package/dist/mcp/server/tools/messages/wait-tool.js +22 -10
- package/dist/mcp/server/tools/onboarding/name-tool.js +5 -2
- package/dist/mcp/server/worker-tool-facade.js +38 -0
- package/dist/mcp/worker-call-context.js +39 -0
- package/dist/shared/activation-routing.js +21 -4
- package/dist/shared/mcp-worker.js +7 -0
- package/dist/shared/room-agent-prompts.js +2 -2
- package/package.json +3 -3
- package/shared/execution-approval-projection.d.mts +32 -0
- package/shared/execution-approval-projection.mjs +107 -0
- package/shared/execution-approval-publication-item.d.mts +20 -0
- package/shared/execution-approval-publication-item.mjs +61 -0
- package/shared/execution-approval-publication.d.mts +53 -0
- package/shared/execution-approval-publication.mjs +136 -0
- package/shared/execution-delegation-decision.d.mts +37 -0
- package/shared/execution-delegation-decision.mjs +73 -0
- package/shared/room-agent-work.d.mts +31 -0
- package/shared/room-agent-work.mjs +44 -0
- package/shared/room-resource-invalidation.d.mts +38 -0
- package/shared/room-resource-invalidation.mjs +50 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { getLocalStatePath, readLocalStateSnapshot, updateLocalState, } from "../../local-state/storage.js";
|
|
3
|
+
import { isLocalRoomStorageEnabled, resolveLocalRoomStorageIdentifiers } from "../../local-state.js";
|
|
4
|
+
import { assertWorkerConnection, pinWorkerConnection, pinnedWorkerConnection, withWorkerCall } from "../../worker-call-context.js";
|
|
5
|
+
import { isMcpWorkerId } from "../../../shared/mcp-worker.js";
|
|
6
|
+
import { pickLocalCodename } from "../../../shared/codenames.js";
|
|
7
|
+
import { buildAgentActorLabel } from "../../../shared/agent-identity.js";
|
|
8
|
+
import { encodeRoomIdPath } from "../../room-id.js";
|
|
9
|
+
import { getGitCurrentBranch } from "../../git-remote.js";
|
|
10
|
+
import { apiCall, getApiUrl } from "./api.js";
|
|
11
|
+
import { resolveOwnerContext } from "./identity/directory.js";
|
|
12
|
+
import { detectAgentIdeLabel, detectAgentRuntimeLabel } from "./identity/config.js";
|
|
13
|
+
import { getSessionLivenessRegistration } from "./identity/liveness.js";
|
|
14
|
+
import { requireValidWorkerBearerRuntime } from "./worker-bearer.js";
|
|
15
|
+
const connecting = new Map();
|
|
16
|
+
function snapshot() {
|
|
17
|
+
const result = readLocalStateSnapshot();
|
|
18
|
+
if (!result.complete)
|
|
19
|
+
throw new Error("Worker state is unavailable; restore it before reconnecting.");
|
|
20
|
+
return result.state;
|
|
21
|
+
}
|
|
22
|
+
async function workerScope() {
|
|
23
|
+
if (requireValidWorkerBearerRuntime().mode !== "owner") {
|
|
24
|
+
throw new Error("Worker handles are for independent MCP chats; supervised identity is supplied by its supervisor.");
|
|
25
|
+
}
|
|
26
|
+
const owner = await resolveOwnerContext();
|
|
27
|
+
return `${getApiUrl()}\n${owner.login?.toLowerCase() ?? `local:${owner.slug}`}`;
|
|
28
|
+
}
|
|
29
|
+
function getWorker(workerId, scope) {
|
|
30
|
+
const worker = snapshot().mcp_workers?.[workerId];
|
|
31
|
+
if (!isMcpWorkerId(workerId) || !worker || worker.scope !== scope) {
|
|
32
|
+
throw new Error("Unknown worker_id for this account and API. Create a worker for this chat or explicitly resume its saved handle.");
|
|
33
|
+
}
|
|
34
|
+
return worker;
|
|
35
|
+
}
|
|
36
|
+
export async function registerMcpWorker(input) {
|
|
37
|
+
const scope = await workerScope();
|
|
38
|
+
if (input.workerId && input.registrationKey)
|
|
39
|
+
throw new Error("Use worker_id to resume or registration_key to create, not both.");
|
|
40
|
+
let workerId = input.workerId;
|
|
41
|
+
if (!workerId) {
|
|
42
|
+
if (!input.registrationKey?.trim())
|
|
43
|
+
throw new Error("A new chat must supply its own registration_key and retain the returned worker_id.");
|
|
44
|
+
const keyHash = createHash("sha256").update(`${scope}\n${input.registrationKey}`).digest("hex");
|
|
45
|
+
snapshot();
|
|
46
|
+
updateLocalState((state) => {
|
|
47
|
+
state.mcp_workers ??= {};
|
|
48
|
+
const prior = Object.values(state.mcp_workers).find((w) => w.scope === scope && w.registration_key_hash === keyHash);
|
|
49
|
+
workerId = prior?.worker_id ?? `worker_${randomUUID().replaceAll("-", "")}`;
|
|
50
|
+
state.mcp_workers[workerId] ??= {
|
|
51
|
+
worker_id: workerId, scope, registration_key_hash: keyHash,
|
|
52
|
+
display_name: input.displayName?.trim() || pickLocalCodename(workerId).display_name,
|
|
53
|
+
rooms: {},
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const worker = getWorker(workerId, scope);
|
|
58
|
+
const identifiers = await resolveLocalRoomStorageIdentifiers(input.roomId);
|
|
59
|
+
const roomId = identifiers.cloudRoomId || input.roomId;
|
|
60
|
+
const local = await isLocalRoomStorageEnabled(input.roomId);
|
|
61
|
+
const key = `${getLocalStatePath()}\n${worker.worker_id}\n${roomId}`;
|
|
62
|
+
const inFlight = connecting.get(key);
|
|
63
|
+
if (inFlight)
|
|
64
|
+
return inFlight;
|
|
65
|
+
const result = connectWorker(input, worker, scope, roomId, local);
|
|
66
|
+
connecting.set(key, result);
|
|
67
|
+
try {
|
|
68
|
+
return await result;
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
connecting.delete(key);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function connectWorker(input, worker, scope, roomId, local) {
|
|
75
|
+
const currentId = worker.rooms[roomId]?.session_id;
|
|
76
|
+
const current = currentId ? snapshot().agent_sessions?.[currentId] : undefined;
|
|
77
|
+
const pinned = currentId ? pinnedWorkerConnection(getLocalStatePath(), currentId) : null;
|
|
78
|
+
if (pinned && current && !current.ended_at && pinned.session_token === current.session_token
|
|
79
|
+
&& !worker.rooms[roomId]?.pending)
|
|
80
|
+
return { worker, session: pinned };
|
|
81
|
+
const owner = await resolveOwnerContext();
|
|
82
|
+
if (!local && !owner.login)
|
|
83
|
+
throw new Error("Sign in before registering a worker in a hosted room.");
|
|
84
|
+
const ide = detectAgentIdeLabel();
|
|
85
|
+
// An opaque per-chat key prevents two chats choosing the same visible name
|
|
86
|
+
// from collapsing into the same routing identity.
|
|
87
|
+
const agentName = worker.worker_id.replace("_", "-");
|
|
88
|
+
const agent = local ? { canonical_key: `${owner.login ?? owner.slug}/${agentName}` }
|
|
89
|
+
: await apiCall("/agents", {
|
|
90
|
+
method: "POST", body: JSON.stringify({ name: agentName, display_name: worker.display_name, owner_label: owner.label }),
|
|
91
|
+
});
|
|
92
|
+
if (!agent.canonical_key)
|
|
93
|
+
throw new Error("Worker identity registration returned no canonical identity.");
|
|
94
|
+
async function finish(operation) {
|
|
95
|
+
let session;
|
|
96
|
+
if (local) {
|
|
97
|
+
const prior = operation.predecessor_id ? snapshot().agent_sessions?.[operation.predecessor_id] : undefined;
|
|
98
|
+
if (prior && prior.session_token !== operation.predecessor_token
|
|
99
|
+
&& prior.session_token !== operation.connection_token)
|
|
100
|
+
throw new Error("Worker registration was superseded.");
|
|
101
|
+
const now = new Date().toISOString();
|
|
102
|
+
session = {
|
|
103
|
+
session_id: prior?.session_id ?? `local_${createHash("sha256").update(`${worker.worker_id}\n${roomId}`).digest("hex")}`, session_token: operation.connection_token,
|
|
104
|
+
room_id: roomId, session_kind: "worker", agent_instance_id: worker.worker_id,
|
|
105
|
+
agent_key: agent.canonical_key, display_name: prior?.display_name ?? worker.display_name,
|
|
106
|
+
actor_label: prior?.actor_label ?? buildAgentActorLabel({ display_name: worker.display_name, owner_label: owner.label, ide_label: ide }),
|
|
107
|
+
owner_label: owner.label, ide_label: ide, runtime: input.runtime || detectAgentRuntimeLabel(),
|
|
108
|
+
requested_base_display_name: worker.display_name,
|
|
109
|
+
created_at: prior?.created_at ?? now, updated_at: now, last_seen_at: now, ended_at: null,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const created = await apiCall(`/rooms/${encodeRoomIdPath(roomId)}/agent-sessions`, {
|
|
114
|
+
method: "POST", body: JSON.stringify({
|
|
115
|
+
actor_key: agent.canonical_key, agent_instance_id: worker.worker_id,
|
|
116
|
+
display_name: worker.display_name, requested_base_display_name: worker.display_name,
|
|
117
|
+
session_kind: "worker", runtime: input.runtime || detectAgentRuntimeLabel(), ide_label: ide,
|
|
118
|
+
repo_branch: getGitCurrentBranch(input.cwd),
|
|
119
|
+
registration_liveness: getSessionLivenessRegistration(input.runtime || detectAgentRuntimeLabel()),
|
|
120
|
+
connection_token: operation.connection_token,
|
|
121
|
+
replace_agent_session_id: operation.predecessor_id ?? null,
|
|
122
|
+
replace_agent_session_token: operation.predecessor_token ?? null,
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
if (!created.session_id || created.session_token !== operation.connection_token
|
|
126
|
+
|| created.agent_instance_id !== worker.worker_id || created.agent_key !== agent.canonical_key
|
|
127
|
+
|| created.room_id !== roomId || created.ended_at) {
|
|
128
|
+
throw new Error("The server did not confirm this worker connection. Upgrade the API before using durable worker handles.");
|
|
129
|
+
}
|
|
130
|
+
// Keep only the session credential; the server's optional bearer is not used here.
|
|
131
|
+
const { assigned_base_display_name, worker_bearer: _unusedBearer, ...record } = created;
|
|
132
|
+
session = { ...record, requested_base_display_name: assigned_base_display_name ?? worker.display_name };
|
|
133
|
+
}
|
|
134
|
+
updateLocalState((state) => {
|
|
135
|
+
const target = state.mcp_workers?.[worker.worker_id]?.rooms[roomId];
|
|
136
|
+
if (target?.pending?.operation_id !== operation.operation_id)
|
|
137
|
+
throw new Error("Worker registration was superseded; retry explicitly.");
|
|
138
|
+
state.agent_sessions ??= {};
|
|
139
|
+
if (local && !target.session_id) {
|
|
140
|
+
const used = new Set(Object.values(state.agent_sessions)
|
|
141
|
+
.filter((other) => other.room_id === roomId && other.session_id !== session.session_id)
|
|
142
|
+
.map((other) => other.display_name));
|
|
143
|
+
let suffix = 1;
|
|
144
|
+
while (used.has(session.display_name))
|
|
145
|
+
session.display_name = `${worker.display_name} ${suffix++}`;
|
|
146
|
+
session.actor_label = buildAgentActorLabel({ display_name: session.display_name, owner_label: owner.label, ide_label: ide });
|
|
147
|
+
}
|
|
148
|
+
state.agent_sessions[session.session_id] = session;
|
|
149
|
+
target.session_id = session.session_id;
|
|
150
|
+
delete target.pending;
|
|
151
|
+
});
|
|
152
|
+
return session;
|
|
153
|
+
}
|
|
154
|
+
// Recover a response lost by an earlier process using its prepared credential.
|
|
155
|
+
// Then rotate once more for this process; never silently share that connection.
|
|
156
|
+
const pending = getWorker(worker.worker_id, scope).rooms[roomId]?.pending;
|
|
157
|
+
if (pending)
|
|
158
|
+
await finish(pending);
|
|
159
|
+
const operation = { operation_id: randomUUID(), connection_token: randomBytes(32).toString("base64url") };
|
|
160
|
+
updateLocalState((state) => {
|
|
161
|
+
const target = state.mcp_workers[worker.worker_id].rooms;
|
|
162
|
+
target[roomId] ??= {};
|
|
163
|
+
if (target[roomId].pending)
|
|
164
|
+
throw new Error("Another registration is in progress for this worker; retry explicitly.");
|
|
165
|
+
const priorId = target[roomId].session_id;
|
|
166
|
+
const prior = priorId ? state.agent_sessions?.[priorId] : undefined;
|
|
167
|
+
operation.predecessor_id = prior?.session_id;
|
|
168
|
+
operation.predecessor_token = prior?.session_token;
|
|
169
|
+
target[roomId].pending = operation;
|
|
170
|
+
});
|
|
171
|
+
const session = await finish(operation);
|
|
172
|
+
pinWorkerConnection(getLocalStatePath(), session);
|
|
173
|
+
return { worker: getWorker(worker.worker_id, scope), session };
|
|
174
|
+
}
|
|
175
|
+
export async function runMcpWorkerCall(workerId, roomId, callback, allowEnded = false) {
|
|
176
|
+
const worker = getWorker(workerId, await workerScope());
|
|
177
|
+
const rooms = Object.keys(worker.rooms);
|
|
178
|
+
const targetRoom = roomId || (rooms.length === 1 ? rooms[0] : null);
|
|
179
|
+
const entry = targetRoom ? worker.rooms[targetRoom] : null;
|
|
180
|
+
const session = entry?.session_id ? pinnedWorkerConnection(getLocalStatePath(), entry.session_id) : null;
|
|
181
|
+
const stored = entry?.session_id ? snapshot().agent_sessions?.[entry.session_id] : null;
|
|
182
|
+
if (!session || !stored || entry?.pending || stored.ended_at || session.session_token !== stored.session_token) {
|
|
183
|
+
throw new Error("This worker has no current connection in this process. Reconnect explicitly with register_agent_session(worker_id, room_id).");
|
|
184
|
+
}
|
|
185
|
+
return await withWorkerCall(session, async () => {
|
|
186
|
+
const result = await callback(session);
|
|
187
|
+
assertWorkerConnection(session, allowEnded);
|
|
188
|
+
return result;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
@@ -19,6 +19,7 @@ export function letAgentsRuntimeContract() {
|
|
|
19
19
|
return {
|
|
20
20
|
format: 1,
|
|
21
21
|
profiles: {
|
|
22
|
+
supervised_mcp_polling: { contract: "custodial_polling_v1", tools: registeredToolNames("supervised_mcp_polling", "codex") },
|
|
22
23
|
cursor_supervised_room_turn: {
|
|
23
24
|
tools: registeredToolNames("supervised_room_turn", "cursor"),
|
|
24
25
|
},
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
// in src/mcp/server/runtime/* so tool modules can import focused responsibilities
|
|
3
3
|
// without turning this file back into the runtime god module.
|
|
4
4
|
import { isLocalRoomStorageEnabled as isStoredLocalRoomStorageEnabled, touchRoomSession as touchStoredRoomSession, } from "../local-state.js";
|
|
5
|
-
import {
|
|
5
|
+
import { hasSupervisedWorkerAuthority } from "./runtime/worker-bearer.js";
|
|
6
6
|
/** A daemon-supervised turn must always use its exact cloud worker route. */
|
|
7
7
|
export async function isLocalRoomStorageEnabled(roomId) {
|
|
8
|
-
return !
|
|
8
|
+
return !hasSupervisedWorkerAuthority() && isStoredLocalRoomStorageEnabled(roomId);
|
|
9
9
|
}
|
|
10
10
|
export function touchRoomSession(roomId, lastMessageId) {
|
|
11
|
-
if (!
|
|
11
|
+
if (!hasSupervisedWorkerAuthority())
|
|
12
12
|
touchStoredRoomSession(roomId, lastMessageId);
|
|
13
13
|
}
|
|
14
14
|
export { API_URL, ApiError, apiCall, getAuthorizationHeader, getLetagentsToken, isMissingRouteError, parseApiErrorPayload, resolveApiPath, } from "./runtime/api.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { parsePositivePgIntegerScopedId } from "../../../shared/message-contracts.mjs";
|
|
1
2
|
import { runWithCurrentSupervisedRoom } from "./runtime/room-state.js";
|
|
2
|
-
import { completeCurrentSupervisedEffect, executeCurrentSupervisedTool, prepareCurrentSupervisedEffect, } from "./runtime/supervisor-bridge.js";
|
|
3
|
+
import { completeCurrentSupervisedEffect, authorizeCustodialPolling, executeCurrentSupervisedTool, prepareCurrentSupervisedEffect, } from "./runtime/supervisor-bridge.js";
|
|
3
4
|
const READ_TOOLS = new Set([
|
|
4
5
|
"get_current_room",
|
|
5
6
|
"check_repo",
|
|
@@ -22,6 +23,40 @@ export function supervisedToolIsMutation(toolName) {
|
|
|
22
23
|
// frames. A read result can be returned live to the provider without copying
|
|
23
24
|
// the entire payload into the durable effect journal.
|
|
24
25
|
const MAX_DURABLE_READ_RESULT_BYTES = 16 * 1024;
|
|
26
|
+
/** Receipt only the bounded page actually returned by wait, never an API tail
|
|
27
|
+
* beyond that page or a cursor inferred from its last visible message. */
|
|
28
|
+
function custodialWaitFrontier(result, inputCursor, roomId) {
|
|
29
|
+
const content = result.content;
|
|
30
|
+
if (result.isError || content.length !== 1 || content[0]?.type !== "text")
|
|
31
|
+
throw new Error("Custodial wait returned no valid bounded page.");
|
|
32
|
+
let output;
|
|
33
|
+
try {
|
|
34
|
+
output = JSON.parse(content[0].text);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new Error("Custodial wait returned no valid bounded page.");
|
|
38
|
+
}
|
|
39
|
+
if (!output || Array.isArray(output) || !Array.isArray(output.messages)
|
|
40
|
+
|| (output.room_id !== undefined && output.room_id !== roomId))
|
|
41
|
+
throw new Error("Custodial wait returned no valid bounded page.");
|
|
42
|
+
const noProgress = output.messages.length === 0 && (output.truncated === undefined || output.truncated === false)
|
|
43
|
+
&& (output.omitted_message_count === undefined || output.omitted_message_count === 0)
|
|
44
|
+
&& (output.skipped_message_count === undefined || output.skipped_message_count === 0)
|
|
45
|
+
&& (output.skipped_message_ids === undefined || (Array.isArray(output.skipped_message_ids) && output.skipped_message_ids.length === 0));
|
|
46
|
+
const frontier = output.last_observed_message_id;
|
|
47
|
+
if (frontier === undefined || frontier === null) {
|
|
48
|
+
if (!noProgress) {
|
|
49
|
+
throw new Error("Custodial wait is missing its bounded observed frontier.");
|
|
50
|
+
}
|
|
51
|
+
return inputCursor;
|
|
52
|
+
}
|
|
53
|
+
const number = parsePositivePgIntegerScopedId(frontier, "msg");
|
|
54
|
+
const inputNumber = parsePositivePgIntegerScopedId(inputCursor, "msg");
|
|
55
|
+
if (number === null || inputNumber === null || number < inputNumber || (number === inputNumber && !noProgress)) {
|
|
56
|
+
throw new Error("Custodial wait returned an invalid observed frontier.");
|
|
57
|
+
}
|
|
58
|
+
return String(frontier);
|
|
59
|
+
}
|
|
25
60
|
function instruction(text, data = {}) {
|
|
26
61
|
const payload = { ...data, instruction: text };
|
|
27
62
|
return {
|
|
@@ -47,7 +82,7 @@ const productionDependencies = {
|
|
|
47
82
|
withRoom: runWithCurrentSupervisedRoom,
|
|
48
83
|
};
|
|
49
84
|
export function profileAwareToolServer(server, profile, dependencies = productionDependencies, supervisedProvider = process.env.LETAGENTS_SUPERVISOR_PROVIDER?.trim() || null) {
|
|
50
|
-
if (profile !== "supervised_room_turn")
|
|
85
|
+
if (profile !== "supervised_room_turn" && profile !== "supervised_mcp_polling")
|
|
51
86
|
return server;
|
|
52
87
|
return new Proxy(server, {
|
|
53
88
|
get(target, property, receiver) {
|
|
@@ -61,6 +96,43 @@ export function profileAwareToolServer(server, profile, dependencies = productio
|
|
|
61
96
|
if (typeof callback !== "function")
|
|
62
97
|
throw new Error(`Tool ${name} has no callback.`);
|
|
63
98
|
const wrapped = async (...call) => {
|
|
99
|
+
if (profile === "supervised_mcp_polling") {
|
|
100
|
+
const authorize = dependencies.authorizePolling ?? ((toolName, prior, wait) => authorizeCustodialPolling(toolName, prior, process.env, {}, wait));
|
|
101
|
+
const input = call[0];
|
|
102
|
+
const extra = (call.length > 1 ? call.at(-1) : undefined);
|
|
103
|
+
let wait;
|
|
104
|
+
if (name === "wait_for_messages") {
|
|
105
|
+
const requestId = extra?.requestId;
|
|
106
|
+
if (!(typeof requestId === "string" || (typeof requestId === "number" && Number.isSafeInteger(requestId)))) {
|
|
107
|
+
throw new Error("Custodial wait is missing its exact MCP request id.");
|
|
108
|
+
}
|
|
109
|
+
if (input?.after_message_id != null && typeof input.after_message_id !== "string")
|
|
110
|
+
throw new Error("Custodial wait requires a valid requested cursor.");
|
|
111
|
+
if ((input?.room_id != null && typeof input.room_id !== "string")
|
|
112
|
+
|| (input?.agent_session_id != null && typeof input.agent_session_id !== "string"))
|
|
113
|
+
throw new Error("Custodial wait requires valid requested identity.");
|
|
114
|
+
wait = { mcpRequestId: requestId, roomCursor: input?.after_message_id ?? null,
|
|
115
|
+
...(typeof input?.room_id === "string" ? { requestedRoomId: input.room_id } : {}),
|
|
116
|
+
...(typeof input?.agent_session_id === "string" ? { requestedAgentSessionId: input.agent_session_id } : {}) };
|
|
117
|
+
}
|
|
118
|
+
const authority = await authorize(name, undefined, wait);
|
|
119
|
+
return dependencies.withRoom(authority.roomId, async () => {
|
|
120
|
+
if (input?.room_id && input.room_id !== authority.roomId)
|
|
121
|
+
throw new Error("Custodial tool room does not match its exact authority.");
|
|
122
|
+
if (name === "wait_for_messages") {
|
|
123
|
+
if (!authority.roomCursor)
|
|
124
|
+
throw new Error("Custodial polling has no durable cursor; refusing a tail fallback.");
|
|
125
|
+
call[0] = { ...input, after_message_id: authority.roomCursor };
|
|
126
|
+
}
|
|
127
|
+
const result = await callback(...call);
|
|
128
|
+
if (wait)
|
|
129
|
+
await authorize(name, authority, { ...wait,
|
|
130
|
+
offeredFrontier: custodialWaitFrontier(result, authority.roomCursor, authority.roomId) });
|
|
131
|
+
else if (name === "read_messages")
|
|
132
|
+
await authorize(name, authority);
|
|
133
|
+
return result;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
64
136
|
const extra = (call.at(-1) ?? {});
|
|
65
137
|
const input = call.length > 1 ? call[0] : {};
|
|
66
138
|
if (extra.requestId === undefined || extra.requestId === null || String(extra.requestId).trim() === "") {
|
|
@@ -5,13 +5,16 @@ import { encodeRoomIdPath, looksLikeInviteCode, normalizeInviteCode } from "../.
|
|
|
5
5
|
import { AGENT_INSTANCE_UUID, RepoRoomAuthRequiredError, apiCall, agentSessionCredentials, currentRoom, detectAgentIdeLabel, detectAgentRuntimeLabel, endStoredAgentSession, ensureAgentIdentity, getSessionLivenessRegistration, getAgentSessionRepoBranch, getStoredAgentSession, getStoredAgentSessionsForRoomIdentity, getTargetRoomId, ensureLocalWorkerAgentSession, isLocalRoomStorageEnabled, joinRoomIdentifier, resolveLocalRoomStorageIdentifiers, resolveClientRequestedBase, saveAgentSession, toPublicAgentSession, toPublicRoomState, toRepoRoomAuthRequiredResult, withAgentIdentity, resolveWorkerToolIdentity, } from "../runtime.js";
|
|
6
6
|
import { requireValidWorkerBearerRuntime, workerModeDisabledToolResult, } from "../runtime/worker-bearer.js";
|
|
7
7
|
import { bindSupervisedWorkerSessionWithContext } from "../runtime/supervisor-bridge.js";
|
|
8
|
+
import { registerMcpWorker } from "../runtime/worker-handles.js";
|
|
8
9
|
export function registerAgentSessionTools(server) {
|
|
9
10
|
// -- register_agent_session -------------------------------------------------
|
|
10
|
-
server.tool("register_agent_session", "
|
|
11
|
+
server.tool("register_agent_session", "Connect a worker to a room. For an independent chat, supply a unique registration_key once, keep the returned worker_id, and use worker_id on room tools. After an MCP restart, reconnect with that worker_id. Separate chats must use separate registration keys. Unregistered traffic remains controller traffic. Legacy agent_session_id registration is retained for existing integrations.", {
|
|
12
|
+
worker_id: z.string().optional().describe("Resume this chat's saved worker handle. Never select another chat's handle automatically."),
|
|
13
|
+
registration_key: z.string().min(1).max(200).optional().describe("Create a worker using a random key generated once for this chat; reuse the exact key if the first registration needs retrying. Do not use a shared name, room, or repository as the key."),
|
|
11
14
|
room_id: z
|
|
12
15
|
.string()
|
|
13
16
|
.optional()
|
|
14
|
-
.describe("Canonical room ID.
|
|
17
|
+
.describe("Canonical room ID. Required for durable worker handles; legacy registration defaults to the current room."),
|
|
15
18
|
session_kind: z
|
|
16
19
|
.enum(["worker", "controller"])
|
|
17
20
|
.optional()
|
|
@@ -28,7 +31,21 @@ export function registerAgentSessionTools(server) {
|
|
|
28
31
|
.string()
|
|
29
32
|
.optional()
|
|
30
33
|
.describe("Worker working directory used for branch detection and exact supervised Codex binding. Defaults to the MCP server's working directory."),
|
|
31
|
-
}, async ({ room_id, session_kind, runtime, display_name, cwd }) => {
|
|
34
|
+
}, async ({ room_id, session_kind, runtime, display_name, cwd, worker_id, registration_key }) => {
|
|
35
|
+
if (worker_id !== undefined || registration_key !== undefined) {
|
|
36
|
+
if (session_kind === "controller")
|
|
37
|
+
throw new Error("Durable handles identify workers, not controllers.");
|
|
38
|
+
const roomId = room_id?.trim();
|
|
39
|
+
if (!roomId)
|
|
40
|
+
throw new Error("Pass room_id explicitly when registering or reconnecting this chat's worker.");
|
|
41
|
+
const result = await registerMcpWorker({ roomId, workerId: worker_id, registrationKey: registration_key,
|
|
42
|
+
displayName: display_name, runtime, cwd });
|
|
43
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
44
|
+
success: true, worker_id: result.worker.worker_id,
|
|
45
|
+
agent_session: toPublicAgentSession(result.session),
|
|
46
|
+
instruction: "Keep worker_id for this chat and pass it to room tools. After an MCP restart, reconnect with register_agent_session(worker_id, room_id). A separate chat needs its own registration_key. Credentials stay private to MCP.",
|
|
47
|
+
}) }] };
|
|
48
|
+
}
|
|
32
49
|
const workerRuntime = requireValidWorkerBearerRuntime();
|
|
33
50
|
if (workerRuntime.mode === "supervised") {
|
|
34
51
|
// Resolve before currentRoom, config, branch, or local storage. The
|
|
@@ -288,7 +305,7 @@ export function registerAgentSessionTools(server) {
|
|
|
288
305
|
? agentSessionCredentials(localSession)
|
|
289
306
|
: {};
|
|
290
307
|
if (await isLocalRoomStorageEnabled(targetRoomId)) {
|
|
291
|
-
const endedSession = endStoredAgentSession(targetSessionId);
|
|
308
|
+
const endedSession = endStoredAgentSession(targetSessionId, undefined, localSession?.session_token);
|
|
292
309
|
return {
|
|
293
310
|
content: [
|
|
294
311
|
{
|
|
@@ -312,7 +329,7 @@ export function registerAgentSessionTools(server) {
|
|
|
312
329
|
? String(result.agent_session.ended_at ?? new Date().toISOString())
|
|
313
330
|
: new Date().toISOString();
|
|
314
331
|
const endedLocalSession = localSession?.session_id === targetSessionId
|
|
315
|
-
? endStoredAgentSession(targetSessionId, endedAt)
|
|
332
|
+
? endStoredAgentSession(targetSessionId, endedAt, localSession.session_token)
|
|
316
333
|
: null;
|
|
317
334
|
return {
|
|
318
335
|
content: [
|
|
@@ -3,6 +3,7 @@ import { encodeRoomIdPath } from "../../../room-id.js";
|
|
|
3
3
|
import { AGENT_MESSAGE_BODY_MAX_BYTES, appendIncludePromptOnly, boundAgentMessageOutput, buildAgentDeliveryHeaders, currentRoom, ensureAgentIdentity, getFallbackProjectId, getLatestLocalChatMessages, getCurrentAgentSessionSnapshot, getTargetRoomId, heartbeatRoomPresence, touchRoomSession, isLocalRoomStorageEnabled, resolveLocalRoomStorageIdentifiers, roomScopedApiCall, toAgentReadableMessages, } from "../../runtime.js";
|
|
4
4
|
import { requireValidWorkerBearerRuntime } from "../../runtime/worker-bearer.js";
|
|
5
5
|
import { jsonToolResponse } from "./response.js";
|
|
6
|
+
import { currentWorkerCall } from "../../../worker-call-context.js";
|
|
6
7
|
export const DEFAULT_READ_MESSAGES_LIMIT = 100;
|
|
7
8
|
// Both the API and the local store clamp a single page to 500 messages.
|
|
8
9
|
export const MAX_MESSAGES_PER_PAGE = 500;
|
|
@@ -77,7 +78,8 @@ export function registerReadMessagesTool(server) {
|
|
|
77
78
|
const localRoomId = targetRoomId ?? currentRoom?.room_id ?? targetProjectId;
|
|
78
79
|
const sessionRoomId = targetRoomId ?? currentRoom?.room_id ?? null;
|
|
79
80
|
const workerRuntime = requireValidWorkerBearerRuntime();
|
|
80
|
-
const
|
|
81
|
+
const boundWorker = currentWorkerCall();
|
|
82
|
+
const agentSessionSnapshot = boundWorker ? { complete: true, session: boundWorker } : workerRuntime.mode === "owner"
|
|
81
83
|
? getCurrentAgentSessionSnapshot(sessionRoomId)
|
|
82
84
|
: { complete: true, session: null };
|
|
83
85
|
const agentSession = agentSessionSnapshot.session;
|
|
@@ -115,7 +117,9 @@ export function registerReadMessagesTool(server) {
|
|
|
115
117
|
limit: effectiveLimit,
|
|
116
118
|
deliveryHeaders,
|
|
117
119
|
});
|
|
118
|
-
|
|
120
|
+
if (!boundWorker) {
|
|
121
|
+
await heartbeatRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, await ensureAgentIdentity());
|
|
122
|
+
}
|
|
119
123
|
const bounded = boundAgentMessageOutput(toAgentReadableMessages(recent.messages), { direction: "suffix", maxBytes: AGENT_MESSAGE_BODY_MAX_BYTES });
|
|
120
124
|
const output = {
|
|
121
125
|
messages: bounded.messages,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { assertWorkerConnection } from "../../../worker-call-context.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { getPollTimeoutCapMs } from "../../../../shared/poll-timeout-cap.js";
|
|
3
4
|
import { encodeRoomIdPath } from "../../../room-id.js";
|
|
4
5
|
import { agentSessionCredentials, AGENT_MESSAGE_BODY_MAX_BYTES, appendIncludePromptOnly, boundAgentMessageOutput, buildAgentDeliveryHeaders, bindSupervisedWorkerSession, scheduleSupervisedWorkerCursorCheckpoint, currentRoom, ensureAgentIdentity, getFallbackProjectId, getLatestLocalChatMessages, getLocalImportedRoutingAuthority, getLocalChatMessages, getLocalChatThreadRoutingMembership, getLastMessageId, getRememberedRoomPresence, getStoredAgentRoutingStateSnapshot, getTargetRoomId, identityFromAgentSession, isLocalRoomStorageEnabled, listLocalActiveTaskOwnerLeases, resolveLocalRoomStorageIdentifiers, resolveAgentSession, roomScopedApiCall, syncRoomPresence, toAgentReadableMessages, touchRoomSession, WORKER_BEARER_AGENT_SESSION_ID, waitForLocalChatMessages, } from "../../runtime.js";
|
|
5
|
-
import { requireValidWorkerBearerRuntime, supervisedBoundedDeliveryDisabledToolResult, } from "../../runtime/worker-bearer.js";
|
|
6
|
+
import { requireValidWorkerBearerRuntime, isCustodialPolling, supervisedBoundedDeliveryDisabledToolResult, } from "../../runtime/worker-bearer.js";
|
|
7
|
+
import { resolveWorkerToolIdentity } from "../../runtime/agent-sessions.js";
|
|
6
8
|
import { attachAgentMessageActivations, createGlobalAgentAddressResolver, decideAgentMessageActivation, isTaskOwnerFollowUpMessageText, } from "../../../../shared/activation-routing.js";
|
|
7
9
|
import { normalizeRoutingSender } from "../../../../../shared/routing-aliases.mjs";
|
|
8
10
|
import { findLocalMessageById, findRemoteMessageById } from "./message-lookup.js";
|
|
@@ -462,14 +464,20 @@ export function registerWaitForMessagesTool(server) {
|
|
|
462
464
|
const targetProjectId = getFallbackProjectId();
|
|
463
465
|
const localRoomId = targetRoomId ?? currentRoom?.room_id ?? targetProjectId;
|
|
464
466
|
const sessionRoomId = targetRoomId ?? currentRoom?.room_id ?? localRoomId ?? null;
|
|
465
|
-
const
|
|
467
|
+
const custodial = isCustodialPolling();
|
|
468
|
+
const routingStateSnapshot = custodial ? { complete: true } : getStoredAgentRoutingStateSnapshot(sessionRoomId ?? "");
|
|
466
469
|
const localStorageEnabled = Boolean(localRoomId && await isLocalRoomStorageEnabled(localRoomId));
|
|
467
470
|
if (localStorageEnabled && !routingStateSnapshot.complete) {
|
|
468
471
|
throw new Error("Local agent routing state is unavailable; retry after restoring the state file.");
|
|
469
472
|
}
|
|
470
|
-
const
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
+
const exactIdentity = custodial ? await resolveWorkerToolIdentity({ roomId: sessionRoomId, agentSessionId: agent_session_id }) : null;
|
|
474
|
+
const identity = exactIdentity?.identity ?? await ensureAgentIdentity();
|
|
475
|
+
const agentSession = exactIdentity?.agentSession ?? resolveWaitAgentSession(sessionRoomId, agent_session_id);
|
|
476
|
+
if (custodial) {
|
|
477
|
+
if (!agentSession || !after_message_id)
|
|
478
|
+
throw new Error("Custodial polling requires exact worker identity and durable cursor.");
|
|
479
|
+
}
|
|
480
|
+
else if (agentSession) {
|
|
473
481
|
// Registration (or a successor generation) must bind strictly once.
|
|
474
482
|
// Later waits use a read-only exact verification capped at 250ms, so a
|
|
475
483
|
// wedged daemon cannot consume the room-poll budget and an old worker
|
|
@@ -513,6 +521,7 @@ export function registerWaitForMessagesTool(server) {
|
|
|
513
521
|
limit: MAX_WAIT_MESSAGES_PER_CALL,
|
|
514
522
|
include_prompt_only: true,
|
|
515
523
|
});
|
|
524
|
+
assertWorkerConnection(agentSession ?? undefined);
|
|
516
525
|
const messages = await attachLocalActivationMetadata(effectiveLocalRoomId, result.messages, agentSession, {
|
|
517
526
|
includeTaskOwnerLeases: !replayingExistingMessages,
|
|
518
527
|
activeSessionRoomId: cloudRoomId || sessionRoomId,
|
|
@@ -570,6 +579,7 @@ export function registerWaitForMessagesTool(server) {
|
|
|
570
579
|
project_id: targetProjectId,
|
|
571
580
|
room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages/poll?${queryString}`),
|
|
572
581
|
project_path: (targetProjectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(targetProjectId)}/messages/poll?${queryString}`),
|
|
582
|
+
preserve_session_cursor: true,
|
|
573
583
|
options: buildWaitForMessagesRequestOptions({
|
|
574
584
|
deliveryHeaders,
|
|
575
585
|
signal: AbortSignal.timeout(clientTimeout),
|
|
@@ -633,12 +643,14 @@ export function registerWaitForMessagesTool(server) {
|
|
|
633
643
|
if (bounded.omittedMessageCount > 0) {
|
|
634
644
|
output.omitted_message_count = bounded.omittedMessageCount;
|
|
635
645
|
}
|
|
636
|
-
|
|
637
|
-
|
|
646
|
+
// The API cursor can cover concealed messages, but not visible messages
|
|
647
|
+
// omitted by our own byte bound. Resume after the retained page instead.
|
|
648
|
+
const observedCursor = !bounded.truncated && apiObservedCursor
|
|
649
|
+
? apiObservedCursor
|
|
650
|
+
: routing.last_observed_message_id ?? undefined;
|
|
651
|
+
if (observedCursor)
|
|
652
|
+
output.last_observed_message_id = observedCursor;
|
|
638
653
|
if (targetRoomId) {
|
|
639
|
-
const observedCursor = apiObservedCursor
|
|
640
|
-
?? routing.last_observed_message_id
|
|
641
|
-
?? getLastMessageId(output);
|
|
642
654
|
touchRoomSession(targetRoomId, observedCursor);
|
|
643
655
|
if (allMessages.length > 0 && agentSession) {
|
|
644
656
|
const firstMsg = allMessages[0];
|
|
@@ -5,7 +5,8 @@ import { apiCall, currentAgentIdentity, currentAgentIdentityKey, detectAgentIdeL
|
|
|
5
5
|
import { jsonTextResponse } from "./responses.js";
|
|
6
6
|
import { workerModeDisabledToolResult } from "../../runtime/worker-bearer.js";
|
|
7
7
|
export function registerSetAgentNameTool(server) {
|
|
8
|
-
server.tool("set_agent_name", "Set or change the
|
|
8
|
+
server.tool("set_agent_name", "Set or change the legacy process identity's display name. Durable MCP workers choose display_name when first registered; this tool cannot rename them.", {
|
|
9
|
+
worker_id: z.string().optional().describe("Durable worker handles cannot be renamed by this legacy tool."),
|
|
9
10
|
name: z
|
|
10
11
|
.string()
|
|
11
12
|
.min(2)
|
|
@@ -15,7 +16,9 @@ export function registerSetAgentNameTool(server) {
|
|
|
15
16
|
.string()
|
|
16
17
|
.optional()
|
|
17
18
|
.describe("Optional conversation ID to scope this name change. When provided, only this conversation uses the new name; other conversations keep their own identity."),
|
|
18
|
-
}, async ({ name: desiredName, conversation_id }) => {
|
|
19
|
+
}, async ({ name: desiredName, conversation_id, worker_id }) => {
|
|
20
|
+
if (worker_id)
|
|
21
|
+
throw new Error("Choose display_name when first registering this worker. set_agent_name only changes the legacy process identity.");
|
|
19
22
|
const disabled = workerModeDisabledToolResult();
|
|
20
23
|
if (disabled)
|
|
21
24
|
return jsonTextResponse(disabled);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runMcpWorkerCall } from "./runtime/worker-handles.js";
|
|
3
|
+
import { getStoredAgentSession } from "../local-state/agent-sessions.js";
|
|
4
|
+
import { isMcpWorkerId } from "../../shared/mcp-worker.js";
|
|
5
|
+
/** One explicit identity path for all worker tools, including shared transports. */
|
|
6
|
+
export function workerAwareToolServer(server) {
|
|
7
|
+
return new Proxy(server, {
|
|
8
|
+
get(target, property, receiver) {
|
|
9
|
+
if (property !== "tool") {
|
|
10
|
+
const value = Reflect.get(target, property, receiver);
|
|
11
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
12
|
+
}
|
|
13
|
+
return (name, description, schema, callback) => {
|
|
14
|
+
// assign_board_manager's session id names the target, not the caller.
|
|
15
|
+
if (name === "register_agent_session" || name === "assign_board_manager"
|
|
16
|
+
|| !schema.room_id) {
|
|
17
|
+
return target.tool(name, description, schema, callback);
|
|
18
|
+
}
|
|
19
|
+
return target.tool(name, description, {
|
|
20
|
+
...schema,
|
|
21
|
+
worker_id: z.string().optional().describe("Stable handle returned by register_agent_session for this chat. Use worker_id or legacy agent_session_id, not both. Reconnect the handle after an MCP process restart."),
|
|
22
|
+
}, async (input, extra) => {
|
|
23
|
+
const { worker_id, ...args } = input;
|
|
24
|
+
if (worker_id === undefined) {
|
|
25
|
+
const session = typeof args.agent_session_id === "string" ? getStoredAgentSession(args.agent_session_id) : null;
|
|
26
|
+
return session && isMcpWorkerId(session.agent_instance_id)
|
|
27
|
+
? runMcpWorkerCall(session.agent_instance_id, args.room_id ?? session.room_id, () => callback(args, extra), name === "disconnect_agent_session")
|
|
28
|
+
: callback(args, extra);
|
|
29
|
+
}
|
|
30
|
+
if (args.agent_session_id !== undefined)
|
|
31
|
+
throw new Error("Choose worker_id or agent_session_id, not both.");
|
|
32
|
+
return runMcpWorkerCall(String(worker_id), args.room_id, (session) => callback({ ...args, room_id: session.room_id,
|
|
33
|
+
...(schema.agent_session_id ? { agent_session_id: session.session_id } : {}) }, extra), name === "disconnect_agent_session");
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { readLocalStateSnapshot, withLocalStateReadLock } from "./local-state/storage.js";
|
|
3
|
+
import { isMcpWorkerId } from "../shared/mcp-worker.js";
|
|
4
|
+
const calls = new AsyncLocalStorage();
|
|
5
|
+
const connections = new Map();
|
|
6
|
+
export function withWorkerCall(session, callback) {
|
|
7
|
+
return calls.run(session, callback);
|
|
8
|
+
}
|
|
9
|
+
export function currentWorkerCall() {
|
|
10
|
+
return calls.getStore();
|
|
11
|
+
}
|
|
12
|
+
function checkConnection(snapshot, session, allowEnded) {
|
|
13
|
+
const current = snapshot.state.agent_sessions?.[session.session_id];
|
|
14
|
+
if (!snapshot.complete || !current || current.session_token !== session.session_token
|
|
15
|
+
|| (!allowEnded && current.ended_at)
|
|
16
|
+
|| snapshot.state.mcp_workers?.[session.agent_instance_id]?.rooms[session.room_id]?.pending) {
|
|
17
|
+
throw new Error("This worker connection was replaced or disconnected. Reconnect explicitly with its worker_id.");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function assertWorkerConnection(session = currentWorkerCall(), allowEnded = false) {
|
|
21
|
+
if (session && isMcpWorkerId(session.agent_instance_id))
|
|
22
|
+
checkConnection(readLocalStateSnapshot(), session, allowEnded);
|
|
23
|
+
}
|
|
24
|
+
export function withWorkerStateFence(callback) {
|
|
25
|
+
const session = currentWorkerCall();
|
|
26
|
+
if (!session || !isMcpWorkerId(session.agent_instance_id))
|
|
27
|
+
return callback();
|
|
28
|
+
return withLocalStateReadLock((snapshot) => {
|
|
29
|
+
checkConnection(snapshot, session, false);
|
|
30
|
+
return callback();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export function pinWorkerConnection(statePath, session) {
|
|
34
|
+
connections.set(`${statePath}\n${session.session_id}`, structuredClone(session));
|
|
35
|
+
}
|
|
36
|
+
export function pinnedWorkerConnection(statePath, sessionId) {
|
|
37
|
+
const call = currentWorkerCall();
|
|
38
|
+
return call?.session_id === sessionId ? call : connections.get(`${statePath}\n${sessionId}`) ?? null;
|
|
39
|
+
}
|
|
@@ -312,7 +312,7 @@ export function resolveGloballyAddressedAgentKeys(message, identities) {
|
|
|
312
312
|
* Build the room-wide alias authority once, then resolve a page of legacy
|
|
313
313
|
* messages without rebuilding every active worker alias set per message.
|
|
314
314
|
*/
|
|
315
|
-
export function createGlobalAgentAddressResolver(identities) {
|
|
315
|
+
export function createGlobalAgentAddressResolver(identities, options = {}) {
|
|
316
316
|
const keysByAlias = new Map();
|
|
317
317
|
for (const identity of identities) {
|
|
318
318
|
const key = normalizedString(identity.agent_key);
|
|
@@ -324,6 +324,23 @@ export function createGlobalAgentAddressResolver(identities) {
|
|
|
324
324
|
keysByAlias.set(alias, keys);
|
|
325
325
|
}
|
|
326
326
|
}
|
|
327
|
+
const resolveExplicitMentionKey = (keys) => {
|
|
328
|
+
if (!keys || keys.size === 0)
|
|
329
|
+
return null;
|
|
330
|
+
if (keys.size === 1)
|
|
331
|
+
return keys.values().next().value;
|
|
332
|
+
const preferredMatches = [...keys].filter((key) => options.preferredExplicitMentionAgentKeys?.has(key));
|
|
333
|
+
if (preferredMatches.length !== 1)
|
|
334
|
+
return null;
|
|
335
|
+
const ownerScopes = new Set();
|
|
336
|
+
for (const key of keys) {
|
|
337
|
+
const scope = options.explicitMentionOwnerScopeByAgentKey?.get(key);
|
|
338
|
+
if (!scope)
|
|
339
|
+
return null;
|
|
340
|
+
ownerScopes.add(scope);
|
|
341
|
+
}
|
|
342
|
+
return ownerScopes.size === 1 ? preferredMatches[0] : null;
|
|
343
|
+
};
|
|
327
344
|
return (message) => {
|
|
328
345
|
const mentions = extractMentionHandles(message.text);
|
|
329
346
|
const broadcast = mentions.some(isBroadcastHandle) || hasBroadcastAddress(message.text);
|
|
@@ -336,9 +353,9 @@ export function createGlobalAgentAddressResolver(identities) {
|
|
|
336
353
|
const alias = normalizeMentionIdentityHandle(mention);
|
|
337
354
|
if (!alias)
|
|
338
355
|
continue;
|
|
339
|
-
const
|
|
340
|
-
if (
|
|
341
|
-
explicitMentionKeys.add(
|
|
356
|
+
const resolvedKey = resolveExplicitMentionKey(keysByAlias.get(alias));
|
|
357
|
+
if (resolvedKey)
|
|
358
|
+
explicitMentionKeys.add(resolvedKey);
|
|
342
359
|
}
|
|
343
360
|
const replyTargetKeys = new Set();
|
|
344
361
|
const replyAliases = normalizedString(message.reply_to?.source) === "agent"
|