letagents 0.12.12 → 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 +63 -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 +4 -2
- package/dist/mcp/server/runtime/agent-sessions.js +14 -5
- package/dist/mcp/server/runtime/api.js +11 -3
- package/dist/mcp/server/runtime/daemon-tool-context.js +11 -0
- package/dist/mcp/server/runtime/messages.js +55 -0
- package/dist/mcp/server/runtime/room-state.js +13 -3
- package/dist/mcp/server/runtime/rooms.js +51 -30
- package/dist/mcp/server/runtime/supervisor-bridge.js +49 -8
- package/dist/mcp/server/runtime/worker-bearer.js +5 -1
- package/dist/mcp/server/runtime.js +3 -3
- package/dist/mcp/server/supervised-tool-facade.js +34 -5
- 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 +2 -0
- package/dist/mcp/server/tools/messages/status-tool.js +2 -0
- package/dist/mcp/server/tools/messages/wait-tool.js +290 -68
- package/dist/mcp/server/tools/onboarding/status-tool.js +4 -4
- package/dist/mcp/server/tools/rooms/inspection-tools.js +15 -10
- package/dist/mcp/server/tools/rooms/repo-initialization-tool.js +2 -1
- package/dist/mcp/server/tools/tasks/board-tools.js +34 -2
- package/dist/mcp/sse-client.js +163 -20
- package/dist/shared/activation-routing.js +146 -23
- 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 +6 -2
- 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
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getPollTimeoutCapMs } from "../../../../shared/poll-timeout-cap.js";
|
|
3
3
|
import { encodeRoomIdPath } from "../../../room-id.js";
|
|
4
|
-
import { agentSessionCredentials, appendIncludePromptOnly, buildAgentDeliveryHeaders, bindSupervisedWorkerSession, scheduleSupervisedWorkerCursorCheckpoint, currentRoom, ensureAgentIdentity, getFallbackProjectId, getLatestLocalChatMessages, getLocalChatMessages, getLastMessageId, getRememberedRoomPresence, getTargetRoomId, identityFromAgentSession, isLocalRoomStorageEnabled,
|
|
4
|
+
import { agentSessionCredentials, AGENT_MESSAGE_BODY_MAX_BYTES, appendIncludePromptOnly, boundAgentMessageOutput, buildAgentDeliveryHeaders, bindSupervisedWorkerSession, scheduleSupervisedWorkerCursorCheckpoint, currentRoom, ensureAgentIdentity, getFallbackProjectId, getLatestLocalChatMessages, getLocalImportedRoutingAuthority, getLocalChatMessages, getLocalChatThreadRoutingMembership, getLastMessageId, getRememberedRoomPresence, getStoredAgentRoutingStateSnapshot, getTargetRoomId, identityFromAgentSession, isLocalRoomStorageEnabled, listLocalActiveTaskOwnerLeases, resolveLocalRoomStorageIdentifiers, resolveAgentSession, roomScopedApiCall, syncRoomPresence, toAgentReadableMessages, touchRoomSession, WORKER_BEARER_AGENT_SESSION_ID, waitForLocalChatMessages, } from "../../runtime.js";
|
|
5
5
|
import { requireValidWorkerBearerRuntime, supervisedBoundedDeliveryDisabledToolResult, } from "../../runtime/worker-bearer.js";
|
|
6
|
-
import { attachAgentMessageActivations } from "../../../../shared/activation-routing.js";
|
|
6
|
+
import { attachAgentMessageActivations, createGlobalAgentAddressResolver, decideAgentMessageActivation, isTaskOwnerFollowUpMessageText, } from "../../../../shared/activation-routing.js";
|
|
7
|
+
import { normalizeRoutingSender } from "../../../../../shared/routing-aliases.mjs";
|
|
7
8
|
import { findLocalMessageById, findRemoteMessageById } from "./message-lookup.js";
|
|
8
9
|
import { fetchRecentRemoteMessages } from "./read-tool.js";
|
|
9
10
|
import { jsonToolResponse } from "./response.js";
|
|
@@ -13,9 +14,9 @@ const DEFAULT_POLL_TIMEOUT_MS = 30000;
|
|
|
13
14
|
// room history (busy rooms archive millions of characters, which blows the
|
|
14
15
|
// tool's token budget). This bounds that no-cursor catch-up to a recent tail.
|
|
15
16
|
export const DEFAULT_WAIT_CATCHUP_LIMIT = 100;
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
//
|
|
17
|
+
export const MAX_WAIT_MESSAGES_PER_CALL = 100;
|
|
18
|
+
// Decide how the initial fetch should behave. Both paths return one bounded
|
|
19
|
+
// page; callers continue from last_observed_message_id when truncated.
|
|
19
20
|
export function planWaitForMessagesFetch(input) {
|
|
20
21
|
if (input.effectiveAfterMessageId) {
|
|
21
22
|
return { mode: "after_cursor", after: input.effectiveAfterMessageId };
|
|
@@ -25,23 +26,12 @@ export function planWaitForMessagesFetch(input) {
|
|
|
25
26
|
limit: input.catchupLimit ?? DEFAULT_WAIT_CATCHUP_LIMIT,
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
|
-
const LOCAL_TASK_OWNER_STATUSES = new Set(["assigned", "in_progress", "blocked", "in_review"]);
|
|
29
29
|
export function buildWaitForMessagesRequestOptions(input) {
|
|
30
30
|
return {
|
|
31
31
|
...(input.signal ? { signal: input.signal } : {}),
|
|
32
32
|
headers: input.deliveryHeaders,
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
-
export function buildWaitForMessagesHistoryPageRequest(input) {
|
|
36
|
-
const querySuffix = input.queryString ? `?${input.queryString}` : "";
|
|
37
|
-
return {
|
|
38
|
-
room_id: input.targetRoomId,
|
|
39
|
-
project_id: input.targetProjectId,
|
|
40
|
-
room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages${querySuffix}`),
|
|
41
|
-
project_path: (targetProjectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(targetProjectId)}/messages${querySuffix}`),
|
|
42
|
-
options: buildWaitForMessagesRequestOptions({ deliveryHeaders: input.deliveryHeaders }),
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
35
|
function isRecord(value) {
|
|
46
36
|
return Boolean(value && typeof value === "object");
|
|
47
37
|
}
|
|
@@ -118,41 +108,233 @@ export function resolveWaitAgentSession(roomId, agentSessionId) {
|
|
|
118
108
|
return resolveAgentSession(roomId, agentSessionId);
|
|
119
109
|
}
|
|
120
110
|
export async function localActivationContext(roomId) {
|
|
121
|
-
const result = await listLocalTasks(roomId, { openOnly: true });
|
|
122
111
|
return {
|
|
123
|
-
activeTaskLeases:
|
|
124
|
-
.flatMap((task) => {
|
|
125
|
-
const agentKey = task.assignee_agent_key?.trim();
|
|
126
|
-
if (!LOCAL_TASK_OWNER_STATUSES.has(task.status) || !agentKey) {
|
|
127
|
-
return [];
|
|
128
|
-
}
|
|
129
|
-
return [{
|
|
130
|
-
kind: "work",
|
|
131
|
-
status: "active",
|
|
132
|
-
actor_label: task.assignee || agentKey,
|
|
133
|
-
agent_key: agentKey,
|
|
134
|
-
agent_instance_id: task.assignee_agent_instance_id,
|
|
135
|
-
agent_session_id: task.assignee_agent_session_id,
|
|
136
|
-
}];
|
|
137
|
-
}),
|
|
112
|
+
activeTaskLeases: await listLocalActiveTaskOwnerLeases(roomId),
|
|
138
113
|
};
|
|
139
114
|
}
|
|
140
|
-
async function attachLocalActivationMetadata(roomId, messages, agentSession, options = {}) {
|
|
115
|
+
export async function attachLocalActivationMetadata(roomId, messages, agentSession, options = {}) {
|
|
141
116
|
if (!agentSession || agentSession.session_kind !== "worker") {
|
|
142
117
|
return messages;
|
|
143
118
|
}
|
|
144
119
|
const records = messages.filter(isRecord);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return attachAgentMessageActivations(records, {
|
|
120
|
+
if (records.length === 0)
|
|
121
|
+
return messages;
|
|
122
|
+
const identity = {
|
|
149
123
|
actor_label: agentSession.actor_label,
|
|
150
124
|
agent_key: agentSession.agent_key,
|
|
151
125
|
agent_instance_id: agentSession.agent_instance_id ?? null,
|
|
152
126
|
agent_session_id: agentSession.session_id,
|
|
153
127
|
display_name: agentSession.display_name,
|
|
154
128
|
session_kind: agentSession.session_kind,
|
|
155
|
-
}
|
|
129
|
+
};
|
|
130
|
+
// Imported cloud rows carry immutable, account-scoped send-time authority.
|
|
131
|
+
// A present wrapper always wins over mutable local aliases, including when
|
|
132
|
+
// the current state population cannot be read completely.
|
|
133
|
+
const activeSessionRoomId = options.activeSessionRoomId?.trim() || roomId;
|
|
134
|
+
const routingState = getStoredAgentRoutingStateSnapshot(activeSessionRoomId);
|
|
135
|
+
const authoritativeLegacyDecisions = new Map();
|
|
136
|
+
const legacyRecords = [];
|
|
137
|
+
const normalizedIdentityKey = normalizeRoutingSender(identity.agent_key);
|
|
138
|
+
const validActivationReasons = new Set([
|
|
139
|
+
"self_message",
|
|
140
|
+
"explicit_mention",
|
|
141
|
+
"explicit_other_mention",
|
|
142
|
+
"broadcast",
|
|
143
|
+
"reply_target",
|
|
144
|
+
"other_reply_target",
|
|
145
|
+
"thread_participant",
|
|
146
|
+
"task_owner",
|
|
147
|
+
"system_event",
|
|
148
|
+
"unaddressed",
|
|
149
|
+
]);
|
|
150
|
+
for (const message of records) {
|
|
151
|
+
const id = messageId(message);
|
|
152
|
+
if (!id)
|
|
153
|
+
continue;
|
|
154
|
+
const imported = getLocalImportedRoutingAuthority(message);
|
|
155
|
+
if (!imported) {
|
|
156
|
+
if (routingState.complete) {
|
|
157
|
+
legacyRecords.push(message);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
authoritativeLegacyDecisions.set(id, {
|
|
161
|
+
decision: "silent",
|
|
162
|
+
reason: "unaddressed",
|
|
163
|
+
addressed: false,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!routingState.accountReaderKey
|
|
169
|
+
|| imported.readerKey !== routingState.accountReaderKey
|
|
170
|
+
|| imported.routing.authority === "invalid") {
|
|
171
|
+
authoritativeLegacyDecisions.set(id, {
|
|
172
|
+
decision: "silent",
|
|
173
|
+
reason: "unaddressed",
|
|
174
|
+
addressed: false,
|
|
175
|
+
});
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const target = imported.routing.recipientSessions.find((candidate) => candidate.agentKey === normalizedIdentityKey);
|
|
179
|
+
const targetSessionId = imported.routing.authority === "receipts"
|
|
180
|
+
? target && "successorAgentSessionId" in target
|
|
181
|
+
? target.successorAgentSessionId ?? target.agentSessionId
|
|
182
|
+
: target?.agentSessionId
|
|
183
|
+
: target?.agentSessionId;
|
|
184
|
+
if (!target || targetSessionId !== identity.agent_session_id) {
|
|
185
|
+
authoritativeLegacyDecisions.set(id, {
|
|
186
|
+
decision: "silent",
|
|
187
|
+
reason: "unaddressed",
|
|
188
|
+
addressed: false,
|
|
189
|
+
});
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const importedReason = imported.routing.authority === "legacy"
|
|
193
|
+
&& "activationReason" in target
|
|
194
|
+
&& validActivationReasons.has(target.activationReason)
|
|
195
|
+
? target.activationReason
|
|
196
|
+
: "explicit_mention";
|
|
197
|
+
authoritativeLegacyDecisions.set(id, {
|
|
198
|
+
decision: "activate",
|
|
199
|
+
reason: importedReason,
|
|
200
|
+
addressed: true,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
if (legacyRecords.length === 0) {
|
|
204
|
+
return attachAgentMessageActivations(records, identity, { authoritativeLegacyDecisions });
|
|
205
|
+
}
|
|
206
|
+
const threadRootIds = legacyRecords.flatMap((message) => {
|
|
207
|
+
const messageId = typeof message.id === "string" ? message.id : "";
|
|
208
|
+
const thread = isRecord(message.thread) ? message.thread : null;
|
|
209
|
+
const rootId = typeof message.thread_root_id === "string"
|
|
210
|
+
? message.thread_root_id
|
|
211
|
+
: typeof thread?.root_message_id === "string"
|
|
212
|
+
? thread.root_message_id
|
|
213
|
+
: "";
|
|
214
|
+
return rootId && rootId !== messageId ? [rootId] : [];
|
|
215
|
+
});
|
|
216
|
+
// Linked local/cloud rooms store messages under the SQLite room id while
|
|
217
|
+
// registered workers remain keyed by the canonical cloud room id. Routing
|
|
218
|
+
// ambiguity must always use that complete canonical population.
|
|
219
|
+
const storedActiveSessions = routingState.sessions;
|
|
220
|
+
const activeIdentities = storedActiveSessions.map((session) => ({
|
|
221
|
+
actor_label: session.actor_label,
|
|
222
|
+
agent_key: session.agent_key,
|
|
223
|
+
agent_instance_id: session.agent_instance_id ?? null,
|
|
224
|
+
agent_session_id: session.session_id,
|
|
225
|
+
display_name: session.display_name,
|
|
226
|
+
session_kind: session.session_kind,
|
|
227
|
+
}));
|
|
228
|
+
if (!activeIdentities.some((candidate) => candidate.agent_session_id === identity.agent_session_id
|
|
229
|
+
&& candidate.agent_key === identity.agent_key)) {
|
|
230
|
+
// The request's authenticated current session is authoritative even when
|
|
231
|
+
// an older local-state file has not persisted it yet.
|
|
232
|
+
activeIdentities.push(identity);
|
|
233
|
+
}
|
|
234
|
+
const sameKeySessions = storedActiveSessions.filter((session) => session.agent_key === identity.agent_key);
|
|
235
|
+
const currentRepresentativeSessionId = sameKeySessions[0]?.session_id
|
|
236
|
+
?? identity.agent_session_id;
|
|
237
|
+
const currentIsActive = currentRepresentativeSessionId === identity.agent_session_id;
|
|
238
|
+
const resolveGlobalAddress = createGlobalAgentAddressResolver(activeIdentities);
|
|
239
|
+
const explicitMentionMessageIds = new Set();
|
|
240
|
+
const replyTargetMessageIds = new Set();
|
|
241
|
+
const exactReplyTargetMessageIds = new Set();
|
|
242
|
+
const selfMessageIds = new Set();
|
|
243
|
+
for (const message of legacyRecords) {
|
|
244
|
+
const messageId = typeof message.id === "string" ? message.id : "";
|
|
245
|
+
if (!messageId)
|
|
246
|
+
continue;
|
|
247
|
+
const addressed = resolveGlobalAddress(message);
|
|
248
|
+
const reply = isRecord(message.reply_to) ? message.reply_to : null;
|
|
249
|
+
const replyPublisherIdentity = isRecord(reply?.agent_identity)
|
|
250
|
+
? reply.agent_identity
|
|
251
|
+
: null;
|
|
252
|
+
const replyPublisherKey = typeof replyPublisherIdentity?.agent_key === "string"
|
|
253
|
+
? replyPublisherIdentity.agent_key.trim()
|
|
254
|
+
: "";
|
|
255
|
+
const replyPublisherSessionId = typeof replyPublisherIdentity?.agent_session_id === "string"
|
|
256
|
+
? replyPublisherIdentity.agent_session_id.trim()
|
|
257
|
+
: "";
|
|
258
|
+
if (replyPublisherKey) {
|
|
259
|
+
addressed.replyTargetKeys.clear();
|
|
260
|
+
const exactReplyIdentity = replyPublisherSessionId
|
|
261
|
+
? activeIdentities.find((candidate) => candidate.agent_key === replyPublisherKey
|
|
262
|
+
&& candidate.agent_session_id === replyPublisherSessionId)
|
|
263
|
+
: undefined;
|
|
264
|
+
if (exactReplyIdentity) {
|
|
265
|
+
if (exactReplyIdentity.agent_session_id === identity.agent_session_id) {
|
|
266
|
+
exactReplyTargetMessageIds.add(messageId);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
else if (activeIdentities.some((candidate) => candidate.agent_key === replyPublisherKey)) {
|
|
270
|
+
addressed.replyTargetKeys.add(replyPublisherKey);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const publisherIdentity = isRecord(message.agent_identity)
|
|
274
|
+
? message.agent_identity
|
|
275
|
+
: null;
|
|
276
|
+
const publisherAgentKey = typeof publisherIdentity?.agent_key === "string"
|
|
277
|
+
? publisherIdentity.agent_key.trim()
|
|
278
|
+
: "";
|
|
279
|
+
if (String(message.source ?? "").trim() === "agent"
|
|
280
|
+
&& (publisherAgentKey
|
|
281
|
+
? publisherAgentKey === identity.agent_key
|
|
282
|
+
: addressed.senderKeys.has(identity.agent_key))) {
|
|
283
|
+
selfMessageIds.add(messageId);
|
|
284
|
+
}
|
|
285
|
+
if (addressed.explicitMentionKeys.has(identity.agent_key)) {
|
|
286
|
+
explicitMentionMessageIds.add(messageId);
|
|
287
|
+
}
|
|
288
|
+
const thread = isRecord(message.thread) ? message.thread : null;
|
|
289
|
+
const rootId = typeof message.thread_root_id === "string"
|
|
290
|
+
? message.thread_root_id
|
|
291
|
+
: typeof thread?.root_message_id === "string"
|
|
292
|
+
? thread.root_message_id
|
|
293
|
+
: "";
|
|
294
|
+
if ((!rootId || rootId === messageId)
|
|
295
|
+
&& (exactReplyTargetMessageIds.has(messageId)
|
|
296
|
+
|| addressed.replyTargetKeys.has(identity.agent_key))) {
|
|
297
|
+
replyTargetMessageIds.add(messageId);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const [activationContext, projectedThreadParticipantRootIds] = await Promise.all([
|
|
301
|
+
options.includeTaskOwnerLeases === false
|
|
302
|
+
|| !records.some((message) => isTaskOwnerFollowUpMessageText(message.text))
|
|
303
|
+
? undefined
|
|
304
|
+
: localActivationContext(roomId),
|
|
305
|
+
getLocalChatThreadRoutingMembership(roomId, threadRootIds, identity, activeIdentities),
|
|
306
|
+
]);
|
|
307
|
+
const exactContext = {
|
|
308
|
+
...activationContext,
|
|
309
|
+
selfMessageIds,
|
|
310
|
+
threadParticipantRootIds: projectedThreadParticipantRootIds,
|
|
311
|
+
explicitMentionMessageIds,
|
|
312
|
+
replyTargetMessageIds,
|
|
313
|
+
};
|
|
314
|
+
const exactTaskSessionIdsForKey = new Set((activationContext?.activeTaskLeases ?? [])
|
|
315
|
+
.filter((lease) => lease.status === "active"
|
|
316
|
+
&& lease.agent_key === identity.agent_key
|
|
317
|
+
&& Boolean(lease.agent_session_id))
|
|
318
|
+
.map((lease) => lease.agent_session_id));
|
|
319
|
+
for (const message of legacyRecords) {
|
|
320
|
+
const messageId = typeof message.id === "string" ? message.id : "";
|
|
321
|
+
if (!messageId)
|
|
322
|
+
continue;
|
|
323
|
+
const resolved = decideAgentMessageActivation(message, identity, exactContext);
|
|
324
|
+
const exactTaskOwner = resolved.reason === "task_owner"
|
|
325
|
+
&& activationContext?.activeTaskLeases?.some((lease) => lease.status === "active"
|
|
326
|
+
&& lease.agent_session_id === identity.agent_session_id
|
|
327
|
+
&& (!lease.agent_key || lease.agent_key === identity.agent_key));
|
|
328
|
+
const eligibleRepresentative = exactReplyTargetMessageIds.has(messageId)
|
|
329
|
+
? true
|
|
330
|
+
: resolved.reason === "task_owner" && exactTaskSessionIdsForKey.size > 0
|
|
331
|
+
? exactTaskSessionIdsForKey.size === 1 && exactTaskOwner
|
|
332
|
+
: currentIsActive;
|
|
333
|
+
authoritativeLegacyDecisions.set(messageId, eligibleRepresentative && resolved.decision === "activate"
|
|
334
|
+
? resolved
|
|
335
|
+
: { decision: "silent", reason: "unaddressed", addressed: false });
|
|
336
|
+
}
|
|
337
|
+
return attachAgentMessageActivations(records, identity, { authoritativeLegacyDecisions });
|
|
156
338
|
}
|
|
157
339
|
// Resolving an out-of-window parent costs a lookup (a by-id fetch, or a
|
|
158
340
|
// page-by-page history scan against older APIs), so resolved (and observed)
|
|
@@ -162,6 +344,9 @@ async function attachLocalActivationMetadata(roomId, messages, agentSession, opt
|
|
|
162
344
|
// as immutable once posted; a bounded insertion-ordered map keeps memory flat
|
|
163
345
|
// for long-running workers.
|
|
164
346
|
const THREAD_CONTEXT_CACHE_MAX = 500;
|
|
347
|
+
export const THREAD_CONTEXT_LOOKUP_MAX = 16;
|
|
348
|
+
export const THREAD_CONTEXT_BYTES_MAX = 256 * 1024;
|
|
349
|
+
const THREAD_CONTEXT_DEADLINE_MS = 1_000;
|
|
165
350
|
const threadContextCache = new Map();
|
|
166
351
|
function threadContextCacheKey(scopeId, targetMessageId) {
|
|
167
352
|
return `${scopeId}:${targetMessageId}`;
|
|
@@ -199,20 +384,30 @@ export async function collectThreadContextMessages(input) {
|
|
|
199
384
|
if (pendingIds.length === 0) {
|
|
200
385
|
// Nothing quotes an out-of-window message (idle polls land here), so skip
|
|
201
386
|
// storage-mode resolution entirely.
|
|
202
|
-
return [];
|
|
387
|
+
return { messages: [], truncated: false };
|
|
203
388
|
}
|
|
204
389
|
const contextMessages = [];
|
|
390
|
+
let contextBytes = 0;
|
|
391
|
+
let lookups = 0;
|
|
392
|
+
let truncated = false;
|
|
393
|
+
const deadline = Date.now() + THREAD_CONTEXT_DEADLINE_MS;
|
|
205
394
|
const useLocalStorage = Boolean(input.localRoomId && await isLocalRoomStorageEnabled(input.localRoomId));
|
|
206
395
|
const localIdentifiers = useLocalStorage
|
|
207
396
|
? await resolveLocalRoomStorageIdentifiers(input.localRoomId)
|
|
208
397
|
: { localRoomId: input.localRoomId };
|
|
209
398
|
const sqliteRoomId = localIdentifiers.localRoomId || input.localRoomId;
|
|
210
399
|
while (pendingIds.length > 0) {
|
|
400
|
+
if (lookups >= THREAD_CONTEXT_LOOKUP_MAX || Date.now() >= deadline) {
|
|
401
|
+
truncated = true;
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
211
404
|
const nextId = pendingIds.shift();
|
|
212
405
|
if (!nextId || seenIds.has(nextId))
|
|
213
406
|
continue;
|
|
214
407
|
seenIds.add(nextId);
|
|
215
408
|
const cached = threadContextCache.get(threadContextCacheKey(scopeId, nextId));
|
|
409
|
+
if (!cached)
|
|
410
|
+
lookups += 1;
|
|
216
411
|
const message = cached
|
|
217
412
|
?? (useLocalStorage && input.localRoomId
|
|
218
413
|
? await findLocalMessageById(sqliteRoomId || input.localRoomId, nextId)
|
|
@@ -226,13 +421,19 @@ export async function collectThreadContextMessages(input) {
|
|
|
226
421
|
if (!cached) {
|
|
227
422
|
rememberThreadContextMessages(scopeId, [message]);
|
|
228
423
|
}
|
|
424
|
+
const messageBytes = Buffer.byteLength(JSON.stringify(message), "utf8");
|
|
425
|
+
if (contextBytes + messageBytes > THREAD_CONTEXT_BYTES_MAX) {
|
|
426
|
+
truncated = true;
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
contextBytes += messageBytes;
|
|
229
430
|
contextMessages.push(message);
|
|
230
431
|
const parentId = replyReferenceId(message);
|
|
231
432
|
if (parentId && !seenIds.has(parentId)) {
|
|
232
433
|
pendingIds.push(parentId);
|
|
233
434
|
}
|
|
234
435
|
}
|
|
235
|
-
return contextMessages;
|
|
436
|
+
return { messages: contextMessages, truncated };
|
|
236
437
|
}
|
|
237
438
|
export function registerWaitForMessagesTool(server) {
|
|
238
439
|
server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat room (HTTP long-poll). Messages labeled activation.for_current_agent.decision=\"silent\" are skipped for the current worker; when last_observed_message_id is present, use it as cursor progress even if messages is empty. Threaded replies include thread_parent_id/thread.root_message_id; use send_thread_message with that id to keep focused side discussion out of the main room. For multi-hour runs, call in a loop: always pass after_message_id from the last message you processed or last_observed_message_id so an empty result means 'nothing new yet', not 'stop working'. Legacy/manual room workers may send a brief continue instruction when another legacy/manual participant posts a premature 'I will wait' closing line. Never send that nudge to or about a daemon-supervised participant, and daemon-supervised workers must not emit it; their supervisor owns wake and retry. Per-call wait is capped (default max 180s unless LETAGENTS_POLL_MAX_MS is set on API and MCP).", {
|
|
@@ -260,8 +461,13 @@ export function registerWaitForMessagesTool(server) {
|
|
|
260
461
|
const targetRoomId = getTargetRoomId(room_id);
|
|
261
462
|
const targetProjectId = getFallbackProjectId();
|
|
262
463
|
const localRoomId = targetRoomId ?? currentRoom?.room_id ?? targetProjectId;
|
|
263
|
-
const identity = await ensureAgentIdentity();
|
|
264
464
|
const sessionRoomId = targetRoomId ?? currentRoom?.room_id ?? localRoomId ?? null;
|
|
465
|
+
const routingStateSnapshot = getStoredAgentRoutingStateSnapshot(sessionRoomId ?? "");
|
|
466
|
+
const localStorageEnabled = Boolean(localRoomId && await isLocalRoomStorageEnabled(localRoomId));
|
|
467
|
+
if (localStorageEnabled && !routingStateSnapshot.complete) {
|
|
468
|
+
throw new Error("Local agent routing state is unavailable; retry after restoring the state file.");
|
|
469
|
+
}
|
|
470
|
+
const identity = await ensureAgentIdentity();
|
|
265
471
|
const agentSession = resolveWaitAgentSession(sessionRoomId, agent_session_id);
|
|
266
472
|
if (agentSession) {
|
|
267
473
|
// Registration (or a successor generation) must bind strictly once.
|
|
@@ -278,8 +484,8 @@ export function registerWaitForMessagesTool(server) {
|
|
|
278
484
|
}
|
|
279
485
|
const maxPollMs = getPollTimeoutCapMs();
|
|
280
486
|
const serverTimeout = Math.min(Math.max(timeout || DEFAULT_POLL_TIMEOUT_MS, 1000), maxPollMs);
|
|
281
|
-
if (localRoomId &&
|
|
282
|
-
const { localRoomId: sqliteRoomId } = await resolveLocalRoomStorageIdentifiers(localRoomId);
|
|
487
|
+
if (localRoomId && localStorageEnabled) {
|
|
488
|
+
const { localRoomId: sqliteRoomId, cloudRoomId, } = await resolveLocalRoomStorageIdentifiers(localRoomId);
|
|
283
489
|
const effectiveLocalRoomId = sqliteRoomId || localRoomId;
|
|
284
490
|
const effectiveAfterMessageId = resolveEffectiveAfterMessageId({
|
|
285
491
|
requestedAfterMessageId: after_message_id,
|
|
@@ -290,11 +496,12 @@ export function registerWaitForMessagesTool(server) {
|
|
|
290
496
|
// keep the unchanged forward "everything after the cursor" behavior.
|
|
291
497
|
const existing = fetchPlan.mode === "catch_up_tail"
|
|
292
498
|
? await getLatestLocalChatMessages(effectiveLocalRoomId, {
|
|
293
|
-
limit: fetchPlan.limit,
|
|
499
|
+
limit: Math.min(fetchPlan.limit, MAX_WAIT_MESSAGES_PER_CALL),
|
|
294
500
|
include_prompt_only: true,
|
|
295
501
|
})
|
|
296
502
|
: await getLocalChatMessages(effectiveLocalRoomId, {
|
|
297
503
|
after: effectiveAfterMessageId,
|
|
504
|
+
limit: MAX_WAIT_MESSAGES_PER_CALL,
|
|
298
505
|
include_prompt_only: true,
|
|
299
506
|
});
|
|
300
507
|
const replayingExistingMessages = existing.messages.length > 0;
|
|
@@ -303,12 +510,18 @@ export function registerWaitForMessagesTool(server) {
|
|
|
303
510
|
: await waitForLocalChatMessages(effectiveLocalRoomId, {
|
|
304
511
|
after: effectiveAfterMessageId,
|
|
305
512
|
timeoutMs: serverTimeout,
|
|
513
|
+
limit: MAX_WAIT_MESSAGES_PER_CALL,
|
|
306
514
|
include_prompt_only: true,
|
|
307
515
|
});
|
|
308
516
|
const messages = await attachLocalActivationMetadata(effectiveLocalRoomId, result.messages, agentSession, {
|
|
309
517
|
includeTaskOwnerLeases: !replayingExistingMessages,
|
|
518
|
+
activeSessionRoomId: cloudRoomId || sessionRoomId,
|
|
519
|
+
});
|
|
520
|
+
const bounded = boundAgentMessageOutput(messages, {
|
|
521
|
+
direction: fetchPlan.mode === "catch_up_tail" ? "suffix" : "prefix",
|
|
522
|
+
maxBytes: AGENT_MESSAGE_BODY_MAX_BYTES,
|
|
310
523
|
});
|
|
311
|
-
const routing = filterSilentActivationMessages(messages);
|
|
524
|
+
const routing = filterSilentActivationMessages(bounded.messages);
|
|
312
525
|
const observedCursor = routing.last_observed_message_id ?? getLastMessageId(result);
|
|
313
526
|
touchRoomSession(effectiveLocalRoomId, observedCursor);
|
|
314
527
|
const threadContext = await collectThreadContextMessages({
|
|
@@ -320,8 +533,13 @@ export function registerWaitForMessagesTool(server) {
|
|
|
320
533
|
return jsonToolResponse({
|
|
321
534
|
room_id: effectiveLocalRoomId,
|
|
322
535
|
...addActivationRoutingTelemetry({
|
|
323
|
-
messages: toAgentReadableMessages(routing.messages, threadContext),
|
|
536
|
+
messages: toAgentReadableMessages(routing.messages, threadContext.messages),
|
|
324
537
|
}, routing),
|
|
538
|
+
...(result.has_more || bounded.truncated ? { truncated: true } : {}),
|
|
539
|
+
...(bounded.omittedMessageCount > 0
|
|
540
|
+
? { omitted_message_count: bounded.omittedMessageCount }
|
|
541
|
+
: {}),
|
|
542
|
+
...(threadContext.truncated ? { thread_context_truncated: true } : {}),
|
|
325
543
|
});
|
|
326
544
|
}
|
|
327
545
|
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, agentSession ? identityFromAgentSession(agentSession) : identity), agentSession);
|
|
@@ -333,6 +551,8 @@ export function registerWaitForMessagesTool(server) {
|
|
|
333
551
|
const deliveryHeaders = buildAgentDeliveryHeaders(agentSession);
|
|
334
552
|
const allMessages = [];
|
|
335
553
|
let roomIdFromResponse;
|
|
554
|
+
let catchUpTruncated = false;
|
|
555
|
+
let apiObservedCursor = null;
|
|
336
556
|
// Long-poll the server (blocks up to serverTimeout for new messages),
|
|
337
557
|
// optionally seeded with a cursor. Shared by the cursor path and by the
|
|
338
558
|
// no-cursor empty-tail fallback so a quiet room still blocks instead of
|
|
@@ -342,6 +562,7 @@ export function registerWaitForMessagesTool(server) {
|
|
|
342
562
|
const params = new URLSearchParams();
|
|
343
563
|
if (after)
|
|
344
564
|
params.set("after", after);
|
|
565
|
+
params.set("limit", String(MAX_WAIT_MESSAGES_PER_CALL));
|
|
345
566
|
params.set("timeout", String(serverTimeout));
|
|
346
567
|
const queryString = params.toString();
|
|
347
568
|
const firstResult = await roomScopedApiCall({
|
|
@@ -356,25 +577,10 @@ export function registerWaitForMessagesTool(server) {
|
|
|
356
577
|
});
|
|
357
578
|
allMessages.push(...(firstResult.messages ?? []));
|
|
358
579
|
roomIdFromResponse = roomIdFromResponse || firstResult.room_id || firstResult.project_id;
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
pageParams.set("after", afterCursor);
|
|
364
|
-
const qs = pageParams.toString();
|
|
365
|
-
const page = await roomScopedApiCall(buildWaitForMessagesHistoryPageRequest({
|
|
366
|
-
targetRoomId,
|
|
367
|
-
targetProjectId,
|
|
368
|
-
queryString: qs,
|
|
369
|
-
deliveryHeaders,
|
|
370
|
-
}));
|
|
371
|
-
const msgs = page.messages ?? [];
|
|
372
|
-
allMessages.push(...msgs);
|
|
373
|
-
if (!page.has_more || msgs.length === 0)
|
|
374
|
-
break;
|
|
375
|
-
afterCursor = msgs[msgs.length - 1]?.id;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
580
|
+
catchUpTruncated = Boolean(firstResult.has_more);
|
|
581
|
+
apiObservedCursor = typeof firstResult.last_observed_message_id === "string"
|
|
582
|
+
? firstResult.last_observed_message_id
|
|
583
|
+
: null;
|
|
378
584
|
};
|
|
379
585
|
if (fetchPlan.mode === "catch_up_tail") {
|
|
380
586
|
// No cursor: catch up on only the bounded recent tail. Mirror
|
|
@@ -385,6 +591,7 @@ export function registerWaitForMessagesTool(server) {
|
|
|
385
591
|
targetRoomId,
|
|
386
592
|
targetProjectId,
|
|
387
593
|
limit: fetchPlan.limit,
|
|
594
|
+
deliveryHeaders,
|
|
388
595
|
});
|
|
389
596
|
if (recent.messages.length > 0) {
|
|
390
597
|
allMessages.push(...recent.messages);
|
|
@@ -402,7 +609,11 @@ export function registerWaitForMessagesTool(server) {
|
|
|
402
609
|
else {
|
|
403
610
|
await longPollFromCursor(fetchPlan.after);
|
|
404
611
|
}
|
|
405
|
-
const
|
|
612
|
+
const bounded = boundAgentMessageOutput(allMessages, {
|
|
613
|
+
direction: fetchPlan.mode === "catch_up_tail" ? "suffix" : "prefix",
|
|
614
|
+
maxBytes: AGENT_MESSAGE_BODY_MAX_BYTES,
|
|
615
|
+
});
|
|
616
|
+
const routing = filterSilentActivationMessages(bounded.messages);
|
|
406
617
|
const threadContext = await collectThreadContextMessages({
|
|
407
618
|
messages: routing.messages,
|
|
408
619
|
localRoomId,
|
|
@@ -410,13 +621,24 @@ export function registerWaitForMessagesTool(server) {
|
|
|
410
621
|
projectId: targetProjectId,
|
|
411
622
|
});
|
|
412
623
|
const output = addActivationRoutingTelemetry({
|
|
413
|
-
messages: toAgentReadableMessages(routing.messages, threadContext),
|
|
624
|
+
messages: toAgentReadableMessages(routing.messages, threadContext.messages),
|
|
414
625
|
}, routing);
|
|
626
|
+
if (threadContext.truncated)
|
|
627
|
+
output.thread_context_truncated = true;
|
|
415
628
|
if (roomIdFromResponse) {
|
|
416
629
|
output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
|
|
417
630
|
}
|
|
631
|
+
if (catchUpTruncated || bounded.truncated)
|
|
632
|
+
output.truncated = true;
|
|
633
|
+
if (bounded.omittedMessageCount > 0) {
|
|
634
|
+
output.omitted_message_count = bounded.omittedMessageCount;
|
|
635
|
+
}
|
|
636
|
+
if (apiObservedCursor)
|
|
637
|
+
output.last_observed_message_id = apiObservedCursor;
|
|
418
638
|
if (targetRoomId) {
|
|
419
|
-
const observedCursor =
|
|
639
|
+
const observedCursor = apiObservedCursor
|
|
640
|
+
?? routing.last_observed_message_id
|
|
641
|
+
?? getLastMessageId(output);
|
|
420
642
|
touchRoomSession(targetRoomId, observedCursor);
|
|
421
643
|
if (allMessages.length > 0 && agentSession) {
|
|
422
644
|
const firstMsg = allMessages[0];
|
|
@@ -83,7 +83,7 @@ export function registerGetOnboardingStatusTool(server) {
|
|
|
83
83
|
const storedAuth = getStoredAuth();
|
|
84
84
|
const pendingAuth = getPendingDeviceAuth();
|
|
85
85
|
const savedCurrentRoom = getStoredCurrentRoom();
|
|
86
|
-
const detectedRoom = configGitContext?.
|
|
86
|
+
const detectedRoom = configGitContext?.activeRoomLocator ?? gitContext?.activeRoomLocator ?? null;
|
|
87
87
|
const api_health = await checkOnboardingApiHealth();
|
|
88
88
|
const authenticated = Boolean(process.env.LETAGENTS_TOKEN || storedAuth);
|
|
89
89
|
let nextStep = "join_room";
|
|
@@ -113,10 +113,10 @@ export function registerGetOnboardingStatusTool(server) {
|
|
|
113
113
|
saved_current_room: toPublicStoredRoomSession(savedCurrentRoom),
|
|
114
114
|
detected_room_from_context: detectedRoom,
|
|
115
115
|
configured_room_from_file: configRoom,
|
|
116
|
-
configured_active_room_from_context: configGitContext?.
|
|
116
|
+
configured_active_room_from_context: configGitContext?.activeRoomLocator ?? null,
|
|
117
117
|
derived_repo_room_from_git: gitContext?.repoRoom ?? null,
|
|
118
|
-
derived_active_git_room: gitContext?.
|
|
119
|
-
derived_branch_room_from_git: gitContext?.
|
|
118
|
+
derived_active_git_room: gitContext?.activeRoomLocator ?? null,
|
|
119
|
+
derived_branch_room_from_git: gitContext?.activeRefRoomLocator ?? null,
|
|
120
120
|
git_current_branch: gitContext?.currentBranch ?? configGitContext?.currentBranch ?? null,
|
|
121
121
|
git_default_branch: gitContext?.defaultBranch ?? configGitContext?.defaultBranch ?? null,
|
|
122
122
|
repo_root: repoRoot,
|
|
@@ -3,8 +3,10 @@ import { join } from "path";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { getRoomFromConfig } from "../../../config-reader.js";
|
|
5
5
|
import { buildActiveGitRoomContext, getGitCurrentBranch, getGitDefaultBranch, getGitRoomContext, } from "../../../git-remote.js";
|
|
6
|
-
import { currentAgentIdentity, currentAgentIdentityKey, currentRoom, getConversationIdentity, getCurrentLiveSessionPayload, getStoredAgentIdentity, listStoredCodexLiveSessions, toPublicAgentIdentity, toPublicCurrentRoomState, withJoinRoomAgentPrompt, } from "../../runtime.js";
|
|
6
|
+
import { currentAgentIdentity, currentAgentIdentityKey, currentRoomMatchesLocator, currentRoom, getConversationIdentity, getCurrentLiveSessionPayload, getStoredAgentIdentity, listStoredCodexLiveSessions, toPublicAgentIdentity, toPublicCurrentRoomState, withJoinRoomAgentPrompt, } from "../../runtime.js";
|
|
7
7
|
import { requireValidWorkerBearerRuntime } from "../../runtime/worker-bearer.js";
|
|
8
|
+
import { getDaemonToolExecutionContext, getRuntimeWorkingDirectory } from "../../runtime/daemon-tool-context.js";
|
|
9
|
+
import { identityFromAgentSession } from "../../runtime/agent-sessions.js";
|
|
8
10
|
import { findExistingConfig, resolveGitRoot, } from "../../repo-context.js";
|
|
9
11
|
import { jsonToolResponse } from "./response.js";
|
|
10
12
|
let ownerAuthStoreLoader = () => import("../../../local-state.js");
|
|
@@ -29,6 +31,7 @@ export function registerRoomInspectionTools(server) {
|
|
|
29
31
|
}
|
|
30
32
|
export async function getCurrentRoomPayload(conversationId) {
|
|
31
33
|
const runtime = requireValidWorkerBearerRuntime();
|
|
34
|
+
const daemonContext = getDaemonToolExecutionContext();
|
|
32
35
|
const publicCurrentRoom = toPublicCurrentRoomState();
|
|
33
36
|
const workerAuth = runtime.mode !== "owner"
|
|
34
37
|
? { source: runtime.mode === "worker" ? "worker_bearer" : "daemon_supervised", expires_at: null, account: null }
|
|
@@ -55,9 +58,11 @@ export async function getCurrentRoomPayload(conversationId) {
|
|
|
55
58
|
...publicCurrentRoom,
|
|
56
59
|
...(runtime.mode === "supervised" ? { room_binding: "daemon_supervised" } : {}),
|
|
57
60
|
...localCodexDetails,
|
|
58
|
-
agent_identity: toPublicAgentIdentity(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
agent_identity: toPublicAgentIdentity(daemonContext
|
|
62
|
+
? identityFromAgentSession(daemonContext.agentSession)
|
|
63
|
+
: getConversationIdentity(conversationId)
|
|
64
|
+
?? currentAgentIdentity
|
|
65
|
+
?? getStoredAgentIdentity(currentAgentIdentityKey)),
|
|
61
66
|
auth: workerAuth ?? (auth
|
|
62
67
|
? {
|
|
63
68
|
source: process.env.LETAGENTS_TOKEN ? "env" : "local_state",
|
|
@@ -73,7 +78,7 @@ export async function getCurrentRoomPayload(conversationId) {
|
|
|
73
78
|
}
|
|
74
79
|
function getRepoInspectionPayload(targetDir) {
|
|
75
80
|
const runtime = requireValidWorkerBearerRuntime();
|
|
76
|
-
const startDir = targetDir ||
|
|
81
|
+
const startDir = targetDir || getRuntimeWorkingDirectory();
|
|
77
82
|
const repoRoot = resolveGitRoot(startDir);
|
|
78
83
|
const configDir = repoRoot ? findExistingConfig(startDir) : null;
|
|
79
84
|
const configPath = configDir ? join(configDir, ".letagents.json") : null;
|
|
@@ -86,8 +91,8 @@ function getRepoInspectionPayload(targetDir) {
|
|
|
86
91
|
defaultBranch: repoRoot ? getGitDefaultBranch(repoRoot) : null,
|
|
87
92
|
})
|
|
88
93
|
: null;
|
|
89
|
-
const detectedRoom = configGitContext?.
|
|
90
|
-
const currentRoomMatchesContext =
|
|
94
|
+
const detectedRoom = configGitContext?.activeRoomLocator ?? gitContext?.activeRoomLocator ?? null;
|
|
95
|
+
const currentRoomMatchesContext = currentRoomMatchesLocator(detectedRoom);
|
|
91
96
|
return {
|
|
92
97
|
cwd: startDir,
|
|
93
98
|
repo_context_status: repoRoot ? "git_repo_detected" : "not_inside_git_repo",
|
|
@@ -95,10 +100,10 @@ function getRepoInspectionPayload(targetDir) {
|
|
|
95
100
|
config_file: configPath ?? null,
|
|
96
101
|
config_contents: readConfigContents(configPath),
|
|
97
102
|
configured_room_from_file: configuredRoom ?? null,
|
|
98
|
-
configured_active_room_from_context: configGitContext?.
|
|
99
|
-
derived_room_from_git: gitContext?.
|
|
103
|
+
configured_active_room_from_context: configGitContext?.activeRoomLocator ?? null,
|
|
104
|
+
derived_room_from_git: gitContext?.activeRoomLocator ?? null,
|
|
100
105
|
derived_repo_room_from_git: gitContext?.repoRoom ?? null,
|
|
101
|
-
derived_branch_room_from_git: gitContext?.
|
|
106
|
+
derived_branch_room_from_git: gitContext?.activeRefRoomLocator ?? null,
|
|
102
107
|
git_current_branch: gitContext?.currentBranch ?? configGitContext?.currentBranch ?? null,
|
|
103
108
|
git_default_branch: gitContext?.defaultBranch ?? configGitContext?.defaultBranch ?? null,
|
|
104
109
|
detected_room_from_context: detectedRoom ?? null,
|
|
@@ -5,6 +5,7 @@ import { getGitRemoteIdentity } from "../../../git-remote.js";
|
|
|
5
5
|
import { joinRoomIdentifier, } from "../../runtime.js";
|
|
6
6
|
import { findExistingConfig, resolveGitRoot, } from "../../repo-context.js";
|
|
7
7
|
import { jsonToolResponse } from "./response.js";
|
|
8
|
+
import { getRuntimeWorkingDirectory } from "../../runtime/daemon-tool-context.js";
|
|
8
9
|
export function registerRepoInitializationTool(server) {
|
|
9
10
|
server.tool("initialize_repo", "Initialize the current repo for Let Agents Chat by creating a .letagents.json config file. " +
|
|
10
11
|
"This explicitly sets up repo-based room auto-join. Reads git remote to derive the room name, " +
|
|
@@ -19,7 +20,7 @@ export function registerRepoInitializationTool(server) {
|
|
|
19
20
|
.optional()
|
|
20
21
|
.describe("Working directory hint for repo detection. Defaults to the current process directory."),
|
|
21
22
|
}, async ({ room, cwd: targetDir }) => {
|
|
22
|
-
const startDir = targetDir ||
|
|
23
|
+
const startDir = targetDir || getRuntimeWorkingDirectory();
|
|
23
24
|
const repoRoot = resolveGitRoot(startDir);
|
|
24
25
|
if (!repoRoot) {
|
|
25
26
|
return jsonToolResponse({
|