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
package/dist/mcp/git-remote.js
CHANGED
|
@@ -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
|
|
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 `
|
|
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
|
|
111
|
-
?
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
activeRoomKind:
|
|
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
|
-
|
|
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
|
|
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,43 @@ export function getCurrentAgentSession(roomId) {
|
|
|
24
31
|
best = session;
|
|
25
32
|
}
|
|
26
33
|
}
|
|
27
|
-
return best;
|
|
34
|
+
return { session: best, complete: snapshot.complete };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Every stored session (active or ended) this identity had in the room,
|
|
38
|
+
* most recently updated first. Re-registration consults the FULL lineage so a
|
|
39
|
+
* replayed prior label reuses the base recorded when that exact label was
|
|
40
|
+
* allocated — a latest-only lookup would lose an older concurrent sibling's
|
|
41
|
+
* base and misread its restart as a deliberate rename.
|
|
42
|
+
*/
|
|
43
|
+
export function getStoredAgentSessionsForRoomIdentity(roomId, agentKey) {
|
|
44
|
+
if (!roomId || !agentKey)
|
|
45
|
+
return [];
|
|
46
|
+
const state = readLocalState();
|
|
47
|
+
return Object.values(state.agent_sessions ?? {})
|
|
48
|
+
.filter((session) => session.room_id === roomId && session.agent_key === agentKey)
|
|
49
|
+
.sort((left, right) => right.updated_at.localeCompare(left.updated_at));
|
|
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
|
+
};
|
|
28
71
|
}
|
|
29
72
|
export function saveAgentSession(session, makeCurrent = true) {
|
|
30
73
|
updateLocalState((state) => {
|
|
@@ -38,6 +81,36 @@ export function saveAgentSession(session, makeCurrent = true) {
|
|
|
38
81
|
});
|
|
39
82
|
return session;
|
|
40
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
|
+
}
|
|
41
114
|
export function endStoredAgentSession(sessionId, endedAt = new Date().toISOString()) {
|
|
42
115
|
let endedSession = null;
|
|
43
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
452
|
-
beginImmediate(database);
|
|
453
|
-
try {
|
|
540
|
+
const row = await runLocalSqliteWriteTransactionAsync(database, () => {
|
|
454
541
|
const number = allocateLocalMessageNumber(database, trimmedRoomId);
|
|
455
|
-
|
|
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(
|
|
477
|
-
database
|
|
478
|
-
|
|
479
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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 };
|