letagents 0.9.0 → 0.10.0

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.
@@ -1,12 +1,16 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
1
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "fs";
2
+ import { randomBytes } from "crypto";
2
3
  import { homedir } from "os";
3
4
  import { dirname, join } from "path";
4
5
  const DEFAULT_STATE_PATH = join(homedir(), ".letagents", "mcp-state.json");
6
+ const STATE_LOCK_WAIT_MS = 25;
7
+ const STATE_LOCK_TIMEOUT_MS = 2_000;
8
+ const STATE_LOCK_STALE_MS = 10_000;
9
+ const STATE_LOCK_SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
5
10
  export function getLocalStatePath() {
6
11
  return process.env.LETAGENTS_STATE_PATH || DEFAULT_STATE_PATH;
7
12
  }
8
- export function readLocalState() {
9
- const statePath = getLocalStatePath();
13
+ function readLocalStateFromPath(statePath) {
10
14
  if (!existsSync(statePath)) {
11
15
  return {};
12
16
  }
@@ -19,18 +23,76 @@ export function readLocalState() {
19
23
  return {};
20
24
  }
21
25
  }
22
- export function writeLocalState(state) {
26
+ export function readLocalState() {
27
+ const statePath = getLocalStatePath();
28
+ return readLocalStateFromPath(statePath);
29
+ }
30
+ function sleepSync(ms) {
31
+ if (ms > 0) {
32
+ Atomics.wait(STATE_LOCK_SLEEP_BUFFER, 0, 0, ms);
33
+ }
34
+ }
35
+ function writeLocalStateUnlocked(statePath, state) {
36
+ const tempPath = `${statePath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
37
+ try {
38
+ writeFileSync(tempPath, JSON.stringify(state, null, 2) + "\n", "utf-8");
39
+ renameSync(tempPath, statePath);
40
+ }
41
+ finally {
42
+ rmSync(tempPath, { force: true });
43
+ }
44
+ }
45
+ function withStateLock(callback) {
23
46
  const statePath = getLocalStatePath();
24
47
  mkdirSync(dirname(statePath), { recursive: true });
25
- const tempPath = `${statePath}.tmp`;
26
- writeFileSync(tempPath, JSON.stringify(state, null, 2) + "\n", "utf-8");
27
- renameSync(tempPath, statePath);
48
+ const lockPath = `${statePath}.lock`;
49
+ const startedAt = Date.now();
50
+ while (true) {
51
+ let lockFd = null;
52
+ try {
53
+ lockFd = openSync(lockPath, "wx");
54
+ return callback(statePath);
55
+ }
56
+ catch (error) {
57
+ const err = error;
58
+ if (err.code !== "EEXIST") {
59
+ throw error;
60
+ }
61
+ try {
62
+ const stats = statSync(lockPath);
63
+ if (Date.now() - stats.mtimeMs > STATE_LOCK_STALE_MS) {
64
+ rmSync(lockPath, { force: true });
65
+ continue;
66
+ }
67
+ }
68
+ catch {
69
+ continue;
70
+ }
71
+ if (Date.now() - startedAt >= STATE_LOCK_TIMEOUT_MS) {
72
+ throw new Error(`Timed out acquiring local state lock at ${lockPath}`);
73
+ }
74
+ sleepSync(STATE_LOCK_WAIT_MS);
75
+ }
76
+ finally {
77
+ if (lockFd !== null) {
78
+ closeSync(lockFd);
79
+ rmSync(lockPath, { force: true });
80
+ }
81
+ }
82
+ }
83
+ }
84
+ export function writeLocalState(state) {
85
+ withStateLock((statePath) => {
86
+ writeLocalStateUnlocked(statePath, state);
87
+ });
28
88
  }
29
89
  export function updateLocalState(updater) {
30
- const current = readLocalState();
31
- const updated = updater(current) ?? current;
32
- writeLocalState(updated);
33
- return updated;
90
+ return withStateLock((statePath) => {
91
+ const current = readLocalStateFromPath(statePath);
92
+ const updated = updater(current) ?? current;
93
+ writeLocalStateUnlocked(statePath, updated);
94
+ return updated;
95
+ });
34
96
  }
35
97
  function isExpired(expiresAt) {
36
98
  if (!expiresAt) {
@@ -4,6 +4,9 @@ export function encodeRoomIdPath(roomId) {
4
4
  .map((segment) => encodeURIComponent(segment))
5
5
  .join("/");
6
6
  }
7
+ export function getCanonicalRoomWebPath(roomId) {
8
+ return `/in/${encodeRoomIdPath(roomId)}`;
9
+ }
7
10
  export function looksLikeInviteCode(value) {
8
11
  return /^[A-Z0-9]{4}(?:-[A-Z0-9]{4})+$/.test(value.trim().toUpperCase());
9
12
  }
@@ -10,12 +10,12 @@ import { execSync } from "child_process";
10
10
  import { SseClient } from "./sse-client.js";
11
11
  import { getRoomFromConfig } from "./config-reader.js";
12
12
  import { getGitRemoteIdentity } from "./git-remote.js";
13
- import { clearPendingDeviceAuth, clearStoredAuth, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, saveRoomSession, setStoredAgentIdentity, setPendingDeviceAuth, setStoredAuth, touchRoomSession, } from "./local-state.js";
14
- import { encodeRoomIdPath, looksLikeInviteCode, normalizeInviteCode, } from "./room-id.js";
13
+ import { clearPendingDeviceAuth, clearStoredAuth, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, saveRoomSession, setStoredAgentIdentity, setPendingDeviceAuth, setStoredAuth, touchRoomSession, updateLocalState, } from "./local-state.js";
14
+ import { encodeRoomIdPath, getCanonicalRoomWebPath, looksLikeInviteCode, normalizeInviteCode, } from "./room-id.js";
15
15
  import { buildAgentActorLabel, formatOwnerAttribution, inferAgentIdeLabel, toTitleCaseCodename, } from "../shared/agent-identity.js";
16
- const CURRENT_AGENT_IDENTITY_KEY = getAgentIdentityStorageKey();
17
16
  let currentRoom = null;
18
- let currentAgentIdentity = getStoredAgentIdentity(CURRENT_AGENT_IDENTITY_KEY);
17
+ let currentAgentIdentityKey = "";
18
+ let currentAgentIdentity = null;
19
19
  let currentAuthenticatedAccount = undefined;
20
20
  let currentAuthenticatedAccountSource = null;
21
21
  let currentAuthenticatedEnvToken = null;
@@ -27,6 +27,10 @@ const AGENT_NAME = (process.env.LETAGENTS_AGENT_NAME || process.env.AGENT_NAME |
27
27
  const AGENT_DISPLAY_NAME = (process.env.LETAGENTS_AGENT_DISPLAY_NAME || "").trim();
28
28
  const AGENT_IDE_LABEL = (process.env.LETAGENTS_AGENT_IDE || process.env.AGENT_IDE || "").trim();
29
29
  const AGENT_OWNER_LABEL = (process.env.LETAGENTS_AGENT_OWNER_LABEL || "").trim();
30
+ const EXPLICIT_AGENT_IDENTITY_KEY = getExplicitAgentIdentityStorageKey();
31
+ currentAgentIdentityKey =
32
+ EXPLICIT_AGENT_IDENTITY_KEY ?? getFallbackAgentIdentityNamespaceKey(detectAgentIdeLabel());
33
+ currentAgentIdentity = getStoredAgentIdentity(currentAgentIdentityKey);
30
34
  // ---------------------------------------------------------------------------
31
35
  // Helpers
32
36
  // ---------------------------------------------------------------------------
@@ -166,7 +170,9 @@ const AGENT_CODENAMES = [
166
170
  "wood",
167
171
  "wren",
168
172
  ];
169
- function getAgentIdentityStorageKey() {
173
+ const AGENT_CODENAME_SPACE = AGENT_CODENAMES.length * AGENT_CODENAMES.length;
174
+ const AGENT_IDENTITY_SLOT_SUFFIX = ":slot:";
175
+ function getExplicitAgentIdentityStorageKey() {
170
176
  const runtimeSignals = [
171
177
  process.env.LETAGENTS_AGENT_INSTANCE_ID,
172
178
  process.env.CODEX_THREAD_ID && `codex:${process.env.CODEX_THREAD_ID}`,
@@ -177,12 +183,26 @@ function getAgentIdentityStorageKey() {
177
183
  if (runtimeSignals.length) {
178
184
  return runtimeSignals[0];
179
185
  }
180
- return `cwd:${process.cwd()}`;
186
+ return null;
187
+ }
188
+ function getFallbackAgentIdentityNamespaceKey(ideLabel) {
189
+ return `cwd:${process.cwd()}:ide:${normalizeAgentBaseName(ideLabel)}`;
181
190
  }
182
191
  function hashStringToIndex(value, modulo) {
183
192
  const digest = createHash("sha256").update(value).digest();
184
193
  return digest.readUInt16BE(0) % modulo;
185
194
  }
195
+ function codenameFromIndex(index) {
196
+ const normalizedIndex = ((index % AGENT_CODENAME_SPACE) + AGENT_CODENAME_SPACE) % AGENT_CODENAME_SPACE;
197
+ const firstIndex = Math.floor(normalizedIndex / AGENT_CODENAMES.length);
198
+ const secondIndex = normalizedIndex % AGENT_CODENAMES.length;
199
+ const first = AGENT_CODENAMES[firstIndex];
200
+ const second = AGENT_CODENAMES[secondIndex];
201
+ return {
202
+ name: normalizeAgentBaseName(`${first}-${second}`),
203
+ display_name: `${toTitleCaseCodename(first)} ${toTitleCaseCodename(second)}`,
204
+ };
205
+ }
186
206
  function detectAgentIdeLabel() {
187
207
  if (AGENT_IDE_LABEL) {
188
208
  return toTitleCaseCodename(AGENT_IDE_LABEL);
@@ -194,6 +214,122 @@ function detectAgentIdeLabel() {
194
214
  const inferred = inferAgentIdeLabel(explicitName);
195
215
  return inferred || "Agent";
196
216
  }
217
+ function getFallbackSlotPrefix(namespaceKey) {
218
+ return `${namespaceKey}${AGENT_IDENTITY_SLOT_SUFFIX}`;
219
+ }
220
+ function getFallbackSlotKey(namespaceKey, slotIndex) {
221
+ return `${getFallbackSlotPrefix(namespaceKey)}${slotIndex}`;
222
+ }
223
+ function parseFallbackSlotIndex(identityKey, namespaceKey) {
224
+ const prefix = getFallbackSlotPrefix(namespaceKey);
225
+ if (!identityKey.startsWith(prefix)) {
226
+ return null;
227
+ }
228
+ const slotIndex = Number.parseInt(identityKey.slice(prefix.length), 10);
229
+ return Number.isInteger(slotIndex) && slotIndex >= 0 ? slotIndex : null;
230
+ }
231
+ function isProcessAlive(pid) {
232
+ if (!pid || !Number.isInteger(pid) || pid <= 0) {
233
+ return false;
234
+ }
235
+ try {
236
+ process.kill(pid, 0);
237
+ return true;
238
+ }
239
+ catch (error) {
240
+ const err = error;
241
+ return err.code === "EPERM";
242
+ }
243
+ }
244
+ function claimFallbackIdentityKey(namespaceKey) {
245
+ let claimedKey = getFallbackSlotKey(namespaceKey, 0);
246
+ let claimedIdentity = null;
247
+ const now = new Date().toISOString();
248
+ updateLocalState((state) => {
249
+ const identities = state.agent_identities ?? {};
250
+ const leases = state.agent_identity_leases ?? {};
251
+ const namespacePrefix = getFallbackSlotPrefix(namespaceKey);
252
+ for (const [identityKey, lease] of Object.entries(leases)) {
253
+ if (!identityKey.startsWith(namespacePrefix) || lease.namespace_key !== namespaceKey) {
254
+ continue;
255
+ }
256
+ if (!isProcessAlive(lease.pid)) {
257
+ delete leases[identityKey];
258
+ }
259
+ }
260
+ const activeLeaseForPid = Object.entries(leases).find(([identityKey, lease]) => identityKey.startsWith(namespacePrefix) &&
261
+ lease.namespace_key === namespaceKey &&
262
+ lease.pid === process.pid);
263
+ if (activeLeaseForPid) {
264
+ const [identityKey, lease] = activeLeaseForPid;
265
+ lease.updated_at = now;
266
+ claimedKey = identityKey;
267
+ claimedIdentity = identities[identityKey] ?? null;
268
+ state.agent_identity_leases = leases;
269
+ return state;
270
+ }
271
+ const slotKeys = new Set();
272
+ for (const identityKey of Object.keys(identities)) {
273
+ if (identityKey.startsWith(namespacePrefix)) {
274
+ slotKeys.add(identityKey);
275
+ }
276
+ }
277
+ for (const identityKey of Object.keys(leases)) {
278
+ if (identityKey.startsWith(namespacePrefix)) {
279
+ slotKeys.add(identityKey);
280
+ }
281
+ }
282
+ const sortedSlotKeys = [...slotKeys].sort((left, right) => {
283
+ const leftIndex = parseFallbackSlotIndex(left, namespaceKey) ?? Number.MAX_SAFE_INTEGER;
284
+ const rightIndex = parseFallbackSlotIndex(right, namespaceKey) ?? Number.MAX_SAFE_INTEGER;
285
+ return leftIndex - rightIndex;
286
+ });
287
+ for (const identityKey of sortedSlotKeys) {
288
+ if (leases[identityKey]) {
289
+ continue;
290
+ }
291
+ leases[identityKey] = {
292
+ namespace_key: namespaceKey,
293
+ pid: process.pid,
294
+ acquired_at: now,
295
+ updated_at: now,
296
+ };
297
+ claimedKey = identityKey;
298
+ claimedIdentity = identities[identityKey] ?? null;
299
+ state.agent_identity_leases = leases;
300
+ return state;
301
+ }
302
+ let nextSlotIndex = 0;
303
+ while (slotKeys.has(getFallbackSlotKey(namespaceKey, nextSlotIndex))) {
304
+ nextSlotIndex += 1;
305
+ }
306
+ claimedKey = getFallbackSlotKey(namespaceKey, nextSlotIndex);
307
+ leases[claimedKey] = {
308
+ namespace_key: namespaceKey,
309
+ pid: process.pid,
310
+ acquired_at: now,
311
+ updated_at: now,
312
+ };
313
+ claimedIdentity = identities[claimedKey] ?? null;
314
+ state.agent_identity_leases = leases;
315
+ return state;
316
+ });
317
+ return {
318
+ identityKey: claimedKey,
319
+ identity: claimedIdentity,
320
+ };
321
+ }
322
+ function ensureAgentIdentityKey(ideLabel) {
323
+ if (EXPLICIT_AGENT_IDENTITY_KEY) {
324
+ currentAgentIdentityKey = EXPLICIT_AGENT_IDENTITY_KEY;
325
+ currentAgentIdentity = getStoredAgentIdentity(currentAgentIdentityKey);
326
+ return currentAgentIdentityKey;
327
+ }
328
+ const claimed = claimFallbackIdentityKey(getFallbackAgentIdentityNamespaceKey(ideLabel));
329
+ currentAgentIdentityKey = claimed.identityKey;
330
+ currentAgentIdentity = claimed.identity;
331
+ return currentAgentIdentityKey;
332
+ }
197
333
  async function getAuthenticatedAgentDirectory() {
198
334
  try {
199
335
  const result = await apiCall("/agents/me");
@@ -213,9 +349,9 @@ async function getAuthenticatedAgentDirectory() {
213
349
  return null;
214
350
  }
215
351
  }
216
- function shouldReuseStoredIdentity(identity) {
352
+ function shouldReuseStoredIdentity(identity, identityKey) {
217
353
  return Boolean(identity &&
218
- identity.runtime_key === CURRENT_AGENT_IDENTITY_KEY &&
354
+ identity.runtime_key === identityKey &&
219
355
  identity.display_name?.trim() &&
220
356
  identity.ide_label?.trim() &&
221
357
  identity.owner_attribution?.trim());
@@ -237,42 +373,38 @@ function resolveExplicitAgentIdentity() {
237
373
  return null;
238
374
  }
239
375
  function pickLocalCodename(runtimeKey, offset = 0) {
240
- const index = (hashStringToIndex(runtimeKey, AGENT_CODENAMES.length) + offset) % AGENT_CODENAMES.length;
241
- const word = AGENT_CODENAMES[index];
242
- return {
243
- name: normalizeAgentBaseName(word),
244
- display_name: toTitleCaseCodename(word),
245
- };
376
+ const index = hashStringToIndex(runtimeKey, AGENT_CODENAME_SPACE) + offset;
377
+ return codenameFromIndex(index);
246
378
  }
247
- async function resolveAgentName(authAvailable) {
379
+ async function resolveAgentName(authAvailable, identityKey) {
248
380
  const explicit = resolveExplicitAgentIdentity();
249
381
  if (explicit) {
250
382
  return explicit;
251
383
  }
252
- if (shouldReuseStoredIdentity(currentAgentIdentity)) {
384
+ if (shouldReuseStoredIdentity(currentAgentIdentity, identityKey)) {
253
385
  return {
254
386
  name: currentAgentIdentity.name,
255
387
  display_name: currentAgentIdentity.display_name,
256
388
  };
257
389
  }
258
390
  if (!authAvailable) {
259
- return pickLocalCodename(CURRENT_AGENT_IDENTITY_KEY);
391
+ return pickLocalCodename(identityKey);
260
392
  }
261
393
  const directory = await getAuthenticatedAgentDirectory();
262
394
  const existingNames = new Set((directory?.agents ?? [])
263
395
  .map((agent) => normalizeAgentBaseName(agent.name || ""))
264
396
  .filter(Boolean));
265
- for (let offset = 0; offset < AGENT_CODENAMES.length; offset += 1) {
266
- const candidate = pickLocalCodename(CURRENT_AGENT_IDENTITY_KEY, offset);
397
+ for (let offset = 0; offset < AGENT_CODENAME_SPACE; offset += 1) {
398
+ const candidate = pickLocalCodename(identityKey, offset);
267
399
  if (!existingNames.has(candidate.name)) {
268
400
  return candidate;
269
401
  }
270
402
  }
271
403
  const fallbackHash = createHash("sha256")
272
- .update(CURRENT_AGENT_IDENTITY_KEY)
404
+ .update(identityKey)
273
405
  .digest("hex")
274
406
  .slice(0, 4);
275
- const fallback = pickLocalCodename(CURRENT_AGENT_IDENTITY_KEY);
407
+ const fallback = pickLocalCodename(identityKey);
276
408
  return {
277
409
  name: `${fallback.name}-${fallbackHash}`,
278
410
  display_name: `${fallback.display_name} ${fallbackHash.toUpperCase()}`,
@@ -384,8 +516,9 @@ async function ensureAgentIdentity() {
384
516
  const owner = await resolveOwnerContext();
385
517
  const authAvailable = Boolean(getLetagentsToken());
386
518
  const ideLabel = detectAgentIdeLabel();
519
+ const identityKey = ensureAgentIdentityKey(ideLabel);
387
520
  const ownerAttribution = formatOwnerAttribution(owner.label);
388
- const { name, display_name: displayName } = await resolveAgentName(authAvailable);
521
+ const { name, display_name: displayName } = await resolveAgentName(authAvailable, identityKey);
389
522
  const actorLabel = buildAgentActorLabel({
390
523
  display_name: displayName,
391
524
  owner_label: owner.label,
@@ -399,7 +532,7 @@ async function ensureAgentIdentity() {
399
532
  ide_label: ideLabel,
400
533
  actor_label: actorLabel,
401
534
  canonical_key: owner.login ? `${owner.login}/${name}` : null,
402
- runtime_key: CURRENT_AGENT_IDENTITY_KEY,
535
+ runtime_key: identityKey,
403
536
  source: "local",
404
537
  resolved_at: new Date().toISOString(),
405
538
  };
@@ -452,7 +585,7 @@ async function ensureAgentIdentity() {
452
585
  currentAgentIdentity = setStoredAgentIdentity({
453
586
  ...resolved,
454
587
  resolved_at: new Date().toISOString(),
455
- }, CURRENT_AGENT_IDENTITY_KEY);
588
+ }, identityKey);
456
589
  }
457
590
  return currentAgentIdentity ?? resolved;
458
591
  }
@@ -628,22 +761,32 @@ function toRoomState(input) {
628
761
  joined_via: input.joined_via,
629
762
  };
630
763
  }
764
+ function getCanonicalRoomWebUrl(roomId) {
765
+ return new URL(getCanonicalRoomWebPath(roomId), `${API_URL}/`).toString();
766
+ }
767
+ function withCanonicalRoomLink(roomId, payload) {
768
+ return {
769
+ ...payload,
770
+ room_path: getCanonicalRoomWebPath(roomId),
771
+ room_url: getCanonicalRoomWebUrl(roomId),
772
+ };
773
+ }
631
774
  function toPublicRoomState(state) {
632
775
  if (!state) {
633
776
  return null;
634
777
  }
635
- return {
778
+ return withCanonicalRoomLink(state.room_id, {
636
779
  room_id: state.room_id,
637
780
  code: state.code ?? null,
638
781
  display_name: state.display_name ?? null,
639
782
  joined_via: state.joined_via,
640
- };
783
+ });
641
784
  }
642
785
  function toPublicStoredRoomSession(session) {
643
786
  if (!session) {
644
787
  return null;
645
788
  }
646
- return {
789
+ return withCanonicalRoomLink(session.room_id, {
647
790
  room_id: session.room_id,
648
791
  code: session.code ?? null,
649
792
  display_name: session.display_name ?? null,
@@ -651,12 +794,12 @@ function toPublicStoredRoomSession(session) {
651
794
  joined_at: session.joined_at,
652
795
  last_seen_at: session.last_seen_at,
653
796
  last_message_id: session.last_message_id ?? null,
654
- };
797
+ });
655
798
  }
656
799
  function toPublicRoomResponse(response, fallbackRoomId) {
657
800
  const { id: _legacyId, project_id: _legacyProjectId, ...rest } = response;
658
801
  return {
659
- ...rest,
802
+ ...withCanonicalRoomLink(typeof rest.room_id === "string" ? rest.room_id : fallbackRoomId, rest),
660
803
  room_id: typeof rest.room_id === "string" ? rest.room_id : fallbackRoomId,
661
804
  };
662
805
  }
@@ -868,18 +1011,35 @@ server.resource("room_messages", new ResourceTemplate("letagents://rooms/{room_i
868
1011
  const normalizedRoomId = String(room_id);
869
1012
  const storedSession = getStoredRoomSession(normalizedRoomId) ??
870
1013
  (currentRoom?.room_id === normalizedRoomId ? getStoredCurrentRoom() : null);
871
- const result = await roomScopedApiCall({
872
- room_id: normalizedRoomId,
873
- project_id: storedSession?.project_id ?? null,
874
- room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages`,
875
- project_path: (projectId) => `/projects/${encodeURIComponent(projectId)}/messages`,
876
- });
1014
+ // Paginate through all pages to return full message history
1015
+ const allMessages = [];
1016
+ let afterCursor;
1017
+ for (;;) {
1018
+ const query = new URLSearchParams();
1019
+ if (afterCursor)
1020
+ query.set("after", afterCursor);
1021
+ const qs = query.toString();
1022
+ const result = await roomScopedApiCall({
1023
+ room_id: normalizedRoomId,
1024
+ project_id: storedSession?.project_id ?? null,
1025
+ room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}`,
1026
+ project_path: (projectId) => `/projects/${encodeURIComponent(projectId)}/messages${qs ? `?${qs}` : ""}`,
1027
+ });
1028
+ const msgs = result.messages ?? [];
1029
+ allMessages.push(...msgs);
1030
+ if (!result.has_more || msgs.length === 0)
1031
+ break;
1032
+ const lastMsg = msgs[msgs.length - 1];
1033
+ if (!lastMsg?.id)
1034
+ break;
1035
+ afterCursor = lastMsg.id;
1036
+ }
877
1037
  return {
878
1038
  contents: [
879
1039
  {
880
1040
  uri: uri.href,
881
1041
  mimeType: "application/json",
882
- text: JSON.stringify(result, null, 2),
1042
+ text: JSON.stringify({ messages: allMessages }, null, 2),
883
1043
  },
884
1044
  ],
885
1045
  };
@@ -972,7 +1132,7 @@ server.tool("get_current_room", "Get information about the currently joined room
972
1132
  ? {
973
1133
  connected: true,
974
1134
  ...toPublicRoomState(currentRoom),
975
- agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(CURRENT_AGENT_IDENTITY_KEY)),
1135
+ agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(currentAgentIdentityKey)),
976
1136
  auth: getStoredAuth()
977
1137
  ? {
978
1138
  source: process.env.LETAGENTS_TOKEN ? "env" : "local_state",
@@ -1163,15 +1323,31 @@ server.tool("get_board", "Get the current task board for the room. By default sh
1163
1323
  params.set("status", status);
1164
1324
  if (open_only !== false)
1165
1325
  params.set("open", "true");
1166
- const qs = params.toString();
1167
- const result = await roomScopedApiCall({
1168
- room_id: targetRoomId,
1169
- project_id: targetProjectId,
1170
- room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/tasks${qs ? `?${qs}` : ""}`,
1171
- project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/tasks${qs ? `?${qs}` : ""}`,
1172
- });
1326
+ // Paginate through all pages to return the full board
1327
+ const allTasks = [];
1328
+ let afterCursor;
1329
+ for (;;) {
1330
+ const pageParams = new URLSearchParams(params);
1331
+ if (afterCursor)
1332
+ pageParams.set("after", afterCursor);
1333
+ const qs = pageParams.toString();
1334
+ const result = await roomScopedApiCall({
1335
+ room_id: targetRoomId,
1336
+ project_id: targetProjectId,
1337
+ room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/tasks${qs ? `?${qs}` : ""}`,
1338
+ project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/tasks${qs ? `?${qs}` : ""}`,
1339
+ });
1340
+ const tasks = result.tasks ?? [];
1341
+ allTasks.push(...tasks);
1342
+ if (!result.has_more || tasks.length === 0)
1343
+ break;
1344
+ const lastTask = tasks[tasks.length - 1];
1345
+ if (!lastTask?.id)
1346
+ break;
1347
+ afterCursor = lastTask.id;
1348
+ }
1173
1349
  return {
1174
- content: [{ type: "text", text: JSON.stringify({ success: true, ...result }, null, 2) }],
1350
+ content: [{ type: "text", text: JSON.stringify({ success: true, tasks: allTasks }, null, 2) }],
1175
1351
  };
1176
1352
  });
1177
1353
  server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted' " +
@@ -1497,17 +1673,41 @@ server.tool("read_messages", "Read all messages from a Let Agents Chat room.", {
1497
1673
  }, async ({ room_id }) => {
1498
1674
  const targetRoomId = getTargetRoomId(room_id);
1499
1675
  const targetProjectId = getFallbackProjectId();
1500
- const result = await roomScopedApiCall({
1501
- room_id: targetRoomId,
1502
- project_id: targetProjectId,
1503
- room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages`,
1504
- project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages`,
1505
- });
1676
+ // Paginate through all pages to honor the "read all messages" contract
1677
+ const allMessages = [];
1678
+ let afterCursor;
1679
+ let roomIdFromResponse;
1680
+ for (;;) {
1681
+ const query = new URLSearchParams();
1682
+ if (afterCursor)
1683
+ query.set("after", afterCursor);
1684
+ const qs = query.toString();
1685
+ const result = await roomScopedApiCall({
1686
+ room_id: targetRoomId,
1687
+ project_id: targetProjectId,
1688
+ room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages${qs ? `?${qs}` : ""}`,
1689
+ project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages${qs ? `?${qs}` : ""}`,
1690
+ });
1691
+ roomIdFromResponse = roomIdFromResponse || result.room_id || result.project_id;
1692
+ const msgs = result.messages ?? [];
1693
+ allMessages.push(...msgs);
1694
+ if (!result.has_more || msgs.length === 0)
1695
+ break;
1696
+ // Use the last message ID as the cursor for the next page
1697
+ const lastMsg = msgs[msgs.length - 1];
1698
+ if (!lastMsg?.id)
1699
+ break;
1700
+ afterCursor = lastMsg.id;
1701
+ }
1702
+ const output = { messages: allMessages };
1703
+ if (roomIdFromResponse) {
1704
+ output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
1705
+ }
1506
1706
  return {
1507
1707
  content: [
1508
1708
  {
1509
1709
  type: "text",
1510
- text: JSON.stringify(result, null, 2),
1710
+ text: JSON.stringify(output, null, 2),
1511
1711
  },
1512
1712
  ],
1513
1713
  };
@@ -1535,21 +1735,49 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
1535
1735
  params.set("after", after_message_id);
1536
1736
  params.set("timeout", String(serverTimeout));
1537
1737
  const queryString = params.toString();
1538
- const result = await roomScopedApiCall({
1738
+ const firstResult = await roomScopedApiCall({
1539
1739
  room_id: targetRoomId,
1540
1740
  project_id: targetProjectId,
1541
1741
  room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages/poll?${queryString}`,
1542
1742
  project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages/poll?${queryString}`,
1543
1743
  options: { signal: AbortSignal.timeout(clientTimeout) },
1544
1744
  });
1745
+ const allMessages = [...(firstResult.messages ?? [])];
1746
+ const roomIdFromResponse = firstResult.room_id || firstResult.project_id;
1747
+ // If the immediate response has more pages, paginate through them
1748
+ if (firstResult.has_more && allMessages.length > 0) {
1749
+ let afterCursor = allMessages[allMessages.length - 1]?.id;
1750
+ while (afterCursor) {
1751
+ const pageParams = new URLSearchParams();
1752
+ pageParams.set("after", afterCursor);
1753
+ const qs = pageParams.toString();
1754
+ const page = await roomScopedApiCall({
1755
+ room_id: targetRoomId,
1756
+ project_id: targetProjectId,
1757
+ room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/messages?${qs}`,
1758
+ project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages?${qs}`,
1759
+ });
1760
+ const msgs = page.messages ?? [];
1761
+ allMessages.push(...msgs);
1762
+ if (!page.has_more || msgs.length === 0)
1763
+ break;
1764
+ afterCursor = msgs[msgs.length - 1]?.id;
1765
+ if (!afterCursor)
1766
+ break;
1767
+ }
1768
+ }
1769
+ const output = { messages: allMessages };
1770
+ if (roomIdFromResponse) {
1771
+ output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
1772
+ }
1545
1773
  if (targetRoomId) {
1546
- touchRoomSession(targetRoomId, getLastMessageId(result));
1774
+ touchRoomSession(targetRoomId, getLastMessageId(output));
1547
1775
  }
1548
1776
  return {
1549
1777
  content: [
1550
1778
  {
1551
1779
  type: "text",
1552
- text: JSON.stringify(result, null, 2),
1780
+ text: JSON.stringify(output, null, 2),
1553
1781
  },
1554
1782
  ],
1555
1783
  };
@@ -1592,7 +1820,7 @@ server.tool("get_onboarding_status", "Inspect local Let Agents MCP auth and room
1592
1820
  account: storedAuth?.account ?? null,
1593
1821
  token_expires_at: storedAuth?.expires_at ?? null,
1594
1822
  pending_device_auth: pendingAuth,
1595
- agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(CURRENT_AGENT_IDENTITY_KEY)),
1823
+ agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(currentAgentIdentityKey)),
1596
1824
  current_room: toPublicRoomState(currentRoom),
1597
1825
  saved_current_room: toPublicStoredRoomSession(savedCurrentRoom),
1598
1826
  detected_room_from_context: detectedRoom,
@@ -1817,7 +2045,7 @@ server.tool("resume_room_session", "Rejoin the last locally saved room context,
1817
2045
  rejoined_from_local_state: true,
1818
2046
  server_session_resumed: false,
1819
2047
  last_message_id_before_restart: savedRoom.last_message_id ?? null,
1820
- room: joined.room,
2048
+ room: toPublicRoomState(joined.room),
1821
2049
  agent_identity: toPublicAgentIdentity(agentIdentity),
1822
2050
  }, null, 2),
1823
2051
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letagents",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Let Agents Chat — MCP server for AI agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/mcp/server.js",