letagents 0.12.12 → 0.12.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/mcp/git-remote.js +7 -7
  2. package/dist/mcp/local-state/agent-sessions.js +63 -5
  3. package/dist/mcp/local-state/local-chat.js +189 -48
  4. package/dist/mcp/local-state/storage.js +16 -9
  5. package/dist/mcp/server/daemon-tool-executor.js +92 -0
  6. package/dist/mcp/server/register-tools.js +4 -2
  7. package/dist/mcp/server/runtime/agent-sessions.js +16 -7
  8. package/dist/mcp/server/runtime/api.js +11 -3
  9. package/dist/mcp/server/runtime/daemon-tool-context.js +11 -0
  10. package/dist/mcp/server/runtime/execution-profile.js +1 -0
  11. package/dist/mcp/server/runtime/messages.js +55 -0
  12. package/dist/mcp/server/runtime/presence.js +3 -3
  13. package/dist/mcp/server/runtime/room-api.js +2 -2
  14. package/dist/mcp/server/runtime/room-state.js +19 -9
  15. package/dist/mcp/server/runtime/rooms.js +54 -33
  16. package/dist/mcp/server/runtime/supervisor-bridge.js +129 -11
  17. package/dist/mcp/server/runtime/tool-surface-policy.js +7 -0
  18. package/dist/mcp/server/runtime/worker-bearer.js +17 -2
  19. package/dist/mcp/server/runtime-contract.js +1 -0
  20. package/dist/mcp/server/runtime.js +6 -6
  21. package/dist/mcp/server/supervised-tool-facade.js +107 -6
  22. package/dist/mcp/server/tools/messages/read-tool.js +54 -97
  23. package/dist/mcp/server/tools/messages/reasoning-tool.js +2 -0
  24. package/dist/mcp/server/tools/messages/send-tool.js +2 -0
  25. package/dist/mcp/server/tools/messages/status-tool.js +2 -0
  26. package/dist/mcp/server/tools/messages/wait-tool.js +303 -71
  27. package/dist/mcp/server/tools/onboarding/status-tool.js +4 -4
  28. package/dist/mcp/server/tools/rooms/inspection-tools.js +15 -10
  29. package/dist/mcp/server/tools/rooms/repo-initialization-tool.js +2 -1
  30. package/dist/mcp/server/tools/tasks/board-tools.js +34 -2
  31. package/dist/mcp/sse-client.js +163 -20
  32. package/dist/shared/activation-routing.js +163 -23
  33. package/dist/shared/agent-presence.js +6 -0
  34. package/dist/shared/desktop-release-manifest.js +63 -0
  35. package/dist/shared/desktop-release.js +60 -0
  36. package/dist/shared/scoped-ids.js +6 -0
  37. package/package.json +7 -3
  38. package/shared/execution-approval-projection.d.mts +32 -0
  39. package/shared/execution-approval-projection.mjs +107 -0
  40. package/shared/execution-approval-publication-item.d.mts +20 -0
  41. package/shared/execution-approval-publication-item.mjs +61 -0
  42. package/shared/execution-approval-publication.d.mts +53 -0
  43. package/shared/execution-approval-publication.mjs +136 -0
  44. package/shared/execution-delegation-decision.d.mts +37 -0
  45. package/shared/execution-delegation-decision.mjs +73 -0
  46. package/shared/message-contracts.d.mts +32 -0
  47. package/shared/message-contracts.mjs +109 -0
  48. package/shared/room-agent-work.d.mts +31 -0
  49. package/shared/room-agent-work.mjs +44 -0
  50. package/shared/room-resource-invalidation.d.mts +38 -0
  51. package/shared/room-resource-invalidation.mjs +50 -0
  52. package/shared/routing-aliases.d.mts +18 -0
  53. package/shared/routing-aliases.mjs +66 -0
  54. package/shared/sqlite-thread-routing.d.mts +72 -0
  55. package/shared/sqlite-thread-routing.mjs +1038 -0
@@ -1,12 +1,13 @@
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 { isSupervisedBoundedTurn, requireValidWorkerBearerRuntime } from "./worker-bearer.js";
8
+ import { hasSupervisedWorkerAuthority, requireValidWorkerBearerRuntime } from "./worker-bearer.js";
9
9
  import { resolveCurrentSupervisedWorkerSession } from "./supervisor-bridge.js";
10
+ import { getDaemonToolExecutionContext, getRuntimeWorkingDirectory } from "./daemon-tool-context.js";
10
11
  // A worker bearer already represents a server-side worker session. This local
11
12
  // marker lets the MCP tool contract stay session-shaped without persisting or
12
13
  // transmitting a second set of credentials.
@@ -119,7 +120,12 @@ export function requireWorkerAgentSession(roomId, sessionId) {
119
120
  export async function resolveWorkerToolIdentity(input) {
120
121
  const runtimeMode = requireValidWorkerBearerRuntime().mode;
121
122
  if (runtimeMode === "supervised") {
122
- const agentSession = await resolveCurrentSupervisedWorkerSession(input.roomId);
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
+ }
123
129
  if (input.agentSessionId
124
130
  && input.agentSessionId !== WORKER_BEARER_AGENT_SESSION_ID
125
131
  && input.agentSessionId !== agentSession.session_id) {
@@ -160,7 +166,7 @@ export async function resolveWorkerToolIdentity(input) {
160
166
  }
161
167
  const agentSession = input.agentSessionId
162
168
  ? requireWorkerAgentSession(input.roomId, input.agentSessionId)
163
- : input.roomId && !isSupervisedBoundedTurn() && await isLocalRoomStorageEnabled(input.roomId)
169
+ : input.roomId && !hasSupervisedWorkerAuthority() && await isLocalRoomStorageEnabled(input.roomId)
164
170
  ? await ensureLocalWorkerAgentSession(input.roomId)
165
171
  : requireWorkerAgentSession(input.roomId, input.agentSessionId);
166
172
  return {
@@ -173,7 +179,7 @@ export async function ensureLocalWorkerAgentSession(roomId, input = {}) {
173
179
  const now = new Date().toISOString();
174
180
  const runtime = input.runtime?.trim() || detectAgentRuntimeLabel();
175
181
  const displayName = input.displayName?.trim() || identity.display_name;
176
- return saveAgentSession({
182
+ const session = {
177
183
  session_id: `local_${randomUUID()}`,
178
184
  session_token: `local_${randomUUID()}`,
179
185
  room_id: roomId,
@@ -195,10 +201,13 @@ export async function ensureLocalWorkerAgentSession(roomId, input = {}) {
195
201
  updated_at: now,
196
202
  last_seen_at: now,
197
203
  ended_at: null,
198
- });
204
+ };
205
+ return session.session_kind === "worker"
206
+ ? replaceLocalWorkerAgentSession(session)
207
+ : saveAgentSession(session);
199
208
  }
200
209
  export function getAgentSessionRepoBranch(cwd) {
201
- const workingDir = cwd?.trim() || process.cwd();
210
+ const workingDir = cwd?.trim() || getRuntimeWorkingDirectory();
202
211
  return getGitCurrentBranch(workingDir);
203
212
  }
204
213
  export function agentSessionCredentials(agentSession) {
@@ -1,4 +1,5 @@
1
1
  import { clearAuthenticatedAccountCache } from "./auth-cache.js";
2
+ import { getDaemonToolExecutionContext } from "./daemon-tool-context.js";
2
3
  import { requireValidWorkerBearerRuntime } from "./worker-bearer.js";
3
4
  import { borrowCurrentSupervisedWorkerCredential, } from "./supervisor-bridge.js";
4
5
  let ownerAuthStoreLoader = () => import("../../local-state.js");
@@ -20,12 +21,18 @@ export class SupervisedWorkerCredentialError extends Error {
20
21
  }
21
22
  }
22
23
  async function getSupervisedCredential() {
24
+ const daemonContext = getDaemonToolExecutionContext();
25
+ if (daemonContext)
26
+ return daemonContext.bearer;
23
27
  const result = await supervisedCredentialBorrower();
24
28
  if (result.state === "available")
25
29
  return result.credential;
26
30
  throw new SupervisedWorkerCredentialError(result.state === "deferred" ? "SUPERVISED_CREDENTIAL_UNAVAILABLE" : "SUPERVISED_CREDENTIAL_STALE");
27
31
  }
28
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
+ }
29
36
  export class ApiError extends Error {
30
37
  status;
31
38
  body;
@@ -76,8 +83,9 @@ export function resolveApiPath(urlOrPath) {
76
83
  return "/auth/device/start";
77
84
  }
78
85
  try {
79
- const parsed = new URL(urlOrPath, `${API_URL}/`);
80
- const apiBase = new URL(`${API_URL}/`);
86
+ const apiUrl = getApiUrl();
87
+ const parsed = new URL(urlOrPath, `${apiUrl}/`);
88
+ const apiBase = new URL(`${apiUrl}/`);
81
89
  if (parsed.origin !== apiBase.origin) {
82
90
  return "/auth/device/start";
83
91
  }
@@ -109,7 +117,7 @@ export async function apiCall(path, options) {
109
117
  headers.set("Authorization", authorizationHeader);
110
118
  }
111
119
  }
112
- const res = await fetch(`${API_URL}${path}`, {
120
+ const res = await fetch(`${getApiUrl()}${path}`, {
113
121
  ...options,
114
122
  headers,
115
123
  });
@@ -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
+ }
@@ -1,5 +1,6 @@
1
1
  export const EXECUTION_PROFILES = [
2
2
  "supervised_room_turn",
3
+ "supervised_mcp_polling",
3
4
  "autonomous_mcp_worker",
4
5
  "interactive_desktop",
5
6
  ];
@@ -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,7 +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
+ import { hasSupervisedWorkerAuthority } from "./worker-bearer.js";
8
8
  const roomPresenceByIdentity = new Map();
9
9
  export function getRememberedRoomPresence(roomId, identity) {
10
10
  if (!roomId || !identity) {
@@ -19,7 +19,7 @@ export async function syncRoomPresence(roomId, identity, presence, agentSession)
19
19
  }
20
20
  roomPresenceByIdentity.set(getRoomIdentityPresenceCacheKey(roomId, resolvedIdentity.actor_label), presence);
21
21
  const { localRoomId, cloudRoomId } = await resolveLocalRoomStorageIdentifiers(roomId);
22
- if (!isSupervisedBoundedTurn() && await isLocalRoomStorageEnabled(roomId)) {
22
+ if (!hasSupervisedWorkerAuthority() && await isLocalRoomStorageEnabled(roomId)) {
23
23
  touchRoomSession(localRoomId || roomId);
24
24
  return;
25
25
  }
@@ -39,7 +39,7 @@ export async function syncRoomPresence(roomId, identity, presence, agentSession)
39
39
  ...agentSessionCredentials(agentSession),
40
40
  }),
41
41
  });
42
- if (!isSupervisedBoundedTurn())
42
+ if (!hasSupervisedWorkerAuthority())
43
43
  touchRoomSession(apiRoomId);
44
44
  }
45
45
  catch (error) {
@@ -4,9 +4,9 @@ import { apiCall, isMissingRouteError, } from "./api.js";
4
4
  import { maybeHandleRepoRoomAuthRequired } from "./device-auth.js";
5
5
  import { getLastMessageId } from "./messages.js";
6
6
  import { currentRoom, getCurrentSupervisedRoomAuthority } from "./room-state.js";
7
- import { isSupervisedBoundedTurn } from "./worker-bearer.js";
7
+ import { hasSupervisedWorkerAuthority } from "./worker-bearer.js";
8
8
  export async function roomScopedApiCall(input) {
9
- const supervised = isSupervisedBoundedTurn();
9
+ const supervised = hasSupervisedWorkerAuthority();
10
10
  const exactRoomAuthority = supervised ? getCurrentSupervisedRoomAuthority() : null;
11
11
  if (supervised && (!exactRoomAuthority || input.room_id !== exactRoomAuthority)) {
12
12
  throw new Error("The daemon-supervised API request is missing its exact per-call room authority.");
@@ -1,9 +1,9 @@
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";
6
+ import { hasSupervisedWorkerAuthority } from "./worker-bearer.js";
7
7
  import { getCurrentSupervisedRoomAuthority, runWithSupervisedRoomAuthority, } from "./supervised-room-authority.js";
8
8
  let mcpServer = null;
9
9
  let sseClient = null;
@@ -15,7 +15,7 @@ export function shutdownRuntime() {
15
15
  sseClient?.unsubscribeAll();
16
16
  }
17
17
  function getSseClient() {
18
- sseClient ??= new SseClient(API_URL, () => getLetagentsToken());
18
+ sseClient ??= new SseClient(getApiUrl(), () => getLetagentsToken());
19
19
  return sseClient;
20
20
  }
21
21
  function getCurrentStreamAgentIdentity() {
@@ -32,6 +32,7 @@ function getCurrentStreamAgentIdentity() {
32
32
  export function toRoomState(input) {
33
33
  return {
34
34
  room_id: input.room_id,
35
+ navigation_locator: input.navigation_locator ?? null,
35
36
  project_id: input.project_id ?? null,
36
37
  code: input.code ?? null,
37
38
  display_name: input.display_name ?? null,
@@ -40,8 +41,12 @@ export function toRoomState(input) {
40
41
  is_local: input.is_local ?? false,
41
42
  };
42
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
+ }
43
48
  function getCanonicalRoomWebUrl(roomId) {
44
- return new URL(getCanonicalRoomWebPath(roomId), `${API_URL}/`).toString();
49
+ return new URL(getCanonicalRoomWebPath(roomId), `${getApiUrl()}/`).toString();
45
50
  }
46
51
  export function withCanonicalRoomLink(roomId, payload) {
47
52
  return {
@@ -87,7 +92,7 @@ export function toPublicRoomResponse(response, fallbackRoomId) {
87
92
  }
88
93
  export function rememberRoom(state, lastMessageId) {
89
94
  currentRoom = state;
90
- if (isSupervisedBoundedTurn())
95
+ if (hasSupervisedWorkerAuthority())
91
96
  return state;
92
97
  saveRoomSession({
93
98
  room_id: state.room_id,
@@ -109,11 +114,16 @@ export function rememberRoom(state, lastMessageId) {
109
114
  }, (_message) => {
110
115
  touchRoomSession(state.room_id);
111
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();
112
122
  });
113
123
  return state;
114
124
  }
115
125
  export function touchCurrentRoom(lastMessageId) {
116
- if (isSupervisedBoundedTurn())
126
+ if (hasSupervisedWorkerAuthority())
117
127
  return;
118
128
  if (!currentRoom) {
119
129
  return;
@@ -121,7 +131,7 @@ export function touchCurrentRoom(lastMessageId) {
121
131
  touchRoomSession(currentRoom.room_id, lastMessageId);
122
132
  }
123
133
  export function getTargetRoomId(roomId) {
124
- if (isSupervisedBoundedTurn()) {
134
+ if (hasSupervisedWorkerAuthority()) {
125
135
  const exactRoomAuthority = getCurrentSupervisedRoomAuthority();
126
136
  if (!exactRoomAuthority) {
127
137
  throw new Error("The daemon-supervised tool has not received its exact room authority.");
@@ -152,7 +162,7 @@ export function toPublicCurrentRoomState() {
152
162
  * repository inspection and can safely rebind after a durable room move.
153
163
  */
154
164
  export function runWithCurrentSupervisedRoom(roomId, callback) {
155
- if (!isSupervisedBoundedTurn()) {
165
+ if (!hasSupervisedWorkerAuthority()) {
156
166
  throw new Error("Only a daemon-supervised bounded turn can bind supervisor room authority.");
157
167
  }
158
168
  const normalized = roomId.trim();
@@ -162,7 +172,7 @@ export function runWithCurrentSupervisedRoom(roomId, callback) {
162
172
  return runWithSupervisedRoomAuthority(normalized, callback);
163
173
  }
164
174
  export function getFallbackProjectId() {
165
- if (isSupervisedBoundedTurn())
175
+ if (hasSupervisedWorkerAuthority())
166
176
  return null;
167
177
  return currentRoom?.project_id ?? null;
168
178
  }
@@ -9,7 +9,7 @@ import { ensureAgentIdentity, toPublicAgentIdentity, withAgentIdentity, } from "
9
9
  import { withJoinRoomAgentPrompt } from "./messages.js";
10
10
  import { syncRoomPresence } from "./presence.js";
11
11
  import { rememberRoom, toPublicRoomResponse, toRoomState, } from "./room-state.js";
12
- import { isSupervisedBoundedTurn, requireValidWorkerBearerRuntime } from "./worker-bearer.js";
12
+ import { hasSupervisedWorkerAuthority, requireValidWorkerBearerRuntime } from "./worker-bearer.js";
13
13
  export function normalizeJoinSessionMode(value) {
14
14
  return String(value || "").trim().toLowerCase() === "live" ? "live" : "current";
15
15
  }
@@ -20,7 +20,7 @@ function isNotFoundApiError(error) {
20
20
  return error instanceof ApiError && error.status === 404;
21
21
  }
22
22
  function parseGeneratedGitRefRoomIdentifier(identifier) {
23
- const match = /^git-room:github\.com:([^/:\s]+\/[^/:\s]+):(branch|tag):[A-Za-z0-9_-]+$/.exec(identifier.trim());
23
+ const match = /^github\.com\/([^/\s]+\/[^/\s]+)\/focus\/git:(branch|tag):[A-Za-z0-9_-]+$/.exec(identifier.trim());
24
24
  if (!match) {
25
25
  return null;
26
26
  }
@@ -30,7 +30,7 @@ function parseGeneratedGitRefRoomIdentifier(identifier) {
30
30
  };
31
31
  }
32
32
  export async function joinRoomIdentifier(identifier, joinedVia, options = {}) {
33
- if (isSupervisedBoundedTurn()) {
33
+ if (hasSupervisedWorkerAuthority()) {
34
34
  throw new Error("Room joins and creation are disabled during a daemon-supervised bounded turn.");
35
35
  }
36
36
  const roomId = joinedVia === "join_code" ? normalizeInviteCode(identifier) : identifier.trim();
@@ -67,6 +67,7 @@ export async function joinRoomIdentifier(identifier, joinedVia, options = {}) {
67
67
  const agentIdentity = await ensureAgentIdentity();
68
68
  const room = rememberRoom(toRoomState({
69
69
  room_id: joinedRoomId,
70
+ navigation_locator: joinedRoomId === roomId ? null : roomId,
70
71
  project_id: typeof response.project_id === "string" ? response.project_id : null,
71
72
  code: typeof response.code === "string"
72
73
  ? response.code
@@ -190,7 +191,7 @@ export async function joinRoomIdentifierWithoutImplicitGitRefCreate(identifier,
190
191
  }));
191
192
  }
192
193
  export async function createInviteRoom() {
193
- if (isSupervisedBoundedTurn()) {
194
+ if (hasSupervisedWorkerAuthority()) {
194
195
  throw new Error("Room joins and creation are disabled during a daemon-supervised bounded turn.");
195
196
  }
196
197
  const project = await apiCall("/projects", { method: "POST" });
@@ -260,7 +261,27 @@ export async function joinNamedRoom(name, sessionMode) {
260
261
  session_mode: sessionMode,
261
262
  });
262
263
  }
263
- function bindWorkerRoomFromContext() {
264
+ async function bindWorkerRoomLocator(roomLocator, source) {
265
+ const response = await apiCall(`/rooms/resolve/${encodeURIComponent(roomLocator)}`);
266
+ if (response.room_exists !== true) {
267
+ throw new ApiError(404, JSON.stringify({ error: "Room not found", code: "ROOM_NOT_FOUND" }));
268
+ }
269
+ const roomId = typeof response.canonical_room_id === "string"
270
+ ? response.canonical_room_id
271
+ : roomLocator;
272
+ return {
273
+ room: rememberRoom(toRoomState({
274
+ room_id: roomId,
275
+ navigation_locator: roomId === roomLocator ? null : roomLocator,
276
+ project_id: roomId,
277
+ display_name: roomId,
278
+ git_room: response.git_room ?? null,
279
+ joined_via: source === ".letagents.json" ? "config" : "git-remote",
280
+ })),
281
+ source,
282
+ };
283
+ }
284
+ async function bindWorkerRoomFromContext() {
264
285
  const configRoom = getRoomFromConfig();
265
286
  if (configRoom) {
266
287
  const gitContext = buildActiveGitRoomContext({
@@ -268,26 +289,26 @@ function bindWorkerRoomFromContext() {
268
289
  currentBranch: getGitCurrentBranch(),
269
290
  defaultBranch: getGitDefaultBranch(),
270
291
  });
271
- const roomId = gitContext.activeRoom ?? configRoom;
272
- return {
273
- room: rememberRoom(toRoomState({
274
- room_id: roomId,
275
- display_name: roomId,
276
- joined_via: "config",
277
- })),
278
- source: ".letagents.json",
279
- };
292
+ const roomId = gitContext.activeRoomLocator ?? configRoom;
293
+ try {
294
+ return await bindWorkerRoomLocator(roomId, ".letagents.json");
295
+ }
296
+ catch (error) {
297
+ if (roomId === configRoom || !isNotFoundApiError(error))
298
+ throw error;
299
+ return bindWorkerRoomLocator(configRoom, ".letagents.json");
300
+ }
280
301
  }
281
302
  const gitContext = getGitRoomContext();
282
- if (gitContext.activeRoom) {
283
- return {
284
- room: rememberRoom(toRoomState({
285
- room_id: gitContext.activeRoom,
286
- display_name: gitContext.activeRoom,
287
- joined_via: "git-remote",
288
- })),
289
- source: "git remote",
290
- };
303
+ if (gitContext.activeRoomLocator) {
304
+ try {
305
+ return await bindWorkerRoomLocator(gitContext.activeRoomLocator, "git remote");
306
+ }
307
+ catch (error) {
308
+ if (!gitContext.repoRoom || !isNotFoundApiError(error))
309
+ throw error;
310
+ return bindWorkerRoomLocator(gitContext.repoRoom, "git remote");
311
+ }
291
312
  }
292
313
  const savedCurrentRoom = getStoredCurrentRoom();
293
314
  if (!savedCurrentRoom) {
@@ -316,9 +337,9 @@ export async function autoJoinFromContext() {
316
337
  return;
317
338
  }
318
339
  if (workerRuntime.mode === "worker") {
319
- const bound = bindWorkerRoomFromContext();
340
+ const bound = await bindWorkerRoomFromContext();
320
341
  if (bound) {
321
- console.error(`🏠 Bound worker bearer to room '${bound.room.room_id}' (from ${bound.source}; no join/create request).`);
342
+ console.error(`🏠 Bound worker bearer to existing room '${bound.room.room_id}' (from ${bound.source}).`);
322
343
  }
323
344
  else {
324
345
  console.error("â„šī¸ Worker bearer has no .letagents.json, git remote, or saved room to bind locally.");
@@ -332,17 +353,17 @@ export async function autoJoinFromContext() {
332
353
  currentBranch: getGitCurrentBranch(),
333
354
  defaultBranch: getGitDefaultBranch(),
334
355
  });
335
- if (gitContext.activeRefRoom && gitContext.currentBranch) {
336
- const joinedBranchRoom = await joinExistingRoomIdentifier(gitContext.activeRefRoom, "config");
356
+ if (gitContext.activeRefRoomLocator && gitContext.currentBranch) {
357
+ const joinedBranchRoom = await joinExistingRoomIdentifier(gitContext.activeRefRoomLocator, "config");
337
358
  if (joinedBranchRoom) {
338
359
  await ensureAgentIdentity();
339
- console.error(`🏠 Auto-joined existing branch room '${gitContext.activeRefRoom}' (from .letagents.json + branch '${gitContext.currentBranch}')`);
360
+ console.error(`🏠 Auto-joined existing branch room '${gitContext.activeRefRoomLocator}' (from .letagents.json + branch '${gitContext.currentBranch}')`);
340
361
  return;
341
362
  }
342
363
  }
343
364
  await joinRoomIdentifier(configRoom, "config");
344
365
  await ensureAgentIdentity();
345
- const branchNote = gitContext.activeRefRoom && gitContext.currentBranch
366
+ const branchNote = gitContext.activeRefRoomLocator && gitContext.currentBranch
346
367
  ? `; branch '${gitContext.currentBranch}' has no existing Git Room`
347
368
  : "";
348
369
  console.error(`🏠 Auto-joined room '${configRoom}' (from .letagents.json${branchNote})`);
@@ -350,17 +371,17 @@ export async function autoJoinFromContext() {
350
371
  }
351
372
  const gitContext = getGitRoomContext();
352
373
  if (gitContext.repoRoom) {
353
- if (gitContext.activeRefRoom && gitContext.currentBranch) {
354
- const joinedBranchRoom = await joinExistingRoomIdentifier(gitContext.activeRefRoom, "git-remote");
374
+ if (gitContext.activeRefRoomLocator && gitContext.currentBranch) {
375
+ const joinedBranchRoom = await joinExistingRoomIdentifier(gitContext.activeRefRoomLocator, "git-remote");
355
376
  if (joinedBranchRoom) {
356
377
  await ensureAgentIdentity();
357
- console.error(`🏠 Auto-joined existing branch room '${gitContext.activeRefRoom}' (inferred from git remote and branch '${gitContext.currentBranch}' — consider adding a .letagents.json)`);
378
+ console.error(`🏠 Auto-joined existing branch room '${gitContext.activeRefRoomLocator}' (inferred from git remote and branch '${gitContext.currentBranch}' — consider adding a .letagents.json)`);
358
379
  return;
359
380
  }
360
381
  }
361
382
  await joinRoomIdentifier(gitContext.repoRoom, "git-remote");
362
383
  await ensureAgentIdentity();
363
- const branchNote = gitContext.activeRefRoom && gitContext.currentBranch
384
+ const branchNote = gitContext.activeRefRoomLocator && gitContext.currentBranch
364
385
  ? `; branch '${gitContext.currentBranch}' has no existing Git Room`
365
386
  : "";
366
387
  console.error(`🏠 Auto-joined room '${gitContext.repoRoom}' (inferred from git remote${branchNote} — consider adding a .letagents.json)`);