letagents 0.12.12 → 0.12.14

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 (55) 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 +16 -7
  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/execution-profile.js +1 -0
  11. package/dist/mcp/server/runtime/messages.js +55 -0
  12. package/dist/mcp/server/runtime/presence.js +3 -3
  13. package/dist/mcp/server/runtime/room-api.js +2 -2
  14. package/dist/mcp/server/runtime/room-state.js +19 -9
  15. package/dist/mcp/server/runtime/rooms.js +54 -33
  16. package/dist/mcp/server/runtime/supervisor-bridge.js +129 -11
  17. package/dist/mcp/server/runtime/tool-surface-policy.js +7 -0
  18. package/dist/mcp/server/runtime/worker-bearer.js +17 -2
  19. package/dist/mcp/server/runtime-contract.js +1 -0
  20. package/dist/mcp/server/runtime.js +6 -6
  21. package/dist/mcp/server/supervised-tool-facade.js +107 -6
  22. package/dist/mcp/server/tools/messages/read-tool.js +54 -97
  23. package/dist/mcp/server/tools/messages/reasoning-tool.js +2 -0
  24. package/dist/mcp/server/tools/messages/send-tool.js +2 -0
  25. package/dist/mcp/server/tools/messages/status-tool.js +2 -0
  26. package/dist/mcp/server/tools/messages/wait-tool.js +303 -71
  27. package/dist/mcp/server/tools/onboarding/status-tool.js +4 -4
  28. package/dist/mcp/server/tools/rooms/inspection-tools.js +15 -10
  29. package/dist/mcp/server/tools/rooms/repo-initialization-tool.js +2 -1
  30. package/dist/mcp/server/tools/tasks/board-tools.js +34 -2
  31. package/dist/mcp/sse-client.js +163 -20
  32. package/dist/shared/activation-routing.js +163 -23
  33. package/dist/shared/agent-presence.js +6 -0
  34. package/dist/shared/desktop-release-manifest.js +63 -0
  35. package/dist/shared/desktop-release.js +60 -0
  36. package/dist/shared/scoped-ids.js +6 -0
  37. package/package.json +7 -3
  38. package/shared/execution-approval-projection.d.mts +32 -0
  39. package/shared/execution-approval-projection.mjs +107 -0
  40. package/shared/execution-approval-publication-item.d.mts +20 -0
  41. package/shared/execution-approval-publication-item.mjs +61 -0
  42. package/shared/execution-approval-publication.d.mts +53 -0
  43. package/shared/execution-approval-publication.mjs +136 -0
  44. package/shared/execution-delegation-decision.d.mts +37 -0
  45. package/shared/execution-delegation-decision.mjs +73 -0
  46. package/shared/message-contracts.d.mts +32 -0
  47. package/shared/message-contracts.mjs +109 -0
  48. package/shared/room-agent-work.d.mts +31 -0
  49. package/shared/room-agent-work.mjs +44 -0
  50. package/shared/room-resource-invalidation.d.mts +38 -0
  51. package/shared/room-resource-invalidation.mjs +50 -0
  52. package/shared/routing-aliases.d.mts +18 -0
  53. package/shared/routing-aliases.mjs +66 -0
  54. package/shared/sqlite-thread-routing.d.mts +72 -0
  55. package/shared/sqlite-thread-routing.mjs +1038 -0
@@ -1,9 +1,11 @@
1
1
  import { z } from "zod";
2
2
  import { getPollTimeoutCapMs } from "../../../../shared/poll-timeout-cap.js";
3
3
  import { encodeRoomIdPath } from "../../../room-id.js";
4
- import { agentSessionCredentials, appendIncludePromptOnly, buildAgentDeliveryHeaders, bindSupervisedWorkerSession, scheduleSupervisedWorkerCursorCheckpoint, 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, supervisedBoundedDeliveryDisabledToolResult, } from "../../runtime/worker-bearer.js";
6
- import { attachAgentMessageActivations } from "../../../../shared/activation-routing.js";
4
+ import { agentSessionCredentials, AGENT_MESSAGE_BODY_MAX_BYTES, appendIncludePromptOnly, boundAgentMessageOutput, buildAgentDeliveryHeaders, bindSupervisedWorkerSession, scheduleSupervisedWorkerCursorCheckpoint, currentRoom, ensureAgentIdentity, getFallbackProjectId, getLatestLocalChatMessages, getLocalImportedRoutingAuthority, getLocalChatMessages, getLocalChatThreadRoutingMembership, getLastMessageId, getRememberedRoomPresence, getStoredAgentRoutingStateSnapshot, getTargetRoomId, identityFromAgentSession, isLocalRoomStorageEnabled, listLocalActiveTaskOwnerLeases, resolveLocalRoomStorageIdentifiers, resolveAgentSession, roomScopedApiCall, syncRoomPresence, toAgentReadableMessages, touchRoomSession, WORKER_BEARER_AGENT_SESSION_ID, waitForLocalChatMessages, } from "../../runtime.js";
5
+ import { requireValidWorkerBearerRuntime, isCustodialPolling, supervisedBoundedDeliveryDisabledToolResult, } from "../../runtime/worker-bearer.js";
6
+ import { resolveWorkerToolIdentity } from "../../runtime/agent-sessions.js";
7
+ import { attachAgentMessageActivations, createGlobalAgentAddressResolver, decideAgentMessageActivation, isTaskOwnerFollowUpMessageText, } from "../../../../shared/activation-routing.js";
8
+ import { normalizeRoutingSender } from "../../../../../shared/routing-aliases.mjs";
7
9
  import { findLocalMessageById, findRemoteMessageById } from "./message-lookup.js";
8
10
  import { fetchRecentRemoteMessages } from "./read-tool.js";
9
11
  import { jsonToolResponse } from "./response.js";
@@ -13,9 +15,9 @@ const DEFAULT_POLL_TIMEOUT_MS = 30000;
13
15
  // room history (busy rooms archive millions of characters, which blows the
14
16
  // tool's token budget). This bounds that no-cursor catch-up to a recent tail.
15
17
  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.
18
+ export const MAX_WAIT_MESSAGES_PER_CALL = 100;
19
+ // Decide how the initial fetch should behave. Both paths return one bounded
20
+ // page; callers continue from last_observed_message_id when truncated.
19
21
  export function planWaitForMessagesFetch(input) {
20
22
  if (input.effectiveAfterMessageId) {
21
23
  return { mode: "after_cursor", after: input.effectiveAfterMessageId };
@@ -25,23 +27,12 @@ export function planWaitForMessagesFetch(input) {
25
27
  limit: input.catchupLimit ?? DEFAULT_WAIT_CATCHUP_LIMIT,
26
28
  };
27
29
  }
28
- const LOCAL_TASK_OWNER_STATUSES = new Set(["assigned", "in_progress", "blocked", "in_review"]);
29
30
  export function buildWaitForMessagesRequestOptions(input) {
30
31
  return {
31
32
  ...(input.signal ? { signal: input.signal } : {}),
32
33
  headers: input.deliveryHeaders,
33
34
  };
34
35
  }
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
36
  function isRecord(value) {
46
37
  return Boolean(value && typeof value === "object");
47
38
  }
@@ -118,41 +109,233 @@ export function resolveWaitAgentSession(roomId, agentSessionId) {
118
109
  return resolveAgentSession(roomId, agentSessionId);
119
110
  }
120
111
  export async function localActivationContext(roomId) {
121
- const result = await listLocalTasks(roomId, { openOnly: true });
122
112
  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
- }),
113
+ activeTaskLeases: await listLocalActiveTaskOwnerLeases(roomId),
138
114
  };
139
115
  }
140
- async function attachLocalActivationMetadata(roomId, messages, agentSession, options = {}) {
116
+ export async function attachLocalActivationMetadata(roomId, messages, agentSession, options = {}) {
141
117
  if (!agentSession || agentSession.session_kind !== "worker") {
142
118
  return messages;
143
119
  }
144
120
  const records = messages.filter(isRecord);
145
- const activationContext = options.includeTaskOwnerLeases === false
146
- ? undefined
147
- : await localActivationContext(roomId);
148
- return attachAgentMessageActivations(records, {
121
+ if (records.length === 0)
122
+ return messages;
123
+ const identity = {
149
124
  actor_label: agentSession.actor_label,
150
125
  agent_key: agentSession.agent_key,
151
126
  agent_instance_id: agentSession.agent_instance_id ?? null,
152
127
  agent_session_id: agentSession.session_id,
153
128
  display_name: agentSession.display_name,
154
129
  session_kind: agentSession.session_kind,
155
- }, activationContext);
130
+ };
131
+ // Imported cloud rows carry immutable, account-scoped send-time authority.
132
+ // A present wrapper always wins over mutable local aliases, including when
133
+ // the current state population cannot be read completely.
134
+ const activeSessionRoomId = options.activeSessionRoomId?.trim() || roomId;
135
+ const routingState = getStoredAgentRoutingStateSnapshot(activeSessionRoomId);
136
+ const authoritativeLegacyDecisions = new Map();
137
+ const legacyRecords = [];
138
+ const normalizedIdentityKey = normalizeRoutingSender(identity.agent_key);
139
+ const validActivationReasons = new Set([
140
+ "self_message",
141
+ "explicit_mention",
142
+ "explicit_other_mention",
143
+ "broadcast",
144
+ "reply_target",
145
+ "other_reply_target",
146
+ "thread_participant",
147
+ "task_owner",
148
+ "system_event",
149
+ "unaddressed",
150
+ ]);
151
+ for (const message of records) {
152
+ const id = messageId(message);
153
+ if (!id)
154
+ continue;
155
+ const imported = getLocalImportedRoutingAuthority(message);
156
+ if (!imported) {
157
+ if (routingState.complete) {
158
+ legacyRecords.push(message);
159
+ }
160
+ else {
161
+ authoritativeLegacyDecisions.set(id, {
162
+ decision: "silent",
163
+ reason: "unaddressed",
164
+ addressed: false,
165
+ });
166
+ }
167
+ continue;
168
+ }
169
+ if (!routingState.accountReaderKey
170
+ || imported.readerKey !== routingState.accountReaderKey
171
+ || imported.routing.authority === "invalid") {
172
+ authoritativeLegacyDecisions.set(id, {
173
+ decision: "silent",
174
+ reason: "unaddressed",
175
+ addressed: false,
176
+ });
177
+ continue;
178
+ }
179
+ const target = imported.routing.recipientSessions.find((candidate) => candidate.agentKey === normalizedIdentityKey);
180
+ const targetSessionId = imported.routing.authority === "receipts"
181
+ ? target && "successorAgentSessionId" in target
182
+ ? target.successorAgentSessionId ?? target.agentSessionId
183
+ : target?.agentSessionId
184
+ : target?.agentSessionId;
185
+ if (!target || targetSessionId !== identity.agent_session_id) {
186
+ authoritativeLegacyDecisions.set(id, {
187
+ decision: "silent",
188
+ reason: "unaddressed",
189
+ addressed: false,
190
+ });
191
+ continue;
192
+ }
193
+ const importedReason = imported.routing.authority === "legacy"
194
+ && "activationReason" in target
195
+ && validActivationReasons.has(target.activationReason)
196
+ ? target.activationReason
197
+ : "explicit_mention";
198
+ authoritativeLegacyDecisions.set(id, {
199
+ decision: "activate",
200
+ reason: importedReason,
201
+ addressed: true,
202
+ });
203
+ }
204
+ if (legacyRecords.length === 0) {
205
+ return attachAgentMessageActivations(records, identity, { authoritativeLegacyDecisions });
206
+ }
207
+ const threadRootIds = legacyRecords.flatMap((message) => {
208
+ const messageId = typeof message.id === "string" ? message.id : "";
209
+ const thread = isRecord(message.thread) ? message.thread : null;
210
+ const rootId = typeof message.thread_root_id === "string"
211
+ ? message.thread_root_id
212
+ : typeof thread?.root_message_id === "string"
213
+ ? thread.root_message_id
214
+ : "";
215
+ return rootId && rootId !== messageId ? [rootId] : [];
216
+ });
217
+ // Linked local/cloud rooms store messages under the SQLite room id while
218
+ // registered workers remain keyed by the canonical cloud room id. Routing
219
+ // ambiguity must always use that complete canonical population.
220
+ const storedActiveSessions = routingState.sessions;
221
+ const activeIdentities = storedActiveSessions.map((session) => ({
222
+ actor_label: session.actor_label,
223
+ agent_key: session.agent_key,
224
+ agent_instance_id: session.agent_instance_id ?? null,
225
+ agent_session_id: session.session_id,
226
+ display_name: session.display_name,
227
+ session_kind: session.session_kind,
228
+ }));
229
+ if (!activeIdentities.some((candidate) => candidate.agent_session_id === identity.agent_session_id
230
+ && candidate.agent_key === identity.agent_key)) {
231
+ // The request's authenticated current session is authoritative even when
232
+ // an older local-state file has not persisted it yet.
233
+ activeIdentities.push(identity);
234
+ }
235
+ const sameKeySessions = storedActiveSessions.filter((session) => session.agent_key === identity.agent_key);
236
+ const currentRepresentativeSessionId = sameKeySessions[0]?.session_id
237
+ ?? identity.agent_session_id;
238
+ const currentIsActive = currentRepresentativeSessionId === identity.agent_session_id;
239
+ const resolveGlobalAddress = createGlobalAgentAddressResolver(activeIdentities);
240
+ const explicitMentionMessageIds = new Set();
241
+ const replyTargetMessageIds = new Set();
242
+ const exactReplyTargetMessageIds = new Set();
243
+ const selfMessageIds = new Set();
244
+ for (const message of legacyRecords) {
245
+ const messageId = typeof message.id === "string" ? message.id : "";
246
+ if (!messageId)
247
+ continue;
248
+ const addressed = resolveGlobalAddress(message);
249
+ const reply = isRecord(message.reply_to) ? message.reply_to : null;
250
+ const replyPublisherIdentity = isRecord(reply?.agent_identity)
251
+ ? reply.agent_identity
252
+ : null;
253
+ const replyPublisherKey = typeof replyPublisherIdentity?.agent_key === "string"
254
+ ? replyPublisherIdentity.agent_key.trim()
255
+ : "";
256
+ const replyPublisherSessionId = typeof replyPublisherIdentity?.agent_session_id === "string"
257
+ ? replyPublisherIdentity.agent_session_id.trim()
258
+ : "";
259
+ if (replyPublisherKey) {
260
+ addressed.replyTargetKeys.clear();
261
+ const exactReplyIdentity = replyPublisherSessionId
262
+ ? activeIdentities.find((candidate) => candidate.agent_key === replyPublisherKey
263
+ && candidate.agent_session_id === replyPublisherSessionId)
264
+ : undefined;
265
+ if (exactReplyIdentity) {
266
+ if (exactReplyIdentity.agent_session_id === identity.agent_session_id) {
267
+ exactReplyTargetMessageIds.add(messageId);
268
+ }
269
+ }
270
+ else if (activeIdentities.some((candidate) => candidate.agent_key === replyPublisherKey)) {
271
+ addressed.replyTargetKeys.add(replyPublisherKey);
272
+ }
273
+ }
274
+ const publisherIdentity = isRecord(message.agent_identity)
275
+ ? message.agent_identity
276
+ : null;
277
+ const publisherAgentKey = typeof publisherIdentity?.agent_key === "string"
278
+ ? publisherIdentity.agent_key.trim()
279
+ : "";
280
+ if (String(message.source ?? "").trim() === "agent"
281
+ && (publisherAgentKey
282
+ ? publisherAgentKey === identity.agent_key
283
+ : addressed.senderKeys.has(identity.agent_key))) {
284
+ selfMessageIds.add(messageId);
285
+ }
286
+ if (addressed.explicitMentionKeys.has(identity.agent_key)) {
287
+ explicitMentionMessageIds.add(messageId);
288
+ }
289
+ const thread = isRecord(message.thread) ? message.thread : null;
290
+ const rootId = typeof message.thread_root_id === "string"
291
+ ? message.thread_root_id
292
+ : typeof thread?.root_message_id === "string"
293
+ ? thread.root_message_id
294
+ : "";
295
+ if ((!rootId || rootId === messageId)
296
+ && (exactReplyTargetMessageIds.has(messageId)
297
+ || addressed.replyTargetKeys.has(identity.agent_key))) {
298
+ replyTargetMessageIds.add(messageId);
299
+ }
300
+ }
301
+ const [activationContext, projectedThreadParticipantRootIds] = await Promise.all([
302
+ options.includeTaskOwnerLeases === false
303
+ || !records.some((message) => isTaskOwnerFollowUpMessageText(message.text))
304
+ ? undefined
305
+ : localActivationContext(roomId),
306
+ getLocalChatThreadRoutingMembership(roomId, threadRootIds, identity, activeIdentities),
307
+ ]);
308
+ const exactContext = {
309
+ ...activationContext,
310
+ selfMessageIds,
311
+ threadParticipantRootIds: projectedThreadParticipantRootIds,
312
+ explicitMentionMessageIds,
313
+ replyTargetMessageIds,
314
+ };
315
+ const exactTaskSessionIdsForKey = new Set((activationContext?.activeTaskLeases ?? [])
316
+ .filter((lease) => lease.status === "active"
317
+ && lease.agent_key === identity.agent_key
318
+ && Boolean(lease.agent_session_id))
319
+ .map((lease) => lease.agent_session_id));
320
+ for (const message of legacyRecords) {
321
+ const messageId = typeof message.id === "string" ? message.id : "";
322
+ if (!messageId)
323
+ continue;
324
+ const resolved = decideAgentMessageActivation(message, identity, exactContext);
325
+ const exactTaskOwner = resolved.reason === "task_owner"
326
+ && activationContext?.activeTaskLeases?.some((lease) => lease.status === "active"
327
+ && lease.agent_session_id === identity.agent_session_id
328
+ && (!lease.agent_key || lease.agent_key === identity.agent_key));
329
+ const eligibleRepresentative = exactReplyTargetMessageIds.has(messageId)
330
+ ? true
331
+ : resolved.reason === "task_owner" && exactTaskSessionIdsForKey.size > 0
332
+ ? exactTaskSessionIdsForKey.size === 1 && exactTaskOwner
333
+ : currentIsActive;
334
+ authoritativeLegacyDecisions.set(messageId, eligibleRepresentative && resolved.decision === "activate"
335
+ ? resolved
336
+ : { decision: "silent", reason: "unaddressed", addressed: false });
337
+ }
338
+ return attachAgentMessageActivations(records, identity, { authoritativeLegacyDecisions });
156
339
  }
157
340
  // Resolving an out-of-window parent costs a lookup (a by-id fetch, or a
158
341
  // page-by-page history scan against older APIs), so resolved (and observed)
@@ -162,6 +345,9 @@ async function attachLocalActivationMetadata(roomId, messages, agentSession, opt
162
345
  // as immutable once posted; a bounded insertion-ordered map keeps memory flat
163
346
  // for long-running workers.
164
347
  const THREAD_CONTEXT_CACHE_MAX = 500;
348
+ export const THREAD_CONTEXT_LOOKUP_MAX = 16;
349
+ export const THREAD_CONTEXT_BYTES_MAX = 256 * 1024;
350
+ const THREAD_CONTEXT_DEADLINE_MS = 1_000;
165
351
  const threadContextCache = new Map();
166
352
  function threadContextCacheKey(scopeId, targetMessageId) {
167
353
  return `${scopeId}:${targetMessageId}`;
@@ -199,20 +385,30 @@ export async function collectThreadContextMessages(input) {
199
385
  if (pendingIds.length === 0) {
200
386
  // Nothing quotes an out-of-window message (idle polls land here), so skip
201
387
  // storage-mode resolution entirely.
202
- return [];
388
+ return { messages: [], truncated: false };
203
389
  }
204
390
  const contextMessages = [];
391
+ let contextBytes = 0;
392
+ let lookups = 0;
393
+ let truncated = false;
394
+ const deadline = Date.now() + THREAD_CONTEXT_DEADLINE_MS;
205
395
  const useLocalStorage = Boolean(input.localRoomId && await isLocalRoomStorageEnabled(input.localRoomId));
206
396
  const localIdentifiers = useLocalStorage
207
397
  ? await resolveLocalRoomStorageIdentifiers(input.localRoomId)
208
398
  : { localRoomId: input.localRoomId };
209
399
  const sqliteRoomId = localIdentifiers.localRoomId || input.localRoomId;
210
400
  while (pendingIds.length > 0) {
401
+ if (lookups >= THREAD_CONTEXT_LOOKUP_MAX || Date.now() >= deadline) {
402
+ truncated = true;
403
+ break;
404
+ }
211
405
  const nextId = pendingIds.shift();
212
406
  if (!nextId || seenIds.has(nextId))
213
407
  continue;
214
408
  seenIds.add(nextId);
215
409
  const cached = threadContextCache.get(threadContextCacheKey(scopeId, nextId));
410
+ if (!cached)
411
+ lookups += 1;
216
412
  const message = cached
217
413
  ?? (useLocalStorage && input.localRoomId
218
414
  ? await findLocalMessageById(sqliteRoomId || input.localRoomId, nextId)
@@ -226,13 +422,19 @@ export async function collectThreadContextMessages(input) {
226
422
  if (!cached) {
227
423
  rememberThreadContextMessages(scopeId, [message]);
228
424
  }
425
+ const messageBytes = Buffer.byteLength(JSON.stringify(message), "utf8");
426
+ if (contextBytes + messageBytes > THREAD_CONTEXT_BYTES_MAX) {
427
+ truncated = true;
428
+ break;
429
+ }
430
+ contextBytes += messageBytes;
229
431
  contextMessages.push(message);
230
432
  const parentId = replyReferenceId(message);
231
433
  if (parentId && !seenIds.has(parentId)) {
232
434
  pendingIds.push(parentId);
233
435
  }
234
436
  }
235
- return contextMessages;
437
+ return { messages: contextMessages, truncated };
236
438
  }
237
439
  export function registerWaitForMessagesTool(server) {
238
440
  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'. Legacy/manual room workers may send a brief continue instruction when another legacy/manual participant posts a premature 'I will wait' closing line. Never send that nudge to or about a daemon-supervised participant, and daemon-supervised workers must not emit it; their supervisor owns wake and retry. Per-call wait is capped (default max 180s unless LETAGENTS_POLL_MAX_MS is set on API and MCP).", {
@@ -260,10 +462,21 @@ export function registerWaitForMessagesTool(server) {
260
462
  const targetRoomId = getTargetRoomId(room_id);
261
463
  const targetProjectId = getFallbackProjectId();
262
464
  const localRoomId = targetRoomId ?? currentRoom?.room_id ?? targetProjectId;
263
- const identity = await ensureAgentIdentity();
264
465
  const sessionRoomId = targetRoomId ?? currentRoom?.room_id ?? localRoomId ?? null;
265
- const agentSession = resolveWaitAgentSession(sessionRoomId, agent_session_id);
266
- if (agentSession) {
466
+ const custodial = isCustodialPolling();
467
+ const routingStateSnapshot = custodial ? { complete: true } : getStoredAgentRoutingStateSnapshot(sessionRoomId ?? "");
468
+ const localStorageEnabled = Boolean(localRoomId && await isLocalRoomStorageEnabled(localRoomId));
469
+ if (localStorageEnabled && !routingStateSnapshot.complete) {
470
+ throw new Error("Local agent routing state is unavailable; retry after restoring the state file.");
471
+ }
472
+ const exactIdentity = custodial ? await resolveWorkerToolIdentity({ roomId: sessionRoomId, agentSessionId: agent_session_id }) : null;
473
+ const identity = exactIdentity?.identity ?? await ensureAgentIdentity();
474
+ const agentSession = exactIdentity?.agentSession ?? resolveWaitAgentSession(sessionRoomId, agent_session_id);
475
+ if (custodial) {
476
+ if (!agentSession || !after_message_id)
477
+ throw new Error("Custodial polling requires exact worker identity and durable cursor.");
478
+ }
479
+ else if (agentSession) {
267
480
  // Registration (or a successor generation) must bind strictly once.
268
481
  // Later waits use a read-only exact verification capped at 250ms, so a
269
482
  // wedged daemon cannot consume the room-poll budget and an old worker
@@ -278,8 +491,8 @@ export function registerWaitForMessagesTool(server) {
278
491
  }
279
492
  const maxPollMs = getPollTimeoutCapMs();
280
493
  const serverTimeout = Math.min(Math.max(timeout || DEFAULT_POLL_TIMEOUT_MS, 1000), maxPollMs);
281
- if (localRoomId && await isLocalRoomStorageEnabled(localRoomId)) {
282
- const { localRoomId: sqliteRoomId } = await resolveLocalRoomStorageIdentifiers(localRoomId);
494
+ if (localRoomId && localStorageEnabled) {
495
+ const { localRoomId: sqliteRoomId, cloudRoomId, } = await resolveLocalRoomStorageIdentifiers(localRoomId);
283
496
  const effectiveLocalRoomId = sqliteRoomId || localRoomId;
284
497
  const effectiveAfterMessageId = resolveEffectiveAfterMessageId({
285
498
  requestedAfterMessageId: after_message_id,
@@ -290,11 +503,12 @@ export function registerWaitForMessagesTool(server) {
290
503
  // keep the unchanged forward "everything after the cursor" behavior.
291
504
  const existing = fetchPlan.mode === "catch_up_tail"
292
505
  ? await getLatestLocalChatMessages(effectiveLocalRoomId, {
293
- limit: fetchPlan.limit,
506
+ limit: Math.min(fetchPlan.limit, MAX_WAIT_MESSAGES_PER_CALL),
294
507
  include_prompt_only: true,
295
508
  })
296
509
  : await getLocalChatMessages(effectiveLocalRoomId, {
297
510
  after: effectiveAfterMessageId,
511
+ limit: MAX_WAIT_MESSAGES_PER_CALL,
298
512
  include_prompt_only: true,
299
513
  });
300
514
  const replayingExistingMessages = existing.messages.length > 0;
@@ -303,12 +517,18 @@ export function registerWaitForMessagesTool(server) {
303
517
  : await waitForLocalChatMessages(effectiveLocalRoomId, {
304
518
  after: effectiveAfterMessageId,
305
519
  timeoutMs: serverTimeout,
520
+ limit: MAX_WAIT_MESSAGES_PER_CALL,
306
521
  include_prompt_only: true,
307
522
  });
308
523
  const messages = await attachLocalActivationMetadata(effectiveLocalRoomId, result.messages, agentSession, {
309
524
  includeTaskOwnerLeases: !replayingExistingMessages,
525
+ activeSessionRoomId: cloudRoomId || sessionRoomId,
526
+ });
527
+ const bounded = boundAgentMessageOutput(messages, {
528
+ direction: fetchPlan.mode === "catch_up_tail" ? "suffix" : "prefix",
529
+ maxBytes: AGENT_MESSAGE_BODY_MAX_BYTES,
310
530
  });
311
- const routing = filterSilentActivationMessages(messages);
531
+ const routing = filterSilentActivationMessages(bounded.messages);
312
532
  const observedCursor = routing.last_observed_message_id ?? getLastMessageId(result);
313
533
  touchRoomSession(effectiveLocalRoomId, observedCursor);
314
534
  const threadContext = await collectThreadContextMessages({
@@ -320,8 +540,13 @@ export function registerWaitForMessagesTool(server) {
320
540
  return jsonToolResponse({
321
541
  room_id: effectiveLocalRoomId,
322
542
  ...addActivationRoutingTelemetry({
323
- messages: toAgentReadableMessages(routing.messages, threadContext),
543
+ messages: toAgentReadableMessages(routing.messages, threadContext.messages),
324
544
  }, routing),
545
+ ...(result.has_more || bounded.truncated ? { truncated: true } : {}),
546
+ ...(bounded.omittedMessageCount > 0
547
+ ? { omitted_message_count: bounded.omittedMessageCount }
548
+ : {}),
549
+ ...(threadContext.truncated ? { thread_context_truncated: true } : {}),
325
550
  });
326
551
  }
327
552
  await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, agentSession ? identityFromAgentSession(agentSession) : identity), agentSession);
@@ -333,6 +558,8 @@ export function registerWaitForMessagesTool(server) {
333
558
  const deliveryHeaders = buildAgentDeliveryHeaders(agentSession);
334
559
  const allMessages = [];
335
560
  let roomIdFromResponse;
561
+ let catchUpTruncated = false;
562
+ let apiObservedCursor = null;
336
563
  // Long-poll the server (blocks up to serverTimeout for new messages),
337
564
  // optionally seeded with a cursor. Shared by the cursor path and by the
338
565
  // no-cursor empty-tail fallback so a quiet room still blocks instead of
@@ -342,6 +569,7 @@ export function registerWaitForMessagesTool(server) {
342
569
  const params = new URLSearchParams();
343
570
  if (after)
344
571
  params.set("after", after);
572
+ params.set("limit", String(MAX_WAIT_MESSAGES_PER_CALL));
345
573
  params.set("timeout", String(serverTimeout));
346
574
  const queryString = params.toString();
347
575
  const firstResult = await roomScopedApiCall({
@@ -349,6 +577,7 @@ export function registerWaitForMessagesTool(server) {
349
577
  project_id: targetProjectId,
350
578
  room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages/poll?${queryString}`),
351
579
  project_path: (targetProjectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(targetProjectId)}/messages/poll?${queryString}`),
580
+ preserve_session_cursor: true,
352
581
  options: buildWaitForMessagesRequestOptions({
353
582
  deliveryHeaders,
354
583
  signal: AbortSignal.timeout(clientTimeout),
@@ -356,25 +585,10 @@ export function registerWaitForMessagesTool(server) {
356
585
  });
357
586
  allMessages.push(...(firstResult.messages ?? []));
358
587
  roomIdFromResponse = roomIdFromResponse || firstResult.room_id || firstResult.project_id;
359
- if (firstResult.has_more && allMessages.length > 0) {
360
- let afterCursor = allMessages[allMessages.length - 1]?.id;
361
- while (afterCursor) {
362
- const pageParams = new URLSearchParams();
363
- pageParams.set("after", afterCursor);
364
- const qs = pageParams.toString();
365
- const page = await roomScopedApiCall(buildWaitForMessagesHistoryPageRequest({
366
- targetRoomId,
367
- targetProjectId,
368
- queryString: qs,
369
- deliveryHeaders,
370
- }));
371
- const msgs = page.messages ?? [];
372
- allMessages.push(...msgs);
373
- if (!page.has_more || msgs.length === 0)
374
- break;
375
- afterCursor = msgs[msgs.length - 1]?.id;
376
- }
377
- }
588
+ catchUpTruncated = Boolean(firstResult.has_more);
589
+ apiObservedCursor = typeof firstResult.last_observed_message_id === "string"
590
+ ? firstResult.last_observed_message_id
591
+ : null;
378
592
  };
379
593
  if (fetchPlan.mode === "catch_up_tail") {
380
594
  // No cursor: catch up on only the bounded recent tail. Mirror
@@ -385,6 +599,7 @@ export function registerWaitForMessagesTool(server) {
385
599
  targetRoomId,
386
600
  targetProjectId,
387
601
  limit: fetchPlan.limit,
602
+ deliveryHeaders,
388
603
  });
389
604
  if (recent.messages.length > 0) {
390
605
  allMessages.push(...recent.messages);
@@ -402,7 +617,11 @@ export function registerWaitForMessagesTool(server) {
402
617
  else {
403
618
  await longPollFromCursor(fetchPlan.after);
404
619
  }
405
- const routing = filterSilentActivationMessages(allMessages);
620
+ const bounded = boundAgentMessageOutput(allMessages, {
621
+ direction: fetchPlan.mode === "catch_up_tail" ? "suffix" : "prefix",
622
+ maxBytes: AGENT_MESSAGE_BODY_MAX_BYTES,
623
+ });
624
+ const routing = filterSilentActivationMessages(bounded.messages);
406
625
  const threadContext = await collectThreadContextMessages({
407
626
  messages: routing.messages,
408
627
  localRoomId,
@@ -410,13 +629,26 @@ export function registerWaitForMessagesTool(server) {
410
629
  projectId: targetProjectId,
411
630
  });
412
631
  const output = addActivationRoutingTelemetry({
413
- messages: toAgentReadableMessages(routing.messages, threadContext),
632
+ messages: toAgentReadableMessages(routing.messages, threadContext.messages),
414
633
  }, routing);
634
+ if (threadContext.truncated)
635
+ output.thread_context_truncated = true;
415
636
  if (roomIdFromResponse) {
416
637
  output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
417
638
  }
639
+ if (catchUpTruncated || bounded.truncated)
640
+ output.truncated = true;
641
+ if (bounded.omittedMessageCount > 0) {
642
+ output.omitted_message_count = bounded.omittedMessageCount;
643
+ }
644
+ // The API cursor can cover concealed messages, but not visible messages
645
+ // omitted by our own byte bound. Resume after the retained page instead.
646
+ const observedCursor = !bounded.truncated && apiObservedCursor
647
+ ? apiObservedCursor
648
+ : routing.last_observed_message_id ?? undefined;
649
+ if (observedCursor)
650
+ output.last_observed_message_id = observedCursor;
418
651
  if (targetRoomId) {
419
- const observedCursor = routing.last_observed_message_id ?? getLastMessageId(output);
420
652
  touchRoomSession(targetRoomId, observedCursor);
421
653
  if (allMessages.length > 0 && agentSession) {
422
654
  const firstMsg = allMessages[0];
@@ -83,7 +83,7 @@ export function registerGetOnboardingStatusTool(server) {
83
83
  const storedAuth = getStoredAuth();
84
84
  const pendingAuth = getPendingDeviceAuth();
85
85
  const savedCurrentRoom = getStoredCurrentRoom();
86
- const detectedRoom = configGitContext?.activeRoom ?? gitContext?.activeRoom ?? null;
86
+ const detectedRoom = configGitContext?.activeRoomLocator ?? gitContext?.activeRoomLocator ?? null;
87
87
  const api_health = await checkOnboardingApiHealth();
88
88
  const authenticated = Boolean(process.env.LETAGENTS_TOKEN || storedAuth);
89
89
  let nextStep = "join_room";
@@ -113,10 +113,10 @@ export function registerGetOnboardingStatusTool(server) {
113
113
  saved_current_room: toPublicStoredRoomSession(savedCurrentRoom),
114
114
  detected_room_from_context: detectedRoom,
115
115
  configured_room_from_file: configRoom,
116
- configured_active_room_from_context: configGitContext?.activeRoom ?? null,
116
+ configured_active_room_from_context: configGitContext?.activeRoomLocator ?? null,
117
117
  derived_repo_room_from_git: gitContext?.repoRoom ?? null,
118
- derived_active_git_room: gitContext?.activeRoom ?? null,
119
- derived_branch_room_from_git: gitContext?.activeRefRoom ?? null,
118
+ derived_active_git_room: gitContext?.activeRoomLocator ?? null,
119
+ derived_branch_room_from_git: gitContext?.activeRefRoomLocator ?? null,
120
120
  git_current_branch: gitContext?.currentBranch ?? configGitContext?.currentBranch ?? null,
121
121
  git_default_branch: gitContext?.defaultBranch ?? configGitContext?.defaultBranch ?? null,
122
122
  repo_root: repoRoot,
@@ -3,8 +3,10 @@ import { join } from "path";
3
3
  import { z } from "zod";
4
4
  import { getRoomFromConfig } from "../../../config-reader.js";
5
5
  import { buildActiveGitRoomContext, getGitCurrentBranch, getGitDefaultBranch, getGitRoomContext, } from "../../../git-remote.js";
6
- import { currentAgentIdentity, currentAgentIdentityKey, currentRoom, getConversationIdentity, getCurrentLiveSessionPayload, getStoredAgentIdentity, listStoredCodexLiveSessions, toPublicAgentIdentity, toPublicCurrentRoomState, withJoinRoomAgentPrompt, } from "../../runtime.js";
6
+ import { currentAgentIdentity, currentAgentIdentityKey, currentRoomMatchesLocator, currentRoom, getConversationIdentity, getCurrentLiveSessionPayload, getStoredAgentIdentity, listStoredCodexLiveSessions, toPublicAgentIdentity, toPublicCurrentRoomState, withJoinRoomAgentPrompt, } from "../../runtime.js";
7
7
  import { requireValidWorkerBearerRuntime } from "../../runtime/worker-bearer.js";
8
+ import { getDaemonToolExecutionContext, getRuntimeWorkingDirectory } from "../../runtime/daemon-tool-context.js";
9
+ import { identityFromAgentSession } from "../../runtime/agent-sessions.js";
8
10
  import { findExistingConfig, resolveGitRoot, } from "../../repo-context.js";
9
11
  import { jsonToolResponse } from "./response.js";
10
12
  let ownerAuthStoreLoader = () => import("../../../local-state.js");
@@ -29,6 +31,7 @@ export function registerRoomInspectionTools(server) {
29
31
  }
30
32
  export async function getCurrentRoomPayload(conversationId) {
31
33
  const runtime = requireValidWorkerBearerRuntime();
34
+ const daemonContext = getDaemonToolExecutionContext();
32
35
  const publicCurrentRoom = toPublicCurrentRoomState();
33
36
  const workerAuth = runtime.mode !== "owner"
34
37
  ? { source: runtime.mode === "worker" ? "worker_bearer" : "daemon_supervised", expires_at: null, account: null }
@@ -55,9 +58,11 @@ export async function getCurrentRoomPayload(conversationId) {
55
58
  ...publicCurrentRoom,
56
59
  ...(runtime.mode === "supervised" ? { room_binding: "daemon_supervised" } : {}),
57
60
  ...localCodexDetails,
58
- agent_identity: toPublicAgentIdentity(getConversationIdentity(conversationId)
59
- ?? currentAgentIdentity
60
- ?? getStoredAgentIdentity(currentAgentIdentityKey)),
61
+ agent_identity: toPublicAgentIdentity(daemonContext
62
+ ? identityFromAgentSession(daemonContext.agentSession)
63
+ : getConversationIdentity(conversationId)
64
+ ?? currentAgentIdentity
65
+ ?? getStoredAgentIdentity(currentAgentIdentityKey)),
61
66
  auth: workerAuth ?? (auth
62
67
  ? {
63
68
  source: process.env.LETAGENTS_TOKEN ? "env" : "local_state",
@@ -73,7 +78,7 @@ export async function getCurrentRoomPayload(conversationId) {
73
78
  }
74
79
  function getRepoInspectionPayload(targetDir) {
75
80
  const runtime = requireValidWorkerBearerRuntime();
76
- const startDir = targetDir || process.cwd();
81
+ const startDir = targetDir || getRuntimeWorkingDirectory();
77
82
  const repoRoot = resolveGitRoot(startDir);
78
83
  const configDir = repoRoot ? findExistingConfig(startDir) : null;
79
84
  const configPath = configDir ? join(configDir, ".letagents.json") : null;
@@ -86,8 +91,8 @@ function getRepoInspectionPayload(targetDir) {
86
91
  defaultBranch: repoRoot ? getGitDefaultBranch(repoRoot) : null,
87
92
  })
88
93
  : null;
89
- const detectedRoom = configGitContext?.activeRoom ?? gitContext?.activeRoom ?? null;
90
- const currentRoomMatchesContext = Boolean(currentRoom && detectedRoom && currentRoom.room_id === detectedRoom);
94
+ const detectedRoom = configGitContext?.activeRoomLocator ?? gitContext?.activeRoomLocator ?? null;
95
+ const currentRoomMatchesContext = currentRoomMatchesLocator(detectedRoom);
91
96
  return {
92
97
  cwd: startDir,
93
98
  repo_context_status: repoRoot ? "git_repo_detected" : "not_inside_git_repo",
@@ -95,10 +100,10 @@ function getRepoInspectionPayload(targetDir) {
95
100
  config_file: configPath ?? null,
96
101
  config_contents: readConfigContents(configPath),
97
102
  configured_room_from_file: configuredRoom ?? null,
98
- configured_active_room_from_context: configGitContext?.activeRoom ?? null,
99
- derived_room_from_git: gitContext?.activeRoom ?? null,
103
+ configured_active_room_from_context: configGitContext?.activeRoomLocator ?? null,
104
+ derived_room_from_git: gitContext?.activeRoomLocator ?? null,
100
105
  derived_repo_room_from_git: gitContext?.repoRoom ?? null,
101
- derived_branch_room_from_git: gitContext?.activeRefRoom ?? null,
106
+ derived_branch_room_from_git: gitContext?.activeRefRoomLocator ?? null,
102
107
  git_current_branch: gitContext?.currentBranch ?? configGitContext?.currentBranch ?? null,
103
108
  git_default_branch: gitContext?.defaultBranch ?? configGitContext?.defaultBranch ?? null,
104
109
  detected_room_from_context: detectedRoom ?? null,