letagents 0.12.11 → 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.
- package/dist/mcp/git-remote.js +7 -7
- package/dist/mcp/local-state/agent-sessions.js +78 -5
- package/dist/mcp/local-state/local-chat.js +189 -48
- package/dist/mcp/local-state/storage.js +16 -9
- package/dist/mcp/server/daemon-tool-executor.js +92 -0
- package/dist/mcp/server/register-tools.js +25 -12
- package/dist/mcp/server/runtime/agent-sessions.js +58 -9
- package/dist/mcp/server/runtime/api.js +40 -4
- package/dist/mcp/server/runtime/daemon-tool-context.js +11 -0
- package/dist/mcp/server/runtime/execution-profile.js +19 -0
- package/dist/mcp/server/runtime/identity/directory.js +2 -2
- package/dist/mcp/server/runtime/messages.js +55 -0
- package/dist/mcp/server/runtime/presence.js +4 -2
- package/dist/mcp/server/runtime/room-api.js +24 -7
- package/dist/mcp/server/runtime/room-state.js +59 -3
- package/dist/mcp/server/runtime/rooms.js +67 -32
- package/dist/mcp/server/runtime/supervised-room-authority.js +8 -0
- package/dist/mcp/server/runtime/supervisor-bridge.js +702 -24
- package/dist/mcp/server/runtime/tool-surface-policy.js +26 -0
- package/dist/mcp/server/runtime/worker-bearer.js +44 -6
- package/dist/mcp/server/runtime-contract.js +27 -0
- package/dist/mcp/server/runtime.js +15 -4
- package/dist/mcp/server/supervised-tool-facade.js +134 -0
- package/dist/mcp/server/tools/agent-sessions.js +74 -7
- package/dist/mcp/server/tools/messages/index.js +3 -2
- package/dist/mcp/server/tools/messages/read-tool.js +54 -97
- package/dist/mcp/server/tools/messages/reasoning-tool.js +2 -0
- package/dist/mcp/server/tools/messages/send-tool.js +5 -0
- package/dist/mcp/server/tools/messages/status-tool.js +2 -0
- package/dist/mcp/server/tools/messages/wait-tool.js +344 -71
- package/dist/mcp/server/tools/onboarding/status-tool.js +9 -8
- package/dist/mcp/server/tools/rooms/inspection-tools.js +40 -22
- package/dist/mcp/server/tools/rooms/repo-initialization-tool.js +2 -1
- package/dist/mcp/server/tools/supervised-room-turn.js +42 -0
- package/dist/mcp/server/tools/tasks/board-tools.js +34 -2
- package/dist/mcp/server.js +14 -7
- package/dist/mcp/sse-client.js +163 -20
- package/dist/shared/activation-routing.js +187 -20
- package/dist/shared/agent-presence.js +6 -0
- package/dist/shared/desktop-release-manifest.js +63 -0
- package/dist/shared/desktop-release.js +60 -0
- package/dist/shared/scoped-ids.js +6 -0
- package/package.json +11 -3
- package/shared/message-contracts.d.mts +32 -0
- package/shared/message-contracts.mjs +109 -0
- package/shared/routing-aliases.d.mts +18 -0
- package/shared/routing-aliases.mjs +66 -0
- package/shared/sqlite-thread-routing.d.mts +72 -0
- package/shared/sqlite-thread-routing.mjs +1038 -0
|
@@ -1,9 +1,10 @@
|
|
|
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 { appendIncludePromptOnly, buildAgentDeliveryHeaders, currentRoom, ensureAgentIdentity, getFallbackProjectId, getLatestLocalChatMessages, getLocalChatMessages, getLastMessageId, getRememberedRoomPresence, getTargetRoomId, identityFromAgentSession, isLocalRoomStorageEnabled,
|
|
5
|
-
import { requireValidWorkerBearerRuntime } 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, supervisedBoundedDeliveryDisabledToolResult, } from "../../runtime/worker-bearer.js";
|
|
6
|
+
import { attachAgentMessageActivations, createGlobalAgentAddressResolver, decideAgentMessageActivation, isTaskOwnerFollowUpMessageText, } from "../../../../shared/activation-routing.js";
|
|
7
|
+
import { normalizeRoutingSender } from "../../../../../shared/routing-aliases.mjs";
|
|
7
8
|
import { findLocalMessageById, findRemoteMessageById } from "./message-lookup.js";
|
|
8
9
|
import { fetchRecentRemoteMessages } from "./read-tool.js";
|
|
9
10
|
import { jsonToolResponse } from "./response.js";
|
|
@@ -13,9 +14,9 @@ const DEFAULT_POLL_TIMEOUT_MS = 30000;
|
|
|
13
14
|
// room history (busy rooms archive millions of characters, which blows the
|
|
14
15
|
// tool's token budget). This bounds that no-cursor catch-up to a recent tail.
|
|
15
16
|
export const DEFAULT_WAIT_CATCHUP_LIMIT = 100;
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
//
|
|
17
|
+
export const MAX_WAIT_MESSAGES_PER_CALL = 100;
|
|
18
|
+
// Decide how the initial fetch should behave. Both paths return one bounded
|
|
19
|
+
// page; callers continue from last_observed_message_id when truncated.
|
|
19
20
|
export function planWaitForMessagesFetch(input) {
|
|
20
21
|
if (input.effectiveAfterMessageId) {
|
|
21
22
|
return { mode: "after_cursor", after: input.effectiveAfterMessageId };
|
|
@@ -25,23 +26,12 @@ export function planWaitForMessagesFetch(input) {
|
|
|
25
26
|
limit: input.catchupLimit ?? DEFAULT_WAIT_CATCHUP_LIMIT,
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
|
-
const LOCAL_TASK_OWNER_STATUSES = new Set(["assigned", "in_progress", "blocked", "in_review"]);
|
|
29
29
|
export function buildWaitForMessagesRequestOptions(input) {
|
|
30
30
|
return {
|
|
31
31
|
...(input.signal ? { signal: input.signal } : {}),
|
|
32
32
|
headers: input.deliveryHeaders,
|
|
33
33
|
};
|
|
34
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
35
|
function isRecord(value) {
|
|
46
36
|
return Boolean(value && typeof value === "object");
|
|
47
37
|
}
|
|
@@ -118,41 +108,233 @@ export function resolveWaitAgentSession(roomId, agentSessionId) {
|
|
|
118
108
|
return resolveAgentSession(roomId, agentSessionId);
|
|
119
109
|
}
|
|
120
110
|
export async function localActivationContext(roomId) {
|
|
121
|
-
const result = await listLocalTasks(roomId, { openOnly: true });
|
|
122
111
|
return {
|
|
123
|
-
activeTaskLeases:
|
|
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
|
-
}),
|
|
112
|
+
activeTaskLeases: await listLocalActiveTaskOwnerLeases(roomId),
|
|
138
113
|
};
|
|
139
114
|
}
|
|
140
|
-
async function attachLocalActivationMetadata(roomId, messages, agentSession, options = {}) {
|
|
115
|
+
export async function attachLocalActivationMetadata(roomId, messages, agentSession, options = {}) {
|
|
141
116
|
if (!agentSession || agentSession.session_kind !== "worker") {
|
|
142
117
|
return messages;
|
|
143
118
|
}
|
|
144
119
|
const records = messages.filter(isRecord);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return attachAgentMessageActivations(records, {
|
|
120
|
+
if (records.length === 0)
|
|
121
|
+
return messages;
|
|
122
|
+
const identity = {
|
|
149
123
|
actor_label: agentSession.actor_label,
|
|
150
124
|
agent_key: agentSession.agent_key,
|
|
151
125
|
agent_instance_id: agentSession.agent_instance_id ?? null,
|
|
152
126
|
agent_session_id: agentSession.session_id,
|
|
153
127
|
display_name: agentSession.display_name,
|
|
154
128
|
session_kind: agentSession.session_kind,
|
|
155
|
-
}
|
|
129
|
+
};
|
|
130
|
+
// Imported cloud rows carry immutable, account-scoped send-time authority.
|
|
131
|
+
// A present wrapper always wins over mutable local aliases, including when
|
|
132
|
+
// the current state population cannot be read completely.
|
|
133
|
+
const activeSessionRoomId = options.activeSessionRoomId?.trim() || roomId;
|
|
134
|
+
const routingState = getStoredAgentRoutingStateSnapshot(activeSessionRoomId);
|
|
135
|
+
const authoritativeLegacyDecisions = new Map();
|
|
136
|
+
const legacyRecords = [];
|
|
137
|
+
const normalizedIdentityKey = normalizeRoutingSender(identity.agent_key);
|
|
138
|
+
const validActivationReasons = new Set([
|
|
139
|
+
"self_message",
|
|
140
|
+
"explicit_mention",
|
|
141
|
+
"explicit_other_mention",
|
|
142
|
+
"broadcast",
|
|
143
|
+
"reply_target",
|
|
144
|
+
"other_reply_target",
|
|
145
|
+
"thread_participant",
|
|
146
|
+
"task_owner",
|
|
147
|
+
"system_event",
|
|
148
|
+
"unaddressed",
|
|
149
|
+
]);
|
|
150
|
+
for (const message of records) {
|
|
151
|
+
const id = messageId(message);
|
|
152
|
+
if (!id)
|
|
153
|
+
continue;
|
|
154
|
+
const imported = getLocalImportedRoutingAuthority(message);
|
|
155
|
+
if (!imported) {
|
|
156
|
+
if (routingState.complete) {
|
|
157
|
+
legacyRecords.push(message);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
authoritativeLegacyDecisions.set(id, {
|
|
161
|
+
decision: "silent",
|
|
162
|
+
reason: "unaddressed",
|
|
163
|
+
addressed: false,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!routingState.accountReaderKey
|
|
169
|
+
|| imported.readerKey !== routingState.accountReaderKey
|
|
170
|
+
|| imported.routing.authority === "invalid") {
|
|
171
|
+
authoritativeLegacyDecisions.set(id, {
|
|
172
|
+
decision: "silent",
|
|
173
|
+
reason: "unaddressed",
|
|
174
|
+
addressed: false,
|
|
175
|
+
});
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const target = imported.routing.recipientSessions.find((candidate) => candidate.agentKey === normalizedIdentityKey);
|
|
179
|
+
const targetSessionId = imported.routing.authority === "receipts"
|
|
180
|
+
? target && "successorAgentSessionId" in target
|
|
181
|
+
? target.successorAgentSessionId ?? target.agentSessionId
|
|
182
|
+
: target?.agentSessionId
|
|
183
|
+
: target?.agentSessionId;
|
|
184
|
+
if (!target || targetSessionId !== identity.agent_session_id) {
|
|
185
|
+
authoritativeLegacyDecisions.set(id, {
|
|
186
|
+
decision: "silent",
|
|
187
|
+
reason: "unaddressed",
|
|
188
|
+
addressed: false,
|
|
189
|
+
});
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const importedReason = imported.routing.authority === "legacy"
|
|
193
|
+
&& "activationReason" in target
|
|
194
|
+
&& validActivationReasons.has(target.activationReason)
|
|
195
|
+
? target.activationReason
|
|
196
|
+
: "explicit_mention";
|
|
197
|
+
authoritativeLegacyDecisions.set(id, {
|
|
198
|
+
decision: "activate",
|
|
199
|
+
reason: importedReason,
|
|
200
|
+
addressed: true,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
if (legacyRecords.length === 0) {
|
|
204
|
+
return attachAgentMessageActivations(records, identity, { authoritativeLegacyDecisions });
|
|
205
|
+
}
|
|
206
|
+
const threadRootIds = legacyRecords.flatMap((message) => {
|
|
207
|
+
const messageId = typeof message.id === "string" ? message.id : "";
|
|
208
|
+
const thread = isRecord(message.thread) ? message.thread : null;
|
|
209
|
+
const rootId = typeof message.thread_root_id === "string"
|
|
210
|
+
? message.thread_root_id
|
|
211
|
+
: typeof thread?.root_message_id === "string"
|
|
212
|
+
? thread.root_message_id
|
|
213
|
+
: "";
|
|
214
|
+
return rootId && rootId !== messageId ? [rootId] : [];
|
|
215
|
+
});
|
|
216
|
+
// Linked local/cloud rooms store messages under the SQLite room id while
|
|
217
|
+
// registered workers remain keyed by the canonical cloud room id. Routing
|
|
218
|
+
// ambiguity must always use that complete canonical population.
|
|
219
|
+
const storedActiveSessions = routingState.sessions;
|
|
220
|
+
const activeIdentities = storedActiveSessions.map((session) => ({
|
|
221
|
+
actor_label: session.actor_label,
|
|
222
|
+
agent_key: session.agent_key,
|
|
223
|
+
agent_instance_id: session.agent_instance_id ?? null,
|
|
224
|
+
agent_session_id: session.session_id,
|
|
225
|
+
display_name: session.display_name,
|
|
226
|
+
session_kind: session.session_kind,
|
|
227
|
+
}));
|
|
228
|
+
if (!activeIdentities.some((candidate) => candidate.agent_session_id === identity.agent_session_id
|
|
229
|
+
&& candidate.agent_key === identity.agent_key)) {
|
|
230
|
+
// The request's authenticated current session is authoritative even when
|
|
231
|
+
// an older local-state file has not persisted it yet.
|
|
232
|
+
activeIdentities.push(identity);
|
|
233
|
+
}
|
|
234
|
+
const sameKeySessions = storedActiveSessions.filter((session) => session.agent_key === identity.agent_key);
|
|
235
|
+
const currentRepresentativeSessionId = sameKeySessions[0]?.session_id
|
|
236
|
+
?? identity.agent_session_id;
|
|
237
|
+
const currentIsActive = currentRepresentativeSessionId === identity.agent_session_id;
|
|
238
|
+
const resolveGlobalAddress = createGlobalAgentAddressResolver(activeIdentities);
|
|
239
|
+
const explicitMentionMessageIds = new Set();
|
|
240
|
+
const replyTargetMessageIds = new Set();
|
|
241
|
+
const exactReplyTargetMessageIds = new Set();
|
|
242
|
+
const selfMessageIds = new Set();
|
|
243
|
+
for (const message of legacyRecords) {
|
|
244
|
+
const messageId = typeof message.id === "string" ? message.id : "";
|
|
245
|
+
if (!messageId)
|
|
246
|
+
continue;
|
|
247
|
+
const addressed = resolveGlobalAddress(message);
|
|
248
|
+
const reply = isRecord(message.reply_to) ? message.reply_to : null;
|
|
249
|
+
const replyPublisherIdentity = isRecord(reply?.agent_identity)
|
|
250
|
+
? reply.agent_identity
|
|
251
|
+
: null;
|
|
252
|
+
const replyPublisherKey = typeof replyPublisherIdentity?.agent_key === "string"
|
|
253
|
+
? replyPublisherIdentity.agent_key.trim()
|
|
254
|
+
: "";
|
|
255
|
+
const replyPublisherSessionId = typeof replyPublisherIdentity?.agent_session_id === "string"
|
|
256
|
+
? replyPublisherIdentity.agent_session_id.trim()
|
|
257
|
+
: "";
|
|
258
|
+
if (replyPublisherKey) {
|
|
259
|
+
addressed.replyTargetKeys.clear();
|
|
260
|
+
const exactReplyIdentity = replyPublisherSessionId
|
|
261
|
+
? activeIdentities.find((candidate) => candidate.agent_key === replyPublisherKey
|
|
262
|
+
&& candidate.agent_session_id === replyPublisherSessionId)
|
|
263
|
+
: undefined;
|
|
264
|
+
if (exactReplyIdentity) {
|
|
265
|
+
if (exactReplyIdentity.agent_session_id === identity.agent_session_id) {
|
|
266
|
+
exactReplyTargetMessageIds.add(messageId);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
else if (activeIdentities.some((candidate) => candidate.agent_key === replyPublisherKey)) {
|
|
270
|
+
addressed.replyTargetKeys.add(replyPublisherKey);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const publisherIdentity = isRecord(message.agent_identity)
|
|
274
|
+
? message.agent_identity
|
|
275
|
+
: null;
|
|
276
|
+
const publisherAgentKey = typeof publisherIdentity?.agent_key === "string"
|
|
277
|
+
? publisherIdentity.agent_key.trim()
|
|
278
|
+
: "";
|
|
279
|
+
if (String(message.source ?? "").trim() === "agent"
|
|
280
|
+
&& (publisherAgentKey
|
|
281
|
+
? publisherAgentKey === identity.agent_key
|
|
282
|
+
: addressed.senderKeys.has(identity.agent_key))) {
|
|
283
|
+
selfMessageIds.add(messageId);
|
|
284
|
+
}
|
|
285
|
+
if (addressed.explicitMentionKeys.has(identity.agent_key)) {
|
|
286
|
+
explicitMentionMessageIds.add(messageId);
|
|
287
|
+
}
|
|
288
|
+
const thread = isRecord(message.thread) ? message.thread : null;
|
|
289
|
+
const rootId = typeof message.thread_root_id === "string"
|
|
290
|
+
? message.thread_root_id
|
|
291
|
+
: typeof thread?.root_message_id === "string"
|
|
292
|
+
? thread.root_message_id
|
|
293
|
+
: "";
|
|
294
|
+
if ((!rootId || rootId === messageId)
|
|
295
|
+
&& (exactReplyTargetMessageIds.has(messageId)
|
|
296
|
+
|| addressed.replyTargetKeys.has(identity.agent_key))) {
|
|
297
|
+
replyTargetMessageIds.add(messageId);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const [activationContext, projectedThreadParticipantRootIds] = await Promise.all([
|
|
301
|
+
options.includeTaskOwnerLeases === false
|
|
302
|
+
|| !records.some((message) => isTaskOwnerFollowUpMessageText(message.text))
|
|
303
|
+
? undefined
|
|
304
|
+
: localActivationContext(roomId),
|
|
305
|
+
getLocalChatThreadRoutingMembership(roomId, threadRootIds, identity, activeIdentities),
|
|
306
|
+
]);
|
|
307
|
+
const exactContext = {
|
|
308
|
+
...activationContext,
|
|
309
|
+
selfMessageIds,
|
|
310
|
+
threadParticipantRootIds: projectedThreadParticipantRootIds,
|
|
311
|
+
explicitMentionMessageIds,
|
|
312
|
+
replyTargetMessageIds,
|
|
313
|
+
};
|
|
314
|
+
const exactTaskSessionIdsForKey = new Set((activationContext?.activeTaskLeases ?? [])
|
|
315
|
+
.filter((lease) => lease.status === "active"
|
|
316
|
+
&& lease.agent_key === identity.agent_key
|
|
317
|
+
&& Boolean(lease.agent_session_id))
|
|
318
|
+
.map((lease) => lease.agent_session_id));
|
|
319
|
+
for (const message of legacyRecords) {
|
|
320
|
+
const messageId = typeof message.id === "string" ? message.id : "";
|
|
321
|
+
if (!messageId)
|
|
322
|
+
continue;
|
|
323
|
+
const resolved = decideAgentMessageActivation(message, identity, exactContext);
|
|
324
|
+
const exactTaskOwner = resolved.reason === "task_owner"
|
|
325
|
+
&& activationContext?.activeTaskLeases?.some((lease) => lease.status === "active"
|
|
326
|
+
&& lease.agent_session_id === identity.agent_session_id
|
|
327
|
+
&& (!lease.agent_key || lease.agent_key === identity.agent_key));
|
|
328
|
+
const eligibleRepresentative = exactReplyTargetMessageIds.has(messageId)
|
|
329
|
+
? true
|
|
330
|
+
: resolved.reason === "task_owner" && exactTaskSessionIdsForKey.size > 0
|
|
331
|
+
? exactTaskSessionIdsForKey.size === 1 && exactTaskOwner
|
|
332
|
+
: currentIsActive;
|
|
333
|
+
authoritativeLegacyDecisions.set(messageId, eligibleRepresentative && resolved.decision === "activate"
|
|
334
|
+
? resolved
|
|
335
|
+
: { decision: "silent", reason: "unaddressed", addressed: false });
|
|
336
|
+
}
|
|
337
|
+
return attachAgentMessageActivations(records, identity, { authoritativeLegacyDecisions });
|
|
156
338
|
}
|
|
157
339
|
// Resolving an out-of-window parent costs a lookup (a by-id fetch, or a
|
|
158
340
|
// page-by-page history scan against older APIs), so resolved (and observed)
|
|
@@ -162,6 +344,9 @@ async function attachLocalActivationMetadata(roomId, messages, agentSession, opt
|
|
|
162
344
|
// as immutable once posted; a bounded insertion-ordered map keeps memory flat
|
|
163
345
|
// for long-running workers.
|
|
164
346
|
const THREAD_CONTEXT_CACHE_MAX = 500;
|
|
347
|
+
export const THREAD_CONTEXT_LOOKUP_MAX = 16;
|
|
348
|
+
export const THREAD_CONTEXT_BYTES_MAX = 256 * 1024;
|
|
349
|
+
const THREAD_CONTEXT_DEADLINE_MS = 1_000;
|
|
165
350
|
const threadContextCache = new Map();
|
|
166
351
|
function threadContextCacheKey(scopeId, targetMessageId) {
|
|
167
352
|
return `${scopeId}:${targetMessageId}`;
|
|
@@ -199,20 +384,30 @@ export async function collectThreadContextMessages(input) {
|
|
|
199
384
|
if (pendingIds.length === 0) {
|
|
200
385
|
// Nothing quotes an out-of-window message (idle polls land here), so skip
|
|
201
386
|
// storage-mode resolution entirely.
|
|
202
|
-
return [];
|
|
387
|
+
return { messages: [], truncated: false };
|
|
203
388
|
}
|
|
204
389
|
const contextMessages = [];
|
|
390
|
+
let contextBytes = 0;
|
|
391
|
+
let lookups = 0;
|
|
392
|
+
let truncated = false;
|
|
393
|
+
const deadline = Date.now() + THREAD_CONTEXT_DEADLINE_MS;
|
|
205
394
|
const useLocalStorage = Boolean(input.localRoomId && await isLocalRoomStorageEnabled(input.localRoomId));
|
|
206
395
|
const localIdentifiers = useLocalStorage
|
|
207
396
|
? await resolveLocalRoomStorageIdentifiers(input.localRoomId)
|
|
208
397
|
: { localRoomId: input.localRoomId };
|
|
209
398
|
const sqliteRoomId = localIdentifiers.localRoomId || input.localRoomId;
|
|
210
399
|
while (pendingIds.length > 0) {
|
|
400
|
+
if (lookups >= THREAD_CONTEXT_LOOKUP_MAX || Date.now() >= deadline) {
|
|
401
|
+
truncated = true;
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
211
404
|
const nextId = pendingIds.shift();
|
|
212
405
|
if (!nextId || seenIds.has(nextId))
|
|
213
406
|
continue;
|
|
214
407
|
seenIds.add(nextId);
|
|
215
408
|
const cached = threadContextCache.get(threadContextCacheKey(scopeId, nextId));
|
|
409
|
+
if (!cached)
|
|
410
|
+
lookups += 1;
|
|
216
411
|
const message = cached
|
|
217
412
|
?? (useLocalStorage && input.localRoomId
|
|
218
413
|
? await findLocalMessageById(sqliteRoomId || input.localRoomId, nextId)
|
|
@@ -226,16 +421,22 @@ export async function collectThreadContextMessages(input) {
|
|
|
226
421
|
if (!cached) {
|
|
227
422
|
rememberThreadContextMessages(scopeId, [message]);
|
|
228
423
|
}
|
|
424
|
+
const messageBytes = Buffer.byteLength(JSON.stringify(message), "utf8");
|
|
425
|
+
if (contextBytes + messageBytes > THREAD_CONTEXT_BYTES_MAX) {
|
|
426
|
+
truncated = true;
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
contextBytes += messageBytes;
|
|
229
430
|
contextMessages.push(message);
|
|
230
431
|
const parentId = replyReferenceId(message);
|
|
231
432
|
if (parentId && !seenIds.has(parentId)) {
|
|
232
433
|
pendingIds.push(parentId);
|
|
233
434
|
}
|
|
234
435
|
}
|
|
235
|
-
return contextMessages;
|
|
436
|
+
return { messages: contextMessages, truncated };
|
|
236
437
|
}
|
|
237
438
|
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'.
|
|
439
|
+
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).", {
|
|
239
440
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room."),
|
|
240
441
|
after_message_id: z
|
|
241
442
|
.string()
|
|
@@ -250,16 +451,41 @@ export function registerWaitForMessagesTool(server) {
|
|
|
250
451
|
.optional()
|
|
251
452
|
.describe("Registered agent session to use. Without this, the MCP transport is treated as controller traffic and is hidden from connected-agent activity."),
|
|
252
453
|
}, async ({ room_id, after_message_id, timeout, agent_session_id }) => {
|
|
454
|
+
// This guard intentionally runs before room resolution, identity setup,
|
|
455
|
+
// local SQLite reads, presence writes, or HTTP traffic. In supervised
|
|
456
|
+
// bounded-turn mode the daemon is the sole inbox owner.
|
|
457
|
+
const boundedDeliveryDisabled = supervisedBoundedDeliveryDisabledToolResult();
|
|
458
|
+
if (boundedDeliveryDisabled) {
|
|
459
|
+
return jsonToolResponse(boundedDeliveryDisabled);
|
|
460
|
+
}
|
|
253
461
|
const targetRoomId = getTargetRoomId(room_id);
|
|
254
462
|
const targetProjectId = getFallbackProjectId();
|
|
255
463
|
const localRoomId = targetRoomId ?? currentRoom?.room_id ?? targetProjectId;
|
|
256
|
-
const identity = await ensureAgentIdentity();
|
|
257
464
|
const sessionRoomId = targetRoomId ?? currentRoom?.room_id ?? localRoomId ?? null;
|
|
465
|
+
const routingStateSnapshot = getStoredAgentRoutingStateSnapshot(sessionRoomId ?? "");
|
|
466
|
+
const localStorageEnabled = Boolean(localRoomId && await isLocalRoomStorageEnabled(localRoomId));
|
|
467
|
+
if (localStorageEnabled && !routingStateSnapshot.complete) {
|
|
468
|
+
throw new Error("Local agent routing state is unavailable; retry after restoring the state file.");
|
|
469
|
+
}
|
|
470
|
+
const identity = await ensureAgentIdentity();
|
|
258
471
|
const agentSession = resolveWaitAgentSession(sessionRoomId, agent_session_id);
|
|
472
|
+
if (agentSession) {
|
|
473
|
+
// Registration (or a successor generation) must bind strictly once.
|
|
474
|
+
// Later waits use a read-only exact verification capped at 250ms, so a
|
|
475
|
+
// wedged daemon cannot consume the room-poll budget and an old worker
|
|
476
|
+
// cannot read after a successor generation takes ownership.
|
|
477
|
+
await bindSupervisedWorkerSession(agentSession, process.env, { allowConfirmedFastPath: true });
|
|
478
|
+
// A cursor is acknowledged only when the worker explicitly uses it to
|
|
479
|
+
// request the next page. Persisting a cursor from the response we are
|
|
480
|
+
// still constructing could skip a message if serialization, presence,
|
|
481
|
+
// or the provider turn fails afterward.
|
|
482
|
+
if (after_message_id)
|
|
483
|
+
scheduleSupervisedWorkerCursorCheckpoint(agentSession, after_message_id);
|
|
484
|
+
}
|
|
259
485
|
const maxPollMs = getPollTimeoutCapMs();
|
|
260
486
|
const serverTimeout = Math.min(Math.max(timeout || DEFAULT_POLL_TIMEOUT_MS, 1000), maxPollMs);
|
|
261
|
-
if (localRoomId &&
|
|
262
|
-
const { localRoomId: sqliteRoomId } = await resolveLocalRoomStorageIdentifiers(localRoomId);
|
|
487
|
+
if (localRoomId && localStorageEnabled) {
|
|
488
|
+
const { localRoomId: sqliteRoomId, cloudRoomId, } = await resolveLocalRoomStorageIdentifiers(localRoomId);
|
|
263
489
|
const effectiveLocalRoomId = sqliteRoomId || localRoomId;
|
|
264
490
|
const effectiveAfterMessageId = resolveEffectiveAfterMessageId({
|
|
265
491
|
requestedAfterMessageId: after_message_id,
|
|
@@ -270,11 +496,12 @@ export function registerWaitForMessagesTool(server) {
|
|
|
270
496
|
// keep the unchanged forward "everything after the cursor" behavior.
|
|
271
497
|
const existing = fetchPlan.mode === "catch_up_tail"
|
|
272
498
|
? await getLatestLocalChatMessages(effectiveLocalRoomId, {
|
|
273
|
-
limit: fetchPlan.limit,
|
|
499
|
+
limit: Math.min(fetchPlan.limit, MAX_WAIT_MESSAGES_PER_CALL),
|
|
274
500
|
include_prompt_only: true,
|
|
275
501
|
})
|
|
276
502
|
: await getLocalChatMessages(effectiveLocalRoomId, {
|
|
277
503
|
after: effectiveAfterMessageId,
|
|
504
|
+
limit: MAX_WAIT_MESSAGES_PER_CALL,
|
|
278
505
|
include_prompt_only: true,
|
|
279
506
|
});
|
|
280
507
|
const replayingExistingMessages = existing.messages.length > 0;
|
|
@@ -283,13 +510,20 @@ export function registerWaitForMessagesTool(server) {
|
|
|
283
510
|
: await waitForLocalChatMessages(effectiveLocalRoomId, {
|
|
284
511
|
after: effectiveAfterMessageId,
|
|
285
512
|
timeoutMs: serverTimeout,
|
|
513
|
+
limit: MAX_WAIT_MESSAGES_PER_CALL,
|
|
286
514
|
include_prompt_only: true,
|
|
287
515
|
});
|
|
288
516
|
const messages = await attachLocalActivationMetadata(effectiveLocalRoomId, result.messages, agentSession, {
|
|
289
517
|
includeTaskOwnerLeases: !replayingExistingMessages,
|
|
518
|
+
activeSessionRoomId: cloudRoomId || sessionRoomId,
|
|
519
|
+
});
|
|
520
|
+
const bounded = boundAgentMessageOutput(messages, {
|
|
521
|
+
direction: fetchPlan.mode === "catch_up_tail" ? "suffix" : "prefix",
|
|
522
|
+
maxBytes: AGENT_MESSAGE_BODY_MAX_BYTES,
|
|
290
523
|
});
|
|
291
|
-
const routing = filterSilentActivationMessages(messages);
|
|
292
|
-
|
|
524
|
+
const routing = filterSilentActivationMessages(bounded.messages);
|
|
525
|
+
const observedCursor = routing.last_observed_message_id ?? getLastMessageId(result);
|
|
526
|
+
touchRoomSession(effectiveLocalRoomId, observedCursor);
|
|
293
527
|
const threadContext = await collectThreadContextMessages({
|
|
294
528
|
messages: routing.messages,
|
|
295
529
|
localRoomId: effectiveLocalRoomId,
|
|
@@ -299,8 +533,13 @@ export function registerWaitForMessagesTool(server) {
|
|
|
299
533
|
return jsonToolResponse({
|
|
300
534
|
room_id: effectiveLocalRoomId,
|
|
301
535
|
...addActivationRoutingTelemetry({
|
|
302
|
-
messages: toAgentReadableMessages(routing.messages, threadContext),
|
|
536
|
+
messages: toAgentReadableMessages(routing.messages, threadContext.messages),
|
|
303
537
|
}, routing),
|
|
538
|
+
...(result.has_more || bounded.truncated ? { truncated: true } : {}),
|
|
539
|
+
...(bounded.omittedMessageCount > 0
|
|
540
|
+
? { omitted_message_count: bounded.omittedMessageCount }
|
|
541
|
+
: {}),
|
|
542
|
+
...(threadContext.truncated ? { thread_context_truncated: true } : {}),
|
|
304
543
|
});
|
|
305
544
|
}
|
|
306
545
|
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, agentSession ? identityFromAgentSession(agentSession) : identity), agentSession);
|
|
@@ -312,6 +551,8 @@ export function registerWaitForMessagesTool(server) {
|
|
|
312
551
|
const deliveryHeaders = buildAgentDeliveryHeaders(agentSession);
|
|
313
552
|
const allMessages = [];
|
|
314
553
|
let roomIdFromResponse;
|
|
554
|
+
let catchUpTruncated = false;
|
|
555
|
+
let apiObservedCursor = null;
|
|
315
556
|
// Long-poll the server (blocks up to serverTimeout for new messages),
|
|
316
557
|
// optionally seeded with a cursor. Shared by the cursor path and by the
|
|
317
558
|
// no-cursor empty-tail fallback so a quiet room still blocks instead of
|
|
@@ -321,6 +562,7 @@ export function registerWaitForMessagesTool(server) {
|
|
|
321
562
|
const params = new URLSearchParams();
|
|
322
563
|
if (after)
|
|
323
564
|
params.set("after", after);
|
|
565
|
+
params.set("limit", String(MAX_WAIT_MESSAGES_PER_CALL));
|
|
324
566
|
params.set("timeout", String(serverTimeout));
|
|
325
567
|
const queryString = params.toString();
|
|
326
568
|
const firstResult = await roomScopedApiCall({
|
|
@@ -335,25 +577,10 @@ export function registerWaitForMessagesTool(server) {
|
|
|
335
577
|
});
|
|
336
578
|
allMessages.push(...(firstResult.messages ?? []));
|
|
337
579
|
roomIdFromResponse = roomIdFromResponse || firstResult.room_id || firstResult.project_id;
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
}
|
|
580
|
+
catchUpTruncated = Boolean(firstResult.has_more);
|
|
581
|
+
apiObservedCursor = typeof firstResult.last_observed_message_id === "string"
|
|
582
|
+
? firstResult.last_observed_message_id
|
|
583
|
+
: null;
|
|
357
584
|
};
|
|
358
585
|
if (fetchPlan.mode === "catch_up_tail") {
|
|
359
586
|
// No cursor: catch up on only the bounded recent tail. Mirror
|
|
@@ -364,6 +591,7 @@ export function registerWaitForMessagesTool(server) {
|
|
|
364
591
|
targetRoomId,
|
|
365
592
|
targetProjectId,
|
|
366
593
|
limit: fetchPlan.limit,
|
|
594
|
+
deliveryHeaders,
|
|
367
595
|
});
|
|
368
596
|
if (recent.messages.length > 0) {
|
|
369
597
|
allMessages.push(...recent.messages);
|
|
@@ -381,7 +609,11 @@ export function registerWaitForMessagesTool(server) {
|
|
|
381
609
|
else {
|
|
382
610
|
await longPollFromCursor(fetchPlan.after);
|
|
383
611
|
}
|
|
384
|
-
const
|
|
612
|
+
const bounded = boundAgentMessageOutput(allMessages, {
|
|
613
|
+
direction: fetchPlan.mode === "catch_up_tail" ? "suffix" : "prefix",
|
|
614
|
+
maxBytes: AGENT_MESSAGE_BODY_MAX_BYTES,
|
|
615
|
+
});
|
|
616
|
+
const routing = filterSilentActivationMessages(bounded.messages);
|
|
385
617
|
const threadContext = await collectThreadContextMessages({
|
|
386
618
|
messages: routing.messages,
|
|
387
619
|
localRoomId,
|
|
@@ -389,13 +621,54 @@ export function registerWaitForMessagesTool(server) {
|
|
|
389
621
|
projectId: targetProjectId,
|
|
390
622
|
});
|
|
391
623
|
const output = addActivationRoutingTelemetry({
|
|
392
|
-
messages: toAgentReadableMessages(routing.messages, threadContext),
|
|
624
|
+
messages: toAgentReadableMessages(routing.messages, threadContext.messages),
|
|
393
625
|
}, routing);
|
|
626
|
+
if (threadContext.truncated)
|
|
627
|
+
output.thread_context_truncated = true;
|
|
394
628
|
if (roomIdFromResponse) {
|
|
395
629
|
output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
|
|
396
630
|
}
|
|
631
|
+
if (catchUpTruncated || bounded.truncated)
|
|
632
|
+
output.truncated = true;
|
|
633
|
+
if (bounded.omittedMessageCount > 0) {
|
|
634
|
+
output.omitted_message_count = bounded.omittedMessageCount;
|
|
635
|
+
}
|
|
636
|
+
if (apiObservedCursor)
|
|
637
|
+
output.last_observed_message_id = apiObservedCursor;
|
|
397
638
|
if (targetRoomId) {
|
|
398
|
-
|
|
639
|
+
const observedCursor = apiObservedCursor
|
|
640
|
+
?? routing.last_observed_message_id
|
|
641
|
+
?? getLastMessageId(output);
|
|
642
|
+
touchRoomSession(targetRoomId, observedCursor);
|
|
643
|
+
if (allMessages.length > 0 && agentSession) {
|
|
644
|
+
const firstMsg = allMessages[0];
|
|
645
|
+
const lastMsg = allMessages[allMessages.length - 1];
|
|
646
|
+
if (typeof firstMsg?.id === "string" && typeof lastMsg?.id === "string") {
|
|
647
|
+
try {
|
|
648
|
+
await roomScopedApiCall({
|
|
649
|
+
room_id: targetRoomId,
|
|
650
|
+
project_id: targetProjectId,
|
|
651
|
+
room_path: (r) => `/rooms/${encodeRoomIdPath(r)}/agents/self/observation`,
|
|
652
|
+
project_path: (p) => `/projects/${encodeURIComponent(p)}/agents/self/observation`,
|
|
653
|
+
options: {
|
|
654
|
+
method: "PUT",
|
|
655
|
+
body: JSON.stringify({
|
|
656
|
+
first_message_id: firstMsg.id,
|
|
657
|
+
last_message_id: lastMsg.id,
|
|
658
|
+
...agentSessionCredentials(agentSession),
|
|
659
|
+
}),
|
|
660
|
+
},
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
// Non-blocking telemetry
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
// Delivery alone is observation evidence (the span above), never a
|
|
669
|
+
// "responding" receipt: an agent that ignores an activation must not
|
|
670
|
+
// present as responding. Receipts advance only on real transitions —
|
|
671
|
+
// send-tool marks "replied" when the agent actually publishes a reply.
|
|
399
672
|
}
|
|
400
673
|
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, agentSession ? identityFromAgentSession(agentSession) : identity), agentSession);
|
|
401
674
|
return jsonToolResponse(output);
|
|
@@ -56,16 +56,17 @@ export function registerGetOnboardingStatusTool(server) {
|
|
|
56
56
|
if (workerRuntime.mode === "invalid") {
|
|
57
57
|
return jsonTextResponse({ success: false, error: "worker_bearer_configuration_invalid", message: workerRuntime.error });
|
|
58
58
|
}
|
|
59
|
-
if (workerRuntime.mode === "worker") {
|
|
59
|
+
if (workerRuntime.mode === "worker" || workerRuntime.mode === "supervised") {
|
|
60
60
|
return jsonTextResponse({
|
|
61
61
|
api_url: API_URL,
|
|
62
|
-
worker_bearer_mode:
|
|
62
|
+
worker_bearer_mode: workerRuntime.mode === "worker",
|
|
63
|
+
supervised_bounded_mode: workerRuntime.mode === "supervised",
|
|
63
64
|
authenticated: true,
|
|
64
|
-
auth_source: "worker_bearer",
|
|
65
|
+
auth_source: workerRuntime.mode === "worker" ? "worker_bearer" : "daemon_supervised",
|
|
65
66
|
account: null,
|
|
66
67
|
pending_device_auth: null,
|
|
67
68
|
next_step: "join_room",
|
|
68
|
-
note: "Owner-auth onboarding and saved-auth state are disabled in worker
|
|
69
|
+
note: "Owner-auth onboarding and saved-auth state are disabled in worker credential mode.",
|
|
69
70
|
});
|
|
70
71
|
}
|
|
71
72
|
const workingDir = cwd || process.cwd();
|
|
@@ -82,7 +83,7 @@ export function registerGetOnboardingStatusTool(server) {
|
|
|
82
83
|
const storedAuth = getStoredAuth();
|
|
83
84
|
const pendingAuth = getPendingDeviceAuth();
|
|
84
85
|
const savedCurrentRoom = getStoredCurrentRoom();
|
|
85
|
-
const detectedRoom = configGitContext?.
|
|
86
|
+
const detectedRoom = configGitContext?.activeRoomLocator ?? gitContext?.activeRoomLocator ?? null;
|
|
86
87
|
const api_health = await checkOnboardingApiHealth();
|
|
87
88
|
const authenticated = Boolean(process.env.LETAGENTS_TOKEN || storedAuth);
|
|
88
89
|
let nextStep = "join_room";
|
|
@@ -112,10 +113,10 @@ export function registerGetOnboardingStatusTool(server) {
|
|
|
112
113
|
saved_current_room: toPublicStoredRoomSession(savedCurrentRoom),
|
|
113
114
|
detected_room_from_context: detectedRoom,
|
|
114
115
|
configured_room_from_file: configRoom,
|
|
115
|
-
configured_active_room_from_context: configGitContext?.
|
|
116
|
+
configured_active_room_from_context: configGitContext?.activeRoomLocator ?? null,
|
|
116
117
|
derived_repo_room_from_git: gitContext?.repoRoom ?? null,
|
|
117
|
-
derived_active_git_room: gitContext?.
|
|
118
|
-
derived_branch_room_from_git: gitContext?.
|
|
118
|
+
derived_active_git_room: gitContext?.activeRoomLocator ?? null,
|
|
119
|
+
derived_branch_room_from_git: gitContext?.activeRefRoomLocator ?? null,
|
|
119
120
|
git_current_branch: gitContext?.currentBranch ?? configGitContext?.currentBranch ?? null,
|
|
120
121
|
git_default_branch: gitContext?.defaultBranch ?? configGitContext?.defaultBranch ?? null,
|
|
121
122
|
repo_root: repoRoot,
|