refactor(sessions): use canonical lifecycle owners (#114936)

This commit is contained in:
Peter Steinberger
2026-07-28 01:19:17 -04:00
committed by GitHub
parent a4d60af42b
commit d894a235e8
8 changed files with 63 additions and 119 deletions
-2
View File
@@ -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 =
@@ -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> | 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> | 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;
@@ -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<T>(
};
}
/** Keeps the broader lifecycle compatibility contract at its public boundary. */
export async function deleteSessionEntryLifecycle(
params: DeleteSessionEntryLifecycleParams,
): Promise<DeleteSessionEntryLifecycleResult> {
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<SessionEntryLifecycleRemoval>;
upserts?: Iterable<SessionEntryLifecycleUpsert>;
activeSessionKey?: string;
maintenanceOverride?: Partial<ResolvedSessionMaintenanceConfig>;
skipMaintenance?: boolean;
preserveActiveWork?: boolean;
archiveReason?: "deleted" | "reset";
restrictArchivedTranscriptsToStoreDir?: boolean;
cleanupArchivedTranscripts?: {
rules: SessionArchivedTranscriptCleanupRule[];
nowMs?: number;
};
pruneUnreferencedArtifacts?: {
olderThanMs: number;
dryRun?: boolean;
};
captureArtifactCleanupError?: boolean;
}): Promise<SessionEntryLifecycleMutationResult> {
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
@@ -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<T> = {
result: T;
};
export type ResetSessionEntryLifecycleParams = {
archivePreviousTranscript?: boolean;
afterEntryMutation?: (mutation: ResetSessionEntryLifecycleMutation) => Promise<void> | void;
agentId?: string;
buildNextEntry: (context: {
currentEntry?: SessionEntry;
primaryKey: string;
}) => Promise<SessionEntry> | 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,
@@ -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",
+4 -44
View File
@@ -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<TFailure extends SessionPatchProjection
| TFailure;
export type {
DeleteSessionEntryLifecycleParams,
DeleteSessionEntryLifecycleResult,
ResetSessionEntryLifecycleParams,
ResetSessionEntryLifecycleResult,
SessionLifecycleArchivedTranscript,
SessionLifecycleArtifactCleanupParams,
@@ -838,49 +841,6 @@ export type {
SessionEntryLifecycleUpsert,
};
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> | 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> | 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;
-2
View File
@@ -135,8 +135,6 @@ export async function sweepCronRunSessions(params: {
agentId: params.agentId,
storePath,
removals,
preserveActiveWork: true,
restrictArchivedTranscriptsToStoreDir: true,
...(archiveRetentionMs == null
? {}
: {
+1 -2
View File
@@ -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) {