Files
openclaw/src/plugin-sdk/session-store-runtime-internal.ts
xingzhou 0a868179e4 fix(session): stop repeated restart recovery after retry budget (#96230)
* fix(session): bound restart recovery across gateway restarts

Co-authored-by: Mason Huang <masonxhuang@proton.me>

* style(session): format restart recovery changes

* fix(session): fence recovery notice state

* test(session): align internal recovery types

* test(session): type recovery completion fixture

* style(session): satisfy recovery lint rules

* test(plugin-sdk): split recovery boundary coverage

* test(plugin-sdk): use tracked recovery temp dirs

* fix(session): preserve stale admission errors

* fix(session): close recovery lifecycle races

* fix(session): rotate rejected recovery dispatch ids

* fix(session): enforce recovery quarantine boundaries

* fix(session): close restart recovery exhaustion gaps

* fix(session): preserve recovery command narrowing

* test(session): type restart recovery fixtures

* fix(session): enforce restart recovery quarantine

* fix(session): close restart recovery races

* fix(session): type recovery lifecycle state

* fix(session): enforce recovery ownership fences

* fix(session): fence recovery lifecycle completion

* fix(session): quarantine cross-process recovery

* fix(session): retire stale recovery ownership

* fix(session): restore rejected recovery admission

* fix(session): preserve restored recovery target

* fix(session): retry pending recovery races

* fix(session): preserve concurrent recovery aborts

* fix(session): quarantine recovery lifecycle state

* fix(session): settle exact recovery delivery fences

* fix(session): keep recovery restoration internal

* fix(session): close recovery admission races

* fix(session): retry durable recovery rollback

* fix(session): guard cached recovery settlement

* fix(session): close recovery ownership gaps

* fix(session): resume after delayed recovery rollback

* fix(session): finalize unrecoverable restart state

* fix(session): retry admitted recovery restoration

* fix(session): preserve recovery cleanup guarantees

* fix(session): restore failed terminal settlement

* chore(plugin-sdk): refresh API baseline

* fix(session): close recovery owner retry races

* test(session): type recovery owner fixture

* refactor(session): remove obsolete owner validator

* fix(session): preserve explicit abort recovery

---------

Co-authored-by: Mason Huang <masonxhuang@proton.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-17 15:36:09 +01:00

108 lines
3.8 KiB
TypeScript

import { MAIN_SESSION_RECOVERY_CLEAR_PATCH } from "../agents/main-session-recovery-clear.js";
import type { SessionAccessScope } from "../config/sessions/session-accessor.js";
import type { InternalSessionEntry, SessionEntry } from "../config/sessions/types.js";
export type SessionStoreReadParams = {
agentId?: string;
env?: NodeJS.ProcessEnv;
hydrateSkillPromptRefs?: boolean;
readConsistency?: "latest";
sessionKey: string;
storePath?: string;
};
export function toSessionAccessScope(params: SessionStoreReadParams): SessionAccessScope {
// Keep plugin-facing options separate from internal accessor-only controls.
return {
sessionKey: params.sessionKey,
...(params.agentId !== undefined ? { agentId: params.agentId } : {}),
...(params.env !== undefined ? { env: params.env } : {}),
...(params.hydrateSkillPromptRefs !== undefined
? { hydrateSkillPromptRefs: params.hydrateSkillPromptRefs }
: {}),
...(params.readConsistency !== undefined ? { readConsistency: params.readConsistency } : {}),
...(params.storePath !== undefined ? { storePath: params.storePath } : {}),
};
}
export function projectPluginSessionEntry(entry: InternalSessionEntry): SessionEntry {
const { mainRestartRecovery: _mainRestartRecovery, ...publicEntry } = entry;
return {
...publicEntry,
...(entry.restartRecoveryRuns
? { restartRecoveryRuns: entry.restartRecoveryRuns.map((run) => ({ ...run })) }
: {}),
};
}
export function projectPluginSessionEntryPatch(
patch: Partial<InternalSessionEntry>,
): Partial<SessionEntry> {
const { mainRestartRecovery: _mainRestartRecovery, ...publicPatch } = patch;
return publicPatch;
}
export function projectPluginSessionStore(
store: Record<string, InternalSessionEntry>,
): Record<string, SessionEntry> {
return Object.fromEntries(
Object.entries(store).map(([sessionKey, entry]) => [
sessionKey,
projectPluginSessionEntry(entry),
]),
);
}
export function activeRecoveryFieldsForSameSession(
existingEntry: InternalSessionEntry | undefined,
nextSessionId: string | undefined,
): Partial<InternalSessionEntry> | undefined {
if (
!existingEntry ||
existingEntry.sessionId !== nextSessionId ||
existingEntry.mainRestartRecovery === undefined
) {
return undefined;
}
return {
abortedLastRun: existingEntry.abortedLastRun,
restartRecoveryRuns: existingEntry.restartRecoveryRuns,
mainRestartRecovery: existingEntry.mainRestartRecovery,
};
}
export function clearRecoveryStateForRotatedSessionPatch(
existingEntry: InternalSessionEntry,
publicPatch: Partial<SessionEntry>,
): Partial<InternalSessionEntry> {
return Object.hasOwn(publicPatch, "sessionId") &&
publicPatch.sessionId !== existingEntry.sessionId
? { ...publicPatch, ...MAIN_SESSION_RECOVERY_CLEAR_PATCH }
: publicPatch;
}
export function reconcilePluginSessionStore(params: {
internalStore: Record<string, InternalSessionEntry>;
publicStore: Record<string, SessionEntry>;
}): void {
for (const sessionKey of Object.keys(params.internalStore)) {
if (!Object.hasOwn(params.publicStore, sessionKey)) {
delete params.internalStore[sessionKey];
}
}
for (const [sessionKey, publicEntry] of Object.entries(params.publicStore)) {
const projectedEntry = projectPluginSessionEntry(publicEntry as InternalSessionEntry);
const existingRecovery = activeRecoveryFieldsForSameSession(
params.internalStore[sessionKey],
projectedEntry.sessionId,
);
const existingEntry = params.internalStore[sessionKey];
params.internalStore[sessionKey] =
existingEntry && existingEntry.sessionId !== projectedEntry.sessionId
? { ...projectedEntry, ...MAIN_SESSION_RECOVERY_CLEAR_PATCH }
: existingRecovery
? { ...projectedEntry, ...existingRecovery }
: projectedEntry;
}
}