From 77d6ad6f65d2a097d38ce4ba6edfdff6c36fedfb Mon Sep 17 00:00:00 2001
From: Alix-007
Date: Mon, 15 Jun 2026 09:30:28 +0800
Subject: [PATCH] fix(tasks): honor explicit agentId in gateway tasks.list and
repair test helper signature
Two code-review findings. (1) gateway taskMatchesAgent fell through to a requester/owner/child session-key scan even when the task had an explicit agentId, so a worker subagent task owned by agent:main:main also matched agentId:main; make explicit task.agentId authoritative and keep the session-key fallback only for legacy records without an agentId, with a gateway tasks.list regression. (2) the cross-agent attribution test passed async (root) to the zero-arg withTaskRegistryTempDir helper (TS2345/TS7006); drop the unused parameter and redundant env assignment.
---
src/gateway/server-methods/tasks.test.ts | 25 ++++++++++++++++++++++++
src/gateway/server-methods/tasks.ts | 11 +++++++----
src/tasks/task-registry.test.ts | 3 +--
3 files changed, 33 insertions(+), 6 deletions(-)
diff --git a/src/gateway/server-methods/tasks.test.ts b/src/gateway/server-methods/tasks.test.ts
index e9335e7eecc..47aaffe9aa6 100644
--- a/src/gateway/server-methods/tasks.test.ts
+++ b/src/gateway/server-methods/tasks.test.ts
@@ -136,6 +136,31 @@ describe("tasks gateway handlers", () => {
expect(listedTask?.runId).toBe("run-running");
});
+ it("treats explicit task agentId as authoritative over the session-key fallback", async () => {
+ // Cross-agent subagent task: the registry derives agentId=worker from the
+ // child session key, while owner/requester keys belong to main. tasks.list
+ // for main must not leak the worker task through the session-key fallback.
+ const workerTask = createTaskRecord({
+ runtime: "subagent",
+ requesterSessionKey: "agent:main:main",
+ ownerKey: "agent:main:main",
+ scopeKind: "session",
+ childSessionKey: "agent:worker:subagent:child",
+ runId: "run-worker-authoritative",
+ task: "Inspect worker state",
+ status: "running",
+ deliveryStatus: "pending",
+ });
+ expect(workerTask.agentId).toBe("worker");
+
+ const mainView = await runTaskHandler("tasks.list", { agentId: "main" });
+ expect(mainView.calls[0]?.[0]).toBe(true);
+ expect(mainView.payload?.tasks ?? []).toEqual([]);
+
+ const workerView = await runTaskHandler("tasks.list", { agentId: "worker" });
+ expect(workerView.payload?.tasks?.map((task) => task.taskId)).toEqual([workerTask.taskId]);
+ });
+
it("gets completed tasks with stable completed status", async () => {
const task = createTaskRecord({
runtime: "cli",
diff --git a/src/gateway/server-methods/tasks.ts b/src/gateway/server-methods/tasks.ts
index 981cc17e997..7c9f3c6b706 100644
--- a/src/gateway/server-methods/tasks.ts
+++ b/src/gateway/server-methods/tasks.ts
@@ -114,15 +114,18 @@ function taskMatchesSession(task: TaskRecord, sessionKey: string | undefined): b
);
}
-// Some records predate a direct `agentId`, so task listings still recover the
-// owning agent from session-style keys instead of hiding those tasks.
+// Explicit `task.agentId` is authoritative: a task that records its own agent
+// must not also match other agents through the session-key fallback. Only
+// records that predate a direct `agentId` recover the owning agent from
+// session-style keys instead of being hidden.
function taskMatchesAgent(task: TaskRecord, agentId: string | undefined): boolean {
const normalized = normalizeOptionalString(agentId);
if (!normalized) {
return true;
}
- if (normalizeOptionalString(task.agentId) === normalized) {
- return true;
+ const explicitAgentId = normalizeOptionalString(task.agentId);
+ if (explicitAgentId) {
+ return explicitAgentId === normalized;
}
return [task.requesterSessionKey, task.childSessionKey, task.ownerKey].some(
(candidate) => parseAgentSessionKey(candidate)?.agentId === normalized,
diff --git a/src/tasks/task-registry.test.ts b/src/tasks/task-registry.test.ts
index 4b2f6d0108f..02dcc1a9b8a 100644
--- a/src/tasks/task-registry.test.ts
+++ b/src/tasks/task-registry.test.ts
@@ -2093,8 +2093,7 @@ describe("task-registry", () => {
});
it("uses the child session agent for cross-agent background task attribution", async () => {
- await withTaskRegistryTempDir(async (root) => {
- process.env.OPENCLAW_STATE_DIR = root;
+ await withTaskRegistryTempDir(async () => {
resetTaskRegistryMemoryForTest({ persist: false });
const created = createTaskRecord({