letagents 0.11.0 → 0.12.0

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.
@@ -135,7 +135,7 @@ export function normalizeAgentBaseName(input) {
135
135
  // ---------------------------------------------------------------------------
136
136
  export function hashStringToIndex(value, modulo) {
137
137
  const digest = createHash("sha256").update(value).digest();
138
- return digest.readUInt16BE(0) % modulo;
138
+ return digest.readUInt32BE(0) % modulo;
139
139
  }
140
140
  export function codenameFromIndex(index) {
141
141
  const normalizedIndex = ((index % AGENT_CODENAME_SPACE) + AGENT_CODENAME_SPACE) % AGENT_CODENAME_SPACE;
@@ -157,6 +157,12 @@ export function getStoredAgentIdentity(identityKey) {
157
157
  if (scoped) {
158
158
  return scoped;
159
159
  }
160
+ // For UUID-based identity keys, do NOT fall back to the shared global
161
+ // agent_identity — that would cause different processes to inherit each
162
+ // other's identity. Only legacy (non-UUID) keys use the global fallback.
163
+ if (identityKey.startsWith("instance:")) {
164
+ return null;
165
+ }
160
166
  }
161
167
  return state.agent_identity ?? null;
162
168
  }
@@ -18,6 +18,30 @@ let currentRoom = null;
18
18
  let currentAgentIdentityKey = "";
19
19
  let currentAgentIdentity = null;
20
20
  let currentAuthenticatedAccount = undefined;
21
+ // ---------------------------------------------------------------------------
22
+ // Conversation-scoped identity (Option C: per-conversation hints)
23
+ // ---------------------------------------------------------------------------
24
+ const MAX_CONVERSATION_IDENTITIES = 20;
25
+ const conversationIdentities = new Map();
26
+ /**
27
+ * Get or set a conversation-scoped identity override.
28
+ * Falls back to the global `currentAgentIdentity` when conversationId is absent.
29
+ */
30
+ function getConversationIdentity(conversationId) {
31
+ if (!conversationId)
32
+ return currentAgentIdentity;
33
+ return conversationIdentities.get(conversationId) ?? currentAgentIdentity;
34
+ }
35
+ function setConversationIdentity(conversationId, identity) {
36
+ // LRU-style eviction: if at cap, remove oldest entry
37
+ if (!conversationIdentities.has(conversationId) &&
38
+ conversationIdentities.size >= MAX_CONVERSATION_IDENTITIES) {
39
+ const oldestKey = conversationIdentities.keys().next().value;
40
+ if (oldestKey !== undefined)
41
+ conversationIdentities.delete(oldestKey);
42
+ }
43
+ conversationIdentities.set(conversationId, identity);
44
+ }
21
45
  let currentAuthenticatedAccountSource = null;
22
46
  let currentAuthenticatedEnvToken = null;
23
47
  // ---------------------------------------------------------------------------
@@ -996,7 +1020,12 @@ server.tool("join_room", "Join a named room on Let Agents Chat. Creates the room
996
1020
  }
997
1021
  });
998
1022
  // -- get_current_room -------------------------------------------------------
999
- server.tool("get_current_room", "Get information about the currently joined room, including how it was joined.", {}, async () => {
1023
+ server.tool("get_current_room", "Get information about the currently joined room, including how it was joined.", {
1024
+ conversation_id: z
1025
+ .string()
1026
+ .optional()
1027
+ .describe("Optional conversation ID to report the conversation-scoped identity instead of the global one."),
1028
+ }, async ({ conversation_id }) => {
1000
1029
  return {
1001
1030
  content: [
1002
1031
  {
@@ -1005,7 +1034,9 @@ server.tool("get_current_room", "Get information about the currently joined room
1005
1034
  ? {
1006
1035
  connected: true,
1007
1036
  ...toPublicRoomState(currentRoom),
1008
- agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(currentAgentIdentityKey)),
1037
+ agent_identity: toPublicAgentIdentity(getConversationIdentity(conversation_id)
1038
+ ?? currentAgentIdentity
1039
+ ?? getStoredAgentIdentity(currentAgentIdentityKey)),
1009
1040
  auth: getStoredAuth()
1010
1041
  ? {
1011
1042
  source: process.env.LETAGENTS_TOKEN ? "env" : "local_state",
@@ -1078,7 +1109,11 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
1078
1109
  .string()
1079
1110
  .optional()
1080
1111
  .describe("Canonical room ID. Defaults to the current room."),
1081
- }, async ({ sender: _sender, status, room_id }) => {
1112
+ conversation_id: z
1113
+ .string()
1114
+ .optional()
1115
+ .describe("Optional conversation ID for per-conversation identity scoping."),
1116
+ }, async ({ sender: _sender, status, room_id, conversation_id }) => {
1082
1117
  const targetRoomId = getTargetRoomId(room_id);
1083
1118
  const targetProjectId = getFallbackProjectId();
1084
1119
  if (!targetRoomId && !targetProjectId) {
@@ -1097,7 +1132,7 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
1097
1132
  }
1098
1133
  // Status messages use a reserved prefix so the UI (and agents) can distinguish
1099
1134
  // them from normal chat messages without changing the data model.
1100
- const identity = await ensureAgentIdentity();
1135
+ const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
1101
1136
  const sender = identity.actor_label;
1102
1137
  const statusText = `[status] ${status}`;
1103
1138
  const message = await roomScopedApiCall({
@@ -1144,7 +1179,8 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
1144
1179
  .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
1145
1180
  source_message_id: z.string().optional().describe("Optional message ID where task was agreed, e.g. 'msg_42'"),
1146
1181
  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 }) => {
1182
+ conversation_id: z.string().optional().describe("Optional conversation ID for per-conversation identity scoping."),
1183
+ }, async ({ title, description, created_by: _createdBy, source_message_id, room_id, conversation_id }) => {
1148
1184
  const targetRoomId = getTargetRoomId(room_id);
1149
1185
  const targetProjectId = getFallbackProjectId();
1150
1186
  if (!targetRoomId && !targetProjectId) {
@@ -1152,7 +1188,7 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
1152
1188
  content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room. Join one first." }) }],
1153
1189
  };
1154
1190
  }
1155
- const identity = await ensureAgentIdentity();
1191
+ const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
1156
1192
  const task = await roomScopedApiCall({
1157
1193
  room_id: targetRoomId,
1158
1194
  project_id: targetProjectId,
@@ -1232,7 +1268,8 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
1232
1268
  .optional()
1233
1269
  .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
1234
1270
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
1235
- }, async ({ task_id, assignee: _assignee, room_id }) => {
1271
+ conversation_id: z.string().optional().describe("Optional conversation ID for per-conversation identity scoping."),
1272
+ }, async ({ task_id, assignee: _assignee, room_id, conversation_id }) => {
1236
1273
  const targetRoomId = getTargetRoomId(room_id);
1237
1274
  const targetProjectId = getFallbackProjectId();
1238
1275
  if (!targetRoomId && !targetProjectId) {
@@ -1241,7 +1278,7 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
1241
1278
  };
1242
1279
  }
1243
1280
  try {
1244
- const identity = await ensureAgentIdentity();
1281
+ const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
1245
1282
  const updated = await roomScopedApiCall({
1246
1283
  room_id: targetRoomId,
1247
1284
  project_id: targetProjectId,
@@ -1278,7 +1315,8 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
1278
1315
  .describe("New assignee for the task. Defaults to the current agent when status=assigned."),
1279
1316
  pr_url: z.string().optional().describe("PR URL to link to the task"),
1280
1317
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
1281
- }, async ({ task_id, status, assignee, pr_url, room_id }) => {
1318
+ conversation_id: z.string().optional().describe("Optional conversation ID for per-conversation identity scoping."),
1319
+ }, async ({ task_id, status, assignee, pr_url, room_id, conversation_id }) => {
1282
1320
  const targetRoomId = getTargetRoomId(room_id);
1283
1321
  const targetProjectId = getFallbackProjectId();
1284
1322
  if (!targetRoomId && !targetProjectId) {
@@ -1288,7 +1326,7 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
1288
1326
  }
1289
1327
  try {
1290
1328
  const identity = status === "assigned" && !assignee
1291
- ? await ensureAgentIdentity()
1329
+ ? (getConversationIdentity(conversation_id) ?? await ensureAgentIdentity())
1292
1330
  : null;
1293
1331
  const updated = await roomScopedApiCall({
1294
1332
  room_id: targetRoomId,
@@ -1329,7 +1367,8 @@ server.tool("complete_task", "Submit a task for review. Moves the task to 'in_re
1329
1367
  task_id: z.string().describe("The task ID to submit for review"),
1330
1368
  pr_url: z.string().optional().describe("GitHub PR URL for the work"),
1331
1369
  room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
1332
- }, async ({ task_id, pr_url, room_id }) => {
1370
+ conversation_id: z.string().optional().describe("Optional conversation ID for per-conversation identity scoping."),
1371
+ }, async ({ task_id, pr_url, room_id, conversation_id: _conversationId }) => {
1333
1372
  const targetRoomId = getTargetRoomId(room_id);
1334
1373
  const targetProjectId = getFallbackProjectId();
1335
1374
  if (!targetRoomId && !targetProjectId) {
@@ -1510,13 +1549,17 @@ server.tool("send_message", "Send a message to a Let Agents Chat room.", {
1510
1549
  .optional()
1511
1550
  .describe("Deprecated override. Agent identity is resolved automatically on room entry."),
1512
1551
  text: z.string().describe("The message text to send"),
1513
- }, async ({ room_id, sender: _sender, text }) => {
1552
+ conversation_id: z
1553
+ .string()
1554
+ .optional()
1555
+ .describe("Optional conversation ID for per-conversation identity scoping."),
1556
+ }, async ({ room_id, sender: _sender, text, conversation_id }) => {
1514
1557
  const targetRoomId = getTargetRoomId(room_id);
1515
1558
  const targetProjectId = getFallbackProjectId();
1516
1559
  if (!targetRoomId && !targetProjectId) {
1517
1560
  throw new Error("No room is currently selected. Join a room first or pass room_id.");
1518
1561
  }
1519
- const identity = await ensureAgentIdentity();
1562
+ const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
1520
1563
  const message = await roomScopedApiCall({
1521
1564
  room_id: targetRoomId,
1522
1565
  project_id: targetProjectId,
@@ -1898,7 +1941,11 @@ server.tool("set_agent_name", "Set or change the agent's display name. The agent
1898
1941
  .min(2)
1899
1942
  .max(64)
1900
1943
  .describe("The desired display name for this agent (2-64 characters)."),
1901
- }, async ({ name: desiredName }) => {
1944
+ conversation_id: z
1945
+ .string()
1946
+ .optional()
1947
+ .describe("Optional conversation ID to scope this name change. When provided, only this conversation uses the new name; other conversations keep their own identity."),
1948
+ }, async ({ name: desiredName, conversation_id }) => {
1902
1949
  const trimmedName = desiredName.trim();
1903
1950
  if (trimmedName.length < 2 || trimmedName.length > 64) {
1904
1951
  return {
@@ -1924,14 +1971,6 @@ server.tool("set_agent_name", "Set or change the agent's display name. The agent
1924
1971
  const slugName = normalizeAgentBaseName(trimmedName);
1925
1972
  try {
1926
1973
  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
1974
  const ideLabel = detectAgentIdeLabel();
1936
1975
  const ownerAttribution = formatOwnerAttribution(owner.label);
1937
1976
  const actorLabel = buildAgentActorLabel({
@@ -1939,6 +1978,22 @@ server.tool("set_agent_name", "Set or change the agent's display name. The agent
1939
1978
  owner_label: owner.label,
1940
1979
  ide_label: ideLabel,
1941
1980
  });
1981
+ // When conversation_id is provided, skip server-side /agents registration
1982
+ // to avoid creating stranded durable agent records from ephemeral renames.
1983
+ let canonicalKey = owner.login ? `${owner.login}/${slugName}` : null;
1984
+ if (!conversation_id) {
1985
+ const registered = await apiCall("/agents", {
1986
+ method: "POST",
1987
+ body: JSON.stringify({
1988
+ name: slugName,
1989
+ display_name: trimmedName,
1990
+ owner_label: owner.label,
1991
+ }),
1992
+ });
1993
+ if (typeof registered.canonical_key === "string") {
1994
+ canonicalKey = registered.canonical_key;
1995
+ }
1996
+ }
1942
1997
  const updatedIdentity = {
1943
1998
  name: slugName,
1944
1999
  display_name: trimmedName,
@@ -1946,14 +2001,19 @@ server.tool("set_agent_name", "Set or change the agent's display name. The agent
1946
2001
  owner_attribution: ownerAttribution,
1947
2002
  ide_label: ideLabel,
1948
2003
  actor_label: actorLabel,
1949
- canonical_key: typeof registered.canonical_key === "string"
1950
- ? registered.canonical_key
1951
- : owner.login ? `${owner.login}/${slugName}` : null,
2004
+ canonical_key: canonicalKey,
1952
2005
  runtime_key: currentAgentIdentityKey,
1953
- source: "api",
2006
+ source: conversation_id ? "local" : "api",
1954
2007
  resolved_at: new Date().toISOString(),
1955
2008
  };
1956
- currentAgentIdentity = setStoredAgentIdentity(updatedIdentity, currentAgentIdentityKey);
2009
+ if (conversation_id) {
2010
+ // Scope to this conversation only
2011
+ setConversationIdentity(conversation_id, updatedIdentity);
2012
+ }
2013
+ else {
2014
+ // Global rename (backward compatible)
2015
+ currentAgentIdentity = setStoredAgentIdentity(updatedIdentity, currentAgentIdentityKey);
2016
+ }
1957
2017
  return {
1958
2018
  content: [
1959
2019
  {
@@ -1961,7 +2021,9 @@ server.tool("set_agent_name", "Set or change the agent's display name. The agent
1961
2021
  text: JSON.stringify({
1962
2022
  success: true,
1963
2023
  message: `Agent name changed to "${trimmedName}".`,
1964
- agent_identity: toPublicAgentIdentity(currentAgentIdentity),
2024
+ agent_identity: toPublicAgentIdentity(conversation_id
2025
+ ? getConversationIdentity(conversation_id)
2026
+ : currentAgentIdentity),
1965
2027
  }, null, 2),
1966
2028
  },
1967
2029
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letagents",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Let Agents Chat — MCP server for AI agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/mcp/server.js",