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
@@ -82,13 +82,13 @@ function githubRepositoryFullNameFromRoom(repoRoom) {
82
82
  function isLikelyDefaultBranchName(branchName) {
83
83
  return branchName === "main" || branchName === "master" || branchName === "trunk";
84
84
  }
85
- export function buildGitRefRoomIdentity(input) {
85
+ export function buildGitRefRoomLocator(input) {
86
86
  const repositoryFullName = githubRepositoryFullNameFromRoom(input.repoRoom);
87
87
  const refName = input.refName.trim();
88
88
  if (!repositoryFullName || !refName) {
89
89
  return null;
90
90
  }
91
- return `git-room:github.com:${repositoryFullName.toLowerCase()}:${input.refType}:${encodeRefForRoomId(refName)}`;
91
+ return `github.com/${repositoryFullName.toLowerCase()}/focus/git:${input.refType}:${encodeRefForRoomId(refName)}`;
92
92
  }
93
93
  export function getGitCurrentBranch(cwd) {
94
94
  return execGit(["branch", "--show-current"], cwd);
@@ -107,8 +107,8 @@ export function buildActiveGitRoomContext(input) {
107
107
  && (defaultBranch
108
108
  ? currentBranch !== defaultBranch
109
109
  : !isLikelyDefaultBranchName(currentBranch)));
110
- const activeRefRoom = input.repoRoom && currentBranch && shouldUseBranchRoom
111
- ? buildGitRefRoomIdentity({
110
+ const activeRefRoomLocator = input.repoRoom && currentBranch && shouldUseBranchRoom
111
+ ? buildGitRefRoomLocator({
112
112
  repoRoom: input.repoRoom,
113
113
  refType: "branch",
114
114
  refName: currentBranch,
@@ -118,9 +118,9 @@ export function buildActiveGitRoomContext(input) {
118
118
  repoRoom: input.repoRoom,
119
119
  currentBranch,
120
120
  defaultBranch,
121
- activeRefRoom,
122
- activeRoom: activeRefRoom ?? input.repoRoom,
123
- activeRoomKind: activeRefRoom ? "branch" : input.repoRoom ? "repo" : null,
121
+ activeRefRoomLocator,
122
+ activeRoomLocator: activeRefRoomLocator ?? input.repoRoom,
123
+ activeRoomKind: activeRefRoomLocator ? "branch" : input.repoRoom ? "repo" : null,
124
124
  };
125
125
  }
126
126
  export function getGitRoomContext(cwd) {
@@ -1,4 +1,4 @@
1
- import { readLocalState, updateLocalState } from "./storage.js";
1
+ import { readLocalState, readLocalStateSnapshot, updateLocalState } from "./storage.js";
2
2
  export function getStoredAgentSession(sessionId) {
3
3
  if (!sessionId) {
4
4
  return null;
@@ -7,15 +7,22 @@ export function getStoredAgentSession(sessionId) {
7
7
  return state.agent_sessions?.[sessionId] ?? null;
8
8
  }
9
9
  export function getCurrentAgentSession(roomId) {
10
- const state = readLocalState();
10
+ return getCurrentAgentSessionSnapshot(roomId).session;
11
+ }
12
+ export function getCurrentAgentSessionSnapshot(roomId) {
13
+ const snapshot = readLocalStateSnapshot();
14
+ const state = snapshot.state;
11
15
  const sessionIds = state.current_agent_session_ids;
12
16
  if (!sessionIds) {
13
- return null;
17
+ return { session: null, complete: snapshot.complete };
14
18
  }
15
19
  if (roomId) {
16
20
  const sessionId = sessionIds[roomId];
17
21
  const session = sessionId ? (state.agent_sessions?.[sessionId] ?? null) : null;
18
- return session && !session.ended_at ? session : null;
22
+ return {
23
+ session: session && !session.ended_at ? session : null,
24
+ complete: snapshot.complete,
25
+ };
19
26
  }
20
27
  let best = null;
21
28
  for (const id of Object.values(sessionIds)) {
@@ -24,7 +31,7 @@ export function getCurrentAgentSession(roomId) {
24
31
  best = session;
25
32
  }
26
33
  }
27
- return best;
34
+ return { session: best, complete: snapshot.complete };
28
35
  }
29
36
  /**
30
37
  * Every stored session (active or ended) this identity had in the room,
@@ -41,6 +48,27 @@ export function getStoredAgentSessionsForRoomIdentity(roomId, agentKey) {
41
48
  .filter((session) => session.room_id === roomId && session.agent_key === agentKey)
42
49
  .sort((left, right) => right.updated_at.localeCompare(left.updated_at));
43
50
  }
51
+ /** Complete active local worker population for global routing ambiguity. */
52
+ export function getStoredActiveAgentSessionsForRoom(roomId) {
53
+ return getStoredAgentRoutingStateSnapshot(roomId).sessions;
54
+ }
55
+ export function getStoredAgentRoutingStateSnapshot(roomId) {
56
+ if (!roomId)
57
+ return { sessions: [], complete: true, accountReaderKey: null };
58
+ const snapshot = readLocalStateSnapshot();
59
+ const sessions = Object.values(snapshot.state.agent_sessions ?? {})
60
+ .filter((session) => session.room_id === roomId
61
+ && session.session_kind === "worker"
62
+ && !session.ended_at)
63
+ .sort((left, right) => left.created_at.localeCompare(right.created_at)
64
+ || left.session_id.localeCompare(right.session_id));
65
+ const accountId = snapshot.state.auth?.account?.id?.trim() || "";
66
+ return {
67
+ sessions,
68
+ complete: snapshot.complete,
69
+ accountReaderKey: accountId ? `account:${accountId}` : null,
70
+ };
71
+ }
44
72
  export function saveAgentSession(session, makeCurrent = true) {
45
73
  updateLocalState((state) => {
46
74
  state.agent_sessions = state.agent_sessions ?? {};
@@ -53,6 +81,36 @@ export function saveAgentSession(session, makeCurrent = true) {
53
81
  });
54
82
  return session;
55
83
  }
84
+ /**
85
+ * Local MCP rooms have one process-owned generation per durable worker key.
86
+ * Replace it atomically so a crashed/restarted process cannot remain the
87
+ * permanent oldest routing representative.
88
+ */
89
+ export function replaceLocalWorkerAgentSession(session) {
90
+ updateLocalState((state) => {
91
+ const endedAt = session.created_at;
92
+ state.agent_sessions = state.agent_sessions ?? {};
93
+ for (const [sessionId, existing] of Object.entries(state.agent_sessions)) {
94
+ if (sessionId !== session.session_id
95
+ && existing.room_id === session.room_id
96
+ && existing.session_kind === "worker"
97
+ && existing.agent_key === session.agent_key
98
+ && !existing.ended_at) {
99
+ state.agent_sessions[sessionId] = {
100
+ ...existing,
101
+ ended_at: endedAt,
102
+ updated_at: endedAt,
103
+ last_seen_at: endedAt,
104
+ };
105
+ }
106
+ }
107
+ state.agent_sessions[session.session_id] = session;
108
+ state.current_agent_session_ids = state.current_agent_session_ids ?? {};
109
+ state.current_agent_session_ids[session.room_id] = session.session_id;
110
+ return state;
111
+ });
112
+ return session;
113
+ }
56
114
  export function endStoredAgentSession(sessionId, endedAt = new Date().toISOString()) {
57
115
  let endedSession = null;
58
116
  updateLocalState((state) => {
@@ -2,12 +2,27 @@ import { createRequire } from "node:module";
2
2
  import { mkdir, readFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
+ import { ensureLocalThreadRoutingProjectionSchemaAsync, getLocalThreadRoutingAgentKeysForRoots, projectLocalThreadRoutingMessage, runLocalSqliteWriteTransactionAsync, scheduleLocalThreadRoutingBackfill, } from "../../../shared/sqlite-thread-routing.mjs";
6
+ import { MESSAGE_SENDER_MAX_CODE_POINTS, MESSAGE_SENDER_MAX_UTF8_BYTES, POSTGRES_INTEGER_MAX, isMessageSenderWithinBounds, parseAccountAgentRoutingEnvelope, parsePositivePgIntegerScopedId, } from "../../../shared/message-contracts.mjs";
7
+ const localImportedRoutingAuthority = Symbol("localImportedRoutingAuthority");
8
+ export function getLocalImportedRoutingAuthority(message) {
9
+ if (!message || typeof message !== "object")
10
+ return null;
11
+ return message[localImportedRoutingAuthority] ?? null;
12
+ }
5
13
  const require = createRequire(import.meta.url);
6
14
  const chatStorageSettingsPath = process.env.LETAGENTS_CHAT_STORAGE_SETTINGS_PATH?.trim() ||
7
15
  join(homedir(), ".letagents", "chat-storage.json");
8
16
  const localChatDatabasePath = process.env.LETAGENTS_LOCAL_CHAT_DB?.trim() ||
9
17
  join(homedir(), ".letagents", "local-chat.sqlite");
10
18
  let db = null;
19
+ let dbInitialization = null;
20
+ let databaseInitializationObserverForTest = null;
21
+ let schemaInitializationObserverForTest = null;
22
+ export function setLocalChatInitializationObserversForTest(observers) {
23
+ databaseInitializationObserverForTest = observers?.database ?? null;
24
+ schemaInitializationObserverForTest = observers?.schema ?? null;
25
+ }
11
26
  const validLocalTaskTransitions = {
12
27
  proposed: ["accepted", "cancelled"],
13
28
  accepted: ["assigned", "cancelled"],
@@ -23,13 +38,7 @@ function formatMessageId(number) {
23
38
  return `msg_${number}`;
24
39
  }
25
40
  function parseMessageNumber(messageId) {
26
- if (!messageId)
27
- return null;
28
- const match = /^msg_(\d+)$/.exec(messageId.trim());
29
- if (!match)
30
- return null;
31
- const parsed = Number(match[1]);
32
- return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
41
+ return parsePositivePgIntegerScopedId(messageId, "msg");
33
42
  }
34
43
  function clampLimit(limit) {
35
44
  if (!Number.isFinite(limit))
@@ -50,6 +59,20 @@ function mapRow(row) {
50
59
  text: String(row.text || ""),
51
60
  agent_prompt_kind: typeof row.agent_prompt_kind === "string" ? row.agent_prompt_kind : null,
52
61
  source: typeof row.source === "string" ? row.source : null,
62
+ publisher_agent_key: typeof row.publisher_agent_key === "string" ? row.publisher_agent_key : null,
63
+ publisher_agent_session_id: typeof row.publisher_agent_session_id === "string"
64
+ ? row.publisher_agent_session_id
65
+ : null,
66
+ account_agent_routing_json: typeof row.account_agent_routing_json === "string"
67
+ ? row.account_agent_routing_json
68
+ : null,
69
+ account_agent_routing_reader_key: typeof row.account_agent_routing_reader_key === "string"
70
+ ? row.account_agent_routing_reader_key
71
+ : null,
72
+ control_authorized: row.control_authorized === null || row.control_authorized === undefined
73
+ ? null
74
+ : Number(row.control_authorized),
75
+ synced_cloud_id: typeof row.synced_cloud_id === "string" ? row.synced_cloud_id : null,
53
76
  timestamp: String(row.timestamp || ""),
54
77
  sync_key: typeof row.sync_key === "string" ? row.sync_key : null,
55
78
  sync_started_at: typeof row.sync_started_at === "string" ? row.sync_started_at : null,
@@ -80,9 +103,19 @@ function visibleMessageClause(includePromptOnly) {
80
103
  ? "1 = 1"
81
104
  : "(agent_prompt_kind IS NULL OR agent_prompt_kind <> 'auto' OR TRIM(text) <> '')";
82
105
  }
106
+ export async function ensureLocalThreadRoutingProjection(database) {
107
+ await ensureLocalThreadRoutingProjectionSchemaAsync(database);
108
+ }
83
109
  function toMessage(row, replyTo, attachments = []) {
84
- return {
110
+ const message = {
85
111
  id: formatMessageId(row.number),
112
+ agent_identity: row.publisher_agent_key
113
+ ? {
114
+ actor_label: row.sender,
115
+ agent_key: row.publisher_agent_key,
116
+ agent_session_id: row.publisher_agent_session_id,
117
+ }
118
+ : null,
86
119
  sender: row.sender,
87
120
  text: row.text,
88
121
  agent_prompt_kind: row.agent_prompt_kind,
@@ -110,17 +143,39 @@ function toMessage(row, replyTo, attachments = []) {
110
143
  }
111
144
  : null,
112
145
  };
146
+ const importedCloudProvenance = Boolean(row.synced_cloud_id && !row.sync_key);
147
+ if (row.account_agent_routing_json || importedCloudProvenance) {
148
+ let routing = { version: 1, authority: "invalid" };
149
+ if (row.account_agent_routing_json) {
150
+ try {
151
+ routing = parseAccountAgentRoutingEnvelope(JSON.parse(row.account_agent_routing_json)) ?? { version: 1, authority: "invalid" };
152
+ }
153
+ catch {
154
+ // Present malformed imported authority is explicit invalid authority.
155
+ }
156
+ }
157
+ Object.defineProperty(message, localImportedRoutingAuthority, {
158
+ value: {
159
+ routing,
160
+ readerKey: row.account_agent_routing_reader_key,
161
+ },
162
+ enumerable: false,
163
+ });
164
+ }
165
+ return message;
113
166
  }
114
- async function getDb() {
115
- if (db)
116
- return db;
167
+ async function initializeDb() {
117
168
  await mkdir(dirname(localChatDatabasePath), { recursive: true });
118
169
  const { DatabaseSync } = require("node:sqlite");
119
- db = new DatabaseSync(localChatDatabasePath);
120
- db.exec("PRAGMA journal_mode = WAL");
121
- db.exec("PRAGMA foreign_keys = ON");
122
- db.exec("PRAGMA busy_timeout = 5000");
123
- db.exec(`
170
+ databaseInitializationObserverForTest?.();
171
+ const database = new DatabaseSync(localChatDatabasePath);
172
+ try {
173
+ schemaInitializationObserverForTest?.();
174
+ database.exec("PRAGMA journal_mode = WAL");
175
+ database.exec("PRAGMA foreign_keys = ON");
176
+ database.exec("PRAGMA busy_timeout = 5000");
177
+ await runLocalSqliteWriteTransactionAsync(database, () => {
178
+ database.exec(`
124
179
  CREATE TABLE IF NOT EXISTS local_chat_room_sequences (
125
180
  room_id TEXT PRIMARY KEY,
126
181
  next_number INTEGER NOT NULL
@@ -134,6 +189,11 @@ async function getDb() {
134
189
  text TEXT NOT NULL,
135
190
  agent_prompt_kind TEXT,
136
191
  source TEXT,
192
+ publisher_agent_key TEXT,
193
+ publisher_agent_session_id TEXT,
194
+ account_agent_routing_json TEXT,
195
+ account_agent_routing_reader_key TEXT,
196
+ control_authorized INTEGER,
137
197
  timestamp TEXT NOT NULL,
138
198
  synced_cloud_id TEXT,
139
199
  synced_at TEXT,
@@ -203,23 +263,28 @@ async function getDb() {
203
263
  PRIMARY KEY (room_id, task_id)
204
264
  );
205
265
  `);
206
- addColumnIfMissing(db, "local_chat_messages", "sync_key", "TEXT");
207
- addColumnIfMissing(db, "local_chat_messages", "sync_started_at", "TEXT");
208
- addColumnIfMissing(db, "local_chat_messages", "thread_root_number", "INTEGER");
209
- addColumnIfMissing(db, "local_rooms", "pinned_at", "TEXT");
210
- addColumnIfMissing(db, "local_tasks", "assignee_agent_key", "TEXT");
211
- addColumnIfMissing(db, "local_tasks", "assignee_agent_instance_id", "TEXT");
212
- addColumnIfMissing(db, "local_tasks", "assignee_agent_session_id", "TEXT");
213
- addColumnIfMissing(db, "local_tasks", "workflow_artifacts_json", "TEXT");
214
- addColumnIfMissing(db, "local_tasks", "workflow_refs_json", "TEXT");
215
- addColumnIfMissing(db, "local_tasks", "sync_started_at", "TEXT");
216
- addColumnIfMissing(db, "local_tasks", "sync_dirty", "INTEGER NOT NULL DEFAULT 0");
217
- addColumnIfMissing(db, "local_tasks", "review_lease_id", "TEXT");
218
- addColumnIfMissing(db, "local_tasks", "review_holder_label", "TEXT");
219
- addColumnIfMissing(db, "local_tasks", "review_agent_key", "TEXT");
220
- addColumnIfMissing(db, "local_tasks", "review_agent_session_id", "TEXT");
221
- addColumnIfMissing(db, "local_tasks", "review_updated_at", "TEXT");
222
- db.exec(`
266
+ addColumnIfMissing(database, "local_chat_messages", "sync_key", "TEXT");
267
+ addColumnIfMissing(database, "local_chat_messages", "sync_started_at", "TEXT");
268
+ addColumnIfMissing(database, "local_chat_messages", "thread_root_number", "INTEGER");
269
+ addColumnIfMissing(database, "local_chat_messages", "publisher_agent_key", "TEXT");
270
+ addColumnIfMissing(database, "local_chat_messages", "publisher_agent_session_id", "TEXT");
271
+ addColumnIfMissing(database, "local_chat_messages", "account_agent_routing_json", "TEXT");
272
+ addColumnIfMissing(database, "local_chat_messages", "account_agent_routing_reader_key", "TEXT");
273
+ addColumnIfMissing(database, "local_chat_messages", "control_authorized", "INTEGER");
274
+ addColumnIfMissing(database, "local_rooms", "pinned_at", "TEXT");
275
+ addColumnIfMissing(database, "local_tasks", "assignee_agent_key", "TEXT");
276
+ addColumnIfMissing(database, "local_tasks", "assignee_agent_instance_id", "TEXT");
277
+ addColumnIfMissing(database, "local_tasks", "assignee_agent_session_id", "TEXT");
278
+ addColumnIfMissing(database, "local_tasks", "workflow_artifacts_json", "TEXT");
279
+ addColumnIfMissing(database, "local_tasks", "workflow_refs_json", "TEXT");
280
+ addColumnIfMissing(database, "local_tasks", "sync_started_at", "TEXT");
281
+ addColumnIfMissing(database, "local_tasks", "sync_dirty", "INTEGER NOT NULL DEFAULT 0");
282
+ addColumnIfMissing(database, "local_tasks", "review_lease_id", "TEXT");
283
+ addColumnIfMissing(database, "local_tasks", "review_holder_label", "TEXT");
284
+ addColumnIfMissing(database, "local_tasks", "review_agent_key", "TEXT");
285
+ addColumnIfMissing(database, "local_tasks", "review_agent_session_id", "TEXT");
286
+ addColumnIfMissing(database, "local_tasks", "review_updated_at", "TEXT");
287
+ database.exec(`
223
288
  CREATE UNIQUE INDEX IF NOT EXISTS local_chat_messages_sync_key_idx
224
289
  ON local_chat_messages (room_id, sync_key)
225
290
  WHERE sync_key IS NOT NULL;
@@ -227,8 +292,29 @@ async function getDb() {
227
292
  ON local_chat_messages (room_id, sync_started_at);
228
293
  CREATE INDEX IF NOT EXISTS local_chat_messages_thread_root_idx
229
294
  ON local_chat_messages (room_id, thread_root_number);
230
- `);
231
- return db;
295
+ `);
296
+ });
297
+ await ensureLocalThreadRoutingProjection(database);
298
+ scheduleLocalThreadRoutingBackfill(database);
299
+ return database;
300
+ }
301
+ catch (error) {
302
+ database.close?.();
303
+ throw error;
304
+ }
305
+ }
306
+ async function getDb() {
307
+ if (db)
308
+ return db;
309
+ dbInitialization ??= initializeDb();
310
+ try {
311
+ const initialized = await dbInitialization;
312
+ db = initialized;
313
+ return initialized;
314
+ }
315
+ finally {
316
+ dbInitialization = null;
317
+ }
232
318
  }
233
319
  function addColumnIfMissing(database, tableName, columnName, definition) {
234
320
  try {
@@ -306,7 +392,7 @@ function allocateLocalMessageNumber(database, roomId) {
306
392
  .prepare("SELECT next_number FROM local_chat_room_sequences WHERE room_id = ?")
307
393
  .get(roomId);
308
394
  const number = Number(row?.next_number || 0);
309
- if (!Number.isInteger(number) || number <= 0) {
395
+ if (!Number.isInteger(number) || number <= 0 || number > POSTGRES_INTEGER_MAX) {
310
396
  throw new Error("Local chat message sequence could not be allocated.");
311
397
  }
312
398
  database
@@ -418,6 +504,9 @@ export async function addLocalChatMessage(roomId, input) {
418
504
  throw new Error("No room is available for this request.");
419
505
  if (!sender)
420
506
  throw new Error("Message sender is required.");
507
+ if (!isMessageSenderWithinBounds(sender)) {
508
+ throw new Error(`Message sender must not exceed ${MESSAGE_SENDER_MAX_CODE_POINTS} characters or ${MESSAGE_SENDER_MAX_UTF8_BYTES} UTF-8 bytes.`);
509
+ }
421
510
  const database = await getDb();
422
511
  const replyToNumber = parseMessageNumber(input.reply_to);
423
512
  const explicitThreadRootNumber = parseMessageNumber(input.thread_root_id);
@@ -448,11 +537,9 @@ export async function addLocalChatMessage(roomId, input) {
448
537
  }
449
538
  }
450
539
  const timestamp = new Date().toISOString();
451
- let row;
452
- beginImmediate(database);
453
- try {
540
+ const row = await runLocalSqliteWriteTransactionAsync(database, () => {
454
541
  const number = allocateLocalMessageNumber(database, trimmedRoomId);
455
- row = {
542
+ const insertedRow = {
456
543
  room_id: trimmedRoomId,
457
544
  number,
458
545
  reply_to_number: replyToNumber,
@@ -461,6 +548,12 @@ export async function addLocalChatMessage(roomId, input) {
461
548
  text: input.text,
462
549
  agent_prompt_kind: input.agent_prompt_kind || null,
463
550
  source: input.source || null,
551
+ publisher_agent_key: input.publisher_agent_key?.trim() || null,
552
+ publisher_agent_session_id: input.publisher_agent_session_id?.trim() || null,
553
+ account_agent_routing_json: null,
554
+ account_agent_routing_reader_key: null,
555
+ control_authorized: null,
556
+ synced_cloud_id: null,
464
557
  timestamp,
465
558
  sync_key: null,
466
559
  sync_started_at: null,
@@ -469,22 +562,32 @@ export async function addLocalChatMessage(roomId, input) {
469
562
  .prepare(`
470
563
  INSERT INTO local_chat_messages (
471
564
  room_id, number, reply_to_number, thread_root_number, sender, text, agent_prompt_kind, source,
565
+ publisher_agent_key, publisher_agent_session_id,
472
566
  timestamp, synced_cloud_id, synced_at, sync_key, sync_started_at
473
567
  )
474
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, NULL)
568
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, NULL)
475
569
  `)
476
- .run(row.room_id, row.number, row.reply_to_number, row.thread_root_number, row.sender, row.text, row.agent_prompt_kind, row.source, row.timestamp);
477
- database.exec("COMMIT");
478
- }
479
- catch (error) {
480
- rollback(database);
481
- throw error;
482
- }
570
+ .run(insertedRow.room_id, insertedRow.number, insertedRow.reply_to_number, insertedRow.thread_root_number, insertedRow.sender, insertedRow.text, insertedRow.agent_prompt_kind, insertedRow.source, insertedRow.publisher_agent_key, insertedRow.publisher_agent_session_id, insertedRow.timestamp);
571
+ projectLocalThreadRoutingMessage(database, insertedRow);
572
+ return insertedRow;
573
+ });
483
574
  return {
484
575
  room_id: trimmedRoomId,
485
576
  ...toMessage(row, replyTarget),
486
577
  };
487
578
  }
579
+ export async function getLocalChatThreadRoutingMembership(roomId, rootMessageIds, identity, activeIdentities = [identity]) {
580
+ const rootNumbers = Array.from(new Set(rootMessageIds
581
+ .map((rootId) => parseMessageNumber(rootId))
582
+ .filter((value) => value !== null)));
583
+ if (rootNumbers.length === 0)
584
+ return new Set();
585
+ const database = await getDb();
586
+ const keysByRoot = await getLocalThreadRoutingAgentKeysForRoots(database, roomId, rootNumbers, activeIdentities);
587
+ return new Set([...keysByRoot]
588
+ .filter(([, keys]) => keys.has(identity.agent_key))
589
+ .map(([rootNumber]) => formatMessageId(rootNumber)));
590
+ }
488
591
  export async function getLocalChatMessages(roomId, options) {
489
592
  const limit = clampLimit(options?.limit);
490
593
  const afterNumber = parseMessageNumber(options?.after);
@@ -675,6 +778,44 @@ export async function listLocalTasks(roomId, options = {}) {
675
778
  .map(mapTaskRow);
676
779
  return { tasks, has_more: false };
677
780
  }
781
+ export async function listLocalActiveTaskOwnerLeases(roomId) {
782
+ const database = await getDb();
783
+ const rows = database
784
+ .prepare(`
785
+ SELECT
786
+ MIN(COALESCE(NULLIF(TRIM(assignee), ''), assignee_agent_key)) AS actor_label,
787
+ assignee_agent_key,
788
+ assignee_agent_instance_id,
789
+ assignee_agent_session_id
790
+ FROM local_tasks
791
+ WHERE room_id = ?
792
+ AND status IN ('assigned', 'in_progress', 'blocked', 'in_review')
793
+ AND assignee_agent_key IS NOT NULL
794
+ AND TRIM(assignee_agent_key) <> ''
795
+ GROUP BY CASE
796
+ WHEN NULLIF(TRIM(assignee_agent_session_id), '') IS NOT NULL
797
+ THEN 'session:' || TRIM(assignee_agent_session_id)
798
+ WHEN NULLIF(TRIM(assignee_agent_instance_id), '') IS NOT NULL
799
+ THEN 'instance:' || TRIM(assignee_agent_key) || ':' || TRIM(assignee_agent_instance_id)
800
+ ELSE 'agent:' || TRIM(assignee_agent_key)
801
+ END
802
+ ORDER BY MIN(created_at) ASC
803
+ LIMIT 2
804
+ `)
805
+ .all(roomId);
806
+ return rows.map((row) => ({
807
+ kind: "work",
808
+ status: "active",
809
+ actor_label: String(row.actor_label || row.assignee_agent_key || ""),
810
+ agent_key: String(row.assignee_agent_key || ""),
811
+ agent_instance_id: typeof row.assignee_agent_instance_id === "string"
812
+ ? row.assignee_agent_instance_id
813
+ : null,
814
+ agent_session_id: typeof row.assignee_agent_session_id === "string"
815
+ ? row.assignee_agent_session_id
816
+ : null,
817
+ }));
818
+ }
678
819
  export async function getLocalTask(roomId, taskId) {
679
820
  const database = await getDb();
680
821
  const row = database
@@ -1,4 +1,4 @@
1
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "fs";
1
+ import { closeSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "fs";
2
2
  import { randomBytes } from "crypto";
3
3
  import { homedir } from "os";
4
4
  import { dirname, join } from "path";
@@ -10,21 +10,28 @@ const STATE_LOCK_SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
10
10
  export function getLocalStatePath() {
11
11
  return process.env.LETAGENTS_STATE_PATH || DEFAULT_STATE_PATH;
12
12
  }
13
- function readLocalStateFromPath(statePath) {
14
- if (!existsSync(statePath)) {
15
- return {};
16
- }
13
+ function readLocalStateSnapshotFromPath(statePath) {
17
14
  try {
18
15
  const raw = readFileSync(statePath, "utf-8");
19
16
  const parsed = JSON.parse(raw);
20
- return typeof parsed === "object" && parsed ? parsed : {};
17
+ return typeof parsed === "object" && parsed && !Array.isArray(parsed)
18
+ ? { state: parsed, complete: true }
19
+ : { state: {}, complete: false };
21
20
  }
22
- catch {
23
- return {};
21
+ catch (error) {
22
+ return error?.code === "ENOENT"
23
+ ? { state: {}, complete: true }
24
+ : { state: {}, complete: false };
24
25
  }
25
26
  }
27
+ function readLocalStateFromPath(statePath) {
28
+ return readLocalStateSnapshotFromPath(statePath).state;
29
+ }
30
+ export function readLocalStateSnapshot() {
31
+ return readLocalStateSnapshotFromPath(getLocalStatePath());
32
+ }
26
33
  export function readLocalState() {
27
- return readLocalStateFromPath(getLocalStatePath());
34
+ return readLocalStateSnapshot().state;
28
35
  }
29
36
  function sleepSync(ms) {
30
37
  if (ms > 0) {
@@ -0,0 +1,92 @@
1
+ import { isAbsolute } from "node:path";
2
+ import { z } from "zod";
3
+ import { registerTools } from "./register-tools.js";
4
+ import { runWithDaemonToolExecutionContext, } from "./runtime/daemon-tool-context.js";
5
+ import { runWithSupervisedRoomAuthority } from "./runtime/supervised-room-authority.js";
6
+ import { durableCompletionResult, supervisedToolIsMutation } from "./supervised-tool-facade.js";
7
+ const handlersByProvider = new Map();
8
+ const SUPERVISED_PROVIDERS = new Set(["claude-code", "cursor", "codex", "open-model"]);
9
+ const WORKSPACE_SCOPED_TOOLS = new Set(["check_repo", "check_repo_visibility", "initialize_repo"]);
10
+ function handlersForProvider(provider) {
11
+ const normalizedProvider = provider.trim().toLowerCase();
12
+ const existing = handlersByProvider.get(normalizedProvider);
13
+ if (existing)
14
+ return existing;
15
+ const handlers = new Map();
16
+ const recorder = {
17
+ tool(name, ...registration) {
18
+ const callback = registration.at(-1);
19
+ if (typeof callback !== "function")
20
+ throw new Error(`Tool ${name} has no callback.`);
21
+ const schemaCandidate = registration.at(-2);
22
+ const inputSchema = schemaCandidate && typeof schemaCandidate === "object" && !Array.isArray(schemaCandidate)
23
+ ? schemaCandidate
24
+ : null;
25
+ handlers.set(name, { callback: callback, inputSchema });
26
+ return {};
27
+ },
28
+ };
29
+ registerTools(recorder, "supervised_room_turn", normalizedProvider || null, { executionOwner: "daemon" });
30
+ handlersByProvider.set(normalizedProvider, handlers);
31
+ return handlers;
32
+ }
33
+ function validateExecutionContext(input) {
34
+ const provider = input.provider.trim().toLowerCase();
35
+ const roomId = input.roomId.trim();
36
+ const toolName = input.toolName.trim();
37
+ const requestId = input.requestId.trim();
38
+ if (!SUPERVISED_PROVIDERS.has(provider))
39
+ throw new Error(`Unsupported supervised provider: ${input.provider}`);
40
+ if (!roomId || roomId.length > 1_024 || /[\u0000-\u001f\u007f]/.test(roomId)) {
41
+ throw new Error("Daemon tool room authority is malformed.");
42
+ }
43
+ if (input.agentSession.room_id !== roomId || input.agentSession.session_kind !== "worker"
44
+ || input.agentSession.runtime.trim().toLowerCase() !== provider || input.agentSession.ended_at) {
45
+ throw new Error("Daemon tool worker session does not match its active room authority.");
46
+ }
47
+ if (!input.bearer.trim())
48
+ throw new Error("Daemon tool worker bearer is required.");
49
+ if (!isAbsolute(input.cwd))
50
+ throw new Error("Daemon tool workspace must be an absolute path.");
51
+ if (!toolName || !requestId)
52
+ throw new Error("Daemon tool name and request id are required.");
53
+ let apiUrl;
54
+ try {
55
+ apiUrl = new URL(input.apiUrl);
56
+ }
57
+ catch {
58
+ throw new Error("Daemon tool API URL is malformed.");
59
+ }
60
+ const loopbackHosts = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
61
+ if (apiUrl.username || apiUrl.password
62
+ || (apiUrl.protocol !== "https:"
63
+ && !(apiUrl.protocol === "http:" && loopbackHosts.has(apiUrl.hostname.toLowerCase())))) {
64
+ throw new Error("Daemon tool API URL must use HTTPS or an exact HTTP loopback host.");
65
+ }
66
+ return { ...input, provider, roomId, toolName, requestId, apiUrl: apiUrl.origin };
67
+ }
68
+ /**
69
+ * Execute one already-authorized supervised tool inside daemon-owned,
70
+ * request-scoped authority. The daemon owns journaling around this call; this
71
+ * module only supplies the exact public MCP implementation from the pinned
72
+ * runtime, without recursively crossing the supervisor bridge.
73
+ */
74
+ export async function executeDaemonTool(input) {
75
+ const context = validateExecutionContext(input);
76
+ const tool = handlersForProvider(context.provider).get(context.toolName);
77
+ if (!tool)
78
+ throw new Error(`Unsupported supervised tool: ${context.toolName}`);
79
+ const parsedInput = tool.inputSchema
80
+ ? await z.object(tool.inputSchema).parseAsync(context.input)
81
+ : context.input;
82
+ const authorizedInput = WORKSPACE_SCOPED_TOOLS.has(context.toolName)
83
+ && parsedInput && typeof parsedInput === "object" && !Array.isArray(parsedInput)
84
+ ? { ...parsedInput, cwd: context.cwd }
85
+ : parsedInput;
86
+ const liveResult = await runWithDaemonToolExecutionContext(context, () => runWithSupervisedRoomAuthority(context.roomId, () => tool.callback(authorizedInput, { requestId: context.requestId })));
87
+ return {
88
+ liveResult,
89
+ durableResult: durableCompletionResult(liveResult, supervisedToolIsMutation(context.toolName)),
90
+ };
91
+ }
92
+ export { supervisedToolIsMutation };
@@ -7,8 +7,10 @@ import { registerTaskTools } from "./tools/tasks.js";
7
7
  import { registerSupervisedRoomTurnTools } from "./tools/supervised-room-turn.js";
8
8
  import { toolSurfaceForExecutionProfile } from "./runtime/tool-surface-policy.js";
9
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) {
11
- const tools = profileAwareToolServer(server, profile, undefined, supervisedProvider);
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);
12
14
  const surface = toolSurfaceForExecutionProfile(profile);
13
15
  registerRoomJoinTools(tools);
14
16
  if (surface.agentSessionLifecycle)