letagents 0.12.20 → 0.12.22

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,8 +1,10 @@
1
+ import { captureLocalSupervisedRouting, ensureLocalSupervisedRoutingSchema, runLocalSupervisedMessageWrite } from "../../../shared/local-supervised-routing.mjs";
1
2
  import { createRequire } from "node:module";
2
3
  import { mkdir, readFile } from "node:fs/promises";
3
4
  import { homedir } from "node:os";
4
5
  import { dirname, join } from "node:path";
5
- import { withWorkerStateFence } from "../worker-call-context.js";
6
+ import { withWorkerStateFence, currentWorkerCall } from "../worker-call-context.js";
7
+ import { ensureLocalWorkLeaseSchema, readLocalWorkLeases, assertLocalTaskLeaseMutation, assertLocalWorkLeaseWorker, claimLocalWorkLease, changeLocalWorkLease } from "../../../shared/local-work-leases.mjs";
6
8
  import { ensureLocalThreadRoutingProjectionSchemaAsync, getLocalThreadRoutingAgentKeysForRoots, projectLocalThreadRoutingMessage, runLocalSqliteWriteTransactionAsync, scheduleLocalThreadRoutingBackfill, } from "../../../shared/sqlite-thread-routing.mjs";
7
9
  import { MESSAGE_SENDER_MAX_CODE_POINTS, MESSAGE_SENDER_MAX_UTF8_BYTES, POSTGRES_INTEGER_MAX, isMessageSenderWithinBounds, parseAccountAgentRoutingEnvelope, parsePositivePgIntegerScopedId, } from "../../../shared/message-contracts.mjs";
8
10
  const localImportedRoutingAuthority = Symbol("localImportedRoutingAuthority");
@@ -176,6 +178,7 @@ async function initializeDb() {
176
178
  database.exec("PRAGMA foreign_keys = ON");
177
179
  database.exec("PRAGMA busy_timeout = 5000");
178
180
  await runLocalSqliteWriteTransactionAsync(database, () => {
181
+ ensureLocalSupervisedRoutingSchema(database);
179
182
  database.exec(`
180
183
  CREATE TABLE IF NOT EXISTS local_chat_room_sequences (
181
184
  room_id TEXT PRIMARY KEY,
@@ -285,6 +288,7 @@ async function initializeDb() {
285
288
  addColumnIfMissing(database, "local_tasks", "review_agent_key", "TEXT");
286
289
  addColumnIfMissing(database, "local_tasks", "review_agent_session_id", "TEXT");
287
290
  addColumnIfMissing(database, "local_tasks", "review_updated_at", "TEXT");
291
+ ensureLocalWorkLeaseSchema(database);
288
292
  database.exec(`
289
293
  CREATE UNIQUE INDEX IF NOT EXISTS local_chat_messages_sync_key_idx
290
294
  ON local_chat_messages (room_id, sync_key)
@@ -527,7 +531,7 @@ export async function addLocalChatMessage(roomId, input) {
527
531
  }
528
532
  }
529
533
  const timestamp = new Date().toISOString();
530
- const row = await runLocalSqliteWriteTransactionAsync(database, () => withWorkerStateFence(() => {
534
+ const row = await runLocalSupervisedMessageWrite(database, trimmedRoomId, threadRootNumber, () => withWorkerStateFence(() => {
531
535
  const number = allocateLocalMessageNumber(database, trimmedRoomId);
532
536
  const insertedRow = {
533
537
  room_id: trimmedRoomId,
@@ -559,6 +563,7 @@ export async function addLocalChatMessage(roomId, input) {
559
563
  `)
560
564
  .run(insertedRow.room_id, insertedRow.number, insertedRow.reply_to_number, insertedRow.thread_root_number, insertedRow.sender, insertedRow.text, insertedRow.agent_prompt_kind, insertedRow.source, insertedRow.publisher_agent_key, insertedRow.publisher_agent_session_id, insertedRow.timestamp);
561
565
  projectLocalThreadRoutingMessage(database, insertedRow);
566
+ captureLocalSupervisedRouting(database, insertedRow);
562
567
  return insertedRow;
563
568
  }));
564
569
  return {
@@ -658,7 +663,7 @@ function allocateLocalTaskId(database, roomId) {
658
663
  .run(roomId);
659
664
  return `task_${number}`;
660
665
  }
661
- function mapTaskRow(row) {
666
+ function mapTaskRow(row, database) {
662
667
  const reviewLeaseId = typeof row.review_lease_id === "string" && row.review_lease_id.trim()
663
668
  ? row.review_lease_id
664
669
  : null;
@@ -675,27 +680,28 @@ function mapTaskRow(row) {
675
680
  pr_url: typeof row.pr_url === "string" ? row.pr_url : null,
676
681
  workflow_artifacts: parseJsonArray(row.workflow_artifacts_json, []),
677
682
  workflow_refs: parseJsonArray(row.workflow_refs_json, []),
678
- active_leases: reviewLeaseId
679
- ? [
680
- {
681
- id: reviewLeaseId,
682
- kind: "review",
683
- holder_label: typeof row.review_holder_label === "string"
684
- ? row.review_holder_label
685
- : null,
686
- agent_key: typeof row.review_agent_key === "string"
687
- ? row.review_agent_key
688
- : null,
689
- agent_session_id: typeof row.review_agent_session_id === "string"
690
- ? row.review_agent_session_id
691
- : null,
692
- status: "active",
693
- updated_at: typeof row.review_updated_at === "string"
694
- ? row.review_updated_at
695
- : null,
696
- },
697
- ]
698
- : [],
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
+ : [])],
699
705
  created_at: String(row.created_at || ""),
700
706
  updated_at: String(row.updated_at || ""),
701
707
  };
@@ -759,7 +765,7 @@ export async function listLocalTasks(roomId, options = {}) {
759
765
  ORDER BY created_at ASC
760
766
  `)
761
767
  .all(...params)
762
- .map(mapTaskRow);
768
+ .map(row => mapTaskRow(row, database));
763
769
  return { tasks, has_more: false };
764
770
  }
765
771
  export async function listLocalActiveTaskOwnerLeases(roomId) {
@@ -805,60 +811,111 @@ export async function getLocalTask(roomId, taskId) {
805
811
  const row = database
806
812
  .prepare("SELECT * FROM local_tasks WHERE room_id = ? AND task_id = ?")
807
813
  .get(roomId, taskId);
808
- return row ? mapTaskRow(row) : null;
814
+ return row ? mapTaskRow(row, database) : null;
809
815
  }
810
816
  export async function updateLocalTask(roomId, taskId, patch) {
811
- const current = await getLocalTask(roomId, taskId);
812
- if (!current)
813
- throw new Error("Task not found.");
814
- const nextStatus = patch.skip_transition_validation === true
815
- ? typeof patch.status === "string" && patch.status.trim()
816
- ? patch.status.trim()
817
- : current.status
818
- : resolveLocalTaskStatus(current.status, patch.status);
819
- const assigneeAgentKey = patch.assignee_agent_key === undefined
820
- ? current.assignee_agent_key
821
- : typeof patch.assignee_agent_key === "string"
822
- ? patch.assignee_agent_key
823
- : null;
824
- const assigneeAgentInstanceId = patch.assignee_agent_key === undefined
825
- ? current.assignee_agent_instance_id
826
- : assigneeAgentKey && typeof patch.assignee_agent_instance_id === "string"
827
- ? patch.assignee_agent_instance_id
828
- : assigneeAgentKey && typeof patch.actor_instance_id === "string"
829
- ? patch.actor_instance_id
830
- : null;
831
- const assigneeAgentSessionId = patch.assignee_agent_key === undefined
832
- ? current.assignee_agent_session_id
833
- : assigneeAgentKey && typeof patch.assignee_agent_session_id === "string"
834
- ? patch.assignee_agent_session_id
835
- : assigneeAgentKey && typeof patch.agent_session_id === "string"
836
- ? patch.agent_session_id
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
837
865
  : null;
838
- const workflowArtifacts = patch.workflow_artifacts === undefined
839
- ? JSON.stringify(current.workflow_artifacts)
840
- : JSON.stringify(Array.isArray(patch.workflow_artifacts) ? patch.workflow_artifacts : []);
841
- const now = new Date().toISOString();
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) {
842
903
  const database = await getDb();
843
- await runLocalSqliteWriteTransactionAsync(database, () => withWorkerStateFence(() => database
844
- .prepare(`
845
- UPDATE local_tasks
846
- SET status = ?,
847
- assignee = ?,
848
- assignee_agent_key = ?,
849
- assignee_agent_instance_id = ?,
850
- assignee_agent_session_id = ?,
851
- pr_url = ?,
852
- workflow_artifacts_json = ?,
853
- sync_dirty = 1,
854
- updated_at = ?
855
- WHERE room_id = ? AND task_id = ?
856
- `)
857
- .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)));
858
- const updated = await getLocalTask(roomId, taskId);
859
- if (!updated)
860
- throw new Error("Task not found.");
861
- return updated;
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
+ }));
862
919
  }
863
920
  export async function claimLocalTaskReviewLease(roomId, taskId, input) {
864
921
  const current = await getLocalTask(roomId, taskId);
@@ -1,3 +1,4 @@
1
+ import { isLocalRoomApi, roomApiOrigin } from "../../../../shared/room-api-origin.mjs";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
3
  import { lstat, readFile, realpath } from "node:fs/promises";
3
4
  import { createConnection } from "node:net";
@@ -356,6 +357,9 @@ export async function borrowSupervisedWorkerCredential(session, env = process.en
356
357
  }
357
358
  function normalizedWorkerApiOrigin(env) {
358
359
  const apiUrl = env.LETAGENTS_API_URL?.trim() || "https://letagents.chat";
360
+ if (isLocalRoomApi(apiUrl) && env.LETAGENTS_SUPERVISED_BOUNDED_TURNS === "1"
361
+ && env.LETAGENTS_EXECUTION_PROFILE === "supervised_room_turn")
362
+ return apiUrl;
359
363
  let parsed;
360
364
  try {
361
365
  parsed = new URL(apiUrl);
@@ -452,7 +456,7 @@ function bindingRequestKey(session, coordinates, env) {
452
456
  session.session_id,
453
457
  session.room_id,
454
458
  tokenDigest,
455
- new URL(env.LETAGENTS_API_URL?.trim() || "https://letagents.chat").origin,
459
+ roomApiOrigin(env.LETAGENTS_API_URL?.trim() || "https://letagents.chat"),
456
460
  ].join("\u0000");
457
461
  }
458
462
  async function verifyConfirmedBinding(session, coordinates, env, protocolVersion, timeoutMs) {
@@ -1,3 +1,4 @@
1
+ import { isLocalRoomApi } from "../../../../shared/room-api-origin.mjs";
1
2
  import { getDaemonToolExecutionContext } from "./daemon-tool-context.js";
2
3
  export const LETAGENTS_AGENT_SESSION_BEARER_ENV = "LETAGENTS_AGENT_SESSION_BEARER";
3
4
  export const LETAGENTS_SUPERVISED_BOUNDED_TURNS_ENV = "LETAGENTS_SUPERVISED_BOUNDED_TURNS";
@@ -40,6 +41,8 @@ export function getWorkerBearerRuntime() {
40
41
  }
41
42
  try {
42
43
  const parsed = new URL(apiUrl);
44
+ if (isLocalRoomApi(apiUrl) && supervised && profile === "supervised_room_turn" && !bearer)
45
+ return { mode: "supervised" };
43
46
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
44
47
  throw new Error("unsupported protocol");
45
48
  }
@@ -1,5 +1,6 @@
1
1
  import { encodeRoomIdPath } from "../../../room-id.js";
2
2
  import { addLocalTask, apiCall, claimLocalTaskReviewLease, getLocalTask, isLocalRoomStorageEnabled, listLocalTasks, releaseLocalTaskReviewLease, resolveLocalRoomStorageIdentifiers, roomScopedApiCall, updateLocalTask, } from "../../runtime.js";
3
+ import { changeLocalTaskWorkLease } from "../../../local-state/local-chat.js";
3
4
  export function taskCollectionRoomPath(roomId) {
4
5
  return `/rooms/${encodeRoomIdPath(roomId)}/tasks`;
5
6
  }
@@ -129,6 +130,17 @@ export async function postCanonicalTaskAction(roomId, taskId, actionPath, body)
129
130
  released_lease: null,
130
131
  };
131
132
  }
133
+ if ((action === "handoff" || action === "release") && existingTask.active_leases?.some(lease => lease.kind === "work")) {
134
+ return await changeLocalTaskWorkLease(sqliteRoomId, taskId, {
135
+ action, lease_id: typeof body.lease_id === "string" ? body.lease_id : null,
136
+ target_actor_key: typeof body.target_actor_key === "string" ? body.target_actor_key : null,
137
+ target_actor_instance_id: typeof body.target_actor_instance_id === "string" ? body.target_actor_instance_id : null,
138
+ target_agent_session_id: typeof body.target_agent_session_id === "string" ? body.target_agent_session_id : null,
139
+ });
140
+ }
141
+ if ((action === "handoff" || action === "release") && body.lease_id) {
142
+ throw new Error("The task lease changed. Refresh the task before trying again.");
143
+ }
132
144
  if (action === "handoff") {
133
145
  const targetActorKey = typeof body.target_actor_key === "string" && body.target_actor_key.trim()
134
146
  ? body.target_actor_key.trim()
@@ -141,6 +153,7 @@ export async function postCanonicalTaskAction(roomId, taskId, actionPath, body)
141
153
  : null;
142
154
  const task = targetActorKey
143
155
  ? await updateLocalTask(sqliteRoomId, taskId, {
156
+ expected_no_work_lease: true,
144
157
  ...(existingTask.status === "accepted" ? { status: "assigned" } : {}),
145
158
  assignee: targetActorKey,
146
159
  assignee_agent_key: targetActorKey,
@@ -158,6 +171,7 @@ export async function postCanonicalTaskAction(roomId, taskId, actionPath, body)
158
171
  if (action === "release") {
159
172
  const releasableStatuses = new Set(["assigned", "in_progress", "blocked", "in_review"]);
160
173
  const patch = {
174
+ expected_no_work_lease: true,
161
175
  assignee: null,
162
176
  assignee_agent_key: null,
163
177
  };