letagents 0.12.8 → 0.12.9

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.
@@ -11,13 +11,15 @@ import { SseClient } from "./sse-client.js";
11
11
  import { getRoomFromConfig } from "./config-reader.js";
12
12
  import { getGitRemoteIdentity } from "./git-remote.js";
13
13
  import { AGENT_CODENAME_SPACE, normalizeSlugSegment, normalizeAgentBaseName, pickLocalCodename, } from "./codenames.js";
14
- import { clearPendingDeviceAuth, clearStoredAuth, getCurrentCodexLiveSession, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, listStoredCodexLiveSessions, saveRoomSession, setStoredAgentIdentity, setPendingDeviceAuth, setStoredAuth, touchRoomSession, updateLocalState, } from "./local-state.js";
14
+ import { clearPendingDeviceAuth, clearStoredAuth, endStoredAgentSession, getCurrentCodexLiveSession, getLocalStatePath, getPendingDeviceAuth, getCurrentAgentSession, getStoredAgentSession, getStoredAgentIdentity, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, listStoredCodexLiveSessions, saveAgentSession, saveRoomSession, setStoredAgentIdentity, setPendingDeviceAuth, setStoredAuth, touchRoomSession, updateLocalState, } from "./local-state.js";
15
15
  import { encodeRoomIdPath, getCanonicalRoomWebPath, looksLikeInviteCode, normalizeInviteCode, } from "./room-id.js";
16
16
  import { buildAgentActorLabel, formatOwnerAttribution, inferAgentIdeLabel, toTitleCaseCodename, } from "../shared/agent-identity.js";
17
17
  import { buildRoomAgentPrompt, normalizeAgentPromptKind, } from "../shared/room-agent-prompts.js";
18
+ import { LETAGENTS_AGENT_SESSION_ID_HEADER, LETAGENTS_AGENT_SESSION_TOKEN_HEADER, LETAGENTS_ORIGIN_ROOM_ID_HEADER, } from "../shared/request-headers.js";
18
19
  import { inspectLocalCodexSession, startLocalCodexSession, stopLocalCodexSession, toPublicCodexLiveSession, } from "./codex-session.js";
19
20
  import { classifyPresenceStatusText, deriveTaskPresenceStatus, getRoomIdentityPresenceCacheKey, } from "./agent-presence.js";
20
21
  import { buildRoomEventsQueryString } from "./room-events-query.js";
22
+ import { AGENT_PRESENCE_STATUSES, } from "../shared/agent-presence.js";
21
23
  import { getPollTimeoutCapMs } from "../shared/poll-timeout-cap.js";
22
24
  let currentRoom = null;
23
25
  let currentAgentIdentityKey = "";
@@ -712,6 +714,17 @@ async function withAgentIdentity(payload) {
712
714
  agent_identity: toPublicAgentIdentity(await ensureAgentIdentity()),
713
715
  };
714
716
  }
717
+ function getCurrentStreamAgentIdentity() {
718
+ const identity = currentAgentIdentity ?? getStoredAgentIdentity(currentAgentIdentityKey);
719
+ if (!identity?.actor_label || !identity.canonical_key) {
720
+ return null;
721
+ }
722
+ return {
723
+ actorLabel: identity.actor_label,
724
+ actorKey: identity.canonical_key,
725
+ actorInstanceId: AGENT_INSTANCE_UUID,
726
+ };
727
+ }
715
728
  function rememberRoom(state, lastMessageId) {
716
729
  currentRoom = state;
717
730
  saveRoomSession({
@@ -726,6 +739,7 @@ function rememberRoom(state, lastMessageId) {
726
739
  sseClient.subscribe({
727
740
  roomId: state.room_id,
728
741
  projectId: state.project_id ?? null,
742
+ agentIdentity: getCurrentStreamAgentIdentity(),
729
743
  }, (_message) => {
730
744
  touchRoomSession(state.room_id);
731
745
  server.server.sendResourceListChanged();
@@ -744,22 +758,24 @@ function getRememberedRoomPresence(roomId, identity) {
744
758
  }
745
759
  return (roomPresenceByIdentity.get(getRoomIdentityPresenceCacheKey(roomId, identity.actor_label)) ?? { status: "idle", status_text: null });
746
760
  }
747
- async function syncRoomPresence(roomId, identity, presence) {
748
- if (!roomId || !identity) {
761
+ async function syncRoomPresence(roomId, identity, presence, agentSession) {
762
+ const resolvedIdentity = agentSession ? identityFromAgentSession(agentSession) : identity;
763
+ if (!roomId || !resolvedIdentity || !agentSession) {
749
764
  return;
750
765
  }
751
- roomPresenceByIdentity.set(getRoomIdentityPresenceCacheKey(roomId, identity.actor_label), presence);
766
+ roomPresenceByIdentity.set(getRoomIdentityPresenceCacheKey(roomId, resolvedIdentity.actor_label), presence);
752
767
  try {
753
768
  await apiCall(`/rooms/${encodeRoomIdPath(roomId)}/presence`, {
754
769
  method: "POST",
755
770
  body: JSON.stringify({
756
- actor_label: identity.actor_label,
757
- agent_key: identity.canonical_key,
758
- display_name: identity.display_name,
759
- owner_label: identity.owner_label,
760
- ide_label: identity.ide_label,
771
+ actor_label: resolvedIdentity.actor_label,
772
+ agent_key: resolvedIdentity.canonical_key,
773
+ display_name: resolvedIdentity.display_name,
774
+ owner_label: resolvedIdentity.owner_label,
775
+ ide_label: resolvedIdentity.ide_label,
761
776
  status: presence.status,
762
777
  status_text: presence.status_text,
778
+ ...agentSessionCredentials(agentSession),
763
779
  }),
764
780
  });
765
781
  touchRoomSession(roomId);
@@ -774,6 +790,15 @@ async function syncRoomPresence(roomId, identity, presence) {
774
790
  async function heartbeatRoomPresence(roomId, identity) {
775
791
  await syncRoomPresence(roomId, identity, getRememberedRoomPresence(roomId, identity));
776
792
  }
793
+ function buildAgentDeliveryHeaders(agentSession) {
794
+ if (!agentSession) {
795
+ return {};
796
+ }
797
+ return {
798
+ [LETAGENTS_AGENT_SESSION_ID_HEADER]: agentSession.session_id,
799
+ [LETAGENTS_AGENT_SESSION_TOKEN_HEADER]: agentSession.session_token,
800
+ };
801
+ }
777
802
  function getTargetRoomId(roomId) {
778
803
  return roomId || currentRoom?.room_id || null;
779
804
  }
@@ -802,6 +827,81 @@ function getCurrentLiveSessionPayload(roomId) {
802
827
  const session = getCurrentCodexLiveSession(roomId);
803
828
  return session ? toPublicCodexLiveSession(session) : null;
804
829
  }
830
+ function toPublicAgentSession(session) {
831
+ if (!session) {
832
+ return null;
833
+ }
834
+ return {
835
+ session_id: session.session_id,
836
+ room_id: session.room_id,
837
+ session_kind: session.session_kind,
838
+ runtime: session.runtime,
839
+ actor_label: session.actor_label,
840
+ agent_key: session.agent_key,
841
+ agent_instance_id: session.agent_instance_id ?? null,
842
+ display_name: session.display_name,
843
+ owner_label: session.owner_label,
844
+ ide_label: session.ide_label,
845
+ created_at: session.created_at,
846
+ updated_at: session.updated_at,
847
+ last_seen_at: session.last_seen_at,
848
+ ended_at: session.ended_at ?? null,
849
+ };
850
+ }
851
+ function resolveAgentSession(roomId, sessionId) {
852
+ if (!sessionId) {
853
+ return getCurrentAgentSession(roomId);
854
+ }
855
+ const session = getStoredAgentSession(sessionId);
856
+ if (!session) {
857
+ throw new Error(`Unknown agent_session_id: ${sessionId}`);
858
+ }
859
+ if (session.ended_at) {
860
+ throw new Error(`agent_session_id ${sessionId} ended at ${session.ended_at}`);
861
+ }
862
+ if (roomId && session.room_id !== roomId) {
863
+ throw new Error(`agent_session_id ${sessionId} is registered for ${session.room_id}, not ${roomId}`);
864
+ }
865
+ return session;
866
+ }
867
+ function identityFromAgentSession(session) {
868
+ return {
869
+ name: normalizeAgentBaseName(session.display_name),
870
+ display_name: session.display_name,
871
+ owner_label: session.owner_label,
872
+ owner_attribution: formatOwnerAttribution(session.owner_label),
873
+ ide_label: session.ide_label,
874
+ actor_label: session.actor_label,
875
+ canonical_key: session.agent_key,
876
+ runtime_key: `agent_session:${session.session_id}`,
877
+ source: "api",
878
+ resolved_at: session.updated_at,
879
+ };
880
+ }
881
+ function requireWorkerAgentSession(roomId, sessionId) {
882
+ const session = resolveAgentSession(roomId, sessionId);
883
+ if (!session) {
884
+ throw new Error("Registered worker agent_session_id is required for this write action. " +
885
+ "Call register_agent_session for this room first, then pass the returned agent_session_id.");
886
+ }
887
+ if (session.session_kind !== "worker") {
888
+ throw new Error("Worker agent_session_id is required for this write action.");
889
+ }
890
+ return session;
891
+ }
892
+ async function resolveWorkerToolIdentity(input) {
893
+ const agentSession = requireWorkerAgentSession(input.roomId, input.agentSessionId);
894
+ return {
895
+ identity: identityFromAgentSession(agentSession),
896
+ agentSession,
897
+ };
898
+ }
899
+ function agentSessionCredentials(agentSession) {
900
+ return {
901
+ agent_session_id: agentSession.session_id,
902
+ agent_session_token: agentSession.session_token,
903
+ };
904
+ }
805
905
  function toAgentReadableMessage(message) {
806
906
  if (!message || typeof message !== "object") {
807
907
  return message;
@@ -826,9 +926,20 @@ function appendIncludePromptOnly(path) {
826
926
  return `${path}${path.includes("?") ? "&" : "?"}include_prompt_only=1`;
827
927
  }
828
928
  async function roomScopedApiCall(input) {
929
+ const headers = {
930
+ ...input.options?.headers,
931
+ };
932
+ if (currentRoom?.room_id &&
933
+ !Object.keys(headers).some((key) => key.toLowerCase() === LETAGENTS_ORIGIN_ROOM_ID_HEADER.toLowerCase())) {
934
+ headers[LETAGENTS_ORIGIN_ROOM_ID_HEADER] = currentRoom.room_id;
935
+ }
936
+ const options = {
937
+ ...input.options,
938
+ headers,
939
+ };
829
940
  if (input.room_id) {
830
941
  try {
831
- const result = await apiCall(input.room_path(input.room_id), input.options);
942
+ const result = await apiCall(input.room_path(input.room_id), options);
832
943
  touchRoomSession(input.room_id, getLastMessageId(result));
833
944
  return result;
834
945
  }
@@ -842,12 +953,16 @@ async function roomScopedApiCall(input) {
842
953
  if (!input.project_id) {
843
954
  throw new Error("No room is available for this request.");
844
955
  }
845
- const result = await apiCall(input.project_path(input.project_id), input.options);
956
+ const result = await apiCall(input.project_path(input.project_id), options);
846
957
  if (input.room_id) {
847
958
  touchRoomSession(input.room_id, getLastMessageId(result));
848
959
  }
849
960
  return result;
850
961
  }
962
+ function normalizeOptionalToolString(value) {
963
+ const trimmed = value?.trim();
964
+ return trimmed ? trimmed : undefined;
965
+ }
851
966
  async function joinRoomIdentifier(identifier, joinedVia) {
852
967
  const roomId = joinedVia === "join_code" ? normalizeInviteCode(identifier) : identifier.trim();
853
968
  try {
@@ -855,6 +970,7 @@ async function joinRoomIdentifier(identifier, joinedVia) {
855
970
  const joinedRoomId = typeof response.room_id === "string"
856
971
  ? response.room_id
857
972
  : roomId;
973
+ const agentIdentity = await ensureAgentIdentity();
858
974
  const room = rememberRoom(toRoomState({
859
975
  room_id: joinedRoomId,
860
976
  project_id: typeof response.project_id === "string" ? response.project_id : null,
@@ -866,10 +982,9 @@ async function joinRoomIdentifier(identifier, joinedVia) {
866
982
  display_name: typeof response.display_name === "string" ? response.display_name : null,
867
983
  joined_via: joinedVia,
868
984
  }));
869
- const agentIdentity = await ensureAgentIdentity();
870
985
  await syncRoomPresence(room.room_id, agentIdentity, {
871
986
  status: "idle",
872
- status_text: "online in room",
987
+ status_text: "available in room",
873
988
  });
874
989
  return {
875
990
  room,
@@ -891,6 +1006,7 @@ async function joinRoomIdentifier(identifier, joinedVia) {
891
1006
  const legacyRoomId = typeof project.code === "string"
892
1007
  ? project.code
893
1008
  : roomId;
1009
+ const agentIdentity = await ensureAgentIdentity();
894
1010
  const room = rememberRoom(toRoomState({
895
1011
  room_id: legacyRoomId,
896
1012
  project_id: typeof project.id === "string" ? project.id : null,
@@ -898,10 +1014,9 @@ async function joinRoomIdentifier(identifier, joinedVia) {
898
1014
  display_name: typeof project.display_name === "string" ? project.display_name : null,
899
1015
  joined_via: joinedVia,
900
1016
  }));
901
- const agentIdentity = await ensureAgentIdentity();
902
1017
  await syncRoomPresence(room.room_id, agentIdentity, {
903
1018
  status: "idle",
904
- status_text: "online in room",
1019
+ status_text: "available in room",
905
1020
  });
906
1021
  return {
907
1022
  room,
@@ -919,6 +1034,7 @@ async function joinRoomIdentifier(identifier, joinedVia) {
919
1034
  : typeof project.code === "string" && project.code.trim()
920
1035
  ? project.code
921
1036
  : roomId;
1037
+ const agentIdentity = await ensureAgentIdentity();
922
1038
  const room = rememberRoom(toRoomState({
923
1039
  room_id: legacyRoomId,
924
1040
  project_id: typeof project.id === "string" ? project.id : null,
@@ -930,10 +1046,9 @@ async function joinRoomIdentifier(identifier, joinedVia) {
930
1046
  display_name: typeof project.display_name === "string" ? project.display_name : null,
931
1047
  joined_via: joinedVia,
932
1048
  }));
933
- const agentIdentity = await ensureAgentIdentity();
934
1049
  await syncRoomPresence(room.room_id, agentIdentity, {
935
1050
  status: "idle",
936
- status_text: "online in room",
1051
+ status_text: "available in room",
937
1052
  });
938
1053
  return {
939
1054
  room,
@@ -962,7 +1077,7 @@ async function createInviteRoom() {
962
1077
  const agentIdentity = await ensureAgentIdentity();
963
1078
  await syncRoomPresence(room.room_id, agentIdentity, {
964
1079
  status: "idle",
965
- status_text: "online in room",
1080
+ status_text: "available in room",
966
1081
  });
967
1082
  return {
968
1083
  room,
@@ -1154,6 +1269,186 @@ server.tool("join_room", "Join a named room on Let Agents Chat. Creates the room
1154
1269
  throw error;
1155
1270
  }
1156
1271
  });
1272
+ // -- register_agent_session -------------------------------------------------
1273
+ 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.", {
1274
+ room_id: z
1275
+ .string()
1276
+ .optional()
1277
+ .describe("Canonical room ID. Defaults to the current room."),
1278
+ session_kind: z
1279
+ .enum(["worker", "controller"])
1280
+ .optional()
1281
+ .describe("Agent session kind. Workers appear in connected agent activity; controllers do not. Defaults to worker."),
1282
+ runtime: z
1283
+ .string()
1284
+ .optional()
1285
+ .describe("Runtime label such as codex, antigravity, or custom harness name."),
1286
+ display_name: z
1287
+ .string()
1288
+ .optional()
1289
+ .describe("Human-readable label for this worker session. Use distinct labels when one MCP process controls multiple workers."),
1290
+ }, async ({ room_id, session_kind, runtime, display_name }) => {
1291
+ const targetRoomId = getTargetRoomId(room_id);
1292
+ if (!targetRoomId) {
1293
+ return {
1294
+ content: [
1295
+ {
1296
+ type: "text",
1297
+ text: JSON.stringify({
1298
+ success: false,
1299
+ error: "No room_id provided and not currently in a room.",
1300
+ hint: "Join a room first, or pass room_id explicitly.",
1301
+ }, null, 2),
1302
+ },
1303
+ ],
1304
+ };
1305
+ }
1306
+ const identity = await ensureAgentIdentity();
1307
+ if (!identity.canonical_key) {
1308
+ return {
1309
+ content: [
1310
+ {
1311
+ type: "text",
1312
+ text: JSON.stringify({
1313
+ success: false,
1314
+ error: "Authenticated agent identity is required before registering an agent session.",
1315
+ }, null, 2),
1316
+ },
1317
+ ],
1318
+ };
1319
+ }
1320
+ const created = await apiCall(`/rooms/${encodeRoomIdPath(targetRoomId)}/agent-sessions`, {
1321
+ method: "POST",
1322
+ body: JSON.stringify({
1323
+ actor_key: identity.canonical_key,
1324
+ actor_label: identity.actor_label,
1325
+ ide_label: identity.ide_label ?? detectAgentIdeLabel(),
1326
+ agent_instance_id: AGENT_INSTANCE_UUID,
1327
+ display_name: display_name?.trim() || identity.display_name,
1328
+ session_kind: session_kind ?? "worker",
1329
+ runtime: runtime?.trim() || detectAgentIdeLabel().toLowerCase(),
1330
+ }),
1331
+ });
1332
+ const sessionId = typeof created.session_id === "string" ? created.session_id : "";
1333
+ const sessionToken = typeof created.session_token === "string" ? created.session_token : "";
1334
+ if (!sessionId || !sessionToken) {
1335
+ throw new Error("Agent session registration response was missing session credentials.");
1336
+ }
1337
+ const session = saveAgentSession({
1338
+ session_id: sessionId,
1339
+ session_token: sessionToken,
1340
+ room_id: typeof created.room_id === "string" ? created.room_id : targetRoomId,
1341
+ session_kind: created.session_kind === "controller" ? "controller" : "worker",
1342
+ runtime: typeof created.runtime === "string" ? created.runtime : "unknown",
1343
+ actor_label: typeof created.actor_label === "string" ? created.actor_label : identity.actor_label,
1344
+ agent_key: typeof created.agent_key === "string" ? created.agent_key : identity.canonical_key,
1345
+ agent_instance_id: typeof created.agent_instance_id === "string" ? created.agent_instance_id : AGENT_INSTANCE_UUID,
1346
+ display_name: typeof created.display_name === "string" ? created.display_name : identity.display_name,
1347
+ owner_label: typeof created.owner_label === "string" ? created.owner_label : identity.owner_label,
1348
+ ide_label: typeof created.ide_label === "string" ? created.ide_label : identity.ide_label ?? detectAgentIdeLabel(),
1349
+ created_at: typeof created.created_at === "string" ? created.created_at : new Date().toISOString(),
1350
+ updated_at: typeof created.updated_at === "string" ? created.updated_at : new Date().toISOString(),
1351
+ last_seen_at: typeof created.last_seen_at === "string" ? created.last_seen_at : new Date().toISOString(),
1352
+ ended_at: typeof created.ended_at === "string" ? created.ended_at : null,
1353
+ });
1354
+ return {
1355
+ content: [
1356
+ {
1357
+ type: "text",
1358
+ text: JSON.stringify({
1359
+ success: true,
1360
+ agent_session: toPublicAgentSession(session),
1361
+ agent_session_id: session.session_id,
1362
+ use_agent_session_id: "Pass this agent_session_id to wait_for_messages, send_message, post_status, and task tools for this specific worker.",
1363
+ }, null, 2),
1364
+ },
1365
+ ],
1366
+ };
1367
+ });
1368
+ // -- disconnect_agent_session ----------------------------------------------
1369
+ server.tool("disconnect_agent_session", "Disconnect/end a registered worker session from a room. Defaults to the current worker session; admins may pass another agent_session_id.", {
1370
+ room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room or the stored session room."),
1371
+ agent_session_id: z.string().optional().describe("Registered agent session to disconnect. Defaults to the current session for the room."),
1372
+ }, async ({ room_id, agent_session_id }) => {
1373
+ let targetRoomId = getTargetRoomId(room_id);
1374
+ const localSession = agent_session_id
1375
+ ? getStoredAgentSession(agent_session_id)
1376
+ : getCurrentAgentSession(targetRoomId ?? currentRoom?.room_id ?? null);
1377
+ if (!targetRoomId && localSession) {
1378
+ targetRoomId = localSession.room_id;
1379
+ }
1380
+ if (!targetRoomId) {
1381
+ return {
1382
+ content: [
1383
+ {
1384
+ type: "text",
1385
+ text: JSON.stringify({
1386
+ success: false,
1387
+ error: "No room_id provided and not currently in a room.",
1388
+ hint: "Join a room first, pass room_id, or pass a locally stored agent_session_id.",
1389
+ }, null, 2),
1390
+ },
1391
+ ],
1392
+ };
1393
+ }
1394
+ const targetSessionId = agent_session_id || localSession?.session_id;
1395
+ if (!targetSessionId) {
1396
+ return {
1397
+ content: [
1398
+ {
1399
+ type: "text",
1400
+ text: JSON.stringify({
1401
+ success: false,
1402
+ error: "No registered agent session was found for this room.",
1403
+ hint: "Pass agent_session_id or call register_agent_session first.",
1404
+ }, null, 2),
1405
+ },
1406
+ ],
1407
+ };
1408
+ }
1409
+ if (localSession?.ended_at) {
1410
+ return {
1411
+ content: [
1412
+ {
1413
+ type: "text",
1414
+ text: JSON.stringify({
1415
+ success: true,
1416
+ already_ended: true,
1417
+ agent_session: toPublicAgentSession(localSession),
1418
+ }, null, 2),
1419
+ },
1420
+ ],
1421
+ };
1422
+ }
1423
+ const requestBody = localSession?.session_id === targetSessionId && localSession.session_kind === "worker"
1424
+ ? agentSessionCredentials(localSession)
1425
+ : {};
1426
+ const result = await apiCall(`/rooms/${encodeRoomIdPath(targetRoomId)}/agent-sessions/${encodeURIComponent(targetSessionId)}/disconnect`, {
1427
+ method: "POST",
1428
+ body: JSON.stringify(requestBody),
1429
+ });
1430
+ const endedAt = result.agent_session && typeof result.agent_session === "object" && "ended_at" in result.agent_session
1431
+ ? String(result.agent_session.ended_at ?? new Date().toISOString())
1432
+ : new Date().toISOString();
1433
+ const endedLocalSession = localSession?.session_id === targetSessionId
1434
+ ? endStoredAgentSession(targetSessionId, endedAt)
1435
+ : null;
1436
+ return {
1437
+ content: [
1438
+ {
1439
+ type: "text",
1440
+ text: JSON.stringify({
1441
+ success: true,
1442
+ room_id: targetRoomId,
1443
+ agent_session_id: targetSessionId,
1444
+ agent_session: result.agent_session ?? toPublicAgentSession(endedLocalSession),
1445
+ delivery_session: result.delivery_session ?? null,
1446
+ local_state: endedLocalSession ? "ended" : "unchanged",
1447
+ }, null, 2),
1448
+ },
1449
+ ],
1450
+ };
1451
+ });
1157
1452
  // -- local Codex live sessions ---------------------------------------------
1158
1453
  server.tool("start_local_codex_session", "Start or reuse a detached local Codex live session for a LetAgents room. The worker will join the room, keep polling, contribute in discussion, and do repo work from the current working directory when asked.", {
1159
1454
  room: z
@@ -1384,8 +1679,12 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
1384
1679
  conversation_id: z
1385
1680
  .string()
1386
1681
  .optional()
1387
- .describe("Optional conversation ID for per-conversation identity scoping."),
1388
- }, async ({ sender: _sender, status, room_id, conversation_id }) => {
1682
+ .describe("Deprecated for worker writes; registered worker session identity is used."),
1683
+ agent_session_id: z
1684
+ .string()
1685
+ .optional()
1686
+ .describe("Registered agent session to use for this status update."),
1687
+ }, async ({ sender: _sender, status, room_id, conversation_id: _conversation_id, agent_session_id }) => {
1389
1688
  const targetRoomId = getTargetRoomId(room_id);
1390
1689
  const targetProjectId = getFallbackProjectId();
1391
1690
  if (!targetRoomId && !targetProjectId) {
@@ -1404,7 +1703,10 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
1404
1703
  }
1405
1704
  // Status messages use a reserved prefix so the UI (and agents) can distinguish
1406
1705
  // them from normal chat messages without changing the data model.
1407
- const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
1706
+ const { identity, agentSession } = await resolveWorkerToolIdentity({
1707
+ roomId: targetRoomId ?? currentRoom?.room_id ?? null,
1708
+ agentSessionId: agent_session_id,
1709
+ });
1408
1710
  const sender = identity.actor_label;
1409
1711
  const statusText = `[status] ${status}`;
1410
1712
  const message = await roomScopedApiCall({
@@ -1414,14 +1716,18 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
1414
1716
  project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages`,
1415
1717
  options: {
1416
1718
  method: "POST",
1417
- body: JSON.stringify({ sender, text: statusText }),
1719
+ body: JSON.stringify({
1720
+ sender,
1721
+ text: statusText,
1722
+ ...agentSessionCredentials(agentSession),
1723
+ }),
1418
1724
  },
1419
1725
  });
1420
1726
  touchCurrentRoom(typeof message.id === "string" ? message.id : undefined);
1421
1727
  await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, {
1422
1728
  status: classifyPresenceStatusText(status, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity).status),
1423
1729
  status_text: status,
1424
- });
1730
+ }, agentSession);
1425
1731
  return {
1426
1732
  content: [
1427
1733
  {
@@ -1438,14 +1744,170 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
1438
1744
  ],
1439
1745
  };
1440
1746
  });
1747
+ // -- post_reasoning ---------------------------------------------------------
1748
+ server.tool("post_reasoning", "Update a structured, replace-in-place reasoning trace for the current room without spamming chat. " +
1749
+ "Use this for visible thought-process updates such as goal, hypothesis, checks in progress, blockers, " +
1750
+ "and next action. Optionally persist a milestone summary into room history when something durable changed.", {
1751
+ summary: z.string().describe("Short current reasoning summary shown in the room UI."),
1752
+ goal: z.string().optional().describe("What the agent is trying to achieve right now."),
1753
+ checking: z.string().optional().describe("What the agent is verifying or inspecting."),
1754
+ hypothesis: z.string().optional().describe("Current working theory or approach."),
1755
+ blocker: z.string().optional().describe("Current blocker, if any."),
1756
+ next_action: z.string().optional().describe("Immediate next step the agent plans to take."),
1757
+ milestone: z.string().optional().describe("Optional durable milestone summary to append to room history."),
1758
+ confidence: z.number().min(0).max(1).optional().describe("Optional confidence score between 0 and 1."),
1759
+ status: z.enum(AGENT_PRESENCE_STATUSES).optional().describe("Optional explicit presence state override."),
1760
+ room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room."),
1761
+ conversation_id: z
1762
+ .string()
1763
+ .optional()
1764
+ .describe("Deprecated for worker writes; registered worker session identity is used."),
1765
+ agent_session_id: z
1766
+ .string()
1767
+ .optional()
1768
+ .describe("Registered agent session to use for this reasoning update."),
1769
+ }, async ({ summary, goal, checking, hypothesis, blocker, next_action, milestone, confidence, status, room_id, conversation_id: _conversation_id, agent_session_id, }) => {
1770
+ const targetRoomId = getTargetRoomId(room_id);
1771
+ const targetProjectId = getFallbackProjectId();
1772
+ if (!targetRoomId && !targetProjectId) {
1773
+ return {
1774
+ content: [
1775
+ {
1776
+ type: "text",
1777
+ text: JSON.stringify({
1778
+ success: false,
1779
+ error: "No room_id provided and not currently in a room.",
1780
+ hint: "Join or create a room first, or pass room_id explicitly.",
1781
+ }, null, 2),
1782
+ },
1783
+ ],
1784
+ };
1785
+ }
1786
+ const normalizedSummary = summary.trim();
1787
+ if (!normalizedSummary) {
1788
+ return {
1789
+ content: [
1790
+ {
1791
+ type: "text",
1792
+ text: JSON.stringify({ success: false, error: "summary is required" }, null, 2),
1793
+ },
1794
+ ],
1795
+ };
1796
+ }
1797
+ const { identity, agentSession } = await resolveWorkerToolIdentity({
1798
+ roomId: targetRoomId ?? currentRoom?.room_id ?? null,
1799
+ agentSessionId: agent_session_id,
1800
+ });
1801
+ const snapshot = {
1802
+ summary: normalizedSummary,
1803
+ goal: normalizeOptionalToolString(goal),
1804
+ checking: normalizeOptionalToolString(checking),
1805
+ hypothesis: normalizeOptionalToolString(hypothesis),
1806
+ blocker: normalizeOptionalToolString(blocker),
1807
+ next_action: normalizeOptionalToolString(next_action),
1808
+ milestone: normalizeOptionalToolString(milestone),
1809
+ confidence,
1810
+ status,
1811
+ };
1812
+ const query = new URLSearchParams({
1813
+ open: "true",
1814
+ actor_label: identity.actor_label,
1815
+ });
1816
+ const sessionsResult = await roomScopedApiCall({
1817
+ room_id: targetRoomId,
1818
+ project_id: targetProjectId,
1819
+ room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/reasoning-sessions?${query.toString()}`,
1820
+ project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/reasoning-sessions?${query.toString()}`,
1821
+ });
1822
+ const existingSession = sessionsResult.sessions?.[0];
1823
+ let result = null;
1824
+ if (existingSession?.id) {
1825
+ result = await roomScopedApiCall({
1826
+ room_id: targetRoomId,
1827
+ project_id: targetProjectId,
1828
+ room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/reasoning-sessions/${encodeURIComponent(existingSession.id)}/updates`,
1829
+ project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/reasoning-sessions/${encodeURIComponent(existingSession.id)}/updates`,
1830
+ options: {
1831
+ method: "POST",
1832
+ body: JSON.stringify({
1833
+ actor_label: identity.actor_label,
1834
+ ...agentSessionCredentials(agentSession),
1835
+ ...snapshot,
1836
+ }),
1837
+ },
1838
+ });
1839
+ }
1840
+ else {
1841
+ result = await roomScopedApiCall({
1842
+ room_id: targetRoomId,
1843
+ project_id: targetProjectId,
1844
+ room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/reasoning-sessions`,
1845
+ project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/reasoning-sessions`,
1846
+ options: {
1847
+ method: "POST",
1848
+ body: JSON.stringify({
1849
+ actor_label: identity.actor_label,
1850
+ agent_key: identity.canonical_key,
1851
+ ...agentSessionCredentials(agentSession),
1852
+ ...snapshot,
1853
+ }),
1854
+ },
1855
+ });
1856
+ }
1857
+ let milestoneMessageId = null;
1858
+ const normalizedMilestone = normalizeOptionalToolString(milestone);
1859
+ if (normalizedMilestone) {
1860
+ const milestoneMessage = await roomScopedApiCall({
1861
+ room_id: targetRoomId,
1862
+ project_id: targetProjectId,
1863
+ room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages`,
1864
+ project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages`,
1865
+ options: {
1866
+ method: "POST",
1867
+ body: JSON.stringify({
1868
+ sender: identity.actor_label,
1869
+ text: normalizedMilestone,
1870
+ ...agentSessionCredentials(agentSession),
1871
+ }),
1872
+ },
1873
+ });
1874
+ milestoneMessageId =
1875
+ typeof milestoneMessage.id === "string" ? milestoneMessage.id : null;
1876
+ touchCurrentRoom(milestoneMessageId ?? undefined);
1877
+ }
1878
+ if (status) {
1879
+ await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, {
1880
+ status,
1881
+ status_text: normalizedSummary,
1882
+ }, agentSession);
1883
+ }
1884
+ else {
1885
+ await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity), agentSession);
1886
+ }
1887
+ return {
1888
+ content: [
1889
+ {
1890
+ type: "text",
1891
+ text: JSON.stringify({
1892
+ success: true,
1893
+ room_id: result?.room_id ?? targetRoomId ?? targetProjectId ?? null,
1894
+ session: result?.session ?? null,
1895
+ update: result?.update ?? null,
1896
+ milestone_message_id: milestoneMessageId,
1897
+ agent_identity: toPublicAgentIdentity(identity),
1898
+ }, null, 2),
1899
+ },
1900
+ ],
1901
+ };
1902
+ });
1441
1903
  // -- Task Board Tools -------------------------------------------------------
1442
1904
  const TASK_STATUSES = [
1443
1905
  "proposed", "accepted", "assigned", "in_progress",
1444
1906
  "blocked", "in_review", "merged", "done", "cancelled",
1445
1907
  ];
1446
1908
  server.tool("add_task", "Add a new task to the room board. Tasks normally start as 'proposed' and must be " +
1447
- "accepted before an agent can claim them, but tasks created by trusted agents already " +
1448
- "active in the room may be auto-accepted. Use this when a human or agent identifies " +
1909
+ "accepted before an agent can claim them. Agent-created tasks require coordinator " +
1910
+ "acceptance before they become claimable. Use this when a human or agent identifies " +
1449
1911
  "work that needs to be done.", {
1450
1912
  title: z.string().describe("Short task title, e.g. 'Wire up Jest test runner'"),
1451
1913
  description: z.string().optional().describe("Longer description of what needs to be done"),
@@ -1455,8 +1917,9 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
1455
1917
  .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
1456
1918
  source_message_id: z.string().optional().describe("Optional message ID where task was agreed, e.g. 'msg_42'"),
1457
1919
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
1458
- conversation_id: z.string().optional().describe("Optional conversation ID for per-conversation identity scoping."),
1459
- }, async ({ title, description, created_by: _createdBy, source_message_id, room_id, conversation_id }) => {
1920
+ conversation_id: z.string().optional().describe("Deprecated for worker writes; registered worker session identity is used."),
1921
+ agent_session_id: z.string().optional().describe("Registered agent session to use for this task action."),
1922
+ }, async ({ title, description, created_by: _createdBy, source_message_id, room_id, conversation_id: _conversation_id, agent_session_id }) => {
1460
1923
  const targetRoomId = getTargetRoomId(room_id);
1461
1924
  const targetProjectId = getFallbackProjectId();
1462
1925
  if (!targetRoomId && !targetProjectId) {
@@ -1464,7 +1927,10 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
1464
1927
  content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room. Join one first." }) }],
1465
1928
  };
1466
1929
  }
1467
- const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
1930
+ const { identity, agentSession } = await resolveWorkerToolIdentity({
1931
+ roomId: targetRoomId ?? currentRoom?.room_id ?? null,
1932
+ agentSessionId: agent_session_id,
1933
+ });
1468
1934
  const task = await roomScopedApiCall({
1469
1935
  room_id: targetRoomId,
1470
1936
  project_id: targetProjectId,
@@ -1476,11 +1942,15 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
1476
1942
  title,
1477
1943
  description,
1478
1944
  created_by: identity.actor_label,
1945
+ actor_label: identity.actor_label,
1946
+ actor_key: identity.canonical_key,
1947
+ actor_instance_id: agentSession?.agent_instance_id || AGENT_INSTANCE_UUID,
1479
1948
  source_message_id,
1949
+ ...agentSessionCredentials(agentSession),
1480
1950
  }),
1481
1951
  },
1482
1952
  });
1483
- await heartbeatRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity);
1953
+ await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity), agentSession);
1484
1954
  return {
1485
1955
  content: [
1486
1956
  {
@@ -1584,8 +2054,9 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
1584
2054
  .optional()
1585
2055
  .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
1586
2056
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
1587
- conversation_id: z.string().optional().describe("Optional conversation ID for per-conversation identity scoping."),
1588
- }, async ({ task_id, assignee: _assignee, room_id, conversation_id }) => {
2057
+ conversation_id: z.string().optional().describe("Deprecated for worker writes; registered worker session identity is used."),
2058
+ agent_session_id: z.string().optional().describe("Registered agent session to use for this task action."),
2059
+ }, async ({ task_id, assignee: _assignee, room_id, conversation_id: _conversation_id, agent_session_id }) => {
1589
2060
  const targetRoomId = getTargetRoomId(room_id);
1590
2061
  const targetProjectId = getFallbackProjectId();
1591
2062
  if (!targetRoomId && !targetProjectId) {
@@ -1594,7 +2065,10 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
1594
2065
  };
1595
2066
  }
1596
2067
  try {
1597
- const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
2068
+ const { identity, agentSession } = await resolveWorkerToolIdentity({
2069
+ roomId: targetRoomId ?? currentRoom?.room_id ?? null,
2070
+ agentSessionId: agent_session_id,
2071
+ });
1598
2072
  const updated = await roomScopedApiCall({
1599
2073
  room_id: targetRoomId,
1600
2074
  project_id: targetProjectId,
@@ -1607,14 +2081,16 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
1607
2081
  assignee: identity.actor_label,
1608
2082
  actor_label: identity.actor_label,
1609
2083
  actor_key: identity.canonical_key,
2084
+ actor_instance_id: agentSession?.agent_instance_id || AGENT_INSTANCE_UUID,
1610
2085
  assignee_agent_key: identity.canonical_key,
2086
+ ...agentSessionCredentials(agentSession),
1611
2087
  }),
1612
2088
  },
1613
2089
  });
1614
2090
  await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, {
1615
2091
  status: "working",
1616
2092
  status_text: `claimed ${task_id}`,
1617
- });
2093
+ }, agentSession);
1618
2094
  return {
1619
2095
  content: [
1620
2096
  {
@@ -1655,8 +2131,9 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
1655
2131
  .optional()
1656
2132
  .describe("Persisted provider-neutral task workflow artifacts to attach to the task"),
1657
2133
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
1658
- conversation_id: z.string().optional().describe("Optional conversation ID for per-conversation identity scoping."),
1659
- }, async ({ task_id, status, assignee, pr_url, workflow_artifacts, room_id, conversation_id }) => {
2134
+ conversation_id: z.string().optional().describe("Deprecated for worker writes; registered worker session identity is used."),
2135
+ agent_session_id: z.string().optional().describe("Registered agent session to use for this task action."),
2136
+ }, async ({ task_id, status, assignee, pr_url, workflow_artifacts, room_id, conversation_id: _conversation_id, agent_session_id }) => {
1660
2137
  const targetRoomId = getTargetRoomId(room_id);
1661
2138
  const targetProjectId = getFallbackProjectId();
1662
2139
  if (!targetRoomId && !targetProjectId) {
@@ -1665,7 +2142,10 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
1665
2142
  };
1666
2143
  }
1667
2144
  try {
1668
- const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
2145
+ const { identity, agentSession } = await resolveWorkerToolIdentity({
2146
+ roomId: targetRoomId ?? currentRoom?.room_id ?? null,
2147
+ agentSessionId: agent_session_id,
2148
+ });
1669
2149
  const nextAssignee = status === "assigned" && !assignee ? identity.actor_label : assignee;
1670
2150
  const nextAssigneeAgentKey = nextAssignee === identity.actor_label ? identity.canonical_key : undefined;
1671
2151
  const updated = await roomScopedApiCall({
@@ -1683,6 +2163,8 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
1683
2163
  workflow_artifacts,
1684
2164
  actor_label: identity.actor_label,
1685
2165
  actor_key: identity.canonical_key,
2166
+ actor_instance_id: agentSession?.agent_instance_id || AGENT_INSTANCE_UUID,
2167
+ ...agentSessionCredentials(agentSession),
1686
2168
  }),
1687
2169
  },
1688
2170
  });
@@ -1691,7 +2173,7 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
1691
2173
  status_text: status
1692
2174
  ? `${task_id} -> ${status}`
1693
2175
  : getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity).status_text,
1694
- });
2176
+ }, agentSession);
1695
2177
  return {
1696
2178
  content: [
1697
2179
  {
@@ -1717,8 +2199,9 @@ server.tool("complete_task", "Submit a task for review. Moves the task to 'in_re
1717
2199
  task_id: z.string().describe("The task ID to submit for review"),
1718
2200
  pr_url: z.string().optional().describe("GitHub PR URL for the work"),
1719
2201
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
1720
- conversation_id: z.string().optional().describe("Optional conversation ID for per-conversation identity scoping."),
1721
- }, async ({ task_id, pr_url, room_id, conversation_id }) => {
2202
+ conversation_id: z.string().optional().describe("Deprecated for worker writes; registered worker session identity is used."),
2203
+ agent_session_id: z.string().optional().describe("Registered agent session to use for this task action."),
2204
+ }, async ({ task_id, pr_url, room_id, conversation_id: _conversation_id, agent_session_id }) => {
1722
2205
  const targetRoomId = getTargetRoomId(room_id);
1723
2206
  const targetProjectId = getFallbackProjectId();
1724
2207
  if (!targetRoomId && !targetProjectId) {
@@ -1727,7 +2210,10 @@ server.tool("complete_task", "Submit a task for review. Moves the task to 'in_re
1727
2210
  };
1728
2211
  }
1729
2212
  try {
1730
- const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
2213
+ const { identity, agentSession } = await resolveWorkerToolIdentity({
2214
+ roomId: targetRoomId ?? currentRoom?.room_id ?? null,
2215
+ agentSessionId: agent_session_id,
2216
+ });
1731
2217
  const updated = await roomScopedApiCall({
1732
2218
  room_id: targetRoomId,
1733
2219
  project_id: targetProjectId,
@@ -1740,13 +2226,15 @@ server.tool("complete_task", "Submit a task for review. Moves the task to 'in_re
1740
2226
  pr_url,
1741
2227
  actor_label: identity.actor_label,
1742
2228
  actor_key: identity.canonical_key,
2229
+ actor_instance_id: agentSession?.agent_instance_id || AGENT_INSTANCE_UUID,
2230
+ ...agentSessionCredentials(agentSession),
1743
2231
  }),
1744
2232
  },
1745
2233
  });
1746
2234
  await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, {
1747
2235
  status: "reviewing",
1748
2236
  status_text: `${task_id} -> in_review`,
1749
- });
2237
+ }, agentSession);
1750
2238
  return {
1751
2239
  content: [{
1752
2240
  type: "text",
@@ -1764,6 +2252,114 @@ server.tool("complete_task", "Submit a task for review. Moves the task to 'in_re
1764
2252
  };
1765
2253
  }
1766
2254
  });
2255
+ server.tool("release_task_lease", "Release or forcibly clear the active work lease on a task so it can be claimed again. " +
2256
+ "Use this when the current worker is blocked, gone, or needs to give the lane back to the room.", {
2257
+ task_id: z.string().describe("The task whose active work lease should be cleared"),
2258
+ lease_id: z.string().optional().describe("Optional expected active lease id for stale-checking"),
2259
+ reason: z.string().optional().describe("Why the lease is being released"),
2260
+ room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
2261
+ conversation_id: z.string().optional().describe("Deprecated for worker writes; registered worker session identity is used."),
2262
+ agent_session_id: z.string().optional().describe("Registered agent session to use for this lease action."),
2263
+ }, async ({ task_id, lease_id, reason, room_id, conversation_id: _conversation_id, agent_session_id }) => {
2264
+ const targetRoomId = getTargetRoomId(room_id);
2265
+ if (!targetRoomId) {
2266
+ return {
2267
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a canonical room." }) }],
2268
+ };
2269
+ }
2270
+ try {
2271
+ const { identity, agentSession } = await resolveWorkerToolIdentity({
2272
+ roomId: targetRoomId,
2273
+ agentSessionId: agent_session_id,
2274
+ });
2275
+ const result = await apiCall(`/rooms/${encodeRoomIdPath(targetRoomId)}/tasks/${encodeURIComponent(task_id)}/lease-action`, {
2276
+ method: "POST",
2277
+ body: JSON.stringify({
2278
+ action: "release",
2279
+ lease_id: lease_id ?? undefined,
2280
+ reason,
2281
+ actor_label: identity.actor_label,
2282
+ actor_key: identity.canonical_key,
2283
+ actor_instance_id: agentSession?.agent_instance_id || AGENT_INSTANCE_UUID,
2284
+ ...agentSessionCredentials(agentSession),
2285
+ }),
2286
+ });
2287
+ await syncRoomPresence(targetRoomId, identity, {
2288
+ status: "working",
2289
+ status_text: `released lease on ${task_id}`,
2290
+ }, agentSession);
2291
+ return {
2292
+ content: [{
2293
+ type: "text",
2294
+ text: JSON.stringify({
2295
+ success: true,
2296
+ ...result,
2297
+ agent_identity: toPublicAgentIdentity(identity),
2298
+ }, null, 2),
2299
+ }],
2300
+ };
2301
+ }
2302
+ catch (error) {
2303
+ return {
2304
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }) }],
2305
+ };
2306
+ }
2307
+ });
2308
+ server.tool("handoff_task_lease", "Transfer the active work lease on a task to another agent. " +
2309
+ "This reassigns the task and mints a fresh work lease for the target agent key.", {
2310
+ task_id: z.string().describe("The task whose active work lease should be transferred"),
2311
+ target_agent_key: z.string().describe("Canonical agent key to receive the new work lease"),
2312
+ lease_id: z.string().optional().describe("Optional expected active lease id for stale-checking"),
2313
+ reason: z.string().optional().describe("Why the lane is being handed off"),
2314
+ room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
2315
+ conversation_id: z.string().optional().describe("Deprecated for worker writes; registered worker session identity is used."),
2316
+ agent_session_id: z.string().optional().describe("Registered agent session to use for this lease action."),
2317
+ }, async ({ task_id, target_agent_key, lease_id, reason, room_id, conversation_id: _conversation_id, agent_session_id }) => {
2318
+ const targetRoomId = getTargetRoomId(room_id);
2319
+ if (!targetRoomId) {
2320
+ return {
2321
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a canonical room." }) }],
2322
+ };
2323
+ }
2324
+ try {
2325
+ const { identity, agentSession } = await resolveWorkerToolIdentity({
2326
+ roomId: targetRoomId,
2327
+ agentSessionId: agent_session_id,
2328
+ });
2329
+ const result = await apiCall(`/rooms/${encodeRoomIdPath(targetRoomId)}/tasks/${encodeURIComponent(task_id)}/lease-action`, {
2330
+ method: "POST",
2331
+ body: JSON.stringify({
2332
+ action: "handoff",
2333
+ lease_id: lease_id ?? undefined,
2334
+ reason,
2335
+ target_actor_key: target_agent_key,
2336
+ actor_label: identity.actor_label,
2337
+ actor_key: identity.canonical_key,
2338
+ actor_instance_id: agentSession?.agent_instance_id || AGENT_INSTANCE_UUID,
2339
+ ...agentSessionCredentials(agentSession),
2340
+ }),
2341
+ });
2342
+ await syncRoomPresence(targetRoomId, identity, {
2343
+ status: "working",
2344
+ status_text: `handed off ${task_id} to ${target_agent_key}`,
2345
+ }, agentSession);
2346
+ return {
2347
+ content: [{
2348
+ type: "text",
2349
+ text: JSON.stringify({
2350
+ success: true,
2351
+ ...result,
2352
+ agent_identity: toPublicAgentIdentity(identity),
2353
+ }, null, 2),
2354
+ }],
2355
+ };
2356
+ }
2357
+ catch (error) {
2358
+ return {
2359
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }) }],
2360
+ };
2361
+ }
2362
+ });
1767
2363
  server.tool("initialize_repo", "Initialize the current repo for Let Agents Chat by creating a .letagents.json config file. " +
1768
2364
  "This explicitly sets up repo-based room auto-join. Reads git remote to derive the room name, " +
1769
2365
  "or accepts a custom room name. Will NOT overwrite an existing .letagents.json. " +
@@ -1923,14 +2519,21 @@ server.tool("send_message", "Send a message to a Let Agents Chat room. Use a sho
1923
2519
  conversation_id: z
1924
2520
  .string()
1925
2521
  .optional()
1926
- .describe("Optional conversation ID for per-conversation identity scoping."),
1927
- }, async ({ room_id, sender: _sender, text, reply_to, conversation_id }) => {
2522
+ .describe("Deprecated for worker writes; registered worker session identity is used."),
2523
+ agent_session_id: z
2524
+ .string()
2525
+ .optional()
2526
+ .describe("Registered agent session to use for this message."),
2527
+ }, async ({ room_id, sender: _sender, text, reply_to, conversation_id: _conversation_id, agent_session_id }) => {
1928
2528
  const targetRoomId = getTargetRoomId(room_id);
1929
2529
  const targetProjectId = getFallbackProjectId();
1930
2530
  if (!targetRoomId && !targetProjectId) {
1931
2531
  throw new Error("No room is currently selected. Join a room first or pass room_id.");
1932
2532
  }
1933
- const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
2533
+ const { identity, agentSession } = await resolveWorkerToolIdentity({
2534
+ roomId: targetRoomId ?? currentRoom?.room_id ?? null,
2535
+ agentSessionId: agent_session_id,
2536
+ });
1934
2537
  const message = await roomScopedApiCall({
1935
2538
  room_id: targetRoomId,
1936
2539
  project_id: targetProjectId,
@@ -1938,11 +2541,16 @@ server.tool("send_message", "Send a message to a Let Agents Chat room. Use a sho
1938
2541
  project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages`,
1939
2542
  options: {
1940
2543
  method: "POST",
1941
- body: JSON.stringify({ sender: identity.actor_label, text, reply_to }),
2544
+ body: JSON.stringify({
2545
+ sender: identity.actor_label,
2546
+ text,
2547
+ reply_to,
2548
+ ...agentSessionCredentials(agentSession),
2549
+ }),
1942
2550
  },
1943
2551
  });
1944
2552
  touchCurrentRoom(typeof message.id === "string" ? message.id : undefined);
1945
- await heartbeatRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity);
2553
+ await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity), agentSession);
1946
2554
  return {
1947
2555
  content: [
1948
2556
  {
@@ -2013,11 +2621,16 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
2013
2621
  .number()
2014
2622
  .optional()
2015
2623
  .describe("Maximum wait time in milliseconds (min 1000, capped by LETAGENTS_POLL_MAX_MS / server). If set to 0, the default timeout will be used."),
2016
- }, async ({ room_id, after_message_id, timeout }) => {
2624
+ agent_session_id: z
2625
+ .string()
2626
+ .optional()
2627
+ .describe("Registered agent session to use. Without this, the MCP transport is treated as a controller and is hidden from connected-agent activity."),
2628
+ }, async ({ room_id, after_message_id, timeout, agent_session_id }) => {
2017
2629
  const targetRoomId = getTargetRoomId(room_id);
2018
2630
  const targetProjectId = getFallbackProjectId();
2019
2631
  const identity = await ensureAgentIdentity();
2020
- await heartbeatRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity);
2632
+ const agentSession = resolveAgentSession(targetRoomId ?? currentRoom?.room_id ?? null, agent_session_id);
2633
+ await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, agentSession ? identityFromAgentSession(agentSession) : identity), agentSession);
2021
2634
  const maxPollMs = getPollTimeoutCapMs();
2022
2635
  const serverTimeout = Math.min(Math.max(timeout || DEFAULT_POLL_TIMEOUT_MS, 1000), maxPollMs);
2023
2636
  const clientTimeout = serverTimeout + (serverTimeout > 120_000 ? 120_000 : 5_000);
@@ -2026,12 +2639,16 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
2026
2639
  params.set("after", after_message_id);
2027
2640
  params.set("timeout", String(serverTimeout));
2028
2641
  const queryString = params.toString();
2642
+ const deliveryHeaders = buildAgentDeliveryHeaders(agentSession);
2029
2643
  const firstResult = await roomScopedApiCall({
2030
2644
  room_id: targetRoomId,
2031
2645
  project_id: targetProjectId,
2032
2646
  room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages/poll?${queryString}`,
2033
2647
  project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages/poll?${queryString}`,
2034
- options: { signal: AbortSignal.timeout(clientTimeout) },
2648
+ options: {
2649
+ signal: AbortSignal.timeout(clientTimeout),
2650
+ headers: deliveryHeaders,
2651
+ },
2035
2652
  });
2036
2653
  const allMessages = [...(firstResult.messages ?? [])];
2037
2654
  const roomIdFromResponse = firstResult.room_id || firstResult.project_id;
@@ -2064,7 +2681,7 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
2064
2681
  if (targetRoomId) {
2065
2682
  touchRoomSession(targetRoomId, getLastMessageId(output));
2066
2683
  }
2067
- await heartbeatRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity);
2684
+ await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, agentSession ? identityFromAgentSession(agentSession) : identity), agentSession);
2068
2685
  return {
2069
2686
  content: [
2070
2687
  {