letagents 0.11.1 → 0.12.1

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,39 @@ 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, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, saveRoomSession, setStoredAgentIdentity, setPendingDeviceAuth, setStoredAuth, touchRoomSession, updateLocalState, } from "./local-state.js";
14
+ import { clearPendingDeviceAuth, clearStoredAuth, getCurrentCodexLiveSession, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, listStoredCodexLiveSessions, 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
+ import { buildRoomAgentPrompt, normalizeAgentPromptKind, } from "../shared/room-agent-prompts.js";
18
+ import { inspectLocalCodexSession, startLocalCodexSession, stopLocalCodexSession, toPublicCodexLiveSession, } from "./codex-session.js";
17
19
  let currentRoom = null;
18
20
  let currentAgentIdentityKey = "";
19
21
  let currentAgentIdentity = null;
20
22
  let currentAuthenticatedAccount = undefined;
23
+ // ---------------------------------------------------------------------------
24
+ // Conversation-scoped identity (Option C: per-conversation hints)
25
+ // ---------------------------------------------------------------------------
26
+ const MAX_CONVERSATION_IDENTITIES = 20;
27
+ const conversationIdentities = new Map();
28
+ /**
29
+ * Get or set a conversation-scoped identity override.
30
+ * Falls back to the global `currentAgentIdentity` when conversationId is absent.
31
+ */
32
+ function getConversationIdentity(conversationId) {
33
+ if (!conversationId)
34
+ return currentAgentIdentity;
35
+ return conversationIdentities.get(conversationId) ?? currentAgentIdentity;
36
+ }
37
+ function setConversationIdentity(conversationId, identity) {
38
+ // LRU-style eviction: if at cap, remove oldest entry
39
+ if (!conversationIdentities.has(conversationId) &&
40
+ conversationIdentities.size >= MAX_CONVERSATION_IDENTITIES) {
41
+ const oldestKey = conversationIdentities.keys().next().value;
42
+ if (oldestKey !== undefined)
43
+ conversationIdentities.delete(oldestKey);
44
+ }
45
+ conversationIdentities.set(conversationId, identity);
46
+ }
21
47
  let currentAuthenticatedAccountSource = null;
22
48
  let currentAuthenticatedEnvToken = null;
23
49
  // ---------------------------------------------------------------------------
@@ -722,6 +748,43 @@ function getLastMessageId(payload) {
722
748
  const lastMessage = messages?.at(-1);
723
749
  return typeof lastMessage?.id === "string" ? lastMessage.id : undefined;
724
750
  }
751
+ function withJoinRoomAgentPrompt(payload) {
752
+ return {
753
+ ...payload,
754
+ agent_prompt_kind: "join",
755
+ agent_prompt: buildRoomAgentPrompt("join"),
756
+ };
757
+ }
758
+ function normalizeJoinSessionMode(value) {
759
+ return String(value || "").trim().toLowerCase() === "live" ? "live" : "current";
760
+ }
761
+ function getCurrentLiveSessionPayload(roomId) {
762
+ const session = getCurrentCodexLiveSession(roomId);
763
+ return session ? toPublicCodexLiveSession(session) : null;
764
+ }
765
+ function toAgentReadableMessage(message) {
766
+ if (!message || typeof message !== "object") {
767
+ return message;
768
+ }
769
+ const record = message;
770
+ const kind = normalizeAgentPromptKind(record.agent_prompt_kind);
771
+ const text = typeof record.text === "string" ? record.text : null;
772
+ if (!kind || text === null) {
773
+ return record;
774
+ }
775
+ return {
776
+ ...record,
777
+ visible_text: text,
778
+ agent_prompt: buildRoomAgentPrompt(kind),
779
+ prompt_injected: kind === "inline",
780
+ };
781
+ }
782
+ function toAgentReadableMessages(messages) {
783
+ return (messages ?? []).map((message) => toAgentReadableMessage(message));
784
+ }
785
+ function appendIncludePromptOnly(path) {
786
+ return `${path}${path.includes("?") ? "&" : "?"}include_prompt_only=1`;
787
+ }
725
788
  async function roomScopedApiCall(input) {
726
789
  if (input.room_id) {
727
790
  try {
@@ -853,18 +916,46 @@ async function createInviteRoom() {
853
916
  },
854
917
  };
855
918
  }
856
- async function joinInviteCode(code) {
919
+ async function buildJoinResponse(input) {
920
+ const basePayload = await withAgentIdentity({
921
+ ...toPublicRoomResponse(input.joined.response, input.joined.room.room_id),
922
+ joined_via: input.joined_via,
923
+ session_mode: input.session_mode,
924
+ });
925
+ if (input.session_mode === "current") {
926
+ return withJoinRoomAgentPrompt(basePayload);
927
+ }
928
+ const liveSession = await startLocalCodexSession({
929
+ room_id: input.joined.room.room_id,
930
+ room_identifier: input.room_identifier,
931
+ room_code: input.joined.room.code ?? null,
932
+ room_display_name: input.joined.room.display_name ?? null,
933
+ joined_via: input.joined_via,
934
+ cwd: process.cwd(),
935
+ });
936
+ return withJoinRoomAgentPrompt({
937
+ ...basePayload,
938
+ local_codex_session: toPublicCodexLiveSession(liveSession.session),
939
+ local_codex_session_started: !liveSession.reused,
940
+ local_codex_session_reused: liveSession.reused,
941
+ });
942
+ }
943
+ async function joinInviteCode(code, sessionMode) {
857
944
  const joined = await joinRoomIdentifier(code, "join_code");
858
- return withAgentIdentity({
859
- ...toPublicRoomResponse(joined.response, joined.room.room_id),
945
+ return buildJoinResponse({
946
+ joined,
947
+ room_identifier: normalizeInviteCode(code),
860
948
  joined_via: "join_code",
949
+ session_mode: sessionMode,
861
950
  });
862
951
  }
863
- async function joinNamedRoom(name) {
952
+ async function joinNamedRoom(name, sessionMode) {
864
953
  const joined = await joinRoomIdentifier(name, "join_room");
865
- return withAgentIdentity({
866
- ...toPublicRoomResponse(joined.response, joined.room.room_id),
954
+ return buildJoinResponse({
955
+ joined,
956
+ room_identifier: name.trim(),
867
957
  joined_via: "join_room",
958
+ session_mode: sessionMode,
868
959
  });
869
960
  }
870
961
  // ---------------------------------------------------------------------------
@@ -895,8 +986,8 @@ server.resource("room_messages", new ResourceTemplate("letagents://rooms/{room_i
895
986
  const result = await roomScopedApiCall({
896
987
  room_id: normalizedRoomId,
897
988
  project_id: storedSession?.project_id ?? null,
898
- room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}`,
899
- project_path: (projectId) => `/projects/${encodeURIComponent(projectId)}/messages${qs ? `?${qs}` : ""}`,
989
+ room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}`),
990
+ project_path: (projectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(projectId)}/messages${qs ? `?${qs}` : ""}`),
900
991
  });
901
992
  const msgs = result.messages ?? [];
902
993
  allMessages.push(...msgs);
@@ -912,7 +1003,7 @@ server.resource("room_messages", new ResourceTemplate("letagents://rooms/{room_i
912
1003
  {
913
1004
  uri: uri.href,
914
1005
  mimeType: "application/json",
915
- text: JSON.stringify({ messages: allMessages }, null, 2),
1006
+ text: JSON.stringify({ messages: toAgentReadableMessages(allMessages) }, null, 2),
916
1007
  },
917
1008
  ],
918
1009
  };
@@ -924,7 +1015,7 @@ server.tool("create_room", "Create a new invite room on Let Agents Chat. Returns
924
1015
  content: [
925
1016
  {
926
1017
  type: "text",
927
- text: JSON.stringify(created.response, null, 2),
1018
+ text: JSON.stringify(withJoinRoomAgentPrompt(created.response), null, 2),
928
1019
  },
929
1020
  ],
930
1021
  };
@@ -936,7 +1027,7 @@ server.tool("create_project", "Legacy alias for create_room. Creates a new invit
936
1027
  content: [
937
1028
  {
938
1029
  type: "text",
939
- text: JSON.stringify(created.response, null, 2),
1030
+ text: JSON.stringify(withJoinRoomAgentPrompt(created.response), null, 2),
940
1031
  },
941
1032
  ],
942
1033
  };
@@ -944,12 +1035,16 @@ server.tool("create_project", "Legacy alias for create_room. Creates a new invit
944
1035
  // -- join_code --------------------------------------------------------------
945
1036
  server.tool("join_code", "Join an existing room using an invite code.", {
946
1037
  code: z.string().describe("The invite code shared for the room (e.g. 'ABCX-7291')"),
947
- }, async ({ code }) => {
1038
+ session_mode: z
1039
+ .enum(["live", "current"])
1040
+ .optional()
1041
+ .describe("Use 'current' (default) for a normal inline join. Use 'live' to start/reuse a detached local Codex room worker."),
1042
+ }, async ({ code, session_mode }) => {
948
1043
  return {
949
1044
  content: [
950
1045
  {
951
1046
  type: "text",
952
- text: JSON.stringify(await joinInviteCode(code), null, 2),
1047
+ text: JSON.stringify(await joinInviteCode(code, normalizeJoinSessionMode(session_mode)), null, 2),
953
1048
  },
954
1049
  ],
955
1050
  };
@@ -957,12 +1052,16 @@ server.tool("join_code", "Join an existing room using an invite code.", {
957
1052
  // -- join_project -----------------------------------------------------------
958
1053
  server.tool("join_project", "Legacy alias for join_code. Join an existing room using an invite code.", {
959
1054
  code: z.string().describe("The invite code shared for the room (e.g. 'ABCX-7291')"),
960
- }, async ({ code }) => {
1055
+ session_mode: z
1056
+ .enum(["live", "current"])
1057
+ .optional()
1058
+ .describe("Use 'current' (default) for a normal inline join. Use 'live' to start/reuse a detached local Codex room worker."),
1059
+ }, async ({ code, session_mode }) => {
961
1060
  return {
962
1061
  content: [
963
1062
  {
964
1063
  type: "text",
965
- text: JSON.stringify(await joinInviteCode(code), null, 2),
1064
+ text: JSON.stringify(await joinInviteCode(code, normalizeJoinSessionMode(session_mode)), null, 2),
966
1065
  },
967
1066
  ],
968
1067
  };
@@ -970,13 +1069,17 @@ server.tool("join_project", "Legacy alias for join_code. Join an existing room u
970
1069
  // -- join_room --------------------------------------------------------------
971
1070
  server.tool("join_room", "Join a named room on Let Agents Chat. Creates the room if it doesn't exist. Use this for repo-based room joining.", {
972
1071
  name: z.string().describe("The room name to join (e.g. 'github.com/owner/repo')"),
973
- }, async ({ name }) => {
1072
+ session_mode: z
1073
+ .enum(["live", "current"])
1074
+ .optional()
1075
+ .describe("Use 'current' (default) for a normal inline join. Use 'live' to start/reuse a detached local Codex room worker."),
1076
+ }, async ({ name, session_mode }) => {
974
1077
  try {
975
1078
  return {
976
1079
  content: [
977
1080
  {
978
1081
  type: "text",
979
- text: JSON.stringify(await joinNamedRoom(name), null, 2),
1082
+ text: JSON.stringify(await joinNamedRoom(name, normalizeJoinSessionMode(session_mode)), null, 2),
980
1083
  },
981
1084
  ],
982
1085
  };
@@ -995,17 +1098,156 @@ server.tool("join_room", "Join a named room on Let Agents Chat. Creates the room
995
1098
  throw error;
996
1099
  }
997
1100
  });
1101
+ // -- local Codex live sessions ---------------------------------------------
1102
+ 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.", {
1103
+ room: z
1104
+ .string()
1105
+ .describe("Invite code or room name to run as a detached local Codex worker."),
1106
+ cwd: z
1107
+ .string()
1108
+ .optional()
1109
+ .describe("Working directory for repo work. Defaults to the current process directory."),
1110
+ stop_phrase: z
1111
+ .string()
1112
+ .optional()
1113
+ .describe("Exact room message text that tells the worker to stop. Defaults to /stop-codex-room."),
1114
+ max_minutes: z
1115
+ .number()
1116
+ .optional()
1117
+ .describe("Optional hard stop in minutes. Defaults to 0, which means run until stopped."),
1118
+ }, async ({ room, cwd, stop_phrase, max_minutes }) => {
1119
+ const joinedVia = looksLikeInviteCode(room) ? "join_code" : "join_room";
1120
+ try {
1121
+ const joined = await joinRoomIdentifier(room, joinedVia);
1122
+ const liveSession = await startLocalCodexSession({
1123
+ room_id: joined.room.room_id,
1124
+ room_identifier: joinedVia === "join_code" ? normalizeInviteCode(room) : room.trim(),
1125
+ room_code: joined.room.code ?? null,
1126
+ room_display_name: joined.room.display_name ?? null,
1127
+ joined_via: joinedVia,
1128
+ cwd: cwd || process.cwd(),
1129
+ stop_phrase,
1130
+ max_minutes,
1131
+ });
1132
+ return {
1133
+ content: [
1134
+ {
1135
+ type: "text",
1136
+ text: JSON.stringify(await withAgentIdentity({
1137
+ success: true,
1138
+ room: toPublicRoomState(joined.room),
1139
+ local_codex_session: toPublicCodexLiveSession(liveSession.session),
1140
+ local_codex_session_started: !liveSession.reused,
1141
+ local_codex_session_reused: liveSession.reused,
1142
+ }), null, 2),
1143
+ },
1144
+ ],
1145
+ };
1146
+ }
1147
+ catch (error) {
1148
+ if (error instanceof RepoRoomAuthRequiredError) {
1149
+ return {
1150
+ content: [
1151
+ {
1152
+ type: "text",
1153
+ text: JSON.stringify(toRepoRoomAuthRequiredResult(error), null, 2),
1154
+ },
1155
+ ],
1156
+ };
1157
+ }
1158
+ throw error;
1159
+ }
1160
+ });
1161
+ server.tool("status_local_codex_session", "Inspect the current detached local Codex live session, or a specific one by session_id.", {
1162
+ session_id: z
1163
+ .string()
1164
+ .optional()
1165
+ .describe("Optional session id. Defaults to the current local Codex live session."),
1166
+ }, async ({ session_id }) => {
1167
+ const status = await inspectLocalCodexSession(session_id, currentRoom?.room_id);
1168
+ if (!status) {
1169
+ return {
1170
+ content: [
1171
+ {
1172
+ type: "text",
1173
+ text: JSON.stringify({ success: false, error: "No local Codex live session found." }, null, 2),
1174
+ },
1175
+ ],
1176
+ };
1177
+ }
1178
+ return {
1179
+ content: [
1180
+ {
1181
+ type: "text",
1182
+ text: JSON.stringify({
1183
+ success: true,
1184
+ session: toPublicCodexLiveSession(status.session),
1185
+ server_reachable: status.server_reachable,
1186
+ thread_status: status.thread_status,
1187
+ turn_status: status.turn_status,
1188
+ recent_items: status.recent_items,
1189
+ }, null, 2),
1190
+ },
1191
+ ],
1192
+ };
1193
+ });
1194
+ server.tool("stop_local_codex_session", "Stop the current detached local Codex live session, or a specific one by session_id.", {
1195
+ session_id: z
1196
+ .string()
1197
+ .optional()
1198
+ .describe("Optional session id. Defaults to the current local Codex live session."),
1199
+ shutdown_server: z
1200
+ .boolean()
1201
+ .optional()
1202
+ .describe("If true, also terminate the spawned codex app-server process when possible."),
1203
+ }, async ({ session_id, shutdown_server }) => {
1204
+ const stopped = await stopLocalCodexSession({
1205
+ session_id,
1206
+ room_id: currentRoom?.room_id,
1207
+ shutdown_server,
1208
+ });
1209
+ if (!stopped) {
1210
+ return {
1211
+ content: [
1212
+ {
1213
+ type: "text",
1214
+ text: JSON.stringify({ success: false, error: "No local Codex live session found." }, null, 2),
1215
+ },
1216
+ ],
1217
+ };
1218
+ }
1219
+ return {
1220
+ content: [
1221
+ {
1222
+ type: "text",
1223
+ text: JSON.stringify({
1224
+ success: true,
1225
+ session: toPublicCodexLiveSession(stopped),
1226
+ }, null, 2),
1227
+ },
1228
+ ],
1229
+ };
1230
+ });
998
1231
  // -- get_current_room -------------------------------------------------------
999
- server.tool("get_current_room", "Get information about the currently joined room, including how it was joined.", {}, async () => {
1232
+ server.tool("get_current_room", "Get information about the currently joined room, including how it was joined.", {
1233
+ conversation_id: z
1234
+ .string()
1235
+ .optional()
1236
+ .describe("Optional conversation ID to report the conversation-scoped identity instead of the global one."),
1237
+ }, async ({ conversation_id }) => {
1000
1238
  return {
1001
1239
  content: [
1002
1240
  {
1003
1241
  type: "text",
1004
1242
  text: JSON.stringify(currentRoom
1005
- ? {
1243
+ ? withJoinRoomAgentPrompt({
1006
1244
  connected: true,
1007
1245
  ...toPublicRoomState(currentRoom),
1008
- agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(currentAgentIdentityKey)),
1246
+ current_local_codex_session: getCurrentLiveSessionPayload(currentRoom.room_id),
1247
+ local_codex_session_count: listStoredCodexLiveSessions().length,
1248
+ agent_identity: toPublicAgentIdentity(getConversationIdentity(conversation_id)
1249
+ ?? currentAgentIdentity
1250
+ ?? getStoredAgentIdentity(currentAgentIdentityKey)),
1009
1251
  auth: getStoredAuth()
1010
1252
  ? {
1011
1253
  source: process.env.LETAGENTS_TOKEN ? "env" : "local_state",
@@ -1013,8 +1255,13 @@ server.tool("get_current_room", "Get information about the currently joined room
1013
1255
  account: getStoredAuth()?.account ?? null,
1014
1256
  }
1015
1257
  : null,
1016
- }
1017
- : { connected: false, message: "Not currently in any room" }, null, 2),
1258
+ })
1259
+ : {
1260
+ connected: false,
1261
+ message: "Not currently in any room",
1262
+ current_local_codex_session: getCurrentLiveSessionPayload(), // no room context
1263
+ local_codex_session_count: listStoredCodexLiveSessions().length,
1264
+ }, null, 2),
1018
1265
  },
1019
1266
  ],
1020
1267
  };
@@ -1078,7 +1325,11 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
1078
1325
  .string()
1079
1326
  .optional()
1080
1327
  .describe("Canonical room ID. Defaults to the current room."),
1081
- }, async ({ sender: _sender, status, room_id }) => {
1328
+ conversation_id: z
1329
+ .string()
1330
+ .optional()
1331
+ .describe("Optional conversation ID for per-conversation identity scoping."),
1332
+ }, async ({ sender: _sender, status, room_id, conversation_id }) => {
1082
1333
  const targetRoomId = getTargetRoomId(room_id);
1083
1334
  const targetProjectId = getFallbackProjectId();
1084
1335
  if (!targetRoomId && !targetProjectId) {
@@ -1097,7 +1348,7 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
1097
1348
  }
1098
1349
  // Status messages use a reserved prefix so the UI (and agents) can distinguish
1099
1350
  // them from normal chat messages without changing the data model.
1100
- const identity = await ensureAgentIdentity();
1351
+ const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
1101
1352
  const sender = identity.actor_label;
1102
1353
  const statusText = `[status] ${status}`;
1103
1354
  const message = await roomScopedApiCall({
@@ -1144,7 +1395,8 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
1144
1395
  .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
1145
1396
  source_message_id: z.string().optional().describe("Optional message ID where task was agreed, e.g. 'msg_42'"),
1146
1397
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
1147
- }, async ({ title, description, created_by: _createdBy, source_message_id, room_id }) => {
1398
+ conversation_id: z.string().optional().describe("Optional conversation ID for per-conversation identity scoping."),
1399
+ }, async ({ title, description, created_by: _createdBy, source_message_id, room_id, conversation_id }) => {
1148
1400
  const targetRoomId = getTargetRoomId(room_id);
1149
1401
  const targetProjectId = getFallbackProjectId();
1150
1402
  if (!targetRoomId && !targetProjectId) {
@@ -1152,7 +1404,7 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
1152
1404
  content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room. Join one first." }) }],
1153
1405
  };
1154
1406
  }
1155
- const identity = await ensureAgentIdentity();
1407
+ const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
1156
1408
  const task = await roomScopedApiCall({
1157
1409
  room_id: targetRoomId,
1158
1410
  project_id: targetProjectId,
@@ -1232,7 +1484,8 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
1232
1484
  .optional()
1233
1485
  .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
1234
1486
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
1235
- }, async ({ task_id, assignee: _assignee, room_id }) => {
1487
+ conversation_id: z.string().optional().describe("Optional conversation ID for per-conversation identity scoping."),
1488
+ }, async ({ task_id, assignee: _assignee, room_id, conversation_id }) => {
1236
1489
  const targetRoomId = getTargetRoomId(room_id);
1237
1490
  const targetProjectId = getFallbackProjectId();
1238
1491
  if (!targetRoomId && !targetProjectId) {
@@ -1241,7 +1494,7 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
1241
1494
  };
1242
1495
  }
1243
1496
  try {
1244
- const identity = await ensureAgentIdentity();
1497
+ const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
1245
1498
  const updated = await roomScopedApiCall({
1246
1499
  room_id: targetRoomId,
1247
1500
  project_id: targetProjectId,
@@ -1278,7 +1531,8 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
1278
1531
  .describe("New assignee for the task. Defaults to the current agent when status=assigned."),
1279
1532
  pr_url: z.string().optional().describe("PR URL to link to the task"),
1280
1533
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
1281
- }, async ({ task_id, status, assignee, pr_url, room_id }) => {
1534
+ conversation_id: z.string().optional().describe("Optional conversation ID for per-conversation identity scoping."),
1535
+ }, async ({ task_id, status, assignee, pr_url, room_id, conversation_id }) => {
1282
1536
  const targetRoomId = getTargetRoomId(room_id);
1283
1537
  const targetProjectId = getFallbackProjectId();
1284
1538
  if (!targetRoomId && !targetProjectId) {
@@ -1288,7 +1542,7 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
1288
1542
  }
1289
1543
  try {
1290
1544
  const identity = status === "assigned" && !assignee
1291
- ? await ensureAgentIdentity()
1545
+ ? (getConversationIdentity(conversation_id) ?? await ensureAgentIdentity())
1292
1546
  : null;
1293
1547
  const updated = await roomScopedApiCall({
1294
1548
  room_id: targetRoomId,
@@ -1329,7 +1583,8 @@ server.tool("complete_task", "Submit a task for review. Moves the task to 'in_re
1329
1583
  task_id: z.string().describe("The task ID to submit for review"),
1330
1584
  pr_url: z.string().optional().describe("GitHub PR URL for the work"),
1331
1585
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
1332
- }, async ({ task_id, pr_url, room_id }) => {
1586
+ conversation_id: z.string().optional().describe("Optional conversation ID for per-conversation identity scoping."),
1587
+ }, async ({ task_id, pr_url, room_id, conversation_id: _conversationId }) => {
1333
1588
  const targetRoomId = getTargetRoomId(room_id);
1334
1589
  const targetProjectId = getFallbackProjectId();
1335
1590
  if (!targetRoomId && !targetProjectId) {
@@ -1510,13 +1765,17 @@ server.tool("send_message", "Send a message to a Let Agents Chat room.", {
1510
1765
  .optional()
1511
1766
  .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
1512
1767
  text: z.string().describe("The message text to send"),
1513
- }, async ({ room_id, sender: _sender, text }) => {
1768
+ conversation_id: z
1769
+ .string()
1770
+ .optional()
1771
+ .describe("Optional conversation ID for per-conversation identity scoping."),
1772
+ }, async ({ room_id, sender: _sender, text, conversation_id }) => {
1514
1773
  const targetRoomId = getTargetRoomId(room_id);
1515
1774
  const targetProjectId = getFallbackProjectId();
1516
1775
  if (!targetRoomId && !targetProjectId) {
1517
1776
  throw new Error("No room is currently selected. Join a room first or pass room_id.");
1518
1777
  }
1519
- const identity = await ensureAgentIdentity();
1778
+ const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
1520
1779
  const message = await roomScopedApiCall({
1521
1780
  room_id: targetRoomId,
1522
1781
  project_id: targetProjectId,
@@ -1558,8 +1817,8 @@ server.tool("read_messages", "Read all messages from a Let Agents Chat room.", {
1558
1817
  const result = await roomScopedApiCall({
1559
1818
  room_id: targetRoomId,
1560
1819
  project_id: targetProjectId,
1561
- room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}`,
1562
- project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages${qs ? `?${qs}` : ""}`,
1820
+ room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}`),
1821
+ project_path: (targetProjectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(targetProjectId)}/messages${qs ? `?${qs}` : ""}`),
1563
1822
  });
1564
1823
  roomIdFromResponse = roomIdFromResponse || result.room_id || result.project_id;
1565
1824
  const msgs = result.messages ?? [];
@@ -1572,7 +1831,7 @@ server.tool("read_messages", "Read all messages from a Let Agents Chat room.", {
1572
1831
  break;
1573
1832
  afterCursor = lastMsg.id;
1574
1833
  }
1575
- const output = { messages: allMessages };
1834
+ const output = { messages: toAgentReadableMessages(allMessages) };
1576
1835
  if (roomIdFromResponse) {
1577
1836
  output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
1578
1837
  }
@@ -1611,8 +1870,8 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
1611
1870
  const firstResult = await roomScopedApiCall({
1612
1871
  room_id: targetRoomId,
1613
1872
  project_id: targetProjectId,
1614
- room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages/poll?${queryString}`,
1615
- project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages/poll?${queryString}`,
1873
+ room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages/poll?${queryString}`),
1874
+ project_path: (targetProjectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(targetProjectId)}/messages/poll?${queryString}`),
1616
1875
  options: { signal: AbortSignal.timeout(clientTimeout) },
1617
1876
  });
1618
1877
  const allMessages = [...(firstResult.messages ?? [])];
@@ -1627,8 +1886,8 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
1627
1886
  const page = await roomScopedApiCall({
1628
1887
  room_id: targetRoomId,
1629
1888
  project_id: targetProjectId,
1630
- room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages?${qs}`,
1631
- project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages?${qs}`,
1889
+ room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages?${qs}`),
1890
+ project_path: (targetProjectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(targetProjectId)}/messages?${qs}`),
1632
1891
  });
1633
1892
  const msgs = page.messages ?? [];
1634
1893
  allMessages.push(...msgs);
@@ -1639,7 +1898,7 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
1639
1898
  break;
1640
1899
  }
1641
1900
  }
1642
- const output = { messages: allMessages };
1901
+ const output = { messages: toAgentReadableMessages(allMessages) };
1643
1902
  if (roomIdFromResponse) {
1644
1903
  output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
1645
1904
  }
@@ -1898,7 +2157,11 @@ server.tool("set_agent_name", "Set or change the agent's display name. The agent
1898
2157
  .min(2)
1899
2158
  .max(64)
1900
2159
  .describe("The desired display name for this agent (2-64 characters)."),
1901
- }, async ({ name: desiredName }) => {
2160
+ conversation_id: z
2161
+ .string()
2162
+ .optional()
2163
+ .describe("Optional conversation ID to scope this name change. When provided, only this conversation uses the new name; other conversations keep their own identity."),
2164
+ }, async ({ name: desiredName, conversation_id }) => {
1902
2165
  const trimmedName = desiredName.trim();
1903
2166
  if (trimmedName.length < 2 || trimmedName.length > 64) {
1904
2167
  return {
@@ -1924,14 +2187,6 @@ server.tool("set_agent_name", "Set or change the agent's display name. The agent
1924
2187
  const slugName = normalizeAgentBaseName(trimmedName);
1925
2188
  try {
1926
2189
  const owner = await resolveOwnerContext();
1927
- const registered = await apiCall("/agents", {
1928
- method: "POST",
1929
- body: JSON.stringify({
1930
- name: slugName,
1931
- display_name: trimmedName,
1932
- owner_label: owner.label,
1933
- }),
1934
- });
1935
2190
  const ideLabel = detectAgentIdeLabel();
1936
2191
  const ownerAttribution = formatOwnerAttribution(owner.label);
1937
2192
  const actorLabel = buildAgentActorLabel({
@@ -1939,6 +2194,22 @@ server.tool("set_agent_name", "Set or change the agent's display name. The agent
1939
2194
  owner_label: owner.label,
1940
2195
  ide_label: ideLabel,
1941
2196
  });
2197
+ // When conversation_id is provided, skip server-side /agents registration
2198
+ // to avoid creating stranded durable agent records from ephemeral renames.
2199
+ let canonicalKey = owner.login ? `${owner.login}/${slugName}` : null;
2200
+ if (!conversation_id) {
2201
+ const registered = await apiCall("/agents", {
2202
+ method: "POST",
2203
+ body: JSON.stringify({
2204
+ name: slugName,
2205
+ display_name: trimmedName,
2206
+ owner_label: owner.label,
2207
+ }),
2208
+ });
2209
+ if (typeof registered.canonical_key === "string") {
2210
+ canonicalKey = registered.canonical_key;
2211
+ }
2212
+ }
1942
2213
  const updatedIdentity = {
1943
2214
  name: slugName,
1944
2215
  display_name: trimmedName,
@@ -1946,14 +2217,19 @@ server.tool("set_agent_name", "Set or change the agent's display name. The agent
1946
2217
  owner_attribution: ownerAttribution,
1947
2218
  ide_label: ideLabel,
1948
2219
  actor_label: actorLabel,
1949
- canonical_key: typeof registered.canonical_key === "string"
1950
- ? registered.canonical_key
1951
- : owner.login ? `${owner.login}/${slugName}` : null,
2220
+ canonical_key: canonicalKey,
1952
2221
  runtime_key: currentAgentIdentityKey,
1953
- source: "api",
2222
+ source: conversation_id ? "local" : "api",
1954
2223
  resolved_at: new Date().toISOString(),
1955
2224
  };
1956
- currentAgentIdentity = setStoredAgentIdentity(updatedIdentity, currentAgentIdentityKey);
2225
+ if (conversation_id) {
2226
+ // Scope to this conversation only
2227
+ setConversationIdentity(conversation_id, updatedIdentity);
2228
+ }
2229
+ else {
2230
+ // Global rename (backward compatible)
2231
+ currentAgentIdentity = setStoredAgentIdentity(updatedIdentity, currentAgentIdentityKey);
2232
+ }
1957
2233
  return {
1958
2234
  content: [
1959
2235
  {
@@ -1961,7 +2237,9 @@ server.tool("set_agent_name", "Set or change the agent's display name. The agent
1961
2237
  text: JSON.stringify({
1962
2238
  success: true,
1963
2239
  message: `Agent name changed to "${trimmedName}".`,
1964
- agent_identity: toPublicAgentIdentity(currentAgentIdentity),
2240
+ agent_identity: toPublicAgentIdentity(conversation_id
2241
+ ? getConversationIdentity(conversation_id)
2242
+ : currentAgentIdentity),
1965
2243
  }, null, 2),
1966
2244
  },
1967
2245
  ],
@@ -2006,14 +2284,14 @@ server.tool("resume_room_session", "Rejoin the last locally saved room context,
2006
2284
  content: [
2007
2285
  {
2008
2286
  type: "text",
2009
- text: JSON.stringify({
2287
+ text: JSON.stringify(withJoinRoomAgentPrompt({
2010
2288
  success: true,
2011
2289
  rejoined_from_local_state: true,
2012
2290
  server_session_resumed: false,
2013
2291
  last_message_id_before_restart: savedRoom.last_message_id ?? null,
2014
2292
  room: toPublicRoomState(joined.room),
2015
2293
  agent_identity: toPublicAgentIdentity(agentIdentity),
2016
- }, null, 2),
2294
+ }), null, 2),
2017
2295
  },
2018
2296
  ],
2019
2297
  };