letagents 0.12.11 → 0.12.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp/git-remote.js +7 -7
- package/dist/mcp/local-state/agent-sessions.js +78 -5
- package/dist/mcp/local-state/local-chat.js +189 -48
- package/dist/mcp/local-state/storage.js +16 -9
- package/dist/mcp/server/daemon-tool-executor.js +92 -0
- package/dist/mcp/server/register-tools.js +25 -12
- package/dist/mcp/server/runtime/agent-sessions.js +58 -9
- package/dist/mcp/server/runtime/api.js +40 -4
- package/dist/mcp/server/runtime/daemon-tool-context.js +11 -0
- package/dist/mcp/server/runtime/execution-profile.js +19 -0
- package/dist/mcp/server/runtime/identity/directory.js +2 -2
- package/dist/mcp/server/runtime/messages.js +55 -0
- package/dist/mcp/server/runtime/presence.js +4 -2
- package/dist/mcp/server/runtime/room-api.js +24 -7
- package/dist/mcp/server/runtime/room-state.js +59 -3
- package/dist/mcp/server/runtime/rooms.js +67 -32
- package/dist/mcp/server/runtime/supervised-room-authority.js +8 -0
- package/dist/mcp/server/runtime/supervisor-bridge.js +702 -24
- package/dist/mcp/server/runtime/tool-surface-policy.js +26 -0
- package/dist/mcp/server/runtime/worker-bearer.js +44 -6
- package/dist/mcp/server/runtime-contract.js +27 -0
- package/dist/mcp/server/runtime.js +15 -4
- package/dist/mcp/server/supervised-tool-facade.js +134 -0
- package/dist/mcp/server/tools/agent-sessions.js +74 -7
- package/dist/mcp/server/tools/messages/index.js +3 -2
- package/dist/mcp/server/tools/messages/read-tool.js +54 -97
- package/dist/mcp/server/tools/messages/reasoning-tool.js +2 -0
- package/dist/mcp/server/tools/messages/send-tool.js +5 -0
- package/dist/mcp/server/tools/messages/status-tool.js +2 -0
- package/dist/mcp/server/tools/messages/wait-tool.js +344 -71
- package/dist/mcp/server/tools/onboarding/status-tool.js +9 -8
- package/dist/mcp/server/tools/rooms/inspection-tools.js +40 -22
- package/dist/mcp/server/tools/rooms/repo-initialization-tool.js +2 -1
- package/dist/mcp/server/tools/supervised-room-turn.js +42 -0
- package/dist/mcp/server/tools/tasks/board-tools.js +34 -2
- package/dist/mcp/server.js +14 -7
- package/dist/mcp/sse-client.js +163 -20
- package/dist/shared/activation-routing.js +187 -20
- package/dist/shared/agent-presence.js +6 -0
- package/dist/shared/desktop-release-manifest.js +63 -0
- package/dist/shared/desktop-release.js +60 -0
- package/dist/shared/scoped-ids.js +6 -0
- package/package.json +11 -3
- package/shared/message-contracts.d.mts +32 -0
- package/shared/message-contracts.mjs +109 -0
- package/shared/routing-aliases.d.mts +18 -0
- package/shared/routing-aliases.mjs +66 -0
- package/shared/sqlite-thread-routing.d.mts +72 -0
- package/shared/sqlite-thread-routing.mjs +1038 -0
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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() ||
|
|
210
|
+
const workingDir = cwd?.trim() || getRuntimeWorkingDirectory();
|
|
163
211
|
return getGitCurrentBranch(workingDir);
|
|
164
212
|
}
|
|
165
213
|
export function agentSessionCredentials(agentSession) {
|
|
166
|
-
|
|
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
|
|
57
|
-
const
|
|
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(`${
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
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), `${
|
|
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
|
}
|