letagents 0.12.10 → 0.12.12
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/README.md +2 -0
- package/dist/api/board-intent-payloads.js +25 -0
- package/dist/mcp/codex-session/runtime-bridge.js +42 -25
- package/dist/mcp/local-state/agent-sessions.js +15 -0
- package/dist/mcp/local-state/local-chat.js +1 -1
- package/dist/mcp/rental-tools/context.js +30 -0
- package/dist/mcp/server/register-tools.js +23 -12
- package/dist/mcp/server/runtime/agent-sessions.js +81 -2
- package/dist/mcp/server/runtime/api.js +63 -13
- package/dist/mcp/server/runtime/execution-profile.js +19 -0
- package/dist/mcp/server/runtime/identity/directory.js +9 -2
- package/dist/mcp/server/runtime/identity.js +2 -1
- 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 +46 -0
- package/dist/mcp/server/runtime/rooms.js +70 -0
- package/dist/mcp/server/runtime/supervised-room-authority.js +8 -0
- package/dist/mcp/server/runtime/supervisor-bridge.js +731 -0
- package/dist/mcp/server/runtime/tool-surface-policy.js +26 -0
- package/dist/mcp/server/runtime/worker-bearer.js +101 -0
- package/dist/mcp/server/runtime-contract.js +27 -0
- package/dist/mcp/server/runtime.js +14 -3
- package/dist/mcp/server/supervised-tool-facade.js +105 -0
- package/dist/mcp/server/tools/agent-sessions.js +113 -5
- package/dist/mcp/server/tools/messages/index.js +3 -2
- package/dist/mcp/server/tools/messages/message-lookup.js +75 -0
- package/dist/mcp/server/tools/messages/read-tool.js +1 -1
- package/dist/mcp/server/tools/messages/send-tool.js +5 -45
- package/dist/mcp/server/tools/messages/wait-tool.js +212 -91
- package/dist/mcp/server/tools/onboarding/device-auth-tools.js +10 -0
- package/dist/mcp/server/tools/onboarding/name-tool.js +5 -1
- package/dist/mcp/server/tools/onboarding/status-tool.js +18 -0
- package/dist/mcp/server/tools/rental/context-tools.js +9 -1
- package/dist/mcp/server/tools/rooms/inspection-tools.js +47 -18
- package/dist/mcp/server/tools/supervised-room-turn.js +42 -0
- package/dist/mcp/server/tools/tasks/board-intent-tools.js +7 -2
- package/dist/mcp/server/tools/tasks/index.js +2 -0
- package/dist/mcp/server/tools/tasks/verdict-tools.js +51 -0
- package/dist/mcp/server.js +16 -7
- package/dist/mcp/sse-client.js +3 -3
- package/dist/shared/activation-routing.js +57 -4
- package/dist/shared/agent-presence.js +1 -0
- package/dist/shared/agent-session-bearer.js +28 -0
- package/dist/shared/board-manager-failover.js +16 -0
- package/dist/shared/room-agent-prompts.js +2 -2
- package/package.json +24 -15
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { encodeRoomIdPath } from "../../../room-id.js";
|
|
3
|
-
import { agentSessionCredentials, addLocalChatMessage,
|
|
3
|
+
import { agentSessionCredentials, addLocalChatMessage, currentRoom, getFallbackProjectId, getRememberedRoomPresence, getTargetRoomId, isLocalRoomStorageEnabled, resolveLocalRoomStorageIdentifiers, resolveWorkerToolIdentity, roomScopedApiCall, syncRoomPresence, toPublicAgentIdentity, touchCurrentRoom, } from "../../runtime.js";
|
|
4
|
+
import { findLocalMessageById, findRemoteMessageById } from "./message-lookup.js";
|
|
4
5
|
import { jsonToolResponse } from "./response.js";
|
|
5
6
|
export function buildSendMessageRequestBody(input) {
|
|
6
7
|
return {
|
|
@@ -22,50 +23,6 @@ function explicitThreadRootId(message) {
|
|
|
22
23
|
}
|
|
23
24
|
return null;
|
|
24
25
|
}
|
|
25
|
-
async function findLocalMessageById(roomId, messageId) {
|
|
26
|
-
let afterCursor;
|
|
27
|
-
for (;;) {
|
|
28
|
-
const result = await getLocalChatMessages(roomId, {
|
|
29
|
-
after: afterCursor,
|
|
30
|
-
include_prompt_only: true,
|
|
31
|
-
});
|
|
32
|
-
const messages = (result.messages ?? []);
|
|
33
|
-
const match = messages.find((message) => message.id === messageId);
|
|
34
|
-
if (match)
|
|
35
|
-
return match;
|
|
36
|
-
if (!result.has_more || messages.length === 0)
|
|
37
|
-
return null;
|
|
38
|
-
const lastMessage = messages[messages.length - 1];
|
|
39
|
-
afterCursor = typeof lastMessage?.id === "string" ? lastMessage.id : undefined;
|
|
40
|
-
if (!afterCursor)
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
async function findRemoteMessageById(input) {
|
|
45
|
-
let afterCursor;
|
|
46
|
-
for (;;) {
|
|
47
|
-
const query = new URLSearchParams();
|
|
48
|
-
if (afterCursor)
|
|
49
|
-
query.set("after", afterCursor);
|
|
50
|
-
const qs = query.toString();
|
|
51
|
-
const result = await roomScopedApiCall({
|
|
52
|
-
room_id: input.roomId,
|
|
53
|
-
project_id: input.projectId,
|
|
54
|
-
room_path: (roomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(roomId)}/messages${qs ? `?${qs}` : ""}`),
|
|
55
|
-
project_path: (projectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(projectId)}/messages${qs ? `?${qs}` : ""}`),
|
|
56
|
-
});
|
|
57
|
-
const messages = result.messages ?? [];
|
|
58
|
-
const match = messages.find((message) => message.id === input.messageId);
|
|
59
|
-
if (match)
|
|
60
|
-
return match;
|
|
61
|
-
if (!result.has_more || messages.length === 0)
|
|
62
|
-
return null;
|
|
63
|
-
const lastMessage = messages[messages.length - 1];
|
|
64
|
-
afterCursor = typeof lastMessage?.id === "string" ? lastMessage.id : undefined;
|
|
65
|
-
if (!afterCursor)
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
26
|
async function findMessageById(input) {
|
|
70
27
|
if (input.localRoomId && await isLocalRoomStorageEnabled(input.localRoomId)) {
|
|
71
28
|
const { localRoomId } = await resolveLocalRoomStorageIdentifiers(input.localRoomId);
|
|
@@ -155,6 +112,9 @@ async function sendMessageFromTool(input) {
|
|
|
155
112
|
});
|
|
156
113
|
touchCurrentRoom(typeof message.id === "string" ? message.id : undefined);
|
|
157
114
|
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity), agentSession);
|
|
115
|
+
// The `replied` receipt transition is server-owned: message creation marks
|
|
116
|
+
// the publisher's receipt on the reply target atomically with the reply
|
|
117
|
+
// itself, for MCP workers and supervised daemon publications alike.
|
|
158
118
|
return jsonToolResponse({
|
|
159
119
|
...message,
|
|
160
120
|
agent_identity: toPublicAgentIdentity(identity),
|
|
@@ -1,10 +1,30 @@
|
|
|
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, getLocalChatMessages, getLastMessageId, getRememberedRoomPresence, getTargetRoomId, identityFromAgentSession, isLocalRoomStorageEnabled, listLocalTasks, resolveLocalRoomStorageIdentifiers, resolveAgentSession, roomScopedApiCall, syncRoomPresence, toAgentReadableMessages, touchRoomSession, waitForLocalChatMessages, } from "../../runtime.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";
|
|
5
6
|
import { attachAgentMessageActivations } from "../../../../shared/activation-routing.js";
|
|
7
|
+
import { findLocalMessageById, findRemoteMessageById } from "./message-lookup.js";
|
|
8
|
+
import { fetchRecentRemoteMessages } from "./read-tool.js";
|
|
6
9
|
import { jsonToolResponse } from "./response.js";
|
|
7
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
|
+
}
|
|
8
28
|
const LOCAL_TASK_OWNER_STATUSES = new Set(["assigned", "in_progress", "blocked", "in_review"]);
|
|
9
29
|
export function buildWaitForMessagesRequestOptions(input) {
|
|
10
30
|
return {
|
|
@@ -88,6 +108,10 @@ function addActivationRoutingTelemetry(output, routing) {
|
|
|
88
108
|
return output;
|
|
89
109
|
}
|
|
90
110
|
export function resolveWaitAgentSession(roomId, agentSessionId) {
|
|
111
|
+
if (requireValidWorkerBearerRuntime().mode === "worker" &&
|
|
112
|
+
(!agentSessionId?.trim() || agentSessionId === WORKER_BEARER_AGENT_SESSION_ID)) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
91
115
|
if (!agentSessionId?.trim()) {
|
|
92
116
|
return null;
|
|
93
117
|
}
|
|
@@ -130,57 +154,53 @@ async function attachLocalActivationMetadata(roomId, messages, agentSession, opt
|
|
|
130
154
|
session_kind: agentSession.session_kind,
|
|
131
155
|
}, activationContext);
|
|
132
156
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (!result.has_more || messages.length === 0)
|
|
145
|
-
return null;
|
|
146
|
-
const lastMessage = messages[messages.length - 1];
|
|
147
|
-
afterCursor = messageId(lastMessage) ?? undefined;
|
|
148
|
-
if (!afterCursor)
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
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}`;
|
|
151
168
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
return match;
|
|
169
|
-
if (!result.has_more || messages.length === 0)
|
|
170
|
-
return null;
|
|
171
|
-
const lastMessage = messages[messages.length - 1];
|
|
172
|
-
afterCursor = messageId(lastMessage) ?? undefined;
|
|
173
|
-
if (!afterCursor)
|
|
174
|
-
return null;
|
|
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);
|
|
175
185
|
}
|
|
176
186
|
}
|
|
177
|
-
|
|
187
|
+
export function resetThreadContextCacheForTests() {
|
|
188
|
+
threadContextCache.clear();
|
|
189
|
+
}
|
|
190
|
+
export async function collectThreadContextMessages(input) {
|
|
178
191
|
const records = input.messages.filter(isRecord);
|
|
179
192
|
const knownIds = new Set(records.map(messageId).filter((id) => Boolean(id)));
|
|
180
193
|
const seenIds = new Set(knownIds);
|
|
181
194
|
const pendingIds = records
|
|
182
195
|
.map(replyReferenceId)
|
|
183
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
|
+
}
|
|
184
204
|
const contextMessages = [];
|
|
185
205
|
const useLocalStorage = Boolean(input.localRoomId && await isLocalRoomStorageEnabled(input.localRoomId));
|
|
186
206
|
const localIdentifiers = useLocalStorage
|
|
@@ -192,15 +212,20 @@ async function collectThreadContextMessages(input) {
|
|
|
192
212
|
if (!nextId || seenIds.has(nextId))
|
|
193
213
|
continue;
|
|
194
214
|
seenIds.add(nextId);
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
+
}));
|
|
202
224
|
if (!message)
|
|
203
225
|
continue;
|
|
226
|
+
if (!cached) {
|
|
227
|
+
rememberThreadContextMessages(scopeId, [message]);
|
|
228
|
+
}
|
|
204
229
|
contextMessages.push(message);
|
|
205
230
|
const parentId = replyReferenceId(message);
|
|
206
231
|
if (parentId && !seenIds.has(parentId)) {
|
|
@@ -210,12 +235,12 @@ async function collectThreadContextMessages(input) {
|
|
|
210
235
|
return contextMessages;
|
|
211
236
|
}
|
|
212
237
|
export function registerWaitForMessagesTool(server) {
|
|
213
|
-
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'.
|
|
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'. 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).", {
|
|
214
239
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room."),
|
|
215
240
|
after_message_id: z
|
|
216
241
|
.string()
|
|
217
242
|
.optional()
|
|
218
|
-
.describe(
|
|
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.`),
|
|
219
244
|
timeout: z
|
|
220
245
|
.number()
|
|
221
246
|
.optional()
|
|
@@ -225,12 +250,32 @@ export function registerWaitForMessagesTool(server) {
|
|
|
225
250
|
.optional()
|
|
226
251
|
.describe("Registered agent session to use. Without this, the MCP transport is treated as controller traffic and is hidden from connected-agent activity."),
|
|
227
252
|
}, async ({ room_id, after_message_id, timeout, agent_session_id }) => {
|
|
253
|
+
// This guard intentionally runs before room resolution, identity setup,
|
|
254
|
+
// local SQLite reads, presence writes, or HTTP traffic. In supervised
|
|
255
|
+
// bounded-turn mode the daemon is the sole inbox owner.
|
|
256
|
+
const boundedDeliveryDisabled = supervisedBoundedDeliveryDisabledToolResult();
|
|
257
|
+
if (boundedDeliveryDisabled) {
|
|
258
|
+
return jsonToolResponse(boundedDeliveryDisabled);
|
|
259
|
+
}
|
|
228
260
|
const targetRoomId = getTargetRoomId(room_id);
|
|
229
261
|
const targetProjectId = getFallbackProjectId();
|
|
230
262
|
const localRoomId = targetRoomId ?? currentRoom?.room_id ?? targetProjectId;
|
|
231
263
|
const identity = await ensureAgentIdentity();
|
|
232
264
|
const sessionRoomId = targetRoomId ?? currentRoom?.room_id ?? localRoomId ?? null;
|
|
233
265
|
const agentSession = resolveWaitAgentSession(sessionRoomId, agent_session_id);
|
|
266
|
+
if (agentSession) {
|
|
267
|
+
// Registration (or a successor generation) must bind strictly once.
|
|
268
|
+
// Later waits use a read-only exact verification capped at 250ms, so a
|
|
269
|
+
// wedged daemon cannot consume the room-poll budget and an old worker
|
|
270
|
+
// cannot read after a successor generation takes ownership.
|
|
271
|
+
await bindSupervisedWorkerSession(agentSession, process.env, { allowConfirmedFastPath: true });
|
|
272
|
+
// A cursor is acknowledged only when the worker explicitly uses it to
|
|
273
|
+
// request the next page. Persisting a cursor from the response we are
|
|
274
|
+
// still constructing could skip a message if serialization, presence,
|
|
275
|
+
// or the provider turn fails afterward.
|
|
276
|
+
if (after_message_id)
|
|
277
|
+
scheduleSupervisedWorkerCursorCheckpoint(agentSession, after_message_id);
|
|
278
|
+
}
|
|
234
279
|
const maxPollMs = getPollTimeoutCapMs();
|
|
235
280
|
const serverTimeout = Math.min(Math.max(timeout || DEFAULT_POLL_TIMEOUT_MS, 1000), maxPollMs);
|
|
236
281
|
if (localRoomId && await isLocalRoomStorageEnabled(localRoomId)) {
|
|
@@ -239,10 +284,19 @@ export function registerWaitForMessagesTool(server) {
|
|
|
239
284
|
const effectiveAfterMessageId = resolveEffectiveAfterMessageId({
|
|
240
285
|
requestedAfterMessageId: after_message_id,
|
|
241
286
|
});
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
287
|
+
const fetchPlan = planWaitForMessagesFetch({ effectiveAfterMessageId });
|
|
288
|
+
// Without a cursor, page BACKWARDS from the tail (getLatest…) so we
|
|
289
|
+
// return the most-recent messages, not the oldest N; with a cursor we
|
|
290
|
+
// keep the unchanged forward "everything after the cursor" behavior.
|
|
291
|
+
const existing = fetchPlan.mode === "catch_up_tail"
|
|
292
|
+
? await getLatestLocalChatMessages(effectiveLocalRoomId, {
|
|
293
|
+
limit: fetchPlan.limit,
|
|
294
|
+
include_prompt_only: true,
|
|
295
|
+
})
|
|
296
|
+
: await getLocalChatMessages(effectiveLocalRoomId, {
|
|
297
|
+
after: effectiveAfterMessageId,
|
|
298
|
+
include_prompt_only: true,
|
|
299
|
+
});
|
|
246
300
|
const replayingExistingMessages = existing.messages.length > 0;
|
|
247
301
|
const result = replayingExistingMessages
|
|
248
302
|
? existing
|
|
@@ -255,7 +309,8 @@ export function registerWaitForMessagesTool(server) {
|
|
|
255
309
|
includeTaskOwnerLeases: !replayingExistingMessages,
|
|
256
310
|
});
|
|
257
311
|
const routing = filterSilentActivationMessages(messages);
|
|
258
|
-
|
|
312
|
+
const observedCursor = routing.last_observed_message_id ?? getLastMessageId(result);
|
|
313
|
+
touchRoomSession(effectiveLocalRoomId, observedCursor);
|
|
259
314
|
const threadContext = await collectThreadContextMessages({
|
|
260
315
|
messages: routing.messages,
|
|
261
316
|
localRoomId: effectiveLocalRoomId,
|
|
@@ -271,46 +326,82 @@ export function registerWaitForMessagesTool(server) {
|
|
|
271
326
|
}
|
|
272
327
|
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, agentSession ? identityFromAgentSession(agentSession) : identity), agentSession);
|
|
273
328
|
const clientTimeout = serverTimeout + (serverTimeout > 120_000 ? 120_000 : 5_000);
|
|
274
|
-
const params = new URLSearchParams();
|
|
275
329
|
const effectiveAfterMessageId = resolveEffectiveAfterMessageId({
|
|
276
330
|
requestedAfterMessageId: after_message_id,
|
|
277
331
|
});
|
|
278
|
-
|
|
279
|
-
params.set("after", effectiveAfterMessageId);
|
|
280
|
-
params.set("timeout", String(serverTimeout));
|
|
281
|
-
const queryString = params.toString();
|
|
332
|
+
const fetchPlan = planWaitForMessagesFetch({ effectiveAfterMessageId });
|
|
282
333
|
const deliveryHeaders = buildAgentDeliveryHeaders(agentSession);
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
targetRoomId,
|
|
303
|
-
targetProjectId,
|
|
304
|
-
queryString: qs,
|
|
334
|
+
const allMessages = [];
|
|
335
|
+
let roomIdFromResponse;
|
|
336
|
+
// Long-poll the server (blocks up to serverTimeout for new messages),
|
|
337
|
+
// optionally seeded with a cursor. Shared by the cursor path and by the
|
|
338
|
+
// no-cursor empty-tail fallback so a quiet room still blocks instead of
|
|
339
|
+
// busy-spinning. When `after` is provided this also catches up any backlog
|
|
340
|
+
// after the cursor via forward pagination.
|
|
341
|
+
const longPollFromCursor = async (after) => {
|
|
342
|
+
const params = new URLSearchParams();
|
|
343
|
+
if (after)
|
|
344
|
+
params.set("after", after);
|
|
345
|
+
params.set("timeout", String(serverTimeout));
|
|
346
|
+
const queryString = params.toString();
|
|
347
|
+
const firstResult = await roomScopedApiCall({
|
|
348
|
+
room_id: targetRoomId,
|
|
349
|
+
project_id: targetProjectId,
|
|
350
|
+
room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages/poll?${queryString}`),
|
|
351
|
+
project_path: (targetProjectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(targetProjectId)}/messages/poll?${queryString}`),
|
|
352
|
+
options: buildWaitForMessagesRequestOptions({
|
|
305
353
|
deliveryHeaders,
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
354
|
+
signal: AbortSignal.timeout(clientTimeout),
|
|
355
|
+
}),
|
|
356
|
+
});
|
|
357
|
+
allMessages.push(...(firstResult.messages ?? []));
|
|
358
|
+
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
|
+
}
|
|
378
|
+
};
|
|
379
|
+
if (fetchPlan.mode === "catch_up_tail") {
|
|
380
|
+
// No cursor: catch up on only the bounded recent tail. Mirror
|
|
381
|
+
// read_messages by paging BACKWARDS from the tail (before=latest) so we
|
|
382
|
+
// return the newest N and never walk the full room history. This also
|
|
383
|
+
// advances the session cursor to the newest message returned.
|
|
384
|
+
const recent = await fetchRecentRemoteMessages({
|
|
385
|
+
targetRoomId,
|
|
386
|
+
targetProjectId,
|
|
387
|
+
limit: fetchPlan.limit,
|
|
388
|
+
});
|
|
389
|
+
if (recent.messages.length > 0) {
|
|
390
|
+
allMessages.push(...recent.messages);
|
|
391
|
+
roomIdFromResponse = recent.roomIdFromResponse;
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
// Empty tail => the room is effectively empty, so there is nothing to
|
|
395
|
+
// dump. Fall through to the long-poll (no cursor) so a worker looping
|
|
396
|
+
// in a quiet room blocks up to the timeout instead of busy-spinning,
|
|
397
|
+
// exactly as the local path does via waitForLocalChatMessages.
|
|
398
|
+
roomIdFromResponse = recent.roomIdFromResponse;
|
|
399
|
+
await longPollFromCursor();
|
|
312
400
|
}
|
|
313
401
|
}
|
|
402
|
+
else {
|
|
403
|
+
await longPollFromCursor(fetchPlan.after);
|
|
404
|
+
}
|
|
314
405
|
const routing = filterSilentActivationMessages(allMessages);
|
|
315
406
|
const threadContext = await collectThreadContextMessages({
|
|
316
407
|
messages: routing.messages,
|
|
@@ -325,7 +416,37 @@ export function registerWaitForMessagesTool(server) {
|
|
|
325
416
|
output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
|
|
326
417
|
}
|
|
327
418
|
if (targetRoomId) {
|
|
328
|
-
|
|
419
|
+
const observedCursor = routing.last_observed_message_id ?? getLastMessageId(output);
|
|
420
|
+
touchRoomSession(targetRoomId, observedCursor);
|
|
421
|
+
if (allMessages.length > 0 && agentSession) {
|
|
422
|
+
const firstMsg = allMessages[0];
|
|
423
|
+
const lastMsg = allMessages[allMessages.length - 1];
|
|
424
|
+
if (typeof firstMsg?.id === "string" && typeof lastMsg?.id === "string") {
|
|
425
|
+
try {
|
|
426
|
+
await roomScopedApiCall({
|
|
427
|
+
room_id: targetRoomId,
|
|
428
|
+
project_id: targetProjectId,
|
|
429
|
+
room_path: (r) => `/rooms/${encodeRoomIdPath(r)}/agents/self/observation`,
|
|
430
|
+
project_path: (p) => `/projects/${encodeURIComponent(p)}/agents/self/observation`,
|
|
431
|
+
options: {
|
|
432
|
+
method: "PUT",
|
|
433
|
+
body: JSON.stringify({
|
|
434
|
+
first_message_id: firstMsg.id,
|
|
435
|
+
last_message_id: lastMsg.id,
|
|
436
|
+
...agentSessionCredentials(agentSession),
|
|
437
|
+
}),
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
// Non-blocking telemetry
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// Delivery alone is observation evidence (the span above), never a
|
|
447
|
+
// "responding" receipt: an agent that ignores an activation must not
|
|
448
|
+
// present as responding. Receipts advance only on real transitions —
|
|
449
|
+
// send-tool marks "replied" when the agent actually publishes a reply.
|
|
329
450
|
}
|
|
330
451
|
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, agentSession ? identityFromAgentSession(agentSession) : identity), agentSession);
|
|
331
452
|
return jsonToolResponse(output);
|
|
@@ -3,6 +3,7 @@ import { getRoomFromConfig } from "../../../config-reader.js";
|
|
|
3
3
|
import { looksLikeInviteCode } from "../../../room-id.js";
|
|
4
4
|
import { apiCall, clearAuthenticatedAccountCache, clearPendingDeviceAuth, clearStoredAuth, currentRoom, ensureAgentIdentity, getPendingDeviceAuth, joinRoomIdentifierWithoutImplicitGitRefCreate, setAuthenticatedAccountCache, setPendingDeviceAuth, setStoredAuth, toPublicAgentIdentity, withCanonicalRoomLink, } from "../../runtime.js";
|
|
5
5
|
import { jsonTextResponse } from "./responses.js";
|
|
6
|
+
import { workerModeDisabledToolResult } from "../../runtime/worker-bearer.js";
|
|
6
7
|
export function registerDeviceAuthTools(server) {
|
|
7
8
|
registerStartDeviceAuthTool(server);
|
|
8
9
|
registerPollDeviceAuthTool(server);
|
|
@@ -19,6 +20,9 @@ function registerStartDeviceAuthTool(server) {
|
|
|
19
20
|
.optional()
|
|
20
21
|
.describe("If true, replaces any existing pending device auth request."),
|
|
21
22
|
}, async ({ room_id, force }) => {
|
|
23
|
+
const disabled = workerModeDisabledToolResult();
|
|
24
|
+
if (disabled)
|
|
25
|
+
return jsonTextResponse(disabled);
|
|
22
26
|
const existing = getPendingDeviceAuth();
|
|
23
27
|
if (existing && !force) {
|
|
24
28
|
return jsonTextResponse({
|
|
@@ -60,6 +64,9 @@ function registerPollDeviceAuthTool(server) {
|
|
|
60
64
|
.optional()
|
|
61
65
|
.describe("If true, tries to join the room immediately after the auth succeeds."),
|
|
62
66
|
}, async ({ request_id, room_id, auto_join }) => {
|
|
67
|
+
const disabled = workerModeDisabledToolResult();
|
|
68
|
+
if (disabled)
|
|
69
|
+
return jsonTextResponse(disabled);
|
|
63
70
|
const pendingAuth = request_id
|
|
64
71
|
? getPendingDeviceAuth()?.request_id === request_id
|
|
65
72
|
? getPendingDeviceAuth()
|
|
@@ -137,6 +144,9 @@ function registerPollDeviceAuthTool(server) {
|
|
|
137
144
|
}
|
|
138
145
|
function registerClearSavedAuthTool(server) {
|
|
139
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);
|
|
140
150
|
clearPendingDeviceAuth();
|
|
141
151
|
clearStoredAuth();
|
|
142
152
|
clearAuthenticatedAccountCache();
|
|
@@ -3,6 +3,7 @@ import { buildAgentActorLabel, formatOwnerAttribution } from "../../../../shared
|
|
|
3
3
|
import { normalizeAgentBaseName } from "../../../../shared/codenames.js";
|
|
4
4
|
import { apiCall, currentAgentIdentity, currentAgentIdentityKey, detectAgentIdeLabel, getConversationIdentity, getLetagentsToken, resolveOwnerContext, setConversationIdentity, storeCurrentAgentIdentity, toPublicAgentIdentity, } from "../../runtime.js";
|
|
5
5
|
import { jsonTextResponse } from "./responses.js";
|
|
6
|
+
import { workerModeDisabledToolResult } from "../../runtime/worker-bearer.js";
|
|
6
7
|
export function registerSetAgentNameTool(server) {
|
|
7
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.", {
|
|
8
9
|
name: z
|
|
@@ -15,6 +16,9 @@ export function registerSetAgentNameTool(server) {
|
|
|
15
16
|
.optional()
|
|
16
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."),
|
|
17
18
|
}, async ({ name: desiredName, conversation_id }) => {
|
|
19
|
+
const disabled = workerModeDisabledToolResult();
|
|
20
|
+
if (disabled)
|
|
21
|
+
return jsonTextResponse(disabled);
|
|
18
22
|
const trimmedName = desiredName.trim();
|
|
19
23
|
if (trimmedName.length < 2 || trimmedName.length > 64) {
|
|
20
24
|
return jsonTextResponse({
|
|
@@ -22,7 +26,7 @@ export function registerSetAgentNameTool(server) {
|
|
|
22
26
|
error: "Name must be between 2 and 64 characters.",
|
|
23
27
|
});
|
|
24
28
|
}
|
|
25
|
-
const authAvailable = Boolean(getLetagentsToken());
|
|
29
|
+
const authAvailable = Boolean(await getLetagentsToken());
|
|
26
30
|
if (!authAvailable) {
|
|
27
31
|
return jsonTextResponse({
|
|
28
32
|
success: false,
|
|
@@ -4,6 +4,7 @@ import { buildActiveGitRoomContext, getGitCurrentBranch, getGitDefaultBranch, ge
|
|
|
4
4
|
import { resolveGitRoot } from "../../repo-context.js";
|
|
5
5
|
import { API_URL, currentAgentIdentity, currentAgentIdentityKey, currentRoom, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAuth, getStoredCurrentRoom, toPublicAgentIdentity, toPublicRoomState, toPublicStoredRoomSession, } from "../../runtime.js";
|
|
6
6
|
import { jsonTextResponse } from "./responses.js";
|
|
7
|
+
import { getWorkerBearerRuntime } from "../../runtime/worker-bearer.js";
|
|
7
8
|
export async function checkOnboardingApiHealth(apiUrl = API_URL, fetchImpl = globalThis.fetch, timeoutMs = 1500) {
|
|
8
9
|
const url = `${apiUrl.replace(/\/+$/, "")}/api/health`;
|
|
9
10
|
const controller = new AbortController();
|
|
@@ -51,6 +52,23 @@ export function registerGetOnboardingStatusTool(server) {
|
|
|
51
52
|
.optional()
|
|
52
53
|
.describe("Working directory to inspect for repo context. Defaults to the current process directory."),
|
|
53
54
|
}, async ({ cwd }) => {
|
|
55
|
+
const workerRuntime = getWorkerBearerRuntime();
|
|
56
|
+
if (workerRuntime.mode === "invalid") {
|
|
57
|
+
return jsonTextResponse({ success: false, error: "worker_bearer_configuration_invalid", message: workerRuntime.error });
|
|
58
|
+
}
|
|
59
|
+
if (workerRuntime.mode === "worker" || workerRuntime.mode === "supervised") {
|
|
60
|
+
return jsonTextResponse({
|
|
61
|
+
api_url: API_URL,
|
|
62
|
+
worker_bearer_mode: workerRuntime.mode === "worker",
|
|
63
|
+
supervised_bounded_mode: workerRuntime.mode === "supervised",
|
|
64
|
+
authenticated: true,
|
|
65
|
+
auth_source: workerRuntime.mode === "worker" ? "worker_bearer" : "daemon_supervised",
|
|
66
|
+
account: null,
|
|
67
|
+
pending_device_auth: null,
|
|
68
|
+
next_step: "join_room",
|
|
69
|
+
note: "Owner-auth onboarding and saved-auth state are disabled in worker credential mode.",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
54
72
|
const workingDir = cwd || process.cwd();
|
|
55
73
|
const repoRoot = resolveGitRoot(workingDir);
|
|
56
74
|
const configRoom = getRoomFromConfig(workingDir);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { rentalReadFile, rentalSearch } from "../../../rental-tools.js";
|
|
2
|
+
import { rentalReadFile, rentalRequestContext, rentalSearch, } from "../../../rental-tools.js";
|
|
3
3
|
import { rentalTextResponse } from "./response.js";
|
|
4
4
|
export function registerRentalContextTools({ server, deps, }) {
|
|
5
5
|
server.tool("rental_read_file", "Read one repo-relative file from the scoped rental workspace through the Context Broker. The server applies the Secret Firewall, records an Exposure Ledger entry, and returns redacted content when needed. This is the only approved file-read path for provider agents in scoped mode.", {
|
|
@@ -32,4 +32,12 @@ export function registerRentalContextTools({ server, deps, }) {
|
|
|
32
32
|
max_results,
|
|
33
33
|
case_sensitive,
|
|
34
34
|
})));
|
|
35
|
+
server.tool("rental_request_context", "Request renter approval to access a file outside the approved rental scope. Use this when rental_read_file returns file_not_found for a path you believe exists in the repo. Creates a pending context access request the renter can approve or deny; once approved, the file is materialized into the workspace and becomes readable via rental_read_file. Repeat calls for the same path return the existing pending request, and re-asking for an already-approved path retries delivery into the workspace (useful if the workspace was not ready at approval time).", {
|
|
36
|
+
session_id: z.string().describe("Rental session id the request belongs to."),
|
|
37
|
+
path: z.string().describe("Repo-relative path you need access to."),
|
|
38
|
+
reason: z
|
|
39
|
+
.string()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("Short justification shown to the renter (max 500 chars)."),
|
|
42
|
+
}, async ({ session_id, path, reason }) => rentalTextResponse(await rentalRequestContext(deps, { session_id, path, reason })));
|
|
35
43
|
}
|