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.
Files changed (49) hide show
  1. package/dist/mcp/git-remote.js +7 -7
  2. package/dist/mcp/local-state/agent-sessions.js +78 -5
  3. package/dist/mcp/local-state/local-chat.js +189 -48
  4. package/dist/mcp/local-state/storage.js +16 -9
  5. package/dist/mcp/server/daemon-tool-executor.js +92 -0
  6. package/dist/mcp/server/register-tools.js +25 -12
  7. package/dist/mcp/server/runtime/agent-sessions.js +58 -9
  8. package/dist/mcp/server/runtime/api.js +40 -4
  9. package/dist/mcp/server/runtime/daemon-tool-context.js +11 -0
  10. package/dist/mcp/server/runtime/execution-profile.js +19 -0
  11. package/dist/mcp/server/runtime/identity/directory.js +2 -2
  12. package/dist/mcp/server/runtime/messages.js +55 -0
  13. package/dist/mcp/server/runtime/presence.js +4 -2
  14. package/dist/mcp/server/runtime/room-api.js +24 -7
  15. package/dist/mcp/server/runtime/room-state.js +59 -3
  16. package/dist/mcp/server/runtime/rooms.js +67 -32
  17. package/dist/mcp/server/runtime/supervised-room-authority.js +8 -0
  18. package/dist/mcp/server/runtime/supervisor-bridge.js +702 -24
  19. package/dist/mcp/server/runtime/tool-surface-policy.js +26 -0
  20. package/dist/mcp/server/runtime/worker-bearer.js +44 -6
  21. package/dist/mcp/server/runtime-contract.js +27 -0
  22. package/dist/mcp/server/runtime.js +15 -4
  23. package/dist/mcp/server/supervised-tool-facade.js +134 -0
  24. package/dist/mcp/server/tools/agent-sessions.js +74 -7
  25. package/dist/mcp/server/tools/messages/index.js +3 -2
  26. package/dist/mcp/server/tools/messages/read-tool.js +54 -97
  27. package/dist/mcp/server/tools/messages/reasoning-tool.js +2 -0
  28. package/dist/mcp/server/tools/messages/send-tool.js +5 -0
  29. package/dist/mcp/server/tools/messages/status-tool.js +2 -0
  30. package/dist/mcp/server/tools/messages/wait-tool.js +344 -71
  31. package/dist/mcp/server/tools/onboarding/status-tool.js +9 -8
  32. package/dist/mcp/server/tools/rooms/inspection-tools.js +40 -22
  33. package/dist/mcp/server/tools/rooms/repo-initialization-tool.js +2 -1
  34. package/dist/mcp/server/tools/supervised-room-turn.js +42 -0
  35. package/dist/mcp/server/tools/tasks/board-tools.js +34 -2
  36. package/dist/mcp/server.js +14 -7
  37. package/dist/mcp/sse-client.js +163 -20
  38. package/dist/shared/activation-routing.js +187 -20
  39. package/dist/shared/agent-presence.js +6 -0
  40. package/dist/shared/desktop-release-manifest.js +63 -0
  41. package/dist/shared/desktop-release.js +60 -0
  42. package/dist/shared/scoped-ids.js +6 -0
  43. package/package.json +11 -3
  44. package/shared/message-contracts.d.mts +32 -0
  45. package/shared/message-contracts.mjs +109 -0
  46. package/shared/routing-aliases.d.mts +18 -0
  47. package/shared/routing-aliases.mjs +66 -0
  48. package/shared/sqlite-thread-routing.d.mts +72 -0
  49. package/shared/sqlite-thread-routing.mjs +1038 -0
@@ -4,16 +4,29 @@ import { registerOnboardingTools } from "./tools/onboarding.js";
4
4
  import { registerRentalTools } from "./tools/rental.js";
5
5
  import { registerRepoInitializationTool, registerRepoVisibilityTool, registerRoomInspectionTools, registerRoomJoinTools, registerRoomResumeTool, } from "./tools/rooms.js";
6
6
  import { registerTaskTools } from "./tools/tasks.js";
7
- export function registerTools(server) {
8
- registerRoomJoinTools(server);
9
- registerAgentSessionTools(server);
10
- registerRoomInspectionTools(server);
11
- registerStatusTools(server);
12
- registerTaskTools(server);
13
- registerRepoInitializationTool(server);
14
- registerMessageTools(server);
15
- registerOnboardingTools(server);
16
- registerRoomResumeTool(server);
17
- registerRentalTools(server);
18
- registerRepoVisibilityTool(server);
7
+ import { registerSupervisedRoomTurnTools } from "./tools/supervised-room-turn.js";
8
+ import { toolSurfaceForExecutionProfile } from "./runtime/tool-surface-policy.js";
9
+ import { profileAwareToolServer } from "./supervised-tool-facade.js";
10
+ export function registerTools(server, profile = "autonomous_mcp_worker", supervisedProvider = process.env.LETAGENTS_SUPERVISOR_PROVIDER?.trim() || null, options = {}) {
11
+ const tools = options.executionOwner === "daemon"
12
+ ? server
13
+ : profileAwareToolServer(server, profile, undefined, supervisedProvider);
14
+ const surface = toolSurfaceForExecutionProfile(profile);
15
+ registerRoomJoinTools(tools);
16
+ if (surface.agentSessionLifecycle)
17
+ registerAgentSessionTools(tools);
18
+ registerRoomInspectionTools(tools);
19
+ registerStatusTools(tools);
20
+ registerTaskTools(tools);
21
+ registerRepoInitializationTool(tools);
22
+ registerMessageTools(tools, { includeDeliveryLoop: surface.deliveryLoop });
23
+ if (profile === "supervised_room_turn" && supervisedProvider === "cursor")
24
+ registerSupervisedRoomTurnTools(tools);
25
+ if (surface.onboarding)
26
+ registerOnboardingTools(tools);
27
+ if (surface.roomResume)
28
+ registerRoomResumeTool(tools);
29
+ if (surface.rental)
30
+ registerRentalTools(tools);
31
+ registerRepoVisibilityTool(tools);
19
32
  }
@@ -1,17 +1,20 @@
1
- import { getStoredAgentSession, isLocalRoomStorageEnabled, saveAgentSession, } from "../../local-state.js";
1
+ import { getStoredAgentSession, isLocalRoomStorageEnabled, replaceLocalWorkerAgentSession, saveAgentSession, } from "../../local-state.js";
2
2
  import { getGitCurrentBranch } from "../../git-remote.js";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { normalizeAgentBaseName } from "../../../shared/codenames.js";
5
5
  import { formatOwnerAttribution } from "../../../shared/agent-identity.js";
6
6
  import { LETAGENTS_AGENT_SESSION_ID_HEADER, LETAGENTS_AGENT_SESSION_TOKEN_HEADER, } from "../../../shared/request-headers.js";
7
7
  import { AGENT_INSTANCE_UUID, detectAgentIdeLabel, detectAgentRuntimeLabel, ensureAgentIdentity, } from "./identity.js";
8
- import { requireValidWorkerBearerRuntime } from "./worker-bearer.js";
8
+ import { isSupervisedBoundedTurn, requireValidWorkerBearerRuntime } from "./worker-bearer.js";
9
+ import { resolveCurrentSupervisedWorkerSession } from "./supervisor-bridge.js";
10
+ import { getDaemonToolExecutionContext, getRuntimeWorkingDirectory } from "./daemon-tool-context.js";
9
11
  // A worker bearer already represents a server-side worker session. This local
10
12
  // marker lets the MCP tool contract stay session-shaped without persisting or
11
13
  // transmitting a second set of credentials.
12
14
  export const WORKER_BEARER_AGENT_SESSION_ID = "worker_bearer";
13
15
  export function buildAgentDeliveryHeaders(agentSession) {
14
- if (!agentSession || requireValidWorkerBearerRuntime().mode === "worker") {
16
+ const runtime = requireValidWorkerBearerRuntime();
17
+ if (!agentSession || runtime.mode === "worker" || runtime.mode === "supervised") {
15
18
  return {};
16
19
  }
17
20
  return {
@@ -62,6 +65,33 @@ export function resolveAgentSession(roomId, sessionId) {
62
65
  }
63
66
  return session;
64
67
  }
68
+ /**
69
+ * The stable base this client declares as `requested_base_display_name` when
70
+ * registering. Rules (task_66):
71
+ * - An explicit display_name that replays the EXACT label of any prior stored
72
+ * session for this room+identity is a resume, not a rename: reuse the base
73
+ * recorded when THAT label was allocated, so a server-decorated label
74
+ * converges. The whole lineage is consulted (most recent first) because a
75
+ * latest-only lookup would lose an older concurrent sibling's base and
76
+ * misread its restart as a deliberate rename.
77
+ * - Any other explicit display_name is deliberate intent and IS the base —
78
+ * a numeric-ending custom name ("Agent 47") is therefore never demoted.
79
+ * - With no explicit name, fall back to the most recent recorded base in the
80
+ * lineage, then to the durable identity's display name.
81
+ */
82
+ export function resolveClientRequestedBase(input) {
83
+ const explicit = input.explicitDisplayName?.trim() || "";
84
+ const lineage = input.priorSessions ?? [];
85
+ if (explicit) {
86
+ const replayed = lineage.find((session) => session.display_name?.trim() === explicit);
87
+ const replayedBase = replayed?.requested_base_display_name?.trim() || "";
88
+ return replayedBase || explicit;
89
+ }
90
+ const latestBase = lineage
91
+ .map((session) => session.requested_base_display_name?.trim() || "")
92
+ .find((base) => base.length > 0);
93
+ return latestBase || input.identityDisplayName.trim();
94
+ }
65
95
  export function identityFromAgentSession(session) {
66
96
  return {
67
97
  name: normalizeAgentBaseName(session.display_name),
@@ -88,7 +118,22 @@ export function requireWorkerAgentSession(roomId, sessionId) {
88
118
  return session;
89
119
  }
90
120
  export async function resolveWorkerToolIdentity(input) {
91
- if (requireValidWorkerBearerRuntime().mode === "worker" &&
121
+ const runtimeMode = requireValidWorkerBearerRuntime().mode;
122
+ if (runtimeMode === "supervised") {
123
+ const daemonContext = getDaemonToolExecutionContext();
124
+ const agentSession = daemonContext?.agentSession
125
+ ?? await resolveCurrentSupervisedWorkerSession(input.roomId);
126
+ if (input.roomId && input.roomId !== agentSession.room_id) {
127
+ throw new Error(`Daemon-supervised worker session is registered for ${agentSession.room_id}, not ${input.roomId}.`);
128
+ }
129
+ if (input.agentSessionId
130
+ && input.agentSessionId !== WORKER_BEARER_AGENT_SESSION_ID
131
+ && input.agentSessionId !== agentSession.session_id) {
132
+ throw new Error(`Daemon-supervised worker session is ${agentSession.session_id}, not ${input.agentSessionId}.`);
133
+ }
134
+ return { identity: identityFromAgentSession(agentSession), agentSession };
135
+ }
136
+ if (runtimeMode === "worker" &&
92
137
  (!input.agentSessionId || input.agentSessionId === WORKER_BEARER_AGENT_SESSION_ID)) {
93
138
  const identity = await ensureAgentIdentity();
94
139
  const now = new Date().toISOString();
@@ -121,7 +166,7 @@ export async function resolveWorkerToolIdentity(input) {
121
166
  }
122
167
  const agentSession = input.agentSessionId
123
168
  ? requireWorkerAgentSession(input.roomId, input.agentSessionId)
124
- : input.roomId && await isLocalRoomStorageEnabled(input.roomId)
169
+ : input.roomId && !isSupervisedBoundedTurn() && await isLocalRoomStorageEnabled(input.roomId)
125
170
  ? await ensureLocalWorkerAgentSession(input.roomId)
126
171
  : requireWorkerAgentSession(input.roomId, input.agentSessionId);
127
172
  return {
@@ -134,7 +179,7 @@ export async function ensureLocalWorkerAgentSession(roomId, input = {}) {
134
179
  const now = new Date().toISOString();
135
180
  const runtime = input.runtime?.trim() || detectAgentRuntimeLabel();
136
181
  const displayName = input.displayName?.trim() || identity.display_name;
137
- return saveAgentSession({
182
+ const session = {
138
183
  session_id: `local_${randomUUID()}`,
139
184
  session_token: `local_${randomUUID()}`,
140
185
  room_id: roomId,
@@ -156,14 +201,18 @@ export async function ensureLocalWorkerAgentSession(roomId, input = {}) {
156
201
  updated_at: now,
157
202
  last_seen_at: now,
158
203
  ended_at: null,
159
- });
204
+ };
205
+ return session.session_kind === "worker"
206
+ ? replaceLocalWorkerAgentSession(session)
207
+ : saveAgentSession(session);
160
208
  }
161
209
  export function getAgentSessionRepoBranch(cwd) {
162
- const workingDir = cwd?.trim() || process.cwd();
210
+ const workingDir = cwd?.trim() || getRuntimeWorkingDirectory();
163
211
  return getGitCurrentBranch(workingDir);
164
212
  }
165
213
  export function agentSessionCredentials(agentSession) {
166
- if (requireValidWorkerBearerRuntime().mode === "worker") {
214
+ const runtime = requireValidWorkerBearerRuntime();
215
+ if (runtime.mode === "worker" || runtime.mode === "supervised") {
167
216
  return {};
168
217
  }
169
218
  return {
@@ -1,10 +1,38 @@
1
1
  import { clearAuthenticatedAccountCache } from "./auth-cache.js";
2
+ import { getDaemonToolExecutionContext } from "./daemon-tool-context.js";
2
3
  import { requireValidWorkerBearerRuntime } from "./worker-bearer.js";
4
+ import { borrowCurrentSupervisedWorkerCredential, } from "./supervisor-bridge.js";
3
5
  let ownerAuthStoreLoader = () => import("../../local-state.js");
6
+ let supervisedCredentialBorrower = () => borrowCurrentSupervisedWorkerCredential();
4
7
  export function setOwnerAuthStoreLoaderForTest(loader) {
5
8
  ownerAuthStoreLoader = loader ?? (() => import("../../local-state.js"));
6
9
  }
10
+ export function setSupervisedCredentialBorrowerForTest(borrower) {
11
+ supervisedCredentialBorrower = borrower ?? (() => borrowCurrentSupervisedWorkerCredential());
12
+ }
13
+ export class SupervisedWorkerCredentialError extends Error {
14
+ code;
15
+ constructor(code) {
16
+ super(code === "SUPERVISED_CREDENTIAL_UNAVAILABLE"
17
+ ? "The daemon-supervised worker credential is not available yet."
18
+ : "The daemon-supervised worker credential is stale or missing its exact context.");
19
+ this.code = code;
20
+ this.name = "SupervisedWorkerCredentialError";
21
+ }
22
+ }
23
+ async function getSupervisedCredential() {
24
+ const daemonContext = getDaemonToolExecutionContext();
25
+ if (daemonContext)
26
+ return daemonContext.bearer;
27
+ const result = await supervisedCredentialBorrower();
28
+ if (result.state === "available")
29
+ return result.credential;
30
+ throw new SupervisedWorkerCredentialError(result.state === "deferred" ? "SUPERVISED_CREDENTIAL_UNAVAILABLE" : "SUPERVISED_CREDENTIAL_STALE");
31
+ }
7
32
  export const API_URL = (process.env.LETAGENTS_API_URL || "http://localhost:3001").replace(/\/+$/, "");
33
+ export function getApiUrl() {
34
+ return getDaemonToolExecutionContext()?.apiUrl.replace(/\/+$/, "") ?? API_URL;
35
+ }
8
36
  export class ApiError extends Error {
9
37
  status;
10
38
  body;
@@ -20,6 +48,8 @@ export async function getLetagentsToken() {
20
48
  if (runtime.mode === "worker") {
21
49
  return runtime.bearer;
22
50
  }
51
+ if (runtime.mode === "supervised")
52
+ return getSupervisedCredential();
23
53
  const envToken = process.env.LETAGENTS_TOKEN?.trim();
24
54
  if (envToken) {
25
55
  return envToken;
@@ -53,8 +83,9 @@ export function resolveApiPath(urlOrPath) {
53
83
  return "/auth/device/start";
54
84
  }
55
85
  try {
56
- const parsed = new URL(urlOrPath, `${API_URL}/`);
57
- const apiBase = new URL(`${API_URL}/`);
86
+ const apiUrl = getApiUrl();
87
+ const parsed = new URL(urlOrPath, `${apiUrl}/`);
88
+ const apiBase = new URL(`${apiUrl}/`);
58
89
  if (parsed.origin !== apiBase.origin) {
59
90
  return "/auth/device/start";
60
91
  }
@@ -75,19 +106,24 @@ export async function apiCall(path, options) {
75
106
  // every caller spelling of Authorization is overwritten.
76
107
  headers.set("Authorization", `Bearer ${runtime.bearer}`);
77
108
  }
109
+ else if (runtime.mode === "supervised") {
110
+ // Resolve on every API request: the daemon may rotate the in-memory
111
+ // credential while Codex remains running.
112
+ headers.set("Authorization", `Bearer ${await getSupervisedCredential()}`);
113
+ }
78
114
  else {
79
115
  const authorizationHeader = await getAuthorizationHeader();
80
116
  if (authorizationHeader && !headers.has("Authorization")) {
81
117
  headers.set("Authorization", authorizationHeader);
82
118
  }
83
119
  }
84
- const res = await fetch(`${API_URL}${path}`, {
120
+ const res = await fetch(`${getApiUrl()}${path}`, {
85
121
  ...options,
86
122
  headers,
87
123
  });
88
124
  if (!res.ok) {
89
125
  const body = await res.text();
90
- if (res.status === 401 && requireValidWorkerBearerRuntime().mode !== "worker") {
126
+ if (res.status === 401 && requireValidWorkerBearerRuntime().mode === "owner") {
91
127
  // Only clear on 401 (invalid/expired credential), NOT on 403
92
128
  // (valid credential but insufficient permissions, e.g., private repo access)
93
129
  const { clearStoredAuth } = await ownerAuthStoreLoader();
@@ -0,0 +1,11 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const daemonToolContext = new AsyncLocalStorage();
3
+ export function getDaemonToolExecutionContext() {
4
+ return daemonToolContext.getStore() ?? null;
5
+ }
6
+ export function runWithDaemonToolExecutionContext(context, callback) {
7
+ return daemonToolContext.run(context, callback);
8
+ }
9
+ export function getRuntimeWorkingDirectory() {
10
+ return getDaemonToolExecutionContext()?.cwd ?? process.cwd();
11
+ }
@@ -0,0 +1,19 @@
1
+ export const EXECUTION_PROFILES = [
2
+ "supervised_room_turn",
3
+ "autonomous_mcp_worker",
4
+ "interactive_desktop",
5
+ ];
6
+ export const LETAGENTS_EXECUTION_PROFILE_ENV = "LETAGENTS_EXECUTION_PROFILE";
7
+ export const LETAGENTS_SUPERVISED_BOUNDED_TURNS_ENV = "LETAGENTS_SUPERVISED_BOUNDED_TURNS";
8
+ export function executionProfile(env = process.env) {
9
+ const configured = env[LETAGENTS_EXECUTION_PROFILE_ENV]?.trim();
10
+ const bounded = env[LETAGENTS_SUPERVISED_BOUNDED_TURNS_ENV]?.trim() === "1";
11
+ if (configured && !EXECUTION_PROFILES.includes(configured)) {
12
+ throw new Error(`Invalid ${LETAGENTS_EXECUTION_PROFILE_ENV}: ${configured}`);
13
+ }
14
+ const profile = (configured || "autonomous_mcp_worker");
15
+ if (bounded !== (profile === "supervised_room_turn")) {
16
+ throw new Error(`${LETAGENTS_EXECUTION_PROFILE_ENV}=supervised_room_turn and ${LETAGENTS_SUPERVISED_BOUNDED_TURNS_ENV}=1 must be configured together.`);
17
+ }
18
+ return profile;
19
+ }
@@ -5,7 +5,7 @@ import { requireValidWorkerBearerRuntime } from "../worker-bearer.js";
5
5
  import { getAuthenticatedAccountCache, setAuthenticatedAccountCache, } from "../auth-cache.js";
6
6
  import { AGENT_OWNER_LABEL, readCommandOutput, } from "./config.js";
7
7
  export async function getAuthenticatedAgentDirectory() {
8
- if (requireValidWorkerBearerRuntime().mode === "worker") {
8
+ if (requireValidWorkerBearerRuntime().mode !== "owner") {
9
9
  return null;
10
10
  }
11
11
  try {
@@ -25,7 +25,7 @@ export async function getAuthenticatedAgentDirectory() {
25
25
  }
26
26
  }
27
27
  async function getAuthenticatedAccountProfile() {
28
- if (requireValidWorkerBearerRuntime().mode === "worker") {
28
+ if (requireValidWorkerBearerRuntime().mode !== "owner") {
29
29
  return null;
30
30
  }
31
31
  const envToken = (process.env.LETAGENTS_TOKEN || "").trim();
@@ -151,6 +151,61 @@ export function toAgentReadableMessages(messages, contextMessages = []) {
151
151
  const summaries = buildThreadSummaries(records, recordsById);
152
152
  return (messages ?? []).map((message) => toAgentReadableMessage(message, recordsById, summaries));
153
153
  }
154
+ export const AGENT_MESSAGE_OUTPUT_MAX_BYTES = 4 * 1024 * 1024;
155
+ export const AGENT_MESSAGE_BODY_MAX_BYTES = AGENT_MESSAGE_OUTPUT_MAX_BYTES / 2;
156
+ function jsonUtf8Bytes(value) {
157
+ const serialized = JSON.stringify(value);
158
+ return Buffer.byteLength(serialized === undefined ? "null" : serialized, "utf8");
159
+ }
160
+ function compactOversizedAgentMessage(message) {
161
+ if (!isRecord(message)) {
162
+ return { content_truncated: true, value_type: typeof message };
163
+ }
164
+ const text = typeof message.text === "string" ? message.text : "";
165
+ return {
166
+ ...(typeof message.id === "string" ? { id: message.id } : {}),
167
+ ...(typeof message.sender === "string" ? { sender: message.sender } : {}),
168
+ ...(typeof message.source === "string" ? { source: message.source } : {}),
169
+ ...(typeof message.timestamp === "string" ? { timestamp: message.timestamp } : {}),
170
+ ...(typeof message.thread_root_id === "string"
171
+ ? { thread_root_id: message.thread_root_id }
172
+ : {}),
173
+ ...(typeof message.thread_reply_to_id === "string"
174
+ ? { thread_reply_to_id: message.thread_reply_to_id }
175
+ : {}),
176
+ text: text.slice(0, 16 * 1024),
177
+ content_truncated: true,
178
+ original_utf8_bytes: jsonUtf8Bytes(message),
179
+ };
180
+ }
181
+ export function boundAgentMessageOutput(messages, options = {}) {
182
+ const direction = options.direction ?? "prefix";
183
+ const maxBytes = Math.max(1024, Math.floor(options.maxBytes ?? AGENT_MESSAGE_OUTPUT_MAX_BYTES));
184
+ const ordered = direction === "suffix" ? [...messages].reverse() : [...messages];
185
+ const selected = [];
186
+ let outputBytes = 2; // JSON array brackets.
187
+ for (const message of ordered) {
188
+ let candidate = message;
189
+ let candidateBytes = jsonUtf8Bytes(candidate);
190
+ if (candidateBytes + 2 > maxBytes) {
191
+ candidate = compactOversizedAgentMessage(candidate);
192
+ candidateBytes = jsonUtf8Bytes(candidate);
193
+ }
194
+ const separatorBytes = selected.length > 0 ? 1 : 0;
195
+ if (outputBytes + separatorBytes + candidateBytes > maxBytes)
196
+ break;
197
+ selected.push(candidate);
198
+ outputBytes += separatorBytes + candidateBytes;
199
+ }
200
+ if (direction === "suffix")
201
+ selected.reverse();
202
+ return {
203
+ messages: selected,
204
+ truncated: selected.length < messages.length,
205
+ omittedMessageCount: messages.length - selected.length,
206
+ outputBytes,
207
+ };
208
+ }
154
209
  export function appendIncludePromptOnly(path) {
155
210
  return `${path}${path.includes("?") ? "&" : "?"}include_prompt_only=1`;
156
211
  }
@@ -4,6 +4,7 @@ import { isLocalRoomStorageEnabled, resolveLocalRoomStorageIdentifiers, touchRoo
4
4
  import { apiCall, isMissingRouteError } from "./api.js";
5
5
  import { agentSessionCredentials, identityFromAgentSession } from "./agent-sessions.js";
6
6
  import { getSessionLivenessRegistration } from "./identity.js";
7
+ import { isSupervisedBoundedTurn } from "./worker-bearer.js";
7
8
  const roomPresenceByIdentity = new Map();
8
9
  export function getRememberedRoomPresence(roomId, identity) {
9
10
  if (!roomId || !identity) {
@@ -18,7 +19,7 @@ export async function syncRoomPresence(roomId, identity, presence, agentSession)
18
19
  }
19
20
  roomPresenceByIdentity.set(getRoomIdentityPresenceCacheKey(roomId, resolvedIdentity.actor_label), presence);
20
21
  const { localRoomId, cloudRoomId } = await resolveLocalRoomStorageIdentifiers(roomId);
21
- if (await isLocalRoomStorageEnabled(roomId)) {
22
+ if (!isSupervisedBoundedTurn() && await isLocalRoomStorageEnabled(roomId)) {
22
23
  touchRoomSession(localRoomId || roomId);
23
24
  return;
24
25
  }
@@ -38,7 +39,8 @@ export async function syncRoomPresence(roomId, identity, presence, agentSession)
38
39
  ...agentSessionCredentials(agentSession),
39
40
  }),
40
41
  });
41
- touchRoomSession(apiRoomId);
42
+ if (!isSupervisedBoundedTurn())
43
+ touchRoomSession(apiRoomId);
42
44
  }
43
45
  catch (error) {
44
46
  if (isMissingRouteError(error)) {
@@ -3,13 +3,24 @@ import { LETAGENTS_ORIGIN_ROOM_ID_HEADER } from "../../../shared/request-headers
3
3
  import { apiCall, isMissingRouteError, } from "./api.js";
4
4
  import { maybeHandleRepoRoomAuthRequired } from "./device-auth.js";
5
5
  import { getLastMessageId } from "./messages.js";
6
- import { currentRoom } from "./room-state.js";
6
+ import { currentRoom, getCurrentSupervisedRoomAuthority } from "./room-state.js";
7
+ import { isSupervisedBoundedTurn } from "./worker-bearer.js";
7
8
  export async function roomScopedApiCall(input) {
9
+ const supervised = isSupervisedBoundedTurn();
10
+ const exactRoomAuthority = supervised ? getCurrentSupervisedRoomAuthority() : null;
11
+ if (supervised && (!exactRoomAuthority || input.room_id !== exactRoomAuthority)) {
12
+ throw new Error("The daemon-supervised API request is missing its exact per-call room authority.");
13
+ }
8
14
  const headers = {
9
15
  ...input.options?.headers,
10
16
  };
11
- if (currentRoom?.room_id &&
12
- !Object.keys(headers).some((key) => key.toLowerCase() === LETAGENTS_ORIGIN_ROOM_ID_HEADER.toLowerCase())) {
17
+ const originHeaderKey = Object.keys(headers).find((key) => key.toLowerCase() === LETAGENTS_ORIGIN_ROOM_ID_HEADER.toLowerCase());
18
+ if (exactRoomAuthority) {
19
+ if (originHeaderKey)
20
+ delete headers[originHeaderKey];
21
+ headers[LETAGENTS_ORIGIN_ROOM_ID_HEADER] = exactRoomAuthority;
22
+ }
23
+ else if (currentRoom?.room_id && !originHeaderKey) {
13
24
  headers[LETAGENTS_ORIGIN_ROOM_ID_HEADER] = currentRoom.room_id;
14
25
  }
15
26
  const options = {
@@ -17,16 +28,20 @@ export async function roomScopedApiCall(input) {
17
28
  headers,
18
29
  };
19
30
  if (input.room_id) {
20
- const { cloudRoomId } = await resolveLocalRoomStorageIdentifiers(input.room_id);
31
+ const cloudRoomId = supervised
32
+ ? null
33
+ : (await resolveLocalRoomStorageIdentifiers(input.room_id)).cloudRoomId;
21
34
  const apiRoomId = cloudRoomId || input.room_id;
22
35
  try {
23
36
  const result = await apiCall(input.room_path(apiRoomId), options);
24
- touchRoomSession(input.room_id, input.preserve_session_cursor ? undefined : getLastMessageId(result));
37
+ if (!supervised) {
38
+ touchRoomSession(input.room_id, input.preserve_session_cursor ? undefined : getLastMessageId(result));
39
+ }
25
40
  return result;
26
41
  }
27
42
  catch (error) {
28
43
  await maybeHandleRepoRoomAuthRequired(error, apiRoomId);
29
- if (!input.project_id || !isMissingRouteError(error)) {
44
+ if (supervised || !input.project_id || !isMissingRouteError(error)) {
30
45
  throw error;
31
46
  }
32
47
  }
@@ -36,7 +51,9 @@ export async function roomScopedApiCall(input) {
36
51
  }
37
52
  const result = await apiCall(input.project_path(input.project_id), options);
38
53
  if (input.room_id) {
39
- touchRoomSession(input.room_id, input.preserve_session_cursor ? undefined : getLastMessageId(result));
54
+ if (!supervised) {
55
+ touchRoomSession(input.room_id, input.preserve_session_cursor ? undefined : getLastMessageId(result));
56
+ }
40
57
  }
41
58
  return result;
42
59
  }
@@ -1,8 +1,10 @@
1
1
  import { SseClient } from "../../sse-client.js";
2
2
  import { getStoredAgentIdentity, saveRoomSession, touchRoomSession, } from "../../local-state.js";
3
3
  import { getCanonicalRoomWebPath, } from "../../room-id.js";
4
- import { API_URL, getLetagentsToken } from "./api.js";
4
+ import { getApiUrl, getLetagentsToken } from "./api.js";
5
5
  import { AGENT_INSTANCE_UUID, currentAgentIdentity, currentAgentIdentityKey, } from "./identity.js";
6
+ import { isSupervisedBoundedTurn } from "./worker-bearer.js";
7
+ import { getCurrentSupervisedRoomAuthority, runWithSupervisedRoomAuthority, } from "./supervised-room-authority.js";
6
8
  let mcpServer = null;
7
9
  let sseClient = null;
8
10
  export let currentRoom = null;
@@ -13,7 +15,7 @@ export function shutdownRuntime() {
13
15
  sseClient?.unsubscribeAll();
14
16
  }
15
17
  function getSseClient() {
16
- sseClient ??= new SseClient(API_URL, () => getLetagentsToken());
18
+ sseClient ??= new SseClient(getApiUrl(), () => getLetagentsToken());
17
19
  return sseClient;
18
20
  }
19
21
  function getCurrentStreamAgentIdentity() {
@@ -30,6 +32,7 @@ function getCurrentStreamAgentIdentity() {
30
32
  export function toRoomState(input) {
31
33
  return {
32
34
  room_id: input.room_id,
35
+ navigation_locator: input.navigation_locator ?? null,
33
36
  project_id: input.project_id ?? null,
34
37
  code: input.code ?? null,
35
38
  display_name: input.display_name ?? null,
@@ -38,8 +41,12 @@ export function toRoomState(input) {
38
41
  is_local: input.is_local ?? false,
39
42
  };
40
43
  }
44
+ export function currentRoomMatchesLocator(locator) {
45
+ const value = locator?.trim();
46
+ return Boolean(value && currentRoom && (currentRoom.room_id === value || currentRoom.navigation_locator === value));
47
+ }
41
48
  function getCanonicalRoomWebUrl(roomId) {
42
- return new URL(getCanonicalRoomWebPath(roomId), `${API_URL}/`).toString();
49
+ return new URL(getCanonicalRoomWebPath(roomId), `${getApiUrl()}/`).toString();
43
50
  }
44
51
  export function withCanonicalRoomLink(roomId, payload) {
45
52
  return {
@@ -85,6 +92,8 @@ export function toPublicRoomResponse(response, fallbackRoomId) {
85
92
  }
86
93
  export function rememberRoom(state, lastMessageId) {
87
94
  currentRoom = state;
95
+ if (isSupervisedBoundedTurn())
96
+ return state;
88
97
  saveRoomSession({
89
98
  room_id: state.room_id,
90
99
  project_id: state.project_id ?? null,
@@ -105,18 +114,65 @@ export function rememberRoom(state, lastMessageId) {
105
114
  }, (_message) => {
106
115
  touchRoomSession(state.room_id);
107
116
  mcpServer?.server.sendResourceListChanged();
117
+ }, () => {
118
+ // The SSE cursor crossed a broker/bridge loss boundary. MCP resources
119
+ // are pull-based, so invalidating the list is the full-state repair.
120
+ touchRoomSession(state.room_id);
121
+ mcpServer?.server.sendResourceListChanged();
108
122
  });
109
123
  return state;
110
124
  }
111
125
  export function touchCurrentRoom(lastMessageId) {
126
+ if (isSupervisedBoundedTurn())
127
+ return;
112
128
  if (!currentRoom) {
113
129
  return;
114
130
  }
115
131
  touchRoomSession(currentRoom.room_id, lastMessageId);
116
132
  }
117
133
  export function getTargetRoomId(roomId) {
134
+ if (isSupervisedBoundedTurn()) {
135
+ const exactRoomAuthority = getCurrentSupervisedRoomAuthority();
136
+ if (!exactRoomAuthority) {
137
+ throw new Error("The daemon-supervised tool has not received its exact room authority.");
138
+ }
139
+ if (roomId && roomId !== exactRoomAuthority) {
140
+ throw new Error(`The daemon-supervised tool is authorized for ${exactRoomAuthority}, not ${roomId}.`);
141
+ }
142
+ return exactRoomAuthority;
143
+ }
118
144
  return roomId || currentRoom?.room_id || null;
119
145
  }
146
+ /** The last room authority returned by this process's exact daemon effect. */
147
+ export { getCurrentSupervisedRoomAuthority } from "./supervised-room-authority.js";
148
+ /** Public room metadata without inventing join provenance or locality. */
149
+ export function toPublicCurrentRoomState() {
150
+ const exactRoomAuthority = getCurrentSupervisedRoomAuthority();
151
+ if (!exactRoomAuthority)
152
+ return toPublicRoomState(currentRoom);
153
+ return {
154
+ ...toPublicRoomState(toRoomState({ room_id: exactRoomAuthority, joined_via: "join_room" })),
155
+ joined_via: null,
156
+ is_local: null,
157
+ };
158
+ }
159
+ /**
160
+ * Bind only the in-memory default used by one supervised MCP process. The
161
+ * daemon response is the authority; this performs no join, storage, SSE, or
162
+ * repository inspection and can safely rebind after a durable room move.
163
+ */
164
+ export function runWithCurrentSupervisedRoom(roomId, callback) {
165
+ if (!isSupervisedBoundedTurn()) {
166
+ throw new Error("Only a daemon-supervised bounded turn can bind supervisor room authority.");
167
+ }
168
+ const normalized = roomId.trim();
169
+ if (!normalized || normalized.length > 1_024 || /[\u0000-\u001f\u007f]/.test(normalized)) {
170
+ throw new Error("The daemon-supervised room authority is malformed.");
171
+ }
172
+ return runWithSupervisedRoomAuthority(normalized, callback);
173
+ }
120
174
  export function getFallbackProjectId() {
175
+ if (isSupervisedBoundedTurn())
176
+ return null;
121
177
  return currentRoom?.project_id ?? null;
122
178
  }