letagents 0.12.12 → 0.12.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/mcp/git-remote.js +7 -7
  2. package/dist/mcp/local-state/agent-sessions.js +63 -5
  3. package/dist/mcp/local-state/local-chat.js +189 -48
  4. package/dist/mcp/local-state/storage.js +16 -9
  5. package/dist/mcp/server/daemon-tool-executor.js +92 -0
  6. package/dist/mcp/server/register-tools.js +4 -2
  7. package/dist/mcp/server/runtime/agent-sessions.js +14 -5
  8. package/dist/mcp/server/runtime/api.js +11 -3
  9. package/dist/mcp/server/runtime/daemon-tool-context.js +11 -0
  10. package/dist/mcp/server/runtime/messages.js +55 -0
  11. package/dist/mcp/server/runtime/room-state.js +13 -3
  12. package/dist/mcp/server/runtime/rooms.js +51 -30
  13. package/dist/mcp/server/runtime/supervisor-bridge.js +49 -8
  14. package/dist/mcp/server/runtime/worker-bearer.js +5 -1
  15. package/dist/mcp/server/runtime.js +3 -3
  16. package/dist/mcp/server/supervised-tool-facade.js +34 -5
  17. package/dist/mcp/server/tools/messages/read-tool.js +54 -97
  18. package/dist/mcp/server/tools/messages/reasoning-tool.js +2 -0
  19. package/dist/mcp/server/tools/messages/send-tool.js +2 -0
  20. package/dist/mcp/server/tools/messages/status-tool.js +2 -0
  21. package/dist/mcp/server/tools/messages/wait-tool.js +290 -68
  22. package/dist/mcp/server/tools/onboarding/status-tool.js +4 -4
  23. package/dist/mcp/server/tools/rooms/inspection-tools.js +15 -10
  24. package/dist/mcp/server/tools/rooms/repo-initialization-tool.js +2 -1
  25. package/dist/mcp/server/tools/tasks/board-tools.js +34 -2
  26. package/dist/mcp/sse-client.js +163 -20
  27. package/dist/shared/activation-routing.js +146 -23
  28. package/dist/shared/agent-presence.js +6 -0
  29. package/dist/shared/desktop-release-manifest.js +63 -0
  30. package/dist/shared/desktop-release.js +60 -0
  31. package/dist/shared/scoped-ids.js +6 -0
  32. package/package.json +6 -2
  33. package/shared/message-contracts.d.mts +32 -0
  34. package/shared/message-contracts.mjs +109 -0
  35. package/shared/routing-aliases.d.mts +18 -0
  36. package/shared/routing-aliases.mjs +66 -0
  37. package/shared/sqlite-thread-routing.d.mts +72 -0
  38. package/shared/sqlite-thread-routing.mjs +1038 -0
@@ -1,30 +1,15 @@
1
1
  import { z } from "zod";
2
2
  import { encodeRoomIdPath } from "../../../room-id.js";
3
- import { appendIncludePromptOnly, currentRoom, ensureAgentIdentity, getFallbackProjectId, getLatestLocalChatMessages, getLocalChatMessages, getTargetRoomId, heartbeatRoomPresence, touchRoomSession, isLocalRoomStorageEnabled, resolveLocalRoomStorageIdentifiers, roomScopedApiCall, toAgentReadableMessages, } from "../../runtime.js";
3
+ import { AGENT_MESSAGE_BODY_MAX_BYTES, appendIncludePromptOnly, boundAgentMessageOutput, buildAgentDeliveryHeaders, currentRoom, ensureAgentIdentity, getFallbackProjectId, getLatestLocalChatMessages, getCurrentAgentSessionSnapshot, getTargetRoomId, heartbeatRoomPresence, touchRoomSession, isLocalRoomStorageEnabled, resolveLocalRoomStorageIdentifiers, roomScopedApiCall, toAgentReadableMessages, } from "../../runtime.js";
4
+ import { requireValidWorkerBearerRuntime } from "../../runtime/worker-bearer.js";
4
5
  import { jsonToolResponse } from "./response.js";
5
6
  export const DEFAULT_READ_MESSAGES_LIMIT = 100;
6
7
  // Both the API and the local store clamp a single page to 500 messages.
7
- const MAX_MESSAGES_PER_PAGE = 500;
8
- export function selectRecentMessages(messages, limit) {
9
- const total = messages.length;
10
- const selected = limit > 0 && total > limit ? messages.slice(total - limit) : messages;
11
- return {
12
- messages: selected,
13
- total_message_count: total,
14
- omitted_message_count: total - selected.length,
15
- };
16
- }
17
- function withRecencyTelemetry(output, selection) {
18
- output.total_message_count = selection.total_message_count;
19
- if (selection.omitted_message_count > 0) {
20
- output.omitted_message_count = selection.omitted_message_count;
21
- output.truncated = true;
22
- }
23
- return output;
24
- }
8
+ export const MAX_MESSAGES_PER_PAGE = 500;
25
9
  // Fetch the most recent messages by paging BACKWARDS from the tail
26
10
  // (before=latest), so a limited read never walks the full room history.
27
11
  export async function fetchRecentRemoteMessages(input) {
12
+ const boundedLimit = Math.max(1, Math.min(MAX_MESSAGES_PER_PAGE, Math.floor(input.limit)));
28
13
  const collected = [];
29
14
  let beforeCursor = "latest";
30
15
  let truncated = false;
@@ -33,7 +18,7 @@ export async function fetchRecentRemoteMessages(input) {
33
18
  for (;;) {
34
19
  const params = new URLSearchParams();
35
20
  params.set("before", beforeCursor);
36
- params.set("limit", String(Math.min(input.limit - collected.length, MAX_MESSAGES_PER_PAGE)));
21
+ params.set("limit", String(Math.min(boundedLimit - collected.length, MAX_MESSAGES_PER_PAGE)));
37
22
  const qs = params.toString();
38
23
  const result = await roomScopedApiCall({
39
24
  room_id: input.targetRoomId,
@@ -44,6 +29,7 @@ export async function fetchRecentRemoteMessages(input) {
44
29
  // page touch the session would walk last_message_id backwards and make
45
30
  // resume_room_session replay already-read history.
46
31
  preserve_session_cursor: true,
32
+ options: { headers: input.deliveryHeaders },
47
33
  });
48
34
  roomIdFromResponse = roomIdFromResponse || result.room_id || result.project_id;
49
35
  const msgs = result.messages ?? [];
@@ -56,7 +42,7 @@ export async function fetchRecentRemoteMessages(input) {
56
42
  const hasOlder = Boolean(result.has_older ?? result.has_more);
57
43
  if (!hasOlder || msgs.length === 0)
58
44
  break;
59
- if (collected.length >= input.limit) {
45
+ if (collected.length >= boundedLimit) {
60
46
  truncated = true;
61
47
  break;
62
48
  }
@@ -73,103 +59,74 @@ export async function fetchRecentRemoteMessages(input) {
73
59
  return { messages: collected, truncated, roomIdFromResponse };
74
60
  }
75
61
  export function registerReadMessagesTool(server) {
76
- server.tool("read_messages", "Read recent messages from a Let Agents Chat room (most recent `limit`, default 100; pass limit: 0 for the full history — expensive in busy rooms). Threaded replies include thread_parent_id/thread.root_message_id; use send_thread_message with that id to continue focused side discussion without polluting the main room. For long-running work, prefer wait_for_messages with after_message_id so you only process new lines and do not treat an empty poll as the end of the mission.", {
62
+ server.tool("read_messages", "Read a bounded recent page from a Let Agents Chat room (most recent `limit`, default 100, maximum 500). Threaded replies include thread_parent_id/thread.root_message_id; use send_thread_message with that id to continue focused side discussion without polluting the main room. For long-running work, prefer wait_for_messages with after_message_id so you only process new lines and do not treat an empty poll as the end of the mission.", {
77
63
  room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room."),
78
64
  limit: z
79
65
  .number()
80
66
  .int()
81
67
  .min(0)
82
68
  .optional()
83
- .describe(`Return only the most recent N messages (default ${DEFAULT_READ_MESSAGES_LIMIT}). Pass 0 for the full history. When older messages are omitted, the response carries truncated=true.`),
69
+ .describe(`Return the most recent N messages (default ${DEFAULT_READ_MESSAGES_LIMIT}, maximum ${MAX_MESSAGES_PER_PAGE}). Zero is retained for compatibility and means the bounded maximum. When older messages are omitted, the response carries truncated=true.`),
84
70
  }, async ({ room_id, limit }) => {
85
- const effectiveLimit = limit ?? DEFAULT_READ_MESSAGES_LIMIT;
71
+ const requestedLimit = limit ?? DEFAULT_READ_MESSAGES_LIMIT;
72
+ const effectiveLimit = requestedLimit > 0
73
+ ? Math.min(requestedLimit, MAX_MESSAGES_PER_PAGE)
74
+ : MAX_MESSAGES_PER_PAGE;
86
75
  const targetRoomId = getTargetRoomId(room_id);
87
76
  const targetProjectId = getFallbackProjectId();
88
77
  const localRoomId = targetRoomId ?? currentRoom?.room_id ?? targetProjectId;
78
+ const sessionRoomId = targetRoomId ?? currentRoom?.room_id ?? null;
79
+ const workerRuntime = requireValidWorkerBearerRuntime();
80
+ const agentSessionSnapshot = workerRuntime.mode === "owner"
81
+ ? getCurrentAgentSessionSnapshot(sessionRoomId)
82
+ : { complete: true, session: null };
83
+ const agentSession = agentSessionSnapshot.session;
84
+ const deliveryHeaders = buildAgentDeliveryHeaders(agentSession);
85
+ if (!agentSessionSnapshot.complete) {
86
+ throw new Error("Local agent routing state is unavailable; retry after restoring the state file.");
87
+ }
89
88
  if (localRoomId && await isLocalRoomStorageEnabled(localRoomId)) {
90
- const { localRoomId: sqliteRoomId } = await resolveLocalRoomStorageIdentifiers(localRoomId);
89
+ const { localRoomId: sqliteRoomId, cloudRoomId, } = await resolveLocalRoomStorageIdentifiers(localRoomId);
91
90
  const effectiveLocalRoomId = sqliteRoomId || localRoomId;
92
- if (effectiveLimit > 0 && effectiveLimit <= MAX_MESSAGES_PER_PAGE) {
93
- const result = await getLatestLocalChatMessages(effectiveLocalRoomId, {
94
- limit: effectiveLimit,
95
- include_prompt_only: true,
96
- });
97
- const output = {
98
- room_id: effectiveLocalRoomId,
99
- messages: toAgentReadableMessages(result.messages ?? []),
100
- };
101
- if (result.has_more)
102
- output.truncated = true;
103
- return jsonToolResponse(output);
104
- }
105
- const allMessages = [];
106
- let afterCursor;
107
- for (;;) {
108
- const result = await getLocalChatMessages(effectiveLocalRoomId, {
109
- after: afterCursor,
110
- include_prompt_only: true,
111
- });
112
- const msgs = result.messages ?? [];
113
- allMessages.push(...msgs);
114
- if (!result.has_more || msgs.length === 0)
115
- break;
116
- const lastMsg = msgs[msgs.length - 1];
117
- if (!lastMsg?.id)
118
- break;
119
- afterCursor = lastMsg.id;
120
- }
121
- const selection = selectRecentMessages(allMessages, effectiveLimit);
122
- return jsonToolResponse(withRecencyTelemetry({
123
- room_id: effectiveLocalRoomId,
124
- messages: toAgentReadableMessages(selection.messages),
125
- }, selection));
126
- }
127
- if (effectiveLimit > 0) {
128
- const recent = await fetchRecentRemoteMessages({
129
- targetRoomId,
130
- targetProjectId,
91
+ const result = await getLatestLocalChatMessages(effectiveLocalRoomId, {
131
92
  limit: effectiveLimit,
93
+ include_prompt_only: true,
94
+ });
95
+ const { attachLocalActivationMetadata } = await import("./wait-tool.js");
96
+ const messages = await attachLocalActivationMetadata(effectiveLocalRoomId, result.messages ?? [], agentSession, {
97
+ includeTaskOwnerLeases: false,
98
+ activeSessionRoomId: cloudRoomId || sessionRoomId,
132
99
  });
133
- await heartbeatRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, await ensureAgentIdentity());
100
+ const bounded = boundAgentMessageOutput(toAgentReadableMessages(messages), { direction: "suffix", maxBytes: AGENT_MESSAGE_BODY_MAX_BYTES });
134
101
  const output = {
135
- messages: toAgentReadableMessages(recent.messages),
102
+ room_id: effectiveLocalRoomId,
103
+ messages: bounded.messages,
136
104
  };
137
- if (recent.truncated)
105
+ if (result.has_more || bounded.truncated)
138
106
  output.truncated = true;
139
- if (recent.roomIdFromResponse) {
140
- output[targetRoomId ? "room_id" : "project_id"] = recent.roomIdFromResponse;
107
+ if (bounded.omittedMessageCount > 0) {
108
+ output.omitted_message_count = bounded.omittedMessageCount;
141
109
  }
142
110
  return jsonToolResponse(output);
143
111
  }
144
- const allMessages = [];
145
- let afterCursor;
146
- let roomIdFromResponse;
147
- for (;;) {
148
- const query = new URLSearchParams();
149
- if (afterCursor)
150
- query.set("after", afterCursor);
151
- const qs = query.toString();
152
- const result = await roomScopedApiCall({
153
- room_id: targetRoomId,
154
- project_id: targetProjectId,
155
- room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}`),
156
- project_path: (targetProjectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(targetProjectId)}/messages${qs ? `?${qs}` : ""}`),
157
- });
158
- roomIdFromResponse = roomIdFromResponse || result.room_id || result.project_id;
159
- const msgs = result.messages ?? [];
160
- allMessages.push(...msgs);
161
- if (!result.has_more || msgs.length === 0)
162
- break;
163
- const lastMsg = msgs[msgs.length - 1];
164
- if (!lastMsg?.id)
165
- break;
166
- afterCursor = lastMsg.id;
167
- }
112
+ const recent = await fetchRecentRemoteMessages({
113
+ targetRoomId,
114
+ targetProjectId,
115
+ limit: effectiveLimit,
116
+ deliveryHeaders,
117
+ });
168
118
  await heartbeatRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, await ensureAgentIdentity());
169
- const selection = selectRecentMessages(allMessages, effectiveLimit);
170
- const output = withRecencyTelemetry({ messages: toAgentReadableMessages(selection.messages) }, selection);
171
- if (roomIdFromResponse) {
172
- output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
119
+ const bounded = boundAgentMessageOutput(toAgentReadableMessages(recent.messages), { direction: "suffix", maxBytes: AGENT_MESSAGE_BODY_MAX_BYTES });
120
+ const output = {
121
+ messages: bounded.messages,
122
+ };
123
+ if (recent.truncated || bounded.truncated)
124
+ output.truncated = true;
125
+ if (bounded.omittedMessageCount > 0) {
126
+ output.omitted_message_count = bounded.omittedMessageCount;
127
+ }
128
+ if (recent.roomIdFromResponse) {
129
+ output[targetRoomId ? "room_id" : "project_id"] = recent.roomIdFromResponse;
173
130
  }
174
131
  return jsonToolResponse(output);
175
132
  });
@@ -61,6 +61,8 @@ export function registerPostReasoningTool(server) {
61
61
  sender: identity.actor_label,
62
62
  text: normalizedMilestone,
63
63
  source: "agent",
64
+ publisher_agent_key: agentSession?.agent_key ?? null,
65
+ publisher_agent_session_id: agentSession?.session_id ?? null,
64
66
  });
65
67
  milestoneMessageId = milestoneMessage.id;
66
68
  touchCurrentRoom(milestoneMessageId);
@@ -87,6 +87,8 @@ async function sendMessageFromTool(input) {
87
87
  reply_to: replyTarget,
88
88
  ...(resolvedThreadRoot ? { thread_root_id: resolvedThreadRoot } : {}),
89
89
  source: "agent",
90
+ publisher_agent_key: agentSession?.agent_key ?? null,
91
+ publisher_agent_session_id: agentSession?.session_id ?? null,
90
92
  });
91
93
  touchCurrentRoom(message.id);
92
94
  return jsonToolResponse({
@@ -45,6 +45,8 @@ export function registerPostStatusTool(server) {
45
45
  sender,
46
46
  text: statusText,
47
47
  source: "agent",
48
+ publisher_agent_key: agentSession?.agent_key ?? null,
49
+ publisher_agent_session_id: agentSession?.session_id ?? null,
48
50
  });
49
51
  touchCurrentRoom(message.id);
50
52
  return jsonToolResponse({