


























@@ -1,5 +1,9 @@
11import { getAcpSessionManager } from "../acp/control-plane/manager.js";
2-import { readAcpSessionEntry } from "../acp/runtime/session-meta.js";
2+import {
3+listAcpSessionEntries,
4+readAcpSessionEntry,
5+type AcpSessionStoreEntry,
6+} from "../acp/runtime/session-meta.js";
37import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
48import type { OpenClawConfig } from "../config/types.openclaw.js";
59import { isCronJobActive } from "../cron/active-jobs.js";
@@ -55,6 +59,7 @@ let configuredCronStorePath: string | undefined;
5559let configuredCronRuntimeAuthoritative = false;
56605761type TaskRegistryMaintenanceRuntime = {
62+listAcpSessionEntries: typeof listAcpSessionEntries;
5863readAcpSessionEntry: typeof readAcpSessionEntry;
5964closeAcpSession?: (params: {
6065cfg: OpenClawConfig;
@@ -85,6 +90,7 @@ type TaskRegistryMaintenanceRuntime = {
8590};
86918792const defaultTaskRegistryMaintenanceRuntime: TaskRegistryMaintenanceRuntime = {
93+ listAcpSessionEntries,
8894 readAcpSessionEntry,
8995closeAcpSession: async ({ cfg, sessionKey, reason }) => {
9096await getAcpSessionManager().closeSession({
@@ -421,6 +427,24 @@ function hasActiveTaskForChildSession(task: TaskRecord, tasks: TaskRecord[]): bo
421427);
422428}
423429430+function hasActiveTaskForChildSessionKey(sessionKey: string, tasks: TaskRecord[]): boolean {
431+const normalized = normalizeOptionalString(sessionKey);
432+if (!normalized) {
433+return false;
434+}
435+return tasks.some(
436+(candidate) =>
437+isActiveTask(candidate) && getNormalizedTaskChildSessionKey(candidate) === normalized,
438+);
439+}
440+441+function getAcpSessionParentKeys(acpEntry: Pick<AcpSessionStoreEntry, "entry">): string[] {
442+return [
443+normalizeOptionalString(acpEntry.entry?.spawnedBy),
444+normalizeOptionalString(acpEntry.entry?.parentSessionKey),
445+].filter((value): value is string => Boolean(value));
446+}
447+424448function isParentOwnedAcpSessionTask(
425449task: TaskRecord,
426450acpEntry: ReturnType<typeof readAcpSessionEntry>,
@@ -431,13 +455,14 @@ function isParentOwnedAcpSessionTask(
431455}
432456const ownerKey = normalizeOptionalString(task.ownerKey);
433457const requesterKey = normalizeOptionalString(task.requesterSessionKey);
434-const parentKeys = [
435-normalizeOptionalString(entry.spawnedBy),
436-normalizeOptionalString(entry.parentSessionKey),
437-].filter((value): value is string => Boolean(value));
458+const parentKeys = getAcpSessionParentKeys({ entry });
438459return parentKeys.some((parentKey) => parentKey === ownerKey || parentKey === requesterKey);
439460}
440461462+function isParentOwnedAcpSessionEntry(acpEntry: Pick<AcpSessionStoreEntry, "entry">): boolean {
463+return getAcpSessionParentKeys(acpEntry).length > 0;
464+}
465+441466function hasActiveSessionBinding(sessionKey: string): boolean {
442467const listBindings = taskRegistryMaintenanceRuntime.listSessionBindingsBySession;
443468if (!listBindings) {
@@ -471,6 +496,23 @@ function shouldCloseTerminalAcpSession(task: TaskRecord, tasks: TaskRecord[]): b
471496return !hasActiveSessionBinding(sessionKey);
472497}
473498499+function shouldCloseOrphanedParentOwnedAcpSession(
500+acpEntry: AcpSessionStoreEntry,
501+tasks: TaskRecord[],
502+): boolean {
503+if (!acpEntry.entry || !acpEntry.acp || !isParentOwnedAcpSessionEntry(acpEntry)) {
504+return false;
505+}
506+const sessionKey = normalizeOptionalString(acpEntry.sessionKey);
507+if (!sessionKey || hasActiveTaskForChildSessionKey(sessionKey, tasks)) {
508+return false;
509+}
510+if (acpEntry.acp.mode === "oneshot") {
511+return true;
512+}
513+return !hasActiveSessionBinding(sessionKey);
514+}
515+474516async function cleanupTerminalAcpSession(task: TaskRecord, tasks: TaskRecord[]): Promise<void> {
475517if (!shouldCloseTerminalAcpSession(task, tasks)) {
476518return;
@@ -512,6 +554,55 @@ async function cleanupTerminalAcpSession(task: TaskRecord, tasks: TaskRecord[]):
512554}
513555}
514556557+async function cleanupOrphanedParentOwnedAcpSessions(tasks: TaskRecord[]): Promise<void> {
558+let acpSessions: AcpSessionStoreEntry[];
559+try {
560+acpSessions = await taskRegistryMaintenanceRuntime.listAcpSessionEntries({});
561+} catch (error) {
562+log.warn("Failed to list ACP sessions during task maintenance", { error });
563+return;
564+}
565+const seenSessionKeys = new Set<string>();
566+for (const acpEntry of acpSessions) {
567+const sessionKey = normalizeOptionalString(acpEntry.sessionKey);
568+if (!sessionKey || seenSessionKeys.has(sessionKey)) {
569+continue;
570+}
571+seenSessionKeys.add(sessionKey);
572+if (!shouldCloseOrphanedParentOwnedAcpSession(acpEntry, tasks)) {
573+continue;
574+}
575+const closeAcpSession = taskRegistryMaintenanceRuntime.closeAcpSession;
576+if (!closeAcpSession) {
577+continue;
578+}
579+try {
580+await closeAcpSession({
581+cfg: acpEntry.cfg,
582+ sessionKey,
583+reason: "orphaned-parent-task-cleanup",
584+});
585+} catch (error) {
586+log.warn("Failed to close orphaned parent-owned ACP session during task maintenance", {
587+ sessionKey,
588+ error,
589+});
590+continue;
591+}
592+try {
593+await taskRegistryMaintenanceRuntime.unbindSessionBindings?.({
594+targetSessionKey: sessionKey,
595+reason: "orphaned-parent-task-cleanup",
596+});
597+} catch (error) {
598+log.warn("Failed to unbind orphaned parent-owned ACP session during task maintenance", {
599+ sessionKey,
600+ error,
601+});
602+}
603+}
604+}
605+515606function markTaskLost(task: TaskRecord, now: number): TaskRecord {
516607const cleanupAfter = task.cleanupAfter ?? projectTaskLost(task, now).cleanupAfter;
517608const updated =
@@ -754,6 +845,7 @@ export async function runTaskRegistryMaintenance(): Promise<TaskRegistryMaintena
754845await yieldToEventLoop();
755846}
756847}
848+await cleanupOrphanedParentOwnedAcpSessions(taskRegistryMaintenanceRuntime.listTaskRecords());
757849return { reconciled, recovered, cleanupStamped, pruned };
758850}
759851此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。