mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
refactor(sessions): use canonical lifecycle owners (#114936)
This commit is contained in:
committed by
GitHub
parent
a4d60af42b
commit
d894a235e8
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -135,8 +135,6 @@ export async function sweepCronRunSessions(params: {
|
||||
agentId: params.agentId,
|
||||
storePath,
|
||||
removals,
|
||||
preserveActiveWork: true,
|
||||
restrictArchivedTranscriptsToStoreDir: true,
|
||||
...(archiveRetentionMs == null
|
||||
? {}
|
||||
: {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user