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.
Files changed (42) hide show
  1. package/README.md +32 -0
  2. package/dist/mcp/local-state/agent-sessions.js +18 -3
  3. package/dist/mcp/local-state/local-chat.js +11 -27
  4. package/dist/mcp/local-state/storage.js +5 -1
  5. package/dist/mcp/server/register-tools.js +4 -1
  6. package/dist/mcp/server/runtime/agent-sessions.js +2 -2
  7. package/dist/mcp/server/runtime/api.js +5 -1
  8. package/dist/mcp/server/runtime/execution-profile.js +1 -0
  9. package/dist/mcp/server/runtime/identity.js +5 -0
  10. package/dist/mcp/server/runtime/presence.js +3 -3
  11. package/dist/mcp/server/runtime/room-api.js +5 -4
  12. package/dist/mcp/server/runtime/room-state.js +6 -6
  13. package/dist/mcp/server/runtime/rooms.js +3 -3
  14. package/dist/mcp/server/runtime/supervisor-bridge.js +80 -3
  15. package/dist/mcp/server/runtime/tool-surface-policy.js +7 -0
  16. package/dist/mcp/server/runtime/worker-bearer.js +12 -1
  17. package/dist/mcp/server/runtime/worker-handles.js +190 -0
  18. package/dist/mcp/server/runtime-contract.js +1 -0
  19. package/dist/mcp/server/runtime.js +3 -3
  20. package/dist/mcp/server/supervised-tool-facade.js +74 -2
  21. package/dist/mcp/server/tools/agent-sessions.js +22 -5
  22. package/dist/mcp/server/tools/messages/read-tool.js +6 -2
  23. package/dist/mcp/server/tools/messages/wait-tool.js +22 -10
  24. package/dist/mcp/server/tools/onboarding/name-tool.js +5 -2
  25. package/dist/mcp/server/worker-tool-facade.js +38 -0
  26. package/dist/mcp/worker-call-context.js +39 -0
  27. package/dist/shared/activation-routing.js +21 -4
  28. package/dist/shared/mcp-worker.js +7 -0
  29. package/dist/shared/room-agent-prompts.js +2 -2
  30. package/package.json +3 -3
  31. package/shared/execution-approval-projection.d.mts +32 -0
  32. package/shared/execution-approval-projection.mjs +107 -0
  33. package/shared/execution-approval-publication-item.d.mts +20 -0
  34. package/shared/execution-approval-publication-item.mjs +61 -0
  35. package/shared/execution-approval-publication.d.mts +53 -0
  36. package/shared/execution-approval-publication.mjs +136 -0
  37. package/shared/execution-delegation-decision.d.mts +37 -0
  38. package/shared/execution-delegation-decision.mjs +73 -0
  39. package/shared/room-agent-work.d.mts +31 -0
  40. package/shared/room-agent-work.mjs +44 -0
  41. package/shared/room-resource-invalidation.d.mts +38 -0
  42. 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 { isSupervisedBoundedTurn } from "./runtime/worker-bearer.js";
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 !isSupervisedBoundedTurn() && isStoredLocalRoomStorageEnabled(roomId);
8
+ return !hasSupervisedWorkerAuthority() && isStoredLocalRoomStorageEnabled(roomId);
9
9
  }
10
10
  export function touchRoomSession(roomId, lastMessageId) {
11
- if (!isSupervisedBoundedTurn())
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", "Register this MCP client as an explicit room agent session. Unregistered MCP traffic is treated as controller traffic and stays out of the connected-agent roster.", {
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. Defaults to the current room."),
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 agentSessionSnapshot = workerRuntime.mode === "owner"
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
- await heartbeatRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, await ensureAgentIdentity());
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 routingStateSnapshot = getStoredAgentRoutingStateSnapshot(sessionRoomId ?? "");
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 identity = await ensureAgentIdentity();
471
- const agentSession = resolveWaitAgentSession(sessionRoomId, agent_session_id);
472
- if (agentSession) {
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
- if (apiObservedCursor)
637
- output.last_observed_message_id = apiObservedCursor;
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 agent's display name. The agent will be known by this name in the room. Use this to pick a custom name instead of the auto-generated codename.", {
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 keys = keysByAlias.get(alias);
340
- if (keys?.size === 1)
341
- explicitMentionKeys.add(keys.values().next().value);
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"