letagents 0.12.10 → 0.12.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/local-chat.js +1 -1
- package/dist/mcp/rental-tools/context.js +30 -0
- package/dist/mcp/server/runtime/agent-sessions.js +40 -1
- package/dist/mcp/server/runtime/api.js +35 -13
- package/dist/mcp/server/runtime/identity/directory.js +9 -2
- package/dist/mcp/server/runtime/identity.js +2 -1
- package/dist/mcp/server/runtime/rooms.js +56 -0
- package/dist/mcp/server/runtime/supervisor-bridge.js +94 -0
- package/dist/mcp/server/runtime/worker-bearer.js +67 -0
- package/dist/mcp/server/runtime.js +1 -1
- package/dist/mcp/server/tools/agent-sessions.js +44 -3
- 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 +2 -45
- package/dist/mcp/server/tools/messages/wait-tool.js +158 -88
- 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 +17 -0
- package/dist/mcp/server/tools/rental/context-tools.js +9 -1
- package/dist/mcp/server/tools/rooms/inspection-tools.js +26 -10
- 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 +2 -0
- package/dist/mcp/sse-client.js +3 -3
- package/dist/shared/activation-routing.js +13 -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 +19 -14
|
@@ -2,7 +2,9 @@ import { z } from "zod";
|
|
|
2
2
|
import { scheduleCodexRuntimeStreamBridgeBind, } from "../../codex-session.js";
|
|
3
3
|
import { getManagedAgentProvider, toManagedAgentStartResponse, } from "../../managed-agent-providers.js";
|
|
4
4
|
import { encodeRoomIdPath, looksLikeInviteCode, normalizeInviteCode } from "../../room-id.js";
|
|
5
|
-
import { AGENT_INSTANCE_UUID, RepoRoomAuthRequiredError, apiCall, agentSessionCredentials, currentRoom, detectAgentIdeLabel, detectAgentRuntimeLabel, endStoredAgentSession, ensureAgentIdentity, getSessionLivenessRegistration, getAgentSessionRepoBranch, getStoredAgentSession, getTargetRoomId, ensureLocalWorkerAgentSession, isLocalRoomStorageEnabled, joinRoomIdentifier, resolveLocalRoomStorageIdentifiers, saveAgentSession, toPublicAgentSession, toPublicRoomState, toRepoRoomAuthRequiredResult, withAgentIdentity, } from "../runtime.js";
|
|
5
|
+
import { AGENT_INSTANCE_UUID, RepoRoomAuthRequiredError, apiCall, agentSessionCredentials, currentRoom, detectAgentIdeLabel, detectAgentRuntimeLabel, endStoredAgentSession, ensureAgentIdentity, getSessionLivenessRegistration, getAgentSessionRepoBranch, getStoredAgentSession, getTargetRoomId, ensureLocalWorkerAgentSession, isLocalRoomStorageEnabled, joinRoomIdentifier, resolveLocalRoomStorageIdentifiers, saveAgentSession, toPublicAgentSession, toPublicRoomState, toRepoRoomAuthRequiredResult, withAgentIdentity, resolveWorkerToolIdentity, } from "../runtime.js";
|
|
6
|
+
import { requireValidWorkerBearerRuntime, workerModeDisabledToolResult, } from "../runtime/worker-bearer.js";
|
|
7
|
+
import { bindSupervisedWorkerSession } from "../runtime/supervisor-bridge.js";
|
|
6
8
|
export function registerAgentSessionTools(server) {
|
|
7
9
|
// -- register_agent_session -------------------------------------------------
|
|
8
10
|
server.tool("register_agent_session", "Register this MCP client as an explicit room agent session. Unregistered MCP traffic is treated as controller traffic and stays out of the connected-agent roster.", {
|
|
@@ -66,6 +68,28 @@ export function registerAgentSessionTools(server) {
|
|
|
66
68
|
],
|
|
67
69
|
};
|
|
68
70
|
}
|
|
71
|
+
const { cloudRoomId } = await resolveLocalRoomStorageIdentifiers(targetRoomId);
|
|
72
|
+
const apiRoomId = cloudRoomId || targetRoomId;
|
|
73
|
+
if (requireValidWorkerBearerRuntime().mode === "worker") {
|
|
74
|
+
// The supplied bearer is issued for an existing server-side agent
|
|
75
|
+
// session. Do not call the owner-only registration endpoint or write
|
|
76
|
+
// any session credential to local storage.
|
|
77
|
+
const { agentSession } = await resolveWorkerToolIdentity({ roomId: apiRoomId });
|
|
78
|
+
return {
|
|
79
|
+
content: [
|
|
80
|
+
{
|
|
81
|
+
type: "text",
|
|
82
|
+
text: JSON.stringify({
|
|
83
|
+
success: true,
|
|
84
|
+
worker_bearer_mode: true,
|
|
85
|
+
agent_session: toPublicAgentSession(agentSession),
|
|
86
|
+
agent_session_id: agentSession.session_id,
|
|
87
|
+
use_agent_session_id: "This local worker-bearer session marker may be passed to room tools. The supplied bearer remains the only server credential.",
|
|
88
|
+
}, null, 2),
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
69
93
|
const identity = await ensureAgentIdentity();
|
|
70
94
|
if (!identity.canonical_key) {
|
|
71
95
|
return {
|
|
@@ -80,8 +104,6 @@ export function registerAgentSessionTools(server) {
|
|
|
80
104
|
],
|
|
81
105
|
};
|
|
82
106
|
}
|
|
83
|
-
const { cloudRoomId } = await resolveLocalRoomStorageIdentifiers(targetRoomId);
|
|
84
|
-
const apiRoomId = cloudRoomId || targetRoomId;
|
|
85
107
|
const created = await apiCall(`/rooms/${encodeRoomIdPath(apiRoomId)}/agent-sessions`, {
|
|
86
108
|
method: "POST",
|
|
87
109
|
body: JSON.stringify({
|
|
@@ -124,6 +146,7 @@ export function registerAgentSessionTools(server) {
|
|
|
124
146
|
last_seen_at: typeof created.last_seen_at === "string" ? created.last_seen_at : new Date().toISOString(),
|
|
125
147
|
ended_at: typeof created.ended_at === "string" ? created.ended_at : null,
|
|
126
148
|
});
|
|
149
|
+
await bindSupervisedWorkerSession(session);
|
|
127
150
|
scheduleCodexRuntimeStreamBridgeBind(session);
|
|
128
151
|
return {
|
|
129
152
|
content: [
|
|
@@ -258,6 +281,12 @@ export function registerAgentSessionTools(server) {
|
|
|
258
281
|
.optional()
|
|
259
282
|
.describe("Optional hard stop in minutes. Defaults to 0, which means run until stopped."),
|
|
260
283
|
}, async ({ room, cwd, stop_phrase, max_minutes }) => {
|
|
284
|
+
const disabled = workerModeDisabledToolResult("Local Codex session orchestration");
|
|
285
|
+
if (disabled) {
|
|
286
|
+
return {
|
|
287
|
+
content: [{ type: "text", text: JSON.stringify(disabled, null, 2) }],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
261
290
|
const joinedVia = looksLikeInviteCode(room) ? "join_code" : "join_room";
|
|
262
291
|
try {
|
|
263
292
|
const joined = await joinRoomIdentifier(room, joinedVia);
|
|
@@ -305,6 +334,12 @@ export function registerAgentSessionTools(server) {
|
|
|
305
334
|
.optional()
|
|
306
335
|
.describe("Optional session id. Defaults to the current local Codex live session."),
|
|
307
336
|
}, async ({ session_id }) => {
|
|
337
|
+
const disabled = workerModeDisabledToolResult("Local Codex session orchestration");
|
|
338
|
+
if (disabled) {
|
|
339
|
+
return {
|
|
340
|
+
content: [{ type: "text", text: JSON.stringify(disabled, null, 2) }],
|
|
341
|
+
};
|
|
342
|
+
}
|
|
308
343
|
const provider = getManagedAgentProvider("codex");
|
|
309
344
|
const status = await provider.inspectLocalSession(session_id, currentRoom?.room_id);
|
|
310
345
|
if (!status) {
|
|
@@ -343,6 +378,12 @@ export function registerAgentSessionTools(server) {
|
|
|
343
378
|
.optional()
|
|
344
379
|
.describe("If true, also terminate the spawned codex app-server process when possible."),
|
|
345
380
|
}, async ({ session_id, shutdown_server }) => {
|
|
381
|
+
const disabled = workerModeDisabledToolResult("Local Codex session orchestration");
|
|
382
|
+
if (disabled) {
|
|
383
|
+
return {
|
|
384
|
+
content: [{ type: "text", text: JSON.stringify(disabled, null, 2) }],
|
|
385
|
+
};
|
|
386
|
+
}
|
|
346
387
|
const provider = getManagedAgentProvider("codex");
|
|
347
388
|
const stopped = await provider.stopLocalSession({
|
|
348
389
|
session_id,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { encodeRoomIdPath } from "../../../room-id.js";
|
|
2
|
+
import { ApiError, appendIncludePromptOnly, getLocalChatMessages, isMissingRouteError, roomScopedApiCall, } from "../../runtime.js";
|
|
3
|
+
function isRecord(value) {
|
|
4
|
+
return Boolean(value && typeof value === "object");
|
|
5
|
+
}
|
|
6
|
+
export async function findLocalMessageById(roomId, messageId) {
|
|
7
|
+
let afterCursor;
|
|
8
|
+
for (;;) {
|
|
9
|
+
const result = await getLocalChatMessages(roomId, {
|
|
10
|
+
after: afterCursor,
|
|
11
|
+
include_prompt_only: true,
|
|
12
|
+
});
|
|
13
|
+
const messages = (result.messages ?? []).filter(isRecord);
|
|
14
|
+
const match = messages.find((message) => message.id === messageId);
|
|
15
|
+
if (match)
|
|
16
|
+
return match;
|
|
17
|
+
if (!result.has_more || messages.length === 0)
|
|
18
|
+
return null;
|
|
19
|
+
const lastMessage = messages[messages.length - 1];
|
|
20
|
+
afterCursor = typeof lastMessage?.id === "string" ? lastMessage.id : undefined;
|
|
21
|
+
if (!afterCursor)
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function findRemoteMessageById(input) {
|
|
26
|
+
if (input.roomId) {
|
|
27
|
+
try {
|
|
28
|
+
const result = await roomScopedApiCall({
|
|
29
|
+
room_id: input.roomId,
|
|
30
|
+
room_path: (roomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(roomId)}/messages/${encodeURIComponent(input.messageId)}`),
|
|
31
|
+
project_path: () => "",
|
|
32
|
+
// Context lookups must not advance the room session cursor.
|
|
33
|
+
preserve_session_cursor: true,
|
|
34
|
+
});
|
|
35
|
+
return isRecord(result.message) ? result.message : null;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (isMissingRouteError(error)) {
|
|
39
|
+
// Older API without the by-id route: fall back to scanning history.
|
|
40
|
+
}
|
|
41
|
+
else if (error instanceof ApiError && error.status === 404) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return scanRemoteMessageHistoryById(input);
|
|
50
|
+
}
|
|
51
|
+
async function scanRemoteMessageHistoryById(input) {
|
|
52
|
+
let afterCursor;
|
|
53
|
+
for (;;) {
|
|
54
|
+
const query = new URLSearchParams();
|
|
55
|
+
if (afterCursor)
|
|
56
|
+
query.set("after", afterCursor);
|
|
57
|
+
const qs = query.toString();
|
|
58
|
+
const result = await roomScopedApiCall({
|
|
59
|
+
room_id: input.roomId,
|
|
60
|
+
project_id: input.projectId,
|
|
61
|
+
room_path: (roomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(roomId)}/messages${qs ? `?${qs}` : ""}`),
|
|
62
|
+
project_path: (projectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(projectId)}/messages${qs ? `?${qs}` : ""}`),
|
|
63
|
+
});
|
|
64
|
+
const messages = (result.messages ?? []).filter(isRecord);
|
|
65
|
+
const match = messages.find((message) => message.id === input.messageId);
|
|
66
|
+
if (match)
|
|
67
|
+
return match;
|
|
68
|
+
if (!result.has_more || messages.length === 0)
|
|
69
|
+
return null;
|
|
70
|
+
const lastMessage = messages[messages.length - 1];
|
|
71
|
+
afterCursor = typeof lastMessage?.id === "string" ? lastMessage.id : undefined;
|
|
72
|
+
if (!afterCursor)
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -24,7 +24,7 @@ function withRecencyTelemetry(output, selection) {
|
|
|
24
24
|
}
|
|
25
25
|
// Fetch the most recent messages by paging BACKWARDS from the tail
|
|
26
26
|
// (before=latest), so a limited read never walks the full room history.
|
|
27
|
-
async function fetchRecentRemoteMessages(input) {
|
|
27
|
+
export async function fetchRecentRemoteMessages(input) {
|
|
28
28
|
const collected = [];
|
|
29
29
|
let beforeCursor = "latest";
|
|
30
30
|
let truncated = false;
|
|
@@ -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);
|
|
@@ -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 { appendIncludePromptOnly, buildAgentDeliveryHeaders, currentRoom, ensureAgentIdentity, getFallbackProjectId, getLatestLocalChatMessages, getLocalChatMessages, getLastMessageId, getRememberedRoomPresence, getTargetRoomId, identityFromAgentSession, isLocalRoomStorageEnabled, listLocalTasks, resolveLocalRoomStorageIdentifiers, resolveAgentSession, roomScopedApiCall, syncRoomPresence, toAgentReadableMessages, touchRoomSession, WORKER_BEARER_AGENT_SESSION_ID, waitForLocalChatMessages, } from "../../runtime.js";
|
|
5
|
+
import { requireValidWorkerBearerRuntime } from "../../runtime/worker-bearer.js";
|
|
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)) {
|
|
@@ -215,7 +240,7 @@ export function registerWaitForMessagesTool(server) {
|
|
|
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()
|
|
@@ -239,10 +264,19 @@ export function registerWaitForMessagesTool(server) {
|
|
|
239
264
|
const effectiveAfterMessageId = resolveEffectiveAfterMessageId({
|
|
240
265
|
requestedAfterMessageId: after_message_id,
|
|
241
266
|
});
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
267
|
+
const fetchPlan = planWaitForMessagesFetch({ effectiveAfterMessageId });
|
|
268
|
+
// Without a cursor, page BACKWARDS from the tail (getLatest…) so we
|
|
269
|
+
// return the most-recent messages, not the oldest N; with a cursor we
|
|
270
|
+
// keep the unchanged forward "everything after the cursor" behavior.
|
|
271
|
+
const existing = fetchPlan.mode === "catch_up_tail"
|
|
272
|
+
? await getLatestLocalChatMessages(effectiveLocalRoomId, {
|
|
273
|
+
limit: fetchPlan.limit,
|
|
274
|
+
include_prompt_only: true,
|
|
275
|
+
})
|
|
276
|
+
: await getLocalChatMessages(effectiveLocalRoomId, {
|
|
277
|
+
after: effectiveAfterMessageId,
|
|
278
|
+
include_prompt_only: true,
|
|
279
|
+
});
|
|
246
280
|
const replayingExistingMessages = existing.messages.length > 0;
|
|
247
281
|
const result = replayingExistingMessages
|
|
248
282
|
? existing
|
|
@@ -271,45 +305,81 @@ export function registerWaitForMessagesTool(server) {
|
|
|
271
305
|
}
|
|
272
306
|
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, agentSession ? identityFromAgentSession(agentSession) : identity), agentSession);
|
|
273
307
|
const clientTimeout = serverTimeout + (serverTimeout > 120_000 ? 120_000 : 5_000);
|
|
274
|
-
const params = new URLSearchParams();
|
|
275
308
|
const effectiveAfterMessageId = resolveEffectiveAfterMessageId({
|
|
276
309
|
requestedAfterMessageId: after_message_id,
|
|
277
310
|
});
|
|
278
|
-
|
|
279
|
-
params.set("after", effectiveAfterMessageId);
|
|
280
|
-
params.set("timeout", String(serverTimeout));
|
|
281
|
-
const queryString = params.toString();
|
|
311
|
+
const fetchPlan = planWaitForMessagesFetch({ effectiveAfterMessageId });
|
|
282
312
|
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,
|
|
313
|
+
const allMessages = [];
|
|
314
|
+
let roomIdFromResponse;
|
|
315
|
+
// Long-poll the server (blocks up to serverTimeout for new messages),
|
|
316
|
+
// optionally seeded with a cursor. Shared by the cursor path and by the
|
|
317
|
+
// no-cursor empty-tail fallback so a quiet room still blocks instead of
|
|
318
|
+
// busy-spinning. When `after` is provided this also catches up any backlog
|
|
319
|
+
// after the cursor via forward pagination.
|
|
320
|
+
const longPollFromCursor = async (after) => {
|
|
321
|
+
const params = new URLSearchParams();
|
|
322
|
+
if (after)
|
|
323
|
+
params.set("after", after);
|
|
324
|
+
params.set("timeout", String(serverTimeout));
|
|
325
|
+
const queryString = params.toString();
|
|
326
|
+
const firstResult = await roomScopedApiCall({
|
|
327
|
+
room_id: targetRoomId,
|
|
328
|
+
project_id: targetProjectId,
|
|
329
|
+
room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages/poll?${queryString}`),
|
|
330
|
+
project_path: (targetProjectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(targetProjectId)}/messages/poll?${queryString}`),
|
|
331
|
+
options: buildWaitForMessagesRequestOptions({
|
|
305
332
|
deliveryHeaders,
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
333
|
+
signal: AbortSignal.timeout(clientTimeout),
|
|
334
|
+
}),
|
|
335
|
+
});
|
|
336
|
+
allMessages.push(...(firstResult.messages ?? []));
|
|
337
|
+
roomIdFromResponse = roomIdFromResponse || firstResult.room_id || firstResult.project_id;
|
|
338
|
+
if (firstResult.has_more && allMessages.length > 0) {
|
|
339
|
+
let afterCursor = allMessages[allMessages.length - 1]?.id;
|
|
340
|
+
while (afterCursor) {
|
|
341
|
+
const pageParams = new URLSearchParams();
|
|
342
|
+
pageParams.set("after", afterCursor);
|
|
343
|
+
const qs = pageParams.toString();
|
|
344
|
+
const page = await roomScopedApiCall(buildWaitForMessagesHistoryPageRequest({
|
|
345
|
+
targetRoomId,
|
|
346
|
+
targetProjectId,
|
|
347
|
+
queryString: qs,
|
|
348
|
+
deliveryHeaders,
|
|
349
|
+
}));
|
|
350
|
+
const msgs = page.messages ?? [];
|
|
351
|
+
allMessages.push(...msgs);
|
|
352
|
+
if (!page.has_more || msgs.length === 0)
|
|
353
|
+
break;
|
|
354
|
+
afterCursor = msgs[msgs.length - 1]?.id;
|
|
355
|
+
}
|
|
312
356
|
}
|
|
357
|
+
};
|
|
358
|
+
if (fetchPlan.mode === "catch_up_tail") {
|
|
359
|
+
// No cursor: catch up on only the bounded recent tail. Mirror
|
|
360
|
+
// read_messages by paging BACKWARDS from the tail (before=latest) so we
|
|
361
|
+
// return the newest N and never walk the full room history. This also
|
|
362
|
+
// advances the session cursor to the newest message returned.
|
|
363
|
+
const recent = await fetchRecentRemoteMessages({
|
|
364
|
+
targetRoomId,
|
|
365
|
+
targetProjectId,
|
|
366
|
+
limit: fetchPlan.limit,
|
|
367
|
+
});
|
|
368
|
+
if (recent.messages.length > 0) {
|
|
369
|
+
allMessages.push(...recent.messages);
|
|
370
|
+
roomIdFromResponse = recent.roomIdFromResponse;
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
// Empty tail => the room is effectively empty, so there is nothing to
|
|
374
|
+
// dump. Fall through to the long-poll (no cursor) so a worker looping
|
|
375
|
+
// in a quiet room blocks up to the timeout instead of busy-spinning,
|
|
376
|
+
// exactly as the local path does via waitForLocalChatMessages.
|
|
377
|
+
roomIdFromResponse = recent.roomIdFromResponse;
|
|
378
|
+
await longPollFromCursor();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
await longPollFromCursor(fetchPlan.after);
|
|
313
383
|
}
|
|
314
384
|
const routing = filterSilentActivationMessages(allMessages);
|
|
315
385
|
const threadContext = await collectThreadContextMessages({
|
|
@@ -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,
|