letagents 0.12.23 → 0.12.24

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,10 +1,14 @@
1
+ import { createLocalTaskStore } from "../../../shared/local-task-store.mjs";
2
+ import { requestLocalBoard } from "../../../shared/local-board-owner.mjs";
3
+ import { getLocalStatePath } from "./storage.js";
1
4
  import { captureLocalSupervisedRouting, ensureLocalSupervisedRoutingSchema, runLocalSupervisedMessageWrite } from "../../../shared/local-supervised-routing.mjs";
2
5
  import { createRequire } from "node:module";
6
+ import { ensureLocalTaskRevisionSchema, observeLocalTaskCommits } from "../../../shared/local-task-revisions.mjs";
3
7
  import { mkdir, readFile } from "node:fs/promises";
4
8
  import { homedir } from "node:os";
5
9
  import { dirname, join } from "node:path";
6
- import { withWorkerStateFence, currentWorkerCall } from "../worker-call-context.js";
7
- import { ensureLocalWorkLeaseSchema, readLocalWorkLeases, assertLocalTaskLeaseMutation, assertLocalWorkLeaseWorker, claimLocalWorkLease, changeLocalWorkLease } from "../../../shared/local-work-leases.mjs";
10
+ import { withWorkerStateFence, currentWorkerCall, assertWorkerConnection } from "../worker-call-context.js";
11
+ import { ensureLocalWorkLeaseSchema } from "../../../shared/local-work-leases.mjs";
8
12
  import { ensureLocalThreadRoutingProjectionSchemaAsync, getLocalThreadRoutingAgentKeysForRoots, projectLocalThreadRoutingMessage, runLocalSqliteWriteTransactionAsync, scheduleLocalThreadRoutingBackfill, } from "../../../shared/sqlite-thread-routing.mjs";
9
13
  import { MESSAGE_SENDER_MAX_CODE_POINTS, MESSAGE_SENDER_MAX_UTF8_BYTES, POSTGRES_INTEGER_MAX, isMessageSenderWithinBounds, parseAccountAgentRoutingEnvelope, parsePositivePgIntegerScopedId, } from "../../../shared/message-contracts.mjs";
10
14
  const localImportedRoutingAuthority = Symbol("localImportedRoutingAuthority");
@@ -26,17 +30,6 @@ export function setLocalChatInitializationObserversForTest(observers) {
26
30
  databaseInitializationObserverForTest = observers?.database ?? null;
27
31
  schemaInitializationObserverForTest = observers?.schema ?? null;
28
32
  }
29
- const validLocalTaskTransitions = {
30
- proposed: ["accepted", "cancelled"],
31
- accepted: ["assigned", "cancelled"],
32
- assigned: ["in_progress", "in_review", "cancelled"],
33
- in_progress: ["blocked", "in_review", "done", "cancelled"],
34
- blocked: ["in_progress", "in_review", "cancelled"],
35
- in_review: ["merged", "in_progress", "blocked", "done", "cancelled"],
36
- merged: ["done", "accepted"],
37
- done: ["accepted"],
38
- cancelled: ["accepted"],
39
- };
40
33
  function formatMessageId(number) {
41
34
  return `msg_${number}`;
42
35
  }
@@ -171,7 +164,7 @@ async function initializeDb() {
171
164
  await mkdir(dirname(localChatDatabasePath), { recursive: true });
172
165
  const { DatabaseSync } = require("node:sqlite");
173
166
  databaseInitializationObserverForTest?.();
174
- const database = new DatabaseSync(localChatDatabasePath);
167
+ const database = observeLocalTaskCommits(new DatabaseSync(localChatDatabasePath));
175
168
  try {
176
169
  schemaInitializationObserverForTest?.();
177
170
  database.exec("PRAGMA journal_mode = WAL");
@@ -289,6 +282,7 @@ async function initializeDb() {
289
282
  addColumnIfMissing(database, "local_tasks", "review_agent_session_id", "TEXT");
290
283
  addColumnIfMissing(database, "local_tasks", "review_updated_at", "TEXT");
291
284
  ensureLocalWorkLeaseSchema(database);
285
+ ensureLocalTaskRevisionSchema(database);
292
286
  database.exec(`
293
287
  CREATE UNIQUE INDEX IF NOT EXISTS local_chat_messages_sync_key_idx
294
288
  ON local_chat_messages (room_id, sync_key)
@@ -331,16 +325,6 @@ function addColumnIfMissing(database, tableName, columnName, definition) {
331
325
  }
332
326
  }
333
327
  }
334
- function resolveLocalTaskStatus(fromStatus, toStatus) {
335
- if (typeof toStatus !== "string" || !toStatus.trim())
336
- return fromStatus;
337
- const nextStatus = toStatus.trim();
338
- if (!validLocalTaskTransitions[fromStatus]?.includes(nextStatus)) {
339
- throw new Error(`Invalid transition: ${fromStatus} -> ${nextStatus}. ` +
340
- `Allowed: ${validLocalTaskTransitions[fromStatus]?.join(", ") || "none"}`);
341
- }
342
- return nextStatus;
343
- }
344
328
  async function hydrateRows(database, rows) {
345
329
  if (rows.length === 0)
346
330
  return [];
@@ -641,352 +625,21 @@ export async function waitForLocalChatMessages(roomId, options) {
641
625
  await new Promise((resolve) => setTimeout(resolve, Math.min(500, deadline - Date.now())));
642
626
  }
643
627
  }
644
- function allocateLocalTaskId(database, roomId) {
645
- database
646
- .prepare(`
647
- INSERT INTO local_task_room_sequences (room_id, next_number)
648
- SELECT ?, COALESCE(MAX(CAST(SUBSTR(task_id, 6) AS INTEGER)), 0) + 1
649
- FROM local_tasks
650
- WHERE room_id = ? AND task_id GLOB 'task_[0-9]*'
651
- ON CONFLICT(room_id) DO NOTHING
652
- `)
653
- .run(roomId, roomId);
654
- const row = database
655
- .prepare("SELECT next_number FROM local_task_room_sequences WHERE room_id = ?")
656
- .get(roomId);
657
- const number = Number(row?.next_number || 0);
658
- if (!Number.isInteger(number) || number <= 0) {
659
- throw new Error("Local task sequence could not be allocated.");
660
- }
661
- database
662
- .prepare("UPDATE local_task_room_sequences SET next_number = next_number + 1 WHERE room_id = ?")
663
- .run(roomId);
664
- return `task_${number}`;
665
- }
666
- function mapTaskRow(row, database) {
667
- const reviewLeaseId = typeof row.review_lease_id === "string" && row.review_lease_id.trim()
668
- ? row.review_lease_id
669
- : null;
670
- return {
671
- id: String(row.task_id || ""),
672
- title: String(row.title || ""),
673
- description: typeof row.description === "string" ? row.description : null,
674
- status: String(row.status || "proposed"),
675
- assignee: typeof row.assignee === "string" ? row.assignee : null,
676
- assignee_agent_key: typeof row.assignee_agent_key === "string" ? row.assignee_agent_key : null,
677
- assignee_agent_instance_id: typeof row.assignee_agent_instance_id === "string" ? row.assignee_agent_instance_id : null,
678
- assignee_agent_session_id: typeof row.assignee_agent_session_id === "string" ? row.assignee_agent_session_id : null,
679
- created_by: typeof row.created_by === "string" ? row.created_by : null,
680
- pr_url: typeof row.pr_url === "string" ? row.pr_url : null,
681
- workflow_artifacts: parseJsonArray(row.workflow_artifacts_json, []),
682
- workflow_refs: parseJsonArray(row.workflow_refs_json, []),
683
- active_leases: [...readLocalWorkLeases(database, String(row.room_id), String(row.task_id))
684
- .map(lease => ({ ...lease, holder_label: lease.actor_label })), ...(reviewLeaseId
685
- ? [
686
- {
687
- id: reviewLeaseId,
688
- kind: "review",
689
- holder_label: typeof row.review_holder_label === "string"
690
- ? row.review_holder_label
691
- : null,
692
- agent_key: typeof row.review_agent_key === "string"
693
- ? row.review_agent_key
694
- : null,
695
- agent_session_id: typeof row.review_agent_session_id === "string"
696
- ? row.review_agent_session_id
697
- : null,
698
- status: "active",
699
- updated_at: typeof row.review_updated_at === "string"
700
- ? row.review_updated_at
701
- : null,
702
- },
703
- ]
704
- : [])],
705
- created_at: String(row.created_at || ""),
706
- updated_at: String(row.updated_at || ""),
707
- };
708
- }
709
- function parseJsonArray(value, fallback) {
710
- if (typeof value !== "string" || !value.trim())
711
- return fallback;
712
- try {
713
- const parsed = JSON.parse(value);
714
- return Array.isArray(parsed) ? parsed : fallback;
715
- }
716
- catch {
717
- return fallback;
718
- }
719
- }
720
- export async function addLocalTask(roomId, input) {
721
- const trimmedRoomId = roomId.trim();
722
- const title = input.title.trim();
723
- if (!trimmedRoomId)
724
- throw new Error("No room is available for this request.");
725
- if (!title)
726
- throw new Error("Task title is required.");
727
- const database = await getDb();
728
- const now = new Date().toISOString();
729
- let taskId = "";
730
- await runLocalSqliteWriteTransactionAsync(database, () => withWorkerStateFence(() => {
731
- taskId = allocateLocalTaskId(database, trimmedRoomId);
732
- database
733
- .prepare(`
734
- INSERT INTO local_tasks (
735
- room_id, task_id, title, description, status, assignee, assignee_agent_key,
736
- assignee_agent_instance_id, assignee_agent_session_id,
737
- created_by, pr_url, workflow_artifacts_json, workflow_refs_json,
738
- synced_cloud_id, sync_key, sync_started_at, sync_dirty, created_at, updated_at
739
- )
740
- VALUES (?, ?, ?, ?, 'proposed', NULL, NULL, NULL, NULL, ?, NULL, NULL, NULL, NULL, ?, NULL, 1, ?, ?)
741
- `)
742
- .run(trimmedRoomId, taskId, title, input.description?.trim() || null, input.created_by || "agent", `local-task:${trimmedRoomId}:${taskId}`, now, now);
743
- }));
744
- const task = await getLocalTask(trimmedRoomId, taskId);
745
- if (!task)
746
- throw new Error("Local task could not be created.");
747
- return task;
748
- }
749
- export async function listLocalTasks(roomId, options = {}) {
750
- const clauses = ["room_id = ?"];
751
- const params = [roomId];
752
- if (options.status) {
753
- clauses.push("status = ?");
754
- params.push(options.status);
755
- }
756
- if (options.openOnly !== false) {
757
- clauses.push("status NOT IN ('done', 'cancelled')");
758
- }
759
- const database = await getDb();
760
- const tasks = database
761
- .prepare(`
762
- SELECT *
763
- FROM local_tasks
764
- WHERE ${clauses.join(" AND ")}
765
- ORDER BY created_at ASC
766
- `)
767
- .all(...params)
768
- .map(row => mapTaskRow(row, database));
769
- return { tasks, has_more: false };
770
- }
771
- export async function listLocalActiveTaskOwnerLeases(roomId) {
772
- const database = await getDb();
773
- const rows = database
774
- .prepare(`
775
- SELECT
776
- MIN(COALESCE(NULLIF(TRIM(assignee), ''), assignee_agent_key)) AS actor_label,
777
- assignee_agent_key,
778
- assignee_agent_instance_id,
779
- assignee_agent_session_id
780
- FROM local_tasks
781
- WHERE room_id = ?
782
- AND status IN ('assigned', 'in_progress', 'blocked', 'in_review')
783
- AND assignee_agent_key IS NOT NULL
784
- AND TRIM(assignee_agent_key) <> ''
785
- GROUP BY CASE
786
- WHEN NULLIF(TRIM(assignee_agent_session_id), '') IS NOT NULL
787
- THEN 'session:' || TRIM(assignee_agent_session_id)
788
- WHEN NULLIF(TRIM(assignee_agent_instance_id), '') IS NOT NULL
789
- THEN 'instance:' || TRIM(assignee_agent_key) || ':' || TRIM(assignee_agent_instance_id)
790
- ELSE 'agent:' || TRIM(assignee_agent_key)
791
- END
792
- ORDER BY MIN(created_at) ASC
793
- LIMIT 2
794
- `)
795
- .all(roomId);
796
- return rows.map((row) => ({
797
- kind: "work",
798
- status: "active",
799
- actor_label: String(row.actor_label || row.assignee_agent_key || ""),
800
- agent_key: String(row.assignee_agent_key || ""),
801
- agent_instance_id: typeof row.assignee_agent_instance_id === "string"
802
- ? row.assignee_agent_instance_id
803
- : null,
804
- agent_session_id: typeof row.assignee_agent_session_id === "string"
805
- ? row.assignee_agent_session_id
806
- : null,
807
- }));
808
- }
809
- export async function getLocalTask(roomId, taskId) {
810
- const database = await getDb();
811
- const row = database
812
- .prepare("SELECT * FROM local_tasks WHERE room_id = ? AND task_id = ?")
813
- .get(roomId, taskId);
814
- return row ? mapTaskRow(row, database) : null;
815
- }
816
- export async function updateLocalTask(roomId, taskId, patch) {
817
- const database = await getDb();
818
- let observed = readLocalWorkLeases(database, roomId, taskId)[0] ?? null;
819
- return runLocalSqliteWriteTransactionAsync(database, () => withWorkerStateFence(() => {
820
- let row = database.prepare("SELECT * FROM local_tasks WHERE room_id=? AND task_id=?").get(roomId, taskId);
821
- if (!row)
822
- throw new Error("Task not found.");
823
- if (patch.expected_no_work_lease === true && readLocalWorkLeases(database, roomId, taskId).length) {
824
- throw new Error("The task lease changed. Refresh the task before trying again.");
825
- }
826
- const caller = currentWorkerCall();
827
- const worker = caller?.agent_key ? {
828
- agent_key: caller.agent_key, session_id: caller.session_id, actor_label: caller.actor_label || caller.agent_key,
829
- agent_instance_id: caller.agent_instance_id,
830
- } : null;
831
- const supervised = worker ? assertLocalWorkLeaseWorker(database, roomId, worker) : false;
832
- if (supervised && worker && patch.status === "assigned") {
833
- if ((patch.assignee_agent_key != null && patch.assignee_agent_key !== worker.agent_key)
834
- || (patch.assignee != null && patch.assignee !== worker.actor_label)) {
835
- throw new Error("Use handoff_task_lease to assign work to another worker.");
836
- }
837
- observed = claimLocalWorkLease(database, roomId, taskId, worker);
838
- row = database.prepare("SELECT * FROM local_tasks WHERE room_id=? AND task_id=?").get(roomId, taskId);
839
- }
840
- if (worker) {
841
- assertLocalTaskLeaseMutation(database, row, worker, observed);
842
- if ((supervised || observed) && ((patch.assignee !== undefined && patch.assignee !== row.assignee)
843
- || (patch.assignee_agent_key !== undefined && patch.assignee_agent_key !== row.assignee_agent_key)
844
- || (patch.assignee_agent_session_id !== undefined && patch.assignee_agent_session_id !== row.assignee_agent_session_id)
845
- || (patch.assignee_agent_key !== undefined && patch.agent_session_id !== undefined && patch.agent_session_id !== row.assignee_agent_session_id)
846
- || (patch.assignee_agent_instance_id !== undefined && patch.assignee_agent_instance_id !== row.assignee_agent_instance_id)
847
- || (patch.assignee_agent_key !== undefined && patch.actor_instance_id !== undefined && patch.actor_instance_id !== row.assignee_agent_instance_id))) {
848
- throw new Error("Use claim_task or handoff_task_lease to change task ownership.");
849
- }
850
- }
851
- else if (observed || readLocalWorkLeases(database, roomId, taskId).length) {
852
- throw new Error("A registered owning worker is required to update this leased task.");
853
- }
854
- const current = mapTaskRow(row, database);
855
- const nextStatus = supervised && patch.status === "assigned" ? current.status
856
- : patch.skip_transition_validation === true
857
- ? typeof patch.status === "string" && patch.status.trim()
858
- ? patch.status.trim()
859
- : current.status
860
- : resolveLocalTaskStatus(current.status, patch.status);
861
- const assigneeAgentKey = patch.assignee_agent_key === undefined
862
- ? current.assignee_agent_key
863
- : typeof patch.assignee_agent_key === "string"
864
- ? patch.assignee_agent_key
865
- : null;
866
- const assigneeAgentInstanceId = patch.assignee_agent_key === undefined || (worker && patch.assignee_agent_key === current.assignee_agent_key)
867
- ? current.assignee_agent_instance_id
868
- : assigneeAgentKey && typeof patch.assignee_agent_instance_id === "string"
869
- ? patch.assignee_agent_instance_id
870
- : assigneeAgentKey && typeof patch.actor_instance_id === "string"
871
- ? patch.actor_instance_id
872
- : null;
873
- const assigneeAgentSessionId = patch.assignee_agent_key === undefined || (worker && patch.assignee_agent_key === current.assignee_agent_key)
874
- ? current.assignee_agent_session_id
875
- : assigneeAgentKey && typeof patch.assignee_agent_session_id === "string"
876
- ? patch.assignee_agent_session_id
877
- : assigneeAgentKey && typeof patch.agent_session_id === "string"
878
- ? patch.agent_session_id
879
- : null;
880
- const workflowArtifacts = patch.workflow_artifacts === undefined
881
- ? JSON.stringify(current.workflow_artifacts)
882
- : JSON.stringify(Array.isArray(patch.workflow_artifacts) ? patch.workflow_artifacts : []);
883
- const now = new Date().toISOString();
884
- database
885
- .prepare(`
886
- UPDATE local_tasks
887
- SET status = ?,
888
- assignee = ?,
889
- assignee_agent_key = ?,
890
- assignee_agent_instance_id = ?,
891
- assignee_agent_session_id = ?,
892
- pr_url = ?,
893
- workflow_artifacts_json = ?,
894
- sync_dirty = 1,
895
- updated_at = ?
896
- WHERE room_id = ? AND task_id = ?
897
- `)
898
- .run(nextStatus, patch.assignee === undefined ? current.assignee : patch.assignee || null, assigneeAgentKey, assigneeAgentInstanceId, assigneeAgentSessionId, patch.pr_url === undefined ? current.pr_url : patch.pr_url || null, workflowArtifacts, now, roomId, taskId);
899
- return mapTaskRow(database.prepare("SELECT * FROM local_tasks WHERE room_id=? AND task_id=?").get(roomId, taskId), database);
900
- }));
901
- }
902
- export async function changeLocalTaskWorkLease(roomId, taskId, input) {
903
- const database = await getDb();
904
- const observed = readLocalWorkLeases(database, roomId, taskId)[0];
905
- if (!observed)
906
- throw new Error("This task has no active work lease.");
907
- return runLocalSqliteWriteTransactionAsync(database, () => withWorkerStateFence(() => {
908
- const worker = currentWorkerCall();
909
- if (!worker?.agent_key)
910
- throw new Error("A registered owning worker is required to change this work lease.");
911
- const result = changeLocalWorkLease(database, roomId, taskId, {
912
- ...input, lease_id: input.lease_id ?? observed.id, epoch: input.epoch ?? observed.epoch,
913
- }, {
914
- agent_key: worker.agent_key, session_id: worker.session_id, actor_label: worker.actor_label || worker.agent_key,
915
- });
916
- const task = mapTaskRow(database.prepare("SELECT * FROM local_tasks WHERE room_id=? AND task_id=?").get(roomId, taskId), database);
917
- return { action: input.action, task, ...result };
918
- }));
919
- }
920
- export async function claimLocalTaskReviewLease(roomId, taskId, input) {
921
- const current = await getLocalTask(roomId, taskId);
922
- if (!current)
923
- throw new Error("Task not found.");
924
- const actorKey = input.agent_key?.trim() || null;
925
- if (actorKey &&
926
- current.assignee_agent_key &&
927
- current.assignee_agent_key === actorKey) {
928
- throw new Error("A worker holding the task cannot also claim review authority.");
929
- }
930
- const currentReviewLease = current.active_leases?.find((lease) => lease.kind === "review");
931
- if (currentReviewLease?.agent_key &&
932
- actorKey &&
933
- currentReviewLease.agent_key !== actorKey) {
934
- throw new Error("Review authority is already held by another local reviewer.");
935
- }
936
- const database = await getDb();
937
- const now = new Date().toISOString();
938
- const leaseId = currentReviewLease?.id || `local_review_${Date.now()}_${Math.random().toString(36).slice(2)}`;
939
- await runLocalSqliteWriteTransactionAsync(database, () => withWorkerStateFence(() => database
940
- .prepare(`
941
- UPDATE local_tasks
942
- SET review_lease_id = ?,
943
- review_holder_label = ?,
944
- review_agent_key = ?,
945
- review_agent_session_id = ?,
946
- review_updated_at = ?,
947
- updated_at = ?
948
- WHERE room_id = ? AND task_id = ?
949
- `)
950
- .run(leaseId, input.holder_label?.trim() || actorKey || "Local reviewer", actorKey, input.agent_session_id?.trim() || null, now, now, roomId, taskId)));
951
- const task = await getLocalTask(roomId, taskId);
952
- const lease = task?.active_leases?.find((entry) => entry.id === leaseId);
953
- if (!task || !lease)
954
- throw new Error("Review authority could not be claimed.");
955
- return { task, lease };
956
- }
957
- export async function releaseLocalTaskReviewLease(roomId, taskId, input = {}) {
958
- const current = await getLocalTask(roomId, taskId);
959
- if (!current)
960
- throw new Error("Task not found.");
961
- const currentReviewLease = current.active_leases?.find((lease) => lease.kind === "review") || null;
962
- if (input.lease_id &&
963
- currentReviewLease &&
964
- input.lease_id !== currentReviewLease.id) {
965
- throw new Error("Review lease id did not match the active local review authority.");
966
- }
967
- const database = await getDb();
968
- const now = new Date().toISOString();
969
- await runLocalSqliteWriteTransactionAsync(database, () => withWorkerStateFence(() => database
970
- .prepare(`
971
- UPDATE local_tasks
972
- SET review_lease_id = NULL,
973
- review_holder_label = NULL,
974
- review_agent_key = NULL,
975
- review_agent_session_id = NULL,
976
- review_updated_at = NULL,
977
- updated_at = ?
978
- WHERE room_id = ? AND task_id = ?
979
- `)
980
- .run(now, roomId, taskId)));
981
- const task = await getLocalTask(roomId, taskId);
982
- if (!task)
983
- throw new Error("Task not found.");
984
- return {
985
- task,
986
- released_lease: currentReviewLease
987
- ? { ...currentReviewLease, status: "released" }
988
- : null,
989
- };
990
- }
628
+ const localTasks = createLocalTaskStore({ getDb, withWorkerStateFence, currentWorkerCall });
629
+ export const listLocalTasks = localTasks.listLocalTasks;
630
+ export const listLocalActiveTaskOwnerLeases = localTasks.listLocalActiveTaskOwnerLeases;
631
+ export const getLocalTask = localTasks.getLocalTask;
632
+ async function mutateLocalTask(operation, args) {
633
+ assertWorkerConnection();
634
+ const result = await requestLocalBoard("mutate", { domain: "mcp", operation, args,
635
+ databasePath: localChatDatabasePath, statePath: getLocalStatePath(), worker: currentWorkerCall() ?? null });
636
+ assertWorkerConnection();
637
+ return result;
638
+ }
639
+ export function addLocalTask(...args) { return mutateLocalTask("addLocalTask", args); }
640
+ export function updateLocalTask(...args) { return mutateLocalTask("updateLocalTask", args); }
641
+ export function changeLocalTaskWorkLease(...args) { return mutateLocalTask("changeLocalTaskWorkLease", args); }
642
+ export function claimLocalTaskReviewLease(...args) { return mutateLocalTask("claimLocalTaskReviewLease", args); }
643
+ export function releaseLocalTaskReviewLease(...args) { return mutateLocalTask("releaseLocalTaskReviewLease", args); }
991
644
  /** Shared local collaboration records use the same database as room chat. */
992
645
  export async function getLocalKnowledgeDatabase() { return getDb(); }
@@ -1,12 +1,9 @@
1
- import { closeSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "fs";
1
+ import { withStateFileLock } from "../../../shared/local-worker-state-fence.mjs";
2
+ import { readFileSync, renameSync, rmSync, writeFileSync, } from "fs";
2
3
  import { randomBytes } from "crypto";
3
4
  import { homedir } from "os";
4
- import { dirname, join } from "path";
5
+ import { join } from "path";
5
6
  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));
10
7
  export function getLocalStatePath() {
11
8
  return process.env.LETAGENTS_STATE_PATH || DEFAULT_STATE_PATH;
12
9
  }
@@ -37,11 +34,6 @@ export function readLocalState() {
37
34
  export function withLocalStateReadLock(callback) {
38
35
  return withStateLock((statePath) => callback(readLocalStateSnapshotFromPath(statePath)));
39
36
  }
40
- function sleepSync(ms) {
41
- if (ms > 0) {
42
- Atomics.wait(STATE_LOCK_SLEEP_BUFFER, 0, 0, ms);
43
- }
44
- }
45
37
  function writeLocalStateUnlocked(statePath, state) {
46
38
  const tempPath = `${statePath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
47
39
  try {
@@ -53,43 +45,7 @@ function writeLocalStateUnlocked(statePath, state) {
53
45
  }
54
46
  }
55
47
  function withStateLock(callback) {
56
- const statePath = getLocalStatePath();
57
- mkdirSync(dirname(statePath), { recursive: true });
58
- const lockPath = `${statePath}.lock`;
59
- const startedAt = Date.now();
60
- while (true) {
61
- let lockFd = null;
62
- try {
63
- lockFd = openSync(lockPath, "wx");
64
- return callback(statePath);
65
- }
66
- catch (error) {
67
- const err = error;
68
- if (err.code !== "EEXIST") {
69
- throw error;
70
- }
71
- try {
72
- const stats = statSync(lockPath);
73
- if (Date.now() - stats.mtimeMs > STATE_LOCK_STALE_MS) {
74
- rmSync(lockPath, { force: true });
75
- continue;
76
- }
77
- }
78
- catch {
79
- continue;
80
- }
81
- if (Date.now() - startedAt >= STATE_LOCK_TIMEOUT_MS) {
82
- throw new Error(`Timed out acquiring local state lock at ${lockPath}`);
83
- }
84
- sleepSync(STATE_LOCK_WAIT_MS);
85
- }
86
- finally {
87
- if (lockFd !== null) {
88
- closeSync(lockFd);
89
- rmSync(lockPath, { force: true });
90
- }
91
- }
92
- }
48
+ return withStateFileLock(getLocalStatePath(), callback);
93
49
  }
94
50
  export function writeLocalState(state) {
95
51
  withStateLock((statePath) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letagents",
3
- "version": "0.12.23",
3
+ "version": "0.12.24",
4
4
  "description": "Let Agents Chat \u2014 MCP server for AI agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/mcp/server.js",
@@ -0,0 +1 @@
1
+ export function claudeToolOperation(name: string): "command" | "file_read" | "file_change" | "network" | "question" | "other";
@@ -0,0 +1,8 @@
1
+ /** Match approval requests to the operation emitted by the native adapter. */
2
+ export function claudeToolOperation(name) {
3
+ return name === "Bash" ? "command"
4
+ : ["Read", "Glob", "Grep"].includes(name) ? "file_read"
5
+ : ["Write", "Edit", "MultiEdit", "NotebookEdit"].includes(name) ? "file_change"
6
+ : ["WebFetch", "WebSearch"].includes(name) ? "network"
7
+ : name === "AskUserQuestion" ? "question" : "other";
8
+ }
@@ -0,0 +1,62 @@
1
+ export interface ConversationPerson {
2
+ id: string;
3
+ login: string;
4
+ display_name: string | null;
5
+ avatar_url: string | null;
6
+ }
7
+ export interface ConversationMember extends ConversationPerson {
8
+ accepted: boolean;
9
+ blocked: boolean;
10
+ }
11
+ export interface ConversationMessage {
12
+ conversation_id: string;
13
+ number: number;
14
+ sender_account_id: string;
15
+ client_message_id: string;
16
+ text: string;
17
+ created_at: string;
18
+ }
19
+ export interface Conversation {
20
+ id: string;
21
+ created_by: string;
22
+ members: ConversationMember[];
23
+ accepted: boolean;
24
+ muted: boolean;
25
+ archived: boolean;
26
+ can_send: boolean;
27
+ unread_count: number;
28
+ last_message: ConversationMessage | null;
29
+ updated_at: string;
30
+ }
31
+ export interface ConversationList {
32
+ conversations: Conversation[];
33
+ version: string;
34
+ }
35
+ export interface ConversationApi {
36
+ list(): Promise<ConversationList>;
37
+ people(query: string): Promise<{ people: ConversationPerson[] }>;
38
+ create(
39
+ accountIds: string[],
40
+ fromConversationId?: string,
41
+ ): Promise<{ conversation_id: string }>;
42
+ messages(
43
+ id: string,
44
+ cursor?: { before?: number; after?: number },
45
+ ): Promise<{ messages: ConversationMessage[]; has_more: boolean }>;
46
+ send(
47
+ id: string,
48
+ text: string,
49
+ clientMessageId: string,
50
+ ): Promise<ConversationMessage>;
51
+ update(
52
+ id: string,
53
+ changes: {
54
+ accept?: boolean;
55
+ last_read_number?: number;
56
+ muted?: boolean;
57
+ archived?: boolean;
58
+ },
59
+ ): Promise<void>;
60
+ block(accountId: string, blocked: boolean): Promise<void>;
61
+ changes(after: string): Promise<{ version: string }>;
62
+ }
@@ -0,0 +1,9 @@
1
+ export const LOCAL_BOARD_MAX_FRAME_BYTES: number;
2
+ export function runWithLocalBoardOwner<T>(assertCurrent: () => void, callback: () => T): T;
3
+ export function registerLocalBoardOwner(assertCurrent: () => void): void;
4
+ export function isLocalBoardOwner(): boolean;
5
+ export function notifyLocalBoardChanged(roomId: string): void;
6
+ export function onLocalBoardChanged(listener: (roomId: string) => void): () => void;
7
+ export function requestLocalBoard<T = unknown>(method: "mutate" | "watch", params: unknown, options?: {
8
+ signal?: AbortSignal; socketPath?: string; timeoutMs?: number;
9
+ }): Promise<T>;