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.
- package/dist/mcp/local-state/local-chat.js +132 -75
- package/dist/mcp/server/runtime/supervisor-bridge.js +5 -1
- package/dist/mcp/server/runtime/worker-bearer.js +3 -0
- package/dist/mcp/server/tools/tasks/api.js +14 -0
- package/dist/shared/activation-routing.js +2 -468
- package/package.json +1 -1
- package/shared/activation-routing.d.mts +139 -0
- package/shared/activation-routing.mjs +468 -0
- package/shared/local-supervised-routing.d.mts +9 -0
- package/shared/local-supervised-routing.mjs +93 -0
- package/shared/local-work-leases.d.mts +24 -0
- package/shared/local-work-leases.mjs +160 -0
- package/shared/message-contracts.d.mts +2 -0
- package/shared/message-contracts.mjs +11 -0
- package/shared/room-api-origin.d.mts +3 -0
- package/shared/room-api-origin.mjs +12 -0
- package/shared/sqlite-thread-routing.d.mts +4 -0
- package/shared/sqlite-thread-routing.mjs +30 -3
|
@@ -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
|
|
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:
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
:
|
|
824
|
-
|
|
825
|
-
?
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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
|
-
|
|
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
|
};
|