letagents 0.12.9 → 0.12.11

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 (124) hide show
  1. package/README.md +18 -15
  2. package/dist/api/board-intent-payloads.js +25 -0
  3. package/dist/mcp/agent-prompt-delivery.js +20 -0
  4. package/dist/mcp/codex-session/app-server.js +89 -0
  5. package/dist/mcp/codex-session/index.js +10 -0
  6. package/dist/mcp/codex-session/rpc-client.js +85 -0
  7. package/dist/mcp/codex-session/runtime-bridge.js +271 -0
  8. package/dist/mcp/codex-session/runtime-summary.js +220 -0
  9. package/dist/mcp/codex-session/session-inspection.js +100 -0
  10. package/dist/mcp/codex-session/session-mapper.js +51 -0
  11. package/dist/mcp/codex-session/session-start.js +182 -0
  12. package/dist/mcp/codex-session/session-status.js +77 -0
  13. package/dist/mcp/codex-session/session-stop.js +43 -0
  14. package/dist/mcp/codex-session/session-supervisor.js +111 -0
  15. package/dist/mcp/codex-session/start-prompt.js +49 -0
  16. package/dist/mcp/codex-session/types.js +1 -0
  17. package/dist/mcp/codex-session.js +1 -672
  18. package/dist/mcp/config-reader.js +10 -2
  19. package/dist/mcp/git-remote.js +90 -14
  20. package/dist/mcp/local-state/agent-identity.js +26 -0
  21. package/dist/mcp/local-state/agent-sessions.js +66 -0
  22. package/dist/mcp/local-state/auth.js +57 -0
  23. package/dist/mcp/local-state/codex-live-sessions.js +59 -0
  24. package/dist/mcp/local-state/index.js +8 -0
  25. package/dist/mcp/local-state/local-chat.js +808 -0
  26. package/dist/mcp/local-state/room-sessions.js +51 -0
  27. package/dist/mcp/local-state/storage.js +95 -0
  28. package/dist/mcp/local-state/types.js +1 -0
  29. package/dist/mcp/local-state.js +1 -351
  30. package/dist/mcp/managed-agent-providers.js +65 -0
  31. package/dist/mcp/rental-tools/activity.js +37 -0
  32. package/dist/mcp/rental-tools/budget.js +36 -0
  33. package/dist/mcp/rental-tools/context.js +92 -0
  34. package/dist/mcp/rental-tools/lifecycle.js +47 -0
  35. package/dist/mcp/rental-tools/metering.js +73 -0
  36. package/dist/mcp/rental-tools/patches.js +100 -0
  37. package/dist/mcp/rental-tools/provider.js +117 -0
  38. package/dist/mcp/rental-tools/shared.js +19 -0
  39. package/dist/mcp/rental-tools/types.js +1 -0
  40. package/dist/mcp/rental-tools.js +7 -0
  41. package/dist/mcp/repo-visibility.js +8 -6
  42. package/dist/mcp/server/register-tools.js +19 -0
  43. package/dist/mcp/server/repo-context.js +28 -0
  44. package/dist/mcp/server/resources.js +77 -0
  45. package/dist/mcp/server/runtime/agent-sessions.js +173 -0
  46. package/dist/mcp/server/runtime/api.js +104 -0
  47. package/dist/mcp/server/runtime/auth-cache.js +20 -0
  48. package/dist/mcp/server/runtime/device-auth.js +48 -0
  49. package/dist/mcp/server/runtime/identity/config.js +58 -0
  50. package/dist/mcp/server/runtime/identity/directory.js +94 -0
  51. package/dist/mcp/server/runtime/identity/liveness.js +32 -0
  52. package/dist/mcp/server/runtime/identity/names.js +74 -0
  53. package/dist/mcp/server/runtime/identity/public.js +19 -0
  54. package/dist/mcp/server/runtime/identity/state.js +32 -0
  55. package/dist/mcp/server/runtime/identity.js +97 -0
  56. package/dist/mcp/server/runtime/messages.js +160 -0
  57. package/dist/mcp/server/runtime/presence.js +52 -0
  58. package/dist/mcp/server/runtime/room-api.js +42 -0
  59. package/dist/mcp/server/runtime/room-state.js +122 -0
  60. package/dist/mcp/server/runtime/rooms.js +374 -0
  61. package/dist/mcp/server/runtime/supervisor-bridge.js +94 -0
  62. package/dist/mcp/server/runtime/worker-bearer.js +67 -0
  63. package/dist/mcp/server/runtime.js +14 -0
  64. package/dist/mcp/server/tools/agent-sessions.js +415 -0
  65. package/dist/mcp/server/tools/messages/index.js +14 -0
  66. package/dist/mcp/server/tools/messages/message-lookup.js +75 -0
  67. package/dist/mcp/server/tools/messages/read-tool.js +176 -0
  68. package/dist/mcp/server/tools/messages/reasoning-tool.js +179 -0
  69. package/dist/mcp/server/tools/messages/response.js +17 -0
  70. package/dist/mcp/server/tools/messages/send-tool.js +160 -0
  71. package/dist/mcp/server/tools/messages/status-tool.js +87 -0
  72. package/dist/mcp/server/tools/messages/wait-tool.js +403 -0
  73. package/dist/mcp/server/tools/messages.js +1 -0
  74. package/dist/mcp/server/tools/onboarding/device-auth-tools.js +158 -0
  75. package/dist/mcp/server/tools/onboarding/name-tool.js +93 -0
  76. package/dist/mcp/server/tools/onboarding/responses.js +10 -0
  77. package/dist/mcp/server/tools/onboarding/status-tool.js +125 -0
  78. package/dist/mcp/server/tools/onboarding.js +8 -0
  79. package/dist/mcp/server/tools/rental/activity-tools.js +43 -0
  80. package/dist/mcp/server/tools/rental/context-tools.js +43 -0
  81. package/dist/mcp/server/tools/rental/index.js +17 -0
  82. package/dist/mcp/server/tools/rental/metering-tools.js +39 -0
  83. package/dist/mcp/server/tools/rental/patch-tools.js +53 -0
  84. package/dist/mcp/server/tools/rental/provider-tools.js +44 -0
  85. package/dist/mcp/server/tools/rental/response.js +10 -0
  86. package/dist/mcp/server/tools/rental/types.js +1 -0
  87. package/dist/mcp/server/tools/rental.js +1 -0
  88. package/dist/mcp/server/tools/rooms/index.js +5 -0
  89. package/dist/mcp/server/tools/rooms/inspection-tools.js +140 -0
  90. package/dist/mcp/server/tools/rooms/join-tools.js +37 -0
  91. package/dist/mcp/server/tools/rooms/repo-initialization-tool.js +113 -0
  92. package/dist/mcp/server/tools/rooms/repo-visibility-tool.js +20 -0
  93. package/dist/mcp/server/tools/rooms/response.js +10 -0
  94. package/dist/mcp/server/tools/rooms/resume-tool.js +48 -0
  95. package/dist/mcp/server/tools/rooms.js +1 -0
  96. package/dist/mcp/server/tools/tasks/api.js +222 -0
  97. package/dist/mcp/server/tools/tasks/artifact-tools.js +75 -0
  98. package/dist/mcp/server/tools/tasks/board-intent-tools.js +326 -0
  99. package/dist/mcp/server/tools/tasks/board-tools.js +73 -0
  100. package/dist/mcp/server/tools/tasks/context.js +35 -0
  101. package/dist/mcp/server/tools/tasks/event-tools.js +40 -0
  102. package/dist/mcp/server/tools/tasks/index.js +16 -0
  103. package/dist/mcp/server/tools/tasks/lease-tools.js +145 -0
  104. package/dist/mcp/server/tools/tasks/mutation-tools.js +121 -0
  105. package/dist/mcp/server/tools/tasks/response.js +13 -0
  106. package/dist/mcp/server/tools/tasks/schemas.js +68 -0
  107. package/dist/mcp/server/tools/tasks/verdict-tools.js +51 -0
  108. package/dist/mcp/server/tools/tasks.js +1 -0
  109. package/dist/mcp/server.js +12 -3157
  110. package/dist/mcp/sse-client.js +9 -17
  111. package/dist/shared/activation-routing.js +272 -0
  112. package/dist/shared/agent-identity.js +21 -5
  113. package/dist/shared/agent-presence.js +5 -3
  114. package/dist/shared/agent-session-bearer.js +28 -0
  115. package/dist/shared/board-manager-failover.js +16 -0
  116. package/dist/shared/rental/lrt.js +214 -0
  117. package/dist/shared/rental/meter-types.js +14 -0
  118. package/dist/shared/room-agent-prompts.js +14 -1
  119. package/package.json +31 -16
  120. package/dist/mcp/__tests__/config-reader.test.js +0 -93
  121. package/dist/mcp/__tests__/git-remote.test.js +0 -37
  122. package/dist/mcp/__tests__/server-helpers.test.js +0 -121
  123. package/dist/shared/agent-reasoning.js +0 -49
  124. /package/dist/{mcp → shared}/codenames.js +0 -0
@@ -0,0 +1,403 @@
1
+ import { z } from "zod";
2
+ import { getPollTimeoutCapMs } from "../../../../shared/poll-timeout-cap.js";
3
+ import { encodeRoomIdPath } from "../../../room-id.js";
4
+ import { appendIncludePromptOnly, buildAgentDeliveryHeaders, currentRoom, ensureAgentIdentity, getFallbackProjectId, getLatestLocalChatMessages, getLocalChatMessages, getLastMessageId, getRememberedRoomPresence, getTargetRoomId, identityFromAgentSession, isLocalRoomStorageEnabled, listLocalTasks, resolveLocalRoomStorageIdentifiers, resolveAgentSession, roomScopedApiCall, syncRoomPresence, toAgentReadableMessages, touchRoomSession, WORKER_BEARER_AGENT_SESSION_ID, waitForLocalChatMessages, } from "../../runtime.js";
5
+ import { requireValidWorkerBearerRuntime } from "../../runtime/worker-bearer.js";
6
+ import { attachAgentMessageActivations } from "../../../../shared/activation-routing.js";
7
+ import { findLocalMessageById, findRemoteMessageById } from "./message-lookup.js";
8
+ import { fetchRecentRemoteMessages } from "./read-tool.js";
9
+ import { jsonToolResponse } from "./response.js";
10
+ const DEFAULT_POLL_TIMEOUT_MS = 30000;
11
+ // When wait_for_messages is called WITHOUT an after_message_id cursor, it must
12
+ // catch up on only the most RECENT messages instead of replaying the entire
13
+ // room history (busy rooms archive millions of characters, which blows the
14
+ // tool's token budget). This bounds that no-cursor catch-up to a recent tail.
15
+ export const DEFAULT_WAIT_CATCHUP_LIMIT = 100;
16
+ // Decide how the initial fetch should behave. With a cursor we return
17
+ // everything after it (genuine "new since I last polled", already bounded);
18
+ // without one we fetch only the bounded recent tail.
19
+ export function planWaitForMessagesFetch(input) {
20
+ if (input.effectiveAfterMessageId) {
21
+ return { mode: "after_cursor", after: input.effectiveAfterMessageId };
22
+ }
23
+ return {
24
+ mode: "catch_up_tail",
25
+ limit: input.catchupLimit ?? DEFAULT_WAIT_CATCHUP_LIMIT,
26
+ };
27
+ }
28
+ const LOCAL_TASK_OWNER_STATUSES = new Set(["assigned", "in_progress", "blocked", "in_review"]);
29
+ export function buildWaitForMessagesRequestOptions(input) {
30
+ return {
31
+ ...(input.signal ? { signal: input.signal } : {}),
32
+ headers: input.deliveryHeaders,
33
+ };
34
+ }
35
+ export function buildWaitForMessagesHistoryPageRequest(input) {
36
+ const querySuffix = input.queryString ? `?${input.queryString}` : "";
37
+ return {
38
+ room_id: input.targetRoomId,
39
+ project_id: input.targetProjectId,
40
+ room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages${querySuffix}`),
41
+ project_path: (targetProjectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(targetProjectId)}/messages${querySuffix}`),
42
+ options: buildWaitForMessagesRequestOptions({ deliveryHeaders: input.deliveryHeaders }),
43
+ };
44
+ }
45
+ function isRecord(value) {
46
+ return Boolean(value && typeof value === "object");
47
+ }
48
+ function messageId(message) {
49
+ return typeof message.id === "string" && message.id.trim() ? message.id : null;
50
+ }
51
+ export function resolveEffectiveAfterMessageId(input) {
52
+ const requested = input.requestedAfterMessageId?.trim();
53
+ return requested || undefined;
54
+ }
55
+ function replyReferenceId(message) {
56
+ const reply = message.reply_to ?? message.replyTo ?? null;
57
+ if (typeof reply === "string" && reply.trim()) {
58
+ return reply;
59
+ }
60
+ if (isRecord(reply) && typeof reply.id === "string" && reply.id.trim()) {
61
+ return reply.id;
62
+ }
63
+ return null;
64
+ }
65
+ function activationDecision(message) {
66
+ if (!isRecord(message))
67
+ return null;
68
+ const activation = message.activation;
69
+ if (!isRecord(activation))
70
+ return null;
71
+ const forCurrentAgent = activation.for_current_agent;
72
+ if (!isRecord(forCurrentAgent))
73
+ return null;
74
+ return typeof forCurrentAgent.decision === "string" ? forCurrentAgent.decision : null;
75
+ }
76
+ export function filterSilentActivationMessages(messages) {
77
+ const visibleMessages = [];
78
+ const skippedMessageIds = [];
79
+ let lastObservedMessageId = null;
80
+ for (const message of messages) {
81
+ if (isRecord(message)) {
82
+ lastObservedMessageId = messageId(message) ?? lastObservedMessageId;
83
+ }
84
+ if (activationDecision(message) === "silent") {
85
+ if (isRecord(message)) {
86
+ const id = messageId(message);
87
+ if (id)
88
+ skippedMessageIds.push(id);
89
+ }
90
+ continue;
91
+ }
92
+ visibleMessages.push(message);
93
+ }
94
+ return {
95
+ messages: visibleMessages,
96
+ skipped_message_ids: skippedMessageIds,
97
+ last_observed_message_id: lastObservedMessageId,
98
+ };
99
+ }
100
+ function addActivationRoutingTelemetry(output, routing) {
101
+ if (routing.last_observed_message_id) {
102
+ output.last_observed_message_id = routing.last_observed_message_id;
103
+ }
104
+ if (routing.skipped_message_ids.length > 0) {
105
+ output.skipped_message_ids = routing.skipped_message_ids;
106
+ output.skipped_message_count = routing.skipped_message_ids.length;
107
+ }
108
+ return output;
109
+ }
110
+ export function resolveWaitAgentSession(roomId, agentSessionId) {
111
+ if (requireValidWorkerBearerRuntime().mode === "worker" &&
112
+ (!agentSessionId?.trim() || agentSessionId === WORKER_BEARER_AGENT_SESSION_ID)) {
113
+ return null;
114
+ }
115
+ if (!agentSessionId?.trim()) {
116
+ return null;
117
+ }
118
+ return resolveAgentSession(roomId, agentSessionId);
119
+ }
120
+ export async function localActivationContext(roomId) {
121
+ const result = await listLocalTasks(roomId, { openOnly: true });
122
+ return {
123
+ activeTaskLeases: result.tasks
124
+ .flatMap((task) => {
125
+ const agentKey = task.assignee_agent_key?.trim();
126
+ if (!LOCAL_TASK_OWNER_STATUSES.has(task.status) || !agentKey) {
127
+ return [];
128
+ }
129
+ return [{
130
+ kind: "work",
131
+ status: "active",
132
+ actor_label: task.assignee || agentKey,
133
+ agent_key: agentKey,
134
+ agent_instance_id: task.assignee_agent_instance_id,
135
+ agent_session_id: task.assignee_agent_session_id,
136
+ }];
137
+ }),
138
+ };
139
+ }
140
+ async function attachLocalActivationMetadata(roomId, messages, agentSession, options = {}) {
141
+ if (!agentSession || agentSession.session_kind !== "worker") {
142
+ return messages;
143
+ }
144
+ const records = messages.filter(isRecord);
145
+ const activationContext = options.includeTaskOwnerLeases === false
146
+ ? undefined
147
+ : await localActivationContext(roomId);
148
+ return attachAgentMessageActivations(records, {
149
+ actor_label: agentSession.actor_label,
150
+ agent_key: agentSession.agent_key,
151
+ agent_instance_id: agentSession.agent_instance_id ?? null,
152
+ agent_session_id: agentSession.session_id,
153
+ display_name: agentSession.display_name,
154
+ session_kind: agentSession.session_kind,
155
+ }, activationContext);
156
+ }
157
+ // Resolving an out-of-window parent costs a lookup (a by-id fetch, or a
158
+ // page-by-page history scan against older APIs), so resolved (and observed)
159
+ // messages are cached per process to keep repeat polls from re-resolving the
160
+ // same thread roots. Message ids are
161
+ // room-scoped, so the key includes the room scope. Message records are treated
162
+ // as immutable once posted; a bounded insertion-ordered map keeps memory flat
163
+ // for long-running workers.
164
+ const THREAD_CONTEXT_CACHE_MAX = 500;
165
+ const threadContextCache = new Map();
166
+ function threadContextCacheKey(scopeId, targetMessageId) {
167
+ return `${scopeId}:${targetMessageId}`;
168
+ }
169
+ export function rememberThreadContextMessages(scopeId, records) {
170
+ for (const record of records) {
171
+ if (!isRecord(record))
172
+ continue;
173
+ const id = messageId(record);
174
+ if (!id)
175
+ continue;
176
+ const key = threadContextCacheKey(scopeId, id);
177
+ threadContextCache.delete(key);
178
+ threadContextCache.set(key, record);
179
+ }
180
+ while (threadContextCache.size > THREAD_CONTEXT_CACHE_MAX) {
181
+ const oldest = threadContextCache.keys().next().value;
182
+ if (oldest === undefined)
183
+ break;
184
+ threadContextCache.delete(oldest);
185
+ }
186
+ }
187
+ export function resetThreadContextCacheForTests() {
188
+ threadContextCache.clear();
189
+ }
190
+ export async function collectThreadContextMessages(input) {
191
+ const records = input.messages.filter(isRecord);
192
+ const knownIds = new Set(records.map(messageId).filter((id) => Boolean(id)));
193
+ const seenIds = new Set(knownIds);
194
+ const pendingIds = records
195
+ .map(replyReferenceId)
196
+ .filter((id) => Boolean(id && !knownIds.has(id)));
197
+ const scopeId = input.roomId ?? input.localRoomId ?? input.projectId ?? "";
198
+ rememberThreadContextMessages(scopeId, records);
199
+ if (pendingIds.length === 0) {
200
+ // Nothing quotes an out-of-window message (idle polls land here), so skip
201
+ // storage-mode resolution entirely.
202
+ return [];
203
+ }
204
+ const contextMessages = [];
205
+ const useLocalStorage = Boolean(input.localRoomId && await isLocalRoomStorageEnabled(input.localRoomId));
206
+ const localIdentifiers = useLocalStorage
207
+ ? await resolveLocalRoomStorageIdentifiers(input.localRoomId)
208
+ : { localRoomId: input.localRoomId };
209
+ const sqliteRoomId = localIdentifiers.localRoomId || input.localRoomId;
210
+ while (pendingIds.length > 0) {
211
+ const nextId = pendingIds.shift();
212
+ if (!nextId || seenIds.has(nextId))
213
+ continue;
214
+ seenIds.add(nextId);
215
+ const cached = threadContextCache.get(threadContextCacheKey(scopeId, nextId));
216
+ const message = cached
217
+ ?? (useLocalStorage && input.localRoomId
218
+ ? await findLocalMessageById(sqliteRoomId || input.localRoomId, nextId)
219
+ : await findRemoteMessageById({
220
+ roomId: input.roomId,
221
+ projectId: input.projectId,
222
+ messageId: nextId,
223
+ }));
224
+ if (!message)
225
+ continue;
226
+ if (!cached) {
227
+ rememberThreadContextMessages(scopeId, [message]);
228
+ }
229
+ contextMessages.push(message);
230
+ const parentId = replyReferenceId(message);
231
+ if (parentId && !seenIds.has(parentId)) {
232
+ pendingIds.push(parentId);
233
+ }
234
+ }
235
+ return contextMessages;
236
+ }
237
+ export function registerWaitForMessagesTool(server) {
238
+ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat room (HTTP long-poll). Messages labeled activation.for_current_agent.decision=\"silent\" are skipped for the current worker; when last_observed_message_id is present, use it as cursor progress even if messages is empty. Threaded replies include thread_parent_id/thread.root_message_id; use send_thread_message with that id to keep focused side discussion out of the main room. For multi-hour runs, call in a loop: always pass after_message_id from the last message you processed or last_observed_message_id so an empty result means 'nothing new yet', not 'stop working'. If someone posted a premature 'I will wait' closing line, use send_message with a brief continue instruction. Per-call wait is capped (default max 180s unless LETAGENTS_POLL_MAX_MS is set on API and MCP).", {
239
+ room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room."),
240
+ after_message_id: z
241
+ .string()
242
+ .optional()
243
+ .describe(`Only return messages after this message ID (e.g. 'msg_3'). If omitted, returns just the most recent ${DEFAULT_WAIT_CATCHUP_LIMIT} messages (bounded recent tail) instead of the full room history, and advances the session cursor to the newest message.`),
244
+ timeout: z
245
+ .number()
246
+ .optional()
247
+ .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."),
248
+ agent_session_id: z
249
+ .string()
250
+ .optional()
251
+ .describe("Registered agent session to use. Without this, the MCP transport is treated as controller traffic and is hidden from connected-agent activity."),
252
+ }, async ({ room_id, after_message_id, timeout, agent_session_id }) => {
253
+ const targetRoomId = getTargetRoomId(room_id);
254
+ const targetProjectId = getFallbackProjectId();
255
+ const localRoomId = targetRoomId ?? currentRoom?.room_id ?? targetProjectId;
256
+ const identity = await ensureAgentIdentity();
257
+ const sessionRoomId = targetRoomId ?? currentRoom?.room_id ?? localRoomId ?? null;
258
+ const agentSession = resolveWaitAgentSession(sessionRoomId, agent_session_id);
259
+ const maxPollMs = getPollTimeoutCapMs();
260
+ const serverTimeout = Math.min(Math.max(timeout || DEFAULT_POLL_TIMEOUT_MS, 1000), maxPollMs);
261
+ if (localRoomId && await isLocalRoomStorageEnabled(localRoomId)) {
262
+ const { localRoomId: sqliteRoomId } = await resolveLocalRoomStorageIdentifiers(localRoomId);
263
+ const effectiveLocalRoomId = sqliteRoomId || localRoomId;
264
+ const effectiveAfterMessageId = resolveEffectiveAfterMessageId({
265
+ requestedAfterMessageId: after_message_id,
266
+ });
267
+ const fetchPlan = planWaitForMessagesFetch({ effectiveAfterMessageId });
268
+ // Without a cursor, page BACKWARDS from the tail (getLatest…) so we
269
+ // return the most-recent messages, not the oldest N; with a cursor we
270
+ // keep the unchanged forward "everything after the cursor" behavior.
271
+ const existing = fetchPlan.mode === "catch_up_tail"
272
+ ? await getLatestLocalChatMessages(effectiveLocalRoomId, {
273
+ limit: fetchPlan.limit,
274
+ include_prompt_only: true,
275
+ })
276
+ : await getLocalChatMessages(effectiveLocalRoomId, {
277
+ after: effectiveAfterMessageId,
278
+ include_prompt_only: true,
279
+ });
280
+ const replayingExistingMessages = existing.messages.length > 0;
281
+ const result = replayingExistingMessages
282
+ ? existing
283
+ : await waitForLocalChatMessages(effectiveLocalRoomId, {
284
+ after: effectiveAfterMessageId,
285
+ timeoutMs: serverTimeout,
286
+ include_prompt_only: true,
287
+ });
288
+ const messages = await attachLocalActivationMetadata(effectiveLocalRoomId, result.messages, agentSession, {
289
+ includeTaskOwnerLeases: !replayingExistingMessages,
290
+ });
291
+ const routing = filterSilentActivationMessages(messages);
292
+ touchRoomSession(effectiveLocalRoomId, routing.last_observed_message_id ?? getLastMessageId(result));
293
+ const threadContext = await collectThreadContextMessages({
294
+ messages: routing.messages,
295
+ localRoomId: effectiveLocalRoomId,
296
+ roomId: targetRoomId,
297
+ projectId: targetProjectId,
298
+ });
299
+ return jsonToolResponse({
300
+ room_id: effectiveLocalRoomId,
301
+ ...addActivationRoutingTelemetry({
302
+ messages: toAgentReadableMessages(routing.messages, threadContext),
303
+ }, routing),
304
+ });
305
+ }
306
+ await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, agentSession ? identityFromAgentSession(agentSession) : identity), agentSession);
307
+ const clientTimeout = serverTimeout + (serverTimeout > 120_000 ? 120_000 : 5_000);
308
+ const effectiveAfterMessageId = resolveEffectiveAfterMessageId({
309
+ requestedAfterMessageId: after_message_id,
310
+ });
311
+ const fetchPlan = planWaitForMessagesFetch({ effectiveAfterMessageId });
312
+ const deliveryHeaders = buildAgentDeliveryHeaders(agentSession);
313
+ const allMessages = [];
314
+ let roomIdFromResponse;
315
+ // Long-poll the server (blocks up to serverTimeout for new messages),
316
+ // optionally seeded with a cursor. Shared by the cursor path and by the
317
+ // no-cursor empty-tail fallback so a quiet room still blocks instead of
318
+ // busy-spinning. When `after` is provided this also catches up any backlog
319
+ // after the cursor via forward pagination.
320
+ const longPollFromCursor = async (after) => {
321
+ const params = new URLSearchParams();
322
+ if (after)
323
+ params.set("after", after);
324
+ params.set("timeout", String(serverTimeout));
325
+ const queryString = params.toString();
326
+ const firstResult = await roomScopedApiCall({
327
+ room_id: targetRoomId,
328
+ project_id: targetProjectId,
329
+ room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages/poll?${queryString}`),
330
+ project_path: (targetProjectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(targetProjectId)}/messages/poll?${queryString}`),
331
+ options: buildWaitForMessagesRequestOptions({
332
+ deliveryHeaders,
333
+ signal: AbortSignal.timeout(clientTimeout),
334
+ }),
335
+ });
336
+ allMessages.push(...(firstResult.messages ?? []));
337
+ roomIdFromResponse = roomIdFromResponse || firstResult.room_id || firstResult.project_id;
338
+ if (firstResult.has_more && allMessages.length > 0) {
339
+ let afterCursor = allMessages[allMessages.length - 1]?.id;
340
+ while (afterCursor) {
341
+ const pageParams = new URLSearchParams();
342
+ pageParams.set("after", afterCursor);
343
+ const qs = pageParams.toString();
344
+ const page = await roomScopedApiCall(buildWaitForMessagesHistoryPageRequest({
345
+ targetRoomId,
346
+ targetProjectId,
347
+ queryString: qs,
348
+ deliveryHeaders,
349
+ }));
350
+ const msgs = page.messages ?? [];
351
+ allMessages.push(...msgs);
352
+ if (!page.has_more || msgs.length === 0)
353
+ break;
354
+ afterCursor = msgs[msgs.length - 1]?.id;
355
+ }
356
+ }
357
+ };
358
+ if (fetchPlan.mode === "catch_up_tail") {
359
+ // No cursor: catch up on only the bounded recent tail. Mirror
360
+ // read_messages by paging BACKWARDS from the tail (before=latest) so we
361
+ // return the newest N and never walk the full room history. This also
362
+ // advances the session cursor to the newest message returned.
363
+ const recent = await fetchRecentRemoteMessages({
364
+ targetRoomId,
365
+ targetProjectId,
366
+ limit: fetchPlan.limit,
367
+ });
368
+ if (recent.messages.length > 0) {
369
+ allMessages.push(...recent.messages);
370
+ roomIdFromResponse = recent.roomIdFromResponse;
371
+ }
372
+ else {
373
+ // Empty tail => the room is effectively empty, so there is nothing to
374
+ // dump. Fall through to the long-poll (no cursor) so a worker looping
375
+ // in a quiet room blocks up to the timeout instead of busy-spinning,
376
+ // exactly as the local path does via waitForLocalChatMessages.
377
+ roomIdFromResponse = recent.roomIdFromResponse;
378
+ await longPollFromCursor();
379
+ }
380
+ }
381
+ else {
382
+ await longPollFromCursor(fetchPlan.after);
383
+ }
384
+ const routing = filterSilentActivationMessages(allMessages);
385
+ const threadContext = await collectThreadContextMessages({
386
+ messages: routing.messages,
387
+ localRoomId,
388
+ roomId: targetRoomId,
389
+ projectId: targetProjectId,
390
+ });
391
+ const output = addActivationRoutingTelemetry({
392
+ messages: toAgentReadableMessages(routing.messages, threadContext),
393
+ }, routing);
394
+ if (roomIdFromResponse) {
395
+ output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
396
+ }
397
+ if (targetRoomId) {
398
+ touchRoomSession(targetRoomId, routing.last_observed_message_id ?? getLastMessageId(output));
399
+ }
400
+ await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, agentSession ? identityFromAgentSession(agentSession) : identity), agentSession);
401
+ return jsonToolResponse(output);
402
+ });
403
+ }
@@ -0,0 +1 @@
1
+ export { registerMessageTools, registerStatusTools } from "./messages/index.js";
@@ -0,0 +1,158 @@
1
+ import { z } from "zod";
2
+ import { getRoomFromConfig } from "../../../config-reader.js";
3
+ import { looksLikeInviteCode } from "../../../room-id.js";
4
+ import { apiCall, clearAuthenticatedAccountCache, clearPendingDeviceAuth, clearStoredAuth, currentRoom, ensureAgentIdentity, getPendingDeviceAuth, joinRoomIdentifierWithoutImplicitGitRefCreate, setAuthenticatedAccountCache, setPendingDeviceAuth, setStoredAuth, toPublicAgentIdentity, withCanonicalRoomLink, } from "../../runtime.js";
5
+ import { jsonTextResponse } from "./responses.js";
6
+ import { workerModeDisabledToolResult } from "../../runtime/worker-bearer.js";
7
+ export function registerDeviceAuthTools(server) {
8
+ registerStartDeviceAuthTool(server);
9
+ registerPollDeviceAuthTool(server);
10
+ registerClearSavedAuthTool(server);
11
+ }
12
+ function registerStartDeviceAuthTool(server) {
13
+ server.tool("start_device_auth", "Start GitHub Device Flow for Let Agents Chat and persist the pending request locally. Use this when private repo access or explicit LetAgents auth is needed.", {
14
+ room_id: z
15
+ .string()
16
+ .optional()
17
+ .describe("Optional room to associate with this auth request for later auto-join."),
18
+ force: z
19
+ .boolean()
20
+ .optional()
21
+ .describe("If true, replaces any existing pending device auth request."),
22
+ }, async ({ room_id, force }) => {
23
+ const disabled = workerModeDisabledToolResult();
24
+ if (disabled)
25
+ return jsonTextResponse(disabled);
26
+ const existing = getPendingDeviceAuth();
27
+ if (existing && !force) {
28
+ return jsonTextResponse({
29
+ success: true,
30
+ reused_existing_request: true,
31
+ ...existing,
32
+ });
33
+ }
34
+ const response = await apiCall("/auth/device/start", {
35
+ method: "POST",
36
+ });
37
+ const pendingAuth = setPendingDeviceAuth({
38
+ request_id: response.request_id,
39
+ user_code: response.user_code,
40
+ verification_uri: response.verification_uri,
41
+ interval_seconds: response.interval,
42
+ expires_at: new Date(Date.now() + response.expires_in * 1000).toISOString(),
43
+ started_at: new Date().toISOString(),
44
+ suggested_room_id: room_id ?? currentRoom?.room_id ?? getRoomFromConfig() ?? undefined,
45
+ });
46
+ return jsonTextResponse({
47
+ success: true,
48
+ ...pendingAuth,
49
+ });
50
+ });
51
+ }
52
+ function registerPollDeviceAuthTool(server) {
53
+ server.tool("poll_device_auth", "Poll a pending GitHub Device Flow request. On success this stores the LetAgents token locally and can optionally join a room immediately.", {
54
+ request_id: z
55
+ .string()
56
+ .optional()
57
+ .describe("The device auth request to poll. Defaults to the locally saved pending request."),
58
+ room_id: z
59
+ .string()
60
+ .optional()
61
+ .describe("Optional room to auto-join after authorization succeeds."),
62
+ auto_join: z
63
+ .boolean()
64
+ .optional()
65
+ .describe("If true, tries to join the room immediately after the auth succeeds."),
66
+ }, async ({ request_id, room_id, auto_join }) => {
67
+ const disabled = workerModeDisabledToolResult();
68
+ if (disabled)
69
+ return jsonTextResponse(disabled);
70
+ const pendingAuth = request_id
71
+ ? getPendingDeviceAuth()?.request_id === request_id
72
+ ? getPendingDeviceAuth()
73
+ : null
74
+ : getPendingDeviceAuth();
75
+ if (!pendingAuth && !request_id) {
76
+ return jsonTextResponse({
77
+ success: false,
78
+ error: "No pending device auth request found.",
79
+ });
80
+ }
81
+ const requestId = request_id || pendingAuth?.request_id;
82
+ if (!requestId) {
83
+ return jsonTextResponse({
84
+ success: false,
85
+ error: "A request_id is required when nothing is saved locally.",
86
+ });
87
+ }
88
+ const result = await apiCall(`/auth/device/poll/${encodeURIComponent(requestId)}`);
89
+ if (result.status === "pending" || result.status === "slow_down") {
90
+ if (pendingAuth) {
91
+ setPendingDeviceAuth({
92
+ ...pendingAuth,
93
+ interval_seconds: result.interval ?? pendingAuth.interval_seconds,
94
+ expires_at: result.expires_in !== undefined
95
+ ? new Date(Date.now() + result.expires_in * 1000).toISOString()
96
+ : pendingAuth.expires_at,
97
+ });
98
+ }
99
+ return jsonTextResponse({ success: true, ...result });
100
+ }
101
+ if (result.status === "denied" || result.status === "expired") {
102
+ clearPendingDeviceAuth();
103
+ clearStoredAuth();
104
+ clearAuthenticatedAccountCache();
105
+ return jsonTextResponse({ success: false, ...result });
106
+ }
107
+ if (!result.letagents_token) {
108
+ throw new Error("Device auth completed without a LetAgents token.");
109
+ }
110
+ clearPendingDeviceAuth();
111
+ const storedAuth = setStoredAuth({
112
+ token: result.letagents_token,
113
+ expires_at: result.expires_at,
114
+ account: result.account,
115
+ stored_at: new Date().toISOString(),
116
+ source: "device_flow",
117
+ });
118
+ setAuthenticatedAccountCache(storedAuth.account ?? undefined, storedAuth.account ? "stored" : null, null);
119
+ let joinedRoom = null;
120
+ const roomToJoin = room_id ||
121
+ pendingAuth?.suggested_room_id ||
122
+ currentRoom?.room_id ||
123
+ getRoomFromConfig() ||
124
+ undefined;
125
+ if (auto_join && roomToJoin) {
126
+ const joinedVia = looksLikeInviteCode(roomToJoin) ? "join_code" : "join_room";
127
+ const joined = await joinRoomIdentifierWithoutImplicitGitRefCreate(roomToJoin, joinedVia, { fallbackToRepo: true });
128
+ joinedRoom = joined.room;
129
+ }
130
+ const agentIdentity = await ensureAgentIdentity();
131
+ return jsonTextResponse({
132
+ success: true,
133
+ status: "authorized",
134
+ account: storedAuth.account ?? null,
135
+ expires_at: storedAuth.expires_at ?? null,
136
+ auto_joined_room: joinedRoom,
137
+ ...(joinedRoom?.room_id
138
+ ? withCanonicalRoomLink(joinedRoom.room_id, {})
139
+ : {}),
140
+ agent_identity: toPublicAgentIdentity(agentIdentity),
141
+ hint: "You can change your agent's display name anytime using the set_agent_name tool.",
142
+ });
143
+ });
144
+ }
145
+ function registerClearSavedAuthTool(server) {
146
+ server.tool("clear_saved_auth", "Clear any locally saved LetAgents auth token and pending device auth request.", {}, async () => {
147
+ const disabled = workerModeDisabledToolResult();
148
+ if (disabled)
149
+ return jsonTextResponse(disabled);
150
+ clearPendingDeviceAuth();
151
+ clearStoredAuth();
152
+ clearAuthenticatedAccountCache();
153
+ return jsonTextResponse({
154
+ success: true,
155
+ env_token_still_present: Boolean(process.env.LETAGENTS_TOKEN),
156
+ });
157
+ });
158
+ }
@@ -0,0 +1,93 @@
1
+ import { z } from "zod";
2
+ import { buildAgentActorLabel, formatOwnerAttribution } from "../../../../shared/agent-identity.js";
3
+ import { normalizeAgentBaseName } from "../../../../shared/codenames.js";
4
+ import { apiCall, currentAgentIdentity, currentAgentIdentityKey, detectAgentIdeLabel, getConversationIdentity, getLetagentsToken, resolveOwnerContext, setConversationIdentity, storeCurrentAgentIdentity, toPublicAgentIdentity, } from "../../runtime.js";
5
+ import { jsonTextResponse } from "./responses.js";
6
+ import { workerModeDisabledToolResult } from "../../runtime/worker-bearer.js";
7
+ export function registerSetAgentNameTool(server) {
8
+ server.tool("set_agent_name", "Set or change the agent's display name. The agent will be known by this name in the room. Use this to pick a custom name instead of the auto-generated codename.", {
9
+ name: z
10
+ .string()
11
+ .min(2)
12
+ .max(64)
13
+ .describe("The desired display name for this agent (2-64 characters)."),
14
+ conversation_id: z
15
+ .string()
16
+ .optional()
17
+ .describe("Optional conversation ID to scope this name change. When provided, only this conversation uses the new name; other conversations keep their own identity."),
18
+ }, async ({ name: desiredName, conversation_id }) => {
19
+ const disabled = workerModeDisabledToolResult();
20
+ if (disabled)
21
+ return jsonTextResponse(disabled);
22
+ const trimmedName = desiredName.trim();
23
+ if (trimmedName.length < 2 || trimmedName.length > 64) {
24
+ return jsonTextResponse({
25
+ success: false,
26
+ error: "Name must be between 2 and 64 characters.",
27
+ });
28
+ }
29
+ const authAvailable = Boolean(await getLetagentsToken());
30
+ if (!authAvailable) {
31
+ return jsonTextResponse({
32
+ success: false,
33
+ error: "Authentication required. Run start_device_auth first.",
34
+ });
35
+ }
36
+ const slugName = normalizeAgentBaseName(trimmedName);
37
+ try {
38
+ const owner = await resolveOwnerContext();
39
+ const ideLabel = detectAgentIdeLabel();
40
+ const ownerAttribution = formatOwnerAttribution(owner.label);
41
+ const actorLabel = buildAgentActorLabel({
42
+ display_name: trimmedName,
43
+ owner_label: owner.label,
44
+ ide_label: ideLabel,
45
+ });
46
+ let canonicalKey = owner.login ? `${owner.login}/${slugName}` : null;
47
+ if (!conversation_id) {
48
+ const registered = await apiCall("/agents", {
49
+ method: "POST",
50
+ body: JSON.stringify({
51
+ name: slugName,
52
+ display_name: trimmedName,
53
+ owner_label: owner.label,
54
+ }),
55
+ });
56
+ if (typeof registered.canonical_key === "string") {
57
+ canonicalKey = registered.canonical_key;
58
+ }
59
+ }
60
+ const updatedIdentity = {
61
+ name: slugName,
62
+ display_name: trimmedName,
63
+ owner_label: owner.label,
64
+ owner_attribution: ownerAttribution,
65
+ ide_label: ideLabel,
66
+ actor_label: actorLabel,
67
+ canonical_key: canonicalKey,
68
+ runtime_key: currentAgentIdentityKey,
69
+ source: conversation_id ? "local" : "api",
70
+ resolved_at: new Date().toISOString(),
71
+ };
72
+ if (conversation_id) {
73
+ setConversationIdentity(conversation_id, updatedIdentity);
74
+ }
75
+ else {
76
+ storeCurrentAgentIdentity(updatedIdentity, currentAgentIdentityKey);
77
+ }
78
+ return jsonTextResponse({
79
+ success: true,
80
+ message: `Agent name changed to "${trimmedName}".`,
81
+ agent_identity: toPublicAgentIdentity(conversation_id
82
+ ? getConversationIdentity(conversation_id)
83
+ : currentAgentIdentity),
84
+ });
85
+ }
86
+ catch (error) {
87
+ return jsonTextResponse({
88
+ success: false,
89
+ error: `Failed to set name: ${error instanceof Error ? error.message : String(error)}`,
90
+ });
91
+ }
92
+ });
93
+ }
@@ -0,0 +1,10 @@
1
+ export function jsonTextResponse(value) {
2
+ return {
3
+ content: [
4
+ {
5
+ type: "text",
6
+ text: JSON.stringify(value, null, 2),
7
+ },
8
+ ],
9
+ };
10
+ }