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({