





























@@ -14,12 +14,7 @@ import {
1414import { resolveGlobalMap } from "openclaw/plugin-sdk/global-singleton";
1515import { resolveStateDir } from "openclaw/plugin-sdk/memory-core-host-runtime-core";
1616import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
17-import { pathExists } from "openclaw/plugin-sdk/security-runtime";
18-import {
19-loadSessionStore,
20-resolveStorePath,
21-updateSessionStore,
22-} from "openclaw/plugin-sdk/session-store-runtime";
17+import { cleanupSessionLifecycleArtifacts } from "openclaw/plugin-sdk/session-store-runtime";
2318import { readDreamsFile, resolveDreamsPath, updateDreamsFile } from "./dreaming-dreams-file.js";
24192520// ── Types ──────────────────────────────────────────────────────────────
@@ -108,7 +103,6 @@ const NARRATIVE_MESSAGE_SETTLE_DELAYS_MS = [50, 150, 300, 750] as const;
108103const DREAMING_SESSION_KEY_PREFIX = "dreaming-narrative-";
109104const DREAMING_TRANSCRIPT_RUN_MARKER = '"runId":"dreaming-narrative-';
110105const DREAMING_ORPHAN_MIN_AGE_MS = 300_000;
111-const SAFE_SESSION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
112106const DIARY_START_MARKER = "<!-- openclaw:dreaming:diary:start -->";
113107const DIARY_END_MARKER = "<!-- openclaw:dreaming:diary:end -->";
114108const BACKFILL_ENTRY_MARKER = "openclaw:dreaming:backfill-entry";
@@ -776,80 +770,6 @@ export async function appendNarrativeEntry(params: {
776770777771// ── Orchestrator ───────────────────────────────────────────────────────
778772779-function normalizeComparablePath(pathname: string): string {
780-return process.platform === "win32" ? pathname.toLowerCase() : pathname;
781-}
782-783-async function normalizeSessionFileForComparison(params: {
784-sessionsDir: string;
785-sessionFile: string;
786-}): Promise<string | null> {
787-const trimmed = params.sessionFile.trim();
788-if (!trimmed) {
789-return null;
790-}
791-const resolved = path.isAbsolute(trimmed) ? trimmed : path.resolve(params.sessionsDir, trimmed);
792-try {
793-return normalizeComparablePath(await fs.realpath(resolved));
794-} catch {
795-return normalizeComparablePath(path.resolve(resolved));
796-}
797-}
798-799-function isDreamingSessionStoreKey(sessionKey: string): boolean {
800-const firstSeparator = sessionKey.indexOf(":");
801-if (firstSeparator < 0) {
802-return sessionKey.startsWith(DREAMING_SESSION_KEY_PREFIX);
803-}
804-const secondSeparator = sessionKey.indexOf(":", firstSeparator + 1);
805-const sessionSegment = secondSeparator < 0 ? sessionKey : sessionKey.slice(secondSeparator + 1);
806-return sessionSegment.startsWith(DREAMING_SESSION_KEY_PREFIX);
807-}
808-809-// A dreaming store row is reclaimable once its narrative run is finished. The
810-// happy path deletes the session in `finally`, but when `deleteSession` throws
811-// (e.g. request-scoped subagent runtime) the row is left behind referencing a
812-// still-present transcript, so the missing-transcript check alone never reaps
813-// it and the session lingers in the sidebar forever (issue #88322). Reclaim a
814-// dreaming row when its transcript is missing, or when the transcript has aged
815-// past the orphan threshold (a live narrative refreshes its transcript well
816-// within that window, so active runs are never reaped).
817-async function isReclaimableDreamingStoreEntry(
818-normalizedSessionFile: string | null,
819-): Promise<boolean> {
820-if (!normalizedSessionFile || !(await pathExists(normalizedSessionFile))) {
821-return true;
822-}
823-try {
824-const stat = await fs.stat(normalizedSessionFile);
825-return Date.now() - stat.mtimeMs >= DREAMING_ORPHAN_MIN_AGE_MS;
826-} catch {
827-return true;
828-}
829-}
830-831-async function normalizeSessionEntryPathForComparison(params: {
832-sessionsDir: string;
833-entry: { sessionFile?: string; sessionId?: string } | undefined;
834-}): Promise<string | null> {
835-const sessionFile = typeof params.entry?.sessionFile === "string" ? params.entry.sessionFile : "";
836-if (sessionFile) {
837-return normalizeSessionFileForComparison({
838-sessionsDir: params.sessionsDir,
839- sessionFile,
840-});
841-}
842-const sessionId =
843-typeof params.entry?.sessionId === "string" ? params.entry.sessionId.trim() : "";
844-if (!SAFE_SESSION_ID_RE.test(sessionId)) {
845-return null;
846-}
847-return normalizeSessionFileForComparison({
848-sessionsDir: params.sessionsDir,
849-sessionFile: `${sessionId}.jsonl`,
850-});
851-}
852-853773async function scrubDreamingNarrativeArtifacts(logger: Logger): Promise<void> {
854774const cfg = getRuntimeConfig();
855775const agentsDir = path.join(resolveStateDir(), "agents");
@@ -868,112 +788,20 @@ async function scrubDreamingNarrativeArtifacts(logger: Logger): Promise<void> {
868788continue;
869789}
870790871-const storePath = resolveStorePath(cfg.session?.store, { agentId: agentEntry.name });
872-const sessionsDir = path.dirname(storePath);
873-let store: Record<string, { sessionFile?: string; sessionId?: string } | undefined>;
874791try {
875-store = loadSessionStore(storePath) as Record<
876-string,
877-{ sessionFile?: string; sessionId?: string } | undefined
878->;
879-} catch {
880-continue;
881-}
882-883-const referencedSessionFiles = new Set<string>();
884-let needsStoreUpdate = false;
885-for (const [key, entry] of Object.entries(store)) {
886-const normalizedSessionFile = await normalizeSessionEntryPathForComparison({
887- sessionsDir,
888- entry,
792+const result = await cleanupSessionLifecycleArtifacts({
793+agentId: agentEntry.name,
794+archiveRemovedEntryTranscripts: false,
795+sessionStore: cfg.session?.store,
796+sessionKeySegmentPrefix: DREAMING_SESSION_KEY_PREFIX,
797+transcriptContentMarker: DREAMING_TRANSCRIPT_RUN_MARKER,
798+orphanTranscriptMinAgeMs: DREAMING_ORPHAN_MIN_AGE_MS,
889799});
890-if (normalizedSessionFile) {
891-referencedSessionFiles.add(normalizedSessionFile);
892-}
893-if (!isDreamingSessionStoreKey(key)) {
894-continue;
895-}
896-if (await isReclaimableDreamingStoreEntry(normalizedSessionFile)) {
897-needsStoreUpdate = true;
898-}
899-}
900-901-if (needsStoreUpdate) {
902-referencedSessionFiles.clear();
903-prunedEntries += await updateSessionStore(storePath, async (lockedStore) => {
904-let prunedForAgent = 0;
905-for (const [key, entry] of Object.entries(lockedStore)) {
906-const normalizedSessionFile = await normalizeSessionEntryPathForComparison({
907- sessionsDir,
908- entry,
909-});
910-if (!isDreamingSessionStoreKey(key)) {
911-if (normalizedSessionFile) {
912-referencedSessionFiles.add(normalizedSessionFile);
913-}
914-continue;
915-}
916-if (await isReclaimableDreamingStoreEntry(normalizedSessionFile)) {
917-// Drop the row and leave the transcript unreferenced so the orphan
918-// transcript pass below archives the aged-out (or missing) file.
919-delete lockedStore[key];
920-prunedForAgent += 1;
921-continue;
922-}
923-if (normalizedSessionFile) {
924-referencedSessionFiles.add(normalizedSessionFile);
925-}
926-}
927-return prunedForAgent;
928-});
929-}
930-931-let sessionFiles: Dirent[];
932-try {
933-sessionFiles = await fs.readdir(sessionsDir, { withFileTypes: true });
800+prunedEntries += result.removedEntries;
801+archivedOrphans += result.archivedTranscriptArtifacts;
934802} catch {
935803continue;
936804}
937-938-for (const fileEntry of sessionFiles) {
939-if (!fileEntry.isFile() || !fileEntry.name.endsWith(".jsonl")) {
940-continue;
941-}
942-const transcriptPath = path.join(sessionsDir, fileEntry.name);
943-const normalizedTranscriptPath =
944-(await normalizeSessionFileForComparison({
945- sessionsDir,
946-sessionFile: fileEntry.name,
947-})) ?? normalizeComparablePath(transcriptPath);
948-if (referencedSessionFiles.has(normalizedTranscriptPath)) {
949-continue;
950-}
951-let stat;
952-try {
953-stat = await fs.stat(transcriptPath);
954-} catch {
955-continue;
956-}
957-if (Date.now() - stat.mtimeMs < DREAMING_ORPHAN_MIN_AGE_MS) {
958-continue;
959-}
960-let content;
961-try {
962-content = await fs.readFile(transcriptPath, "utf-8");
963-} catch {
964-continue;
965-}
966-if (!content.includes(DREAMING_TRANSCRIPT_RUN_MARKER)) {
967-continue;
968-}
969-const archivedPath = `${transcriptPath}.deleted.${Date.now()}`;
970-try {
971-await fs.rename(transcriptPath, archivedPath);
972-archivedOrphans += 1;
973-} catch {
974-// best-effort scrubber
975-}
976-}
977805}
978806979807if (prunedEntries > 0 || archivedOrphans > 0) {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。