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.
- package/dist/mcp/local-state/local-chat.js +24 -371
- package/dist/mcp/local-state/storage.js +4 -48
- package/package.json +1 -1
- package/shared/claude-tool-operation.d.mts +1 -0
- package/shared/claude-tool-operation.mjs +8 -0
- package/shared/conversation-contracts.d.mts +62 -0
- package/shared/local-board-owner.d.mts +9 -0
- package/shared/local-board-owner.mjs +80 -0
- package/shared/local-task-revisions.d.mts +4 -0
- package/shared/local-task-revisions.mjs +97 -0
- package/shared/local-task-store.d.mts +43 -0
- package/shared/local-task-store.mjs +374 -0
- package/shared/local-worker-state-fence.d.mts +2 -0
- package/shared/local-worker-state-fence.mjs +69 -0
- package/shared/ui/ConversationIcon.vue +28 -0
- package/shared/ui/PrivateMessages.vue +1001 -0
- package/shared/ui/private-messages.css +817 -0
|
@@ -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
|
|
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
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
@@ -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>;
|