























@@ -53,6 +53,9 @@ afterEach(() => {
5353function createTaskRegistryMaintenanceHarness(params: {
5454tasks: TaskRecord[];
5555sessionStore?: Record<string, SessionEntry>;
56+loadSessionStore?: TaskRegistryMaintenanceRuntime["loadSessionStore"];
57+resolveStorePath?: TaskRegistryMaintenanceRuntime["resolveStorePath"];
58+deriveSessionChatTypeFromKey?: TaskRegistryMaintenanceRuntime["deriveSessionChatTypeFromKey"];
5659acpEntry?: AcpSessionStoreEntry["entry"];
5760activeCronJobIds?: string[];
5861activeRunIds?: string[];
@@ -87,8 +90,11 @@ function createTaskRegistryMaintenanceHarness(params: {
8790entry: undefined,
8891storeReadFailed: false,
8992} satisfies AcpSessionStoreEntry),
90-loadSessionStore: () => sessionStore,
91-resolveStorePath: () => "",
93+loadSessionStore: params.loadSessionStore ?? (() => sessionStore),
94+resolveStorePath: params.resolveStorePath ?? (() => ""),
95+ ...(params.deriveSessionChatTypeFromKey
96+ ? { deriveSessionChatTypeFromKey: params.deriveSessionChatTypeFromKey }
97+ : {}),
9298isCronJobActive: (jobId: string) => activeCronJobIds.has(jobId),
9399getAgentRunContext: (runId: string) =>
94100activeRunIds.has(runId) ? { sessionKey: "main" } : undefined,
@@ -101,6 +107,15 @@ function createTaskRegistryMaintenanceHarness(params: {
101107 ? { agentId, rest: rest.join(":") }
102108 : null;
103109},
110+hasActiveTaskForChildSessionKey: ({ sessionKey, excludeTaskId }) => {
111+const normalized = sessionKey.trim().toLowerCase();
112+return Array.from(currentTasks.values()).some(
113+(task) =>
114+task.taskId !== excludeTaskId &&
115+(task.status === "queued" || task.status === "running") &&
116+task.childSessionKey?.trim().toLowerCase() === normalized,
117+);
118+},
104119deleteTaskRecordById: (taskId: string) => currentTasks.delete(taskId),
105120ensureTaskRegistryReady: () => {},
106121getTaskById: (taskId: string) => currentTasks.get(taskId),
@@ -162,6 +177,46 @@ function createTaskRegistryMaintenanceHarness(params: {
162177}
163178164179describe("task-registry maintenance issue #60299", () => {
180+it("reuses session store reads across stale subagent task checks in one pass", async () => {
181+const tasks = Array.from({ length: 10 }, (_, index) =>
182+makeStaleTask({
183+runtime: "subagent",
184+taskId: `task-subagent-stale-${index}`,
185+childSessionKey: `agent:main:subagent:stale-${index}`,
186+}),
187+);
188+const loadSessionStoreMock = vi.fn(() => ({}));
189+190+createTaskRegistryMaintenanceHarness({
191+ tasks,
192+loadSessionStore: loadSessionStoreMock,
193+resolveStorePath: () => "/tmp/openclaw-test-sessions-main.json",
194+});
195+196+expect(await runTaskRegistryMaintenance()).toMatchObject({ reconciled: tasks.length });
197+expect(loadSessionStoreMock).toHaveBeenCalledTimes(1);
198+});
199+200+it("reuses CLI channel session type derivation across duplicate stale task checks", async () => {
201+const childSessionKey = "agent:main:discord:direct:user-1";
202+const tasks = Array.from({ length: 10 }, (_, index) =>
203+makeStaleTask({
204+runtime: "cli",
205+taskId: `task-cli-channel-stale-${index}`,
206+ childSessionKey,
207+}),
208+);
209+const deriveSessionChatTypeMock = vi.fn(() => "direct" as const);
210+211+createTaskRegistryMaintenanceHarness({
212+ tasks,
213+deriveSessionChatTypeFromKey: deriveSessionChatTypeMock,
214+});
215+216+expect(await runTaskRegistryMaintenance()).toMatchObject({ reconciled: tasks.length });
217+expect(deriveSessionChatTypeMock).toHaveBeenCalledTimes(1);
218+});
219+165220it("marks stale cron tasks lost once the runtime no longer tracks the job as active", async () => {
166221const childSessionKey = "agent:main:workspace:channel:test-channel";
167222const task = makeStaleTask({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。