diff --git a/src/config/sessions/cleanup-service.ts b/src/config/sessions/cleanup-service.ts index 98cb4f31cb7a..cefa0b2af5f3 100644 --- a/src/config/sessions/cleanup-service.ts +++ b/src/config/sessions/cleanup-service.ts @@ -580,12 +580,10 @@ export async function runSessionsCleanup(params: { storePath: target.storePath, removals, activeSessionKey: opts.activeKey, - preserveActiveWork: true, maintenanceOverride: { ...maintenance, mode, }, - restrictArchivedTranscriptsToStoreDir: true, }); const postApplyStore = loadCleanupSessionStore(target, { createIfMissing: true }); const appliedUnreferencedArtifacts = diff --git a/src/config/sessions/session-accessor.lifecycle-types.ts b/src/config/sessions/session-accessor.lifecycle-types.ts index 3cf10a52101f..db7a221fbba7 100644 --- a/src/config/sessions/session-accessor.lifecycle-types.ts +++ b/src/config/sessions/session-accessor.lifecycle-types.ts @@ -42,6 +42,26 @@ export type ResetSessionEntryLifecycleMutation = Omit< "archivedTranscripts" >; +export type ResetSessionEntryLifecycleParams = { + /** Preserve legacy rotation archival unless the caller appended an in-log boundary. */ + archivePreviousTranscript?: boolean; + /** Runs after the persisted entry changes and any requested archival completes. */ + afterEntryMutation?: (mutation: ResetSessionEntryLifecycleMutation) => Promise | void; + /** Agent owner used to resolve backend transcript artifacts. */ + agentId?: string; + /** Builds the persisted replacement entry from the current backend row. */ + buildNextEntry: (context: { + currentEntry?: SessionEntry; + primaryKey: string; + }) => Promise | SessionEntry; + /** Atomically append this boundary with the reset entry mutation. */ + resetBoundaryReason?: SessionResetBoundaryReason; + /** Explicit store target for SQLite session ownership. */ + storePath: string; + /** Canonical key plus aliases that identify the logical entry. */ + target: SessionLifecycleStoreTarget; +}; + export type DeleteSessionEntryLifecycleResult = { archivedTranscripts: SessionLifecycleArchivedTranscript[]; deleted: boolean; @@ -51,6 +71,29 @@ export type DeleteSessionEntryLifecycleResult = { deletedSessionId?: string; }; +export type DeleteSessionEntryLifecycleParams = { + /** Agent owner used to resolve backend transcript artifacts. */ + agentId?: string; + /** Whether transcript artifacts should be archived/deleted with the entry. */ + archiveTranscript: boolean; + /** Delete transcript rows without writing an archive artifact. */ + deleteTranscriptWithoutArchive?: boolean; + /** Optional exact row guard checked under the storage writer lock. */ + expectedEntry?: SessionEntry; + /** Optional provider-run identity guard checked under the storage writer lock. */ + expectedSessionId?: string | null; + /** Optional owner revision guard checked under the storage writer lock. */ + expectedLifecycleRevision?: string; + /** Optional persisted revision guard checked under the storage writer lock. */ + expectedUpdatedAt?: number; + /** Fail when the underlying store cannot confirm a durable write. */ + requireWriteSuccess?: boolean; + /** Explicit store target for SQLite session ownership. */ + storePath: string; + /** Canonical key plus aliases that identify the logical entry. */ + target: SessionLifecycleStoreTarget; +}; + export type SessionEntryLifecycleRemoval = { sessionKey: string; expectedEntry?: SessionEntry; diff --git a/src/config/sessions/session-accessor.lifecycle.ts b/src/config/sessions/session-accessor.lifecycle.ts index 2fcaf1246153..4a1ac5f6a37c 100644 --- a/src/config/sessions/session-accessor.lifecycle.ts +++ b/src/config/sessions/session-accessor.lifecycle.ts @@ -16,19 +16,12 @@ import { replaceSessionEntry, patchSessionEntry, } from "./session-accessor.entry.js"; -import type { - DeleteSessionEntryLifecycleResult, - SessionArchivedTranscriptCleanupRule, - SessionEntryLifecycleMutationResult, - SessionEntryLifecycleRemoval, - SessionEntryLifecycleUpsert, -} from "./session-accessor.lifecycle-types.js"; import { - applySqliteSessionEntryLifecycleMutation, + applySqliteSessionEntryLifecycleMutation as applySessionEntryLifecycleMutation, applySqliteSessionEntryReplacements as applySessionEntryReplacements, applySqliteSessionStoreProjection as applySessionStoreProjection, cleanupSqliteSessionLifecycleArtifacts as cleanupSessionLifecycleArtifacts, - deleteSqliteSessionEntryLifecycle, + deleteSqliteSessionEntryLifecycle as deleteSessionEntryLifecycle, purgeSqliteDeletedAgentSessionEntries as purgeDeletedAgentSessionEntries, rollbackSqliteAgentHarnessSessionEntryLifecycle as rollbackAgentHarnessSessionEntryLifecycle, rollbackSqlitePluginOwnedSessionEntryLifecycle as rollbackPluginOwnedSessionEntryLifecycle, @@ -47,18 +40,18 @@ import type { SessionPatchProjectionContext, SessionPatchProjectionFailure, SessionPatchProjectionResult, - DeleteSessionEntryLifecycleParams, } from "./session-accessor.types.js"; import { resolveProjectionExistingEntry } from "./session-entry-selection.js"; -import type { ResolvedSessionMaintenanceConfig } from "./store-maintenance.js"; import type { SessionCompactionCheckpoint, SessionEntry } from "./types.js"; // Session lifecycle storage is canonical SQLite; direct exports keep reset, // rollback, cleanup, and bulk projections on their actual transaction owner. export { + applySessionEntryLifecycleMutation, applySessionEntryReplacements, applySessionStoreProjection, cleanupSessionLifecycleArtifacts, + deleteSessionEntryLifecycle, purgeDeletedAgentSessionEntries, resetSessionEntryLifecycle, rollbackAgentHarnessSessionEntryLifecycle, @@ -290,38 +283,6 @@ export async function preserveTemporarySessionMapping( }; } -/** Keeps the broader lifecycle compatibility contract at its public boundary. */ -export async function deleteSessionEntryLifecycle( - params: DeleteSessionEntryLifecycleParams, -): Promise { - return await deleteSqliteSessionEntryLifecycle(params); -} - -/** Applies exact entry lifecycle mutations and artifact cleanup at the storage boundary. */ -export async function applySessionEntryLifecycleMutation(params: { - agentId?: string; - storePath: string; - removals?: Iterable; - upserts?: Iterable; - activeSessionKey?: string; - maintenanceOverride?: Partial; - skipMaintenance?: boolean; - preserveActiveWork?: boolean; - archiveReason?: "deleted" | "reset"; - restrictArchivedTranscriptsToStoreDir?: boolean; - cleanupArchivedTranscripts?: { - rules: SessionArchivedTranscriptCleanupRule[]; - nowMs?: number; - }; - pruneUnreferencedArtifacts?: { - olderThanMs: number; - dryRun?: boolean; - }; - captureArtifactCleanupError?: boolean; -}): Promise { - return await applySqliteSessionEntryLifecycleMutation(params); -} - /** * Clears plugin host-owned state inside one resolved session store. * This is an internal transaction-sized boundary for the storage backend, not diff --git a/src/config/sessions/session-accessor.sqlite-contract.ts b/src/config/sessions/session-accessor.sqlite-contract.ts index fb2cffcfe454..0cd8d67e34da 100644 --- a/src/config/sessions/session-accessor.sqlite-contract.ts +++ b/src/config/sessions/session-accessor.sqlite-contract.ts @@ -2,8 +2,9 @@ import type { SessionTranscriptUpdate } from "../../sessions/transcript-events.j import type { OpenClawConfig } from "../types.openclaw.js"; import type { DeletedAgentSessionEntryPurgeParams, + DeleteSessionEntryLifecycleParams, DeleteSessionEntryLifecycleResult, - ResetSessionEntryLifecycleMutation, + ResetSessionEntryLifecycleParams, ResetSessionEntryLifecycleResult, SessionEntryLifecycleMutationResult, SessionEntryLifecycleRemoval, @@ -177,34 +178,11 @@ export type SessionEntryReplacementUpdate = { result: T; }; -export type ResetSessionEntryLifecycleParams = { - archivePreviousTranscript?: boolean; - afterEntryMutation?: (mutation: ResetSessionEntryLifecycleMutation) => Promise | void; - agentId?: string; - buildNextEntry: (context: { - currentEntry?: SessionEntry; - primaryKey: string; - }) => Promise | SessionEntry; - resetBoundaryReason?: import("./session-reset-boundary-event.js").SessionResetBoundaryReason; - storePath: string; - target: SessionLifecycleStoreTarget; -}; - -export type DeleteSessionEntryLifecycleParams = { - agentId?: string; - archiveTranscript: boolean; - deleteTranscriptWithoutArchive?: boolean; - expectedEntry?: SessionEntry; - expectedSessionId?: string | null; - expectedLifecycleRevision?: string; - expectedUpdatedAt?: number; - storePath: string; - target: SessionLifecycleStoreTarget; -}; - export type { DeletedAgentSessionEntryPurgeParams, + DeleteSessionEntryLifecycleParams, DeleteSessionEntryLifecycleResult, + ResetSessionEntryLifecycleParams, ResetSessionEntryLifecycleResult, SessionEntryLifecycleMutationResult, SessionEntryLifecycleRemoval, diff --git a/src/config/sessions/session-accessor.test.ts b/src/config/sessions/session-accessor.test.ts index 1685e7223a8b..a336bf6e3ea5 100644 --- a/src/config/sessions/session-accessor.test.ts +++ b/src/config/sessions/session-accessor.test.ts @@ -57,7 +57,9 @@ import { readSqliteSessionEntryKeys, } from "./session-accessor.sqlite-entry-store.js"; import { + applySqliteSessionEntryLifecycleMutation, appendSqliteTranscriptEventSync, + deleteSqliteSessionEntryLifecycle, importSqliteSessionRows, loadExactSqliteSessionEntry, replaceSqliteSessionEntrySync, @@ -124,6 +126,11 @@ describe("session accessor seam", () => { fs.rmSync(tempDir, { recursive: true, force: true }); }); + it("exposes the canonical SQLite session lifecycle owners", () => { + expect(applySessionEntryLifecycleMutation).toBe(applySqliteSessionEntryLifecycleMutation); + expect(deleteSessionEntryLifecycle).toBe(deleteSqliteSessionEntryLifecycle); + }); + it("loads, lists, and patches session entries without exposing the file store shape", async () => { const scope = { sessionKey: "agent:main:main", diff --git a/src/config/sessions/session-accessor.types.ts b/src/config/sessions/session-accessor.types.ts index 5e369ba0f001..b903e3dbbd59 100644 --- a/src/config/sessions/session-accessor.types.ts +++ b/src/config/sessions/session-accessor.types.ts @@ -1,8 +1,9 @@ import type { SessionTranscriptUpdate } from "../../sessions/transcript-events.js"; import type { OpenClawConfig } from "../types.openclaw.js"; import type { + DeleteSessionEntryLifecycleParams, DeleteSessionEntryLifecycleResult, - ResetSessionEntryLifecycleMutation, + ResetSessionEntryLifecycleParams, ResetSessionEntryLifecycleResult, DeletedAgentSessionEntryPurgeParams, SessionArchivedTranscriptCleanupRule, @@ -822,7 +823,9 @@ export type SessionPatchProjectionResult Promise | void; - /** Agent owner used to resolve backend transcript artifacts. */ - agentId?: string; - /** Builds the persisted replacement entry from the current backend row. */ - buildNextEntry: (context: { - currentEntry?: SessionEntry; - primaryKey: string; - }) => Promise | SessionEntry; - /** Atomically append this boundary with the reset entry mutation. */ - resetBoundaryReason?: import("./session-reset-boundary-event.js").SessionResetBoundaryReason; - /** Explicit store target for file-backed stores and SQLite migration adapters. */ - storePath: string; - /** Canonical key plus aliases that identify the logical entry. */ - target: SessionLifecycleStoreTarget; -}; - -export type DeleteSessionEntryLifecycleParams = { - /** Agent owner used to resolve backend transcript artifacts. */ - agentId?: string; - /** Whether transcript artifacts should be archived/deleted with the entry. */ - archiveTranscript: boolean; - /** Delete transcript rows without writing an archive artifact. */ - deleteTranscriptWithoutArchive?: boolean; - /** Optional exact row guard checked under the storage writer lock. */ - expectedEntry?: SessionEntry; - /** Optional provider-run identity guard checked under the storage writer lock. */ - expectedSessionId?: string | null; - /** Optional owner revision guard checked under the storage writer lock. */ - expectedLifecycleRevision?: string; - /** Optional persisted revision guard checked under the storage writer lock. */ - expectedUpdatedAt?: number; - /** Fail when the underlying store cannot confirm a durable write. */ - requireWriteSuccess?: boolean; - /** Explicit store target for file-backed stores and SQLite migration adapters. */ - storePath: string; - /** Canonical key plus aliases that identify the logical entry. */ - target: SessionLifecycleStoreTarget; -}; - export type CanonicalizeSessionEntryAliasesResult = { canonicalKey: string; entry?: SessionEntry; diff --git a/src/cron/session-reaper.ts b/src/cron/session-reaper.ts index 72da20ebb5ff..697b3bb69207 100644 --- a/src/cron/session-reaper.ts +++ b/src/cron/session-reaper.ts @@ -135,8 +135,6 @@ export async function sweepCronRunSessions(params: { agentId: params.agentId, storePath, removals, - preserveActiveWork: true, - restrictArchivedTranscriptsToStoreDir: true, ...(archiveRetentionMs == null ? {} : { diff --git a/src/infra/heartbeat-runner-execution.ts b/src/infra/heartbeat-runner-execution.ts index 70dc35552c53..bf50af430333 100644 --- a/src/infra/heartbeat-runner-execution.ts +++ b/src/infra/heartbeat-runner-execution.ts @@ -489,9 +489,9 @@ export async function prepareHeartbeatRunStage(wake: ReadyHeartbeatWake) { ] : []; const lifecycleResult = await applySessionEntryLifecycleMutation({ + activeSessionKey: isolatedSessionKey, storePath: isolatedStorePath, removals, - preserveActiveWork: true, upserts: [ { sessionKey: isolatedSessionKey, @@ -513,7 +513,6 @@ export async function prepareHeartbeatRunStage(wake: ReadyHeartbeatWake) { }, }, ], - restrictArchivedTranscriptsToStoreDir: true, captureArtifactCleanupError: true, }); if (lifecycleResult.artifactCleanupError) {