openclaw / src /agents /command /session.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
e249c6d verified
Raw
History Blame Contribute Delete
25.6 kB
/**
* Resolves command session ids, keys, stores, and persisted thinking state.
*/
import crypto from "node:crypto";
import path from "node:path";
import type { MsgContext } from "../../auto-reply/templating.js";
import {
normalizeThinkLevel,
normalizeVerboseLevel,
type ThinkLevel,
type VerboseLevel,
} from "../../auto-reply/thinking.js";
import { tryResolveLegacyCompatibilityAgentId } from "../../config/legacy.default-agent-owner.js";
import { hasProviderOwnedSession } from "../../config/sessions/entry-freshness.js";
import { isInternalSessionEffectsKey } from "../../config/sessions/internal-session-key.js";
import {
hasTerminalMainSessionTranscriptNewerThanRegistrySync,
resolveSessionLifecycleTimestamps,
} from "../../config/sessions/lifecycle.js";
import {
canonicalizeMainSessionAlias,
resolveAgentIdFromSessionKey,
resolveExplicitAgentSessionKey,
} from "../../config/sessions/main-session.js";
import { resolveSessionStorePathCore } from "../../config/sessions/paths.js";
import {
evaluateSessionFreshness,
resolveSessionResetPolicy,
} from "../../config/sessions/reset-policy.js";
import { resolveChannelResetConfig, resolveSessionResetType } from "../../config/sessions/reset.js";
import {
listSessionEntriesReadOnly,
loadExactSessionEntryReadOnly,
type SessionEntrySummary,
} from "../../config/sessions/session-accessor.js";
import { resolveSessionKey } from "../../config/sessions/session-key.js";
import {
resolvePersistedSessionStoreOwner,
resolvePersistedSessionStoreOwnerForKey,
} from "../../config/sessions/session-store-owner.js";
import { normalizeStoreSessionKey } from "../../config/sessions/store-entry.js";
import type { InternalSessionEntry } from "../../config/sessions/types.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import {
classifySessionKeyShape,
isUnscopedSessionKeySentinel,
normalizeAgentId,
normalizeMainKey,
parseAgentSessionKey,
} from "../../routing/session-key.js";
import { isModelSelectionLocked } from "../../sessions/model-overrides.js";
import { resolveSessionIdMatchSelection } from "../../sessions/session-id-resolution.js";
import { sessionDeliveryChannel } from "../../utils/delivery-context.shared.js";
import {
AgentSelectionRequiredError,
listAgentIds,
resolveDefaultAgentId,
} from "../agent-scope.js";
import { clearBootstrapSnapshotOnSessionRollover } from "../bootstrap-cache.js";
import { clearAllCliSessions } from "../cli-session.js";
import { transitionMainSessionRecovery } from "../main-session-recovery/main-session-recovery-state.js";
/** Resolved command session identity plus backing store metadata. */
type SessionResolution = {
sessionId: string;
sessionKey?: string;
sessionEntry?: InternalSessionEntry;
storePath: string;
isNewSession: boolean;
previousSessionId?: string;
persistedThinking?: ThinkLevel;
persistedVerbose?: VerboseLevel;
};
type SessionKeyResolution = {
agentId?: string;
sessionKey?: string;
sessionEntry?: InternalSessionEntry;
storePath: string;
};
export function clearRotatedSessionMetadata(entry: InternalSessionEntry): InternalSessionEntry {
const next = {
...entry,
sessionFile: undefined,
status: undefined,
lifecycleRunId: undefined,
lastRunId: undefined,
startedAt: undefined,
endedAt: undefined,
runtimeMs: undefined,
abortedLastRun: undefined,
restartRecoveryForceSafeTools: undefined,
restartRecoveryDeliveryContext: undefined,
restartRecoveryDeliveryMediaUrls: undefined,
restartRecoveryDisableMessageTool: undefined,
restartRecoverySuppressTextDelivery: undefined,
restartRecoveryDeliveryRequestFingerprint: undefined,
restartRecoveryDeliveryRunId: undefined,
restartRecoveryDeliverySourceRunId: undefined,
restartRecoveryBeforeAgentReplyState: undefined,
restartRecoveryDeliveryReceiptState: undefined,
restartRecoveryDeliveryToolCallId: undefined,
restartRecoveryRequesterAccountId: undefined,
restartRecoveryRequesterSenderId: undefined,
restartRecoverySameChannelThreadRequired: undefined,
restartRecoverySourceIngress: undefined,
restartRecoverySourceReplyDeliveryMode: undefined,
restartRecoveryTerminalDeliveryEvidence: undefined,
restartRecoveryTerminalRunIds: undefined,
sessionStartedAt: undefined,
sessionDiffBaseline: undefined,
sessionDiffBaselineCapture: undefined,
lastInteractionAt: undefined,
pendingTranscriptRepair: undefined,
};
transitionMainSessionRecovery(next, { kind: "clear" });
clearAllCliSessions(next);
return next;
}
type SessionIdMatchSet = {
candidates: SessionIdMatchCandidate[];
ownerConflict: boolean;
};
type SessionIdMatchCandidate = {
sessionKey: string;
entry: InternalSessionEntry;
resolution: Omit<SessionKeyResolution, "sessionEntry">;
primary: boolean;
};
function selectSessionIdMatchCandidate(
candidates: SessionIdMatchCandidate[],
sessionId: string,
): SessionIdMatchCandidate | undefined {
const selection = resolveSessionIdMatchSelection(
candidates.map((candidate) => [candidate.sessionKey, candidate.entry]),
sessionId,
);
if (selection.kind !== "selected") {
return undefined;
}
return candidates
.filter((candidate) => candidate.sessionKey === selection.sessionKey)
.toSorted((left, right) => {
const updatedAt = (right.entry.updatedAt ?? 0) - (left.entry.updatedAt ?? 0);
if (updatedAt !== 0) {
return updatedAt;
}
if (left.primary !== right.primary) {
return left.primary ? -1 : 1;
}
return (left.resolution.agentId ?? "").localeCompare(right.resolution.agentId ?? "");
})[0];
}
function loadCommandSessionEntries(params: {
agentId?: string;
storePath: string;
}): SessionEntrySummary[] {
return listSessionEntriesReadOnly({
storePath: params.storePath,
...(params.agentId ? { agentId: params.agentId } : {}),
clone: false,
});
}
/** Builds the synthetic session key used for explicit session-id runs. */
export function buildExplicitSessionIdSessionKey(params: {
sessionId: string;
agentId?: string;
}): string {
return `agent:${normalizeAgentId(params.agentId)}:explicit:${params.sessionId.trim()}`;
}
function collectSessionIdMatchesForRequest(opts: {
cfg: OpenClawConfig;
sessionEntries: SessionEntrySummary[];
storePath: string;
storeAgentId?: string;
sessionId: string;
searchOtherAgentStores: boolean;
}): SessionIdMatchSet {
const candidates: SessionIdMatchCandidate[] = [];
let ownerConflict = false;
const configuredAgentIds = listAgentIds(opts.cfg).map(normalizeAgentId);
const compatibilityAgentId = tryResolveLegacyCompatibilityAgentId(opts.cfg);
const persistedStoreOwner = resolvePersistedSessionStoreOwner(opts.cfg);
const configuredStoreOwners = new Map<string, Set<string>>();
for (const agentId of configuredAgentIds) {
const configuredStorePath = path.resolve(
resolveSessionStorePathCore(opts.cfg.session?.store, { agentId }),
);
const owners = configuredStoreOwners.get(configuredStorePath) ?? new Set<string>();
owners.add(agentId);
configuredStoreOwners.set(configuredStorePath, owners);
}
const addMatches = (
candidateEntries: SessionEntrySummary[],
candidateStorePath: string,
candidateAgentId: string | undefined,
options?: { primary?: boolean },
): void => {
for (const { sessionKey: candidateKey, entry: candidateEntry } of candidateEntries) {
if (candidateEntry?.sessionId !== opts.sessionId) {
continue;
}
const normalizedCandidateAgentId = candidateAgentId
? normalizeAgentId(candidateAgentId)
: undefined;
const scopedCandidateAgentId =
normalizedCandidateAgentId && configuredAgentIds.includes(normalizedCandidateAgentId)
? normalizedCandidateAgentId
: undefined;
const pathOwners = configuredStoreOwners.get(path.resolve(candidateStorePath));
const pathOwnedAgentId =
pathOwners?.size === 1 ? pathOwners.values().next().value : undefined;
const parsedAgentId = parseAgentSessionKey(candidateKey)?.agentId;
const normalizedParsedAgentId = parsedAgentId ? normalizeAgentId(parsedAgentId) : undefined;
if (normalizedParsedAgentId && !configuredAgentIds.includes(normalizedParsedAgentId)) {
continue;
}
const isLegacyUnscopedKey = classifySessionKeyShape(candidateKey) === "legacy_or_alias";
// A persisted fixed-store owner is authoritative even after retirement: retired rows stay
// unavailable instead of being reassigned by path cardinality or scan order.
const legacyUnscopedOwner = isLegacyUnscopedKey
? persistedStoreOwner.kind === "configured"
? persistedStoreOwner.agentId
: persistedStoreOwner.kind === "retired"
? undefined
: (pathOwnedAgentId ??
(opts.searchOtherAgentStores ? undefined : scopedCandidateAgentId) ??
compatibilityAgentId)
: undefined;
const matchedAgentId =
normalizedParsedAgentId ??
(isLegacyUnscopedKey
? legacyUnscopedOwner
: (scopedCandidateAgentId ?? compatibilityAgentId));
if (isLegacyUnscopedKey && persistedStoreOwner.kind === "retired") {
ownerConflict = true;
continue;
}
if (
!opts.searchOtherAgentStores &&
scopedCandidateAgentId &&
matchedAgentId &&
normalizeAgentId(matchedAgentId) !== scopedCandidateAgentId
) {
ownerConflict = true;
continue;
}
candidates.push({
sessionKey: candidateKey,
entry: candidateEntry,
primary: options?.primary === true,
resolution: {
...(matchedAgentId ? { agentId: normalizeAgentId(matchedAgentId) } : {}),
sessionKey: candidateKey,
storePath: candidateStorePath,
},
});
}
};
addMatches(opts.sessionEntries, opts.storePath, opts.storeAgentId, { primary: true });
if (!opts.searchOtherAgentStores) {
return { candidates, ownerConflict };
}
for (const agentId of configuredAgentIds) {
if (agentId === opts.storeAgentId) {
continue;
}
const candidateStorePath = resolveSessionStorePathCore(opts.cfg.session?.store, { agentId });
addMatches(
loadCommandSessionEntries({
agentId,
storePath: candidateStorePath,
}),
candidateStorePath,
agentId,
);
}
return { candidates, ownerConflict };
}
/**
* Resolve an existing stored session key for a session id from a specific agent store.
* This scopes the lookup to the target store without implicitly converting `agentId`
* into that agent's main session key.
*/
export function resolveStoredSessionKeyForSessionId(opts: {
cfg: OpenClawConfig;
sessionId: string;
agentId?: string;
}): SessionKeyResolution {
const sessionId = opts.sessionId.trim();
const requestedAgentId = opts.agentId?.trim() ? normalizeAgentId(opts.agentId) : undefined;
const persistedStoreOwner = resolvePersistedSessionStoreOwner(opts.cfg);
const storeAgentId =
requestedAgentId ??
(persistedStoreOwner.kind === "configured" ? persistedStoreOwner.agentId : undefined) ??
tryResolveLegacyCompatibilityAgentId(opts.cfg) ??
resolveDefaultAgentId(opts.cfg, {
surface: "stored session lookup",
hint: "Pass an explicit agent id when looking up a session by id.",
});
const storePath = resolveSessionStorePathCore(opts.cfg.session?.store, {
agentId: storeAgentId,
});
const sessionEntries = loadCommandSessionEntries({
storePath,
agentId: storeAgentId,
});
if (!sessionId) {
return { sessionKey: undefined, storePath };
}
const resolveMatchedAgentId = (sessionKey: string): string | undefined => {
const scopedAgentId = parseAgentSessionKey(sessionKey)?.agentId;
if (scopedAgentId) {
return normalizeAgentId(scopedAgentId);
}
const persistedRowOwner = resolvePersistedSessionStoreOwnerForKey(opts.cfg, sessionKey);
return persistedRowOwner.kind === "configured"
? persistedRowOwner.agentId
: persistedRowOwner.kind === "retired"
? undefined
: (requestedAgentId ?? tryResolveLegacyCompatibilityAgentId(opts.cfg));
};
const sessionIdMatches = sessionEntries.filter(({ entry }) => entry.sessionId === sessionId);
const selectionMatches = requestedAgentId
? sessionIdMatches.filter(
({ sessionKey }) => resolveMatchedAgentId(sessionKey) === requestedAgentId,
)
: sessionIdMatches;
if (requestedAgentId && selectionMatches.length === 0 && sessionIdMatches.length > 0) {
throw new AgentSelectionRequiredError(listAgentIds(opts.cfg), {
surface: `stored session id "${sessionId}"`,
hint: `The matching rows belong to a different agent than agent "${requestedAgentId}".`,
});
}
const selection = resolveSessionIdMatchSelection(
selectionMatches.map(({ sessionKey, entry }) => [sessionKey, entry]),
sessionId,
);
if (selection.kind !== "selected") {
return { agentId: requestedAgentId, sessionKey: undefined, storePath };
}
const sessionKey = selection.sessionKey;
const persistedRowOwner = resolvePersistedSessionStoreOwnerForKey(opts.cfg, sessionKey);
const resolvedAgentId = resolveMatchedAgentId(sessionKey);
if (!resolvedAgentId) {
throw new AgentSelectionRequiredError(listAgentIds(opts.cfg), {
surface: `stored session key "${sessionKey}"`,
hint:
persistedRowOwner.kind === "retired"
? `The shared fixed-store row belongs to retired agent "${persistedRowOwner.agentId}".`
: "Pass an explicit agent id when looking up an unscoped session by id.",
});
}
if (requestedAgentId && requestedAgentId !== resolvedAgentId) {
throw new AgentSelectionRequiredError(listAgentIds(opts.cfg), {
surface: `stored session key "${sessionKey}"`,
hint: `The matching row belongs to agent "${resolvedAgentId}", not agent "${requestedAgentId}".`,
});
}
return {
agentId: resolvedAgentId,
sessionKey,
sessionEntry: structuredClone(
selectionMatches.find((match) => match.sessionKey === sessionKey)?.entry,
),
storePath,
};
}
function resolveSessionKeyForRequestInternal(opts: {
cfg: OpenClawConfig;
to?: string;
sessionId?: string;
sessionKey?: string;
agentId?: string;
createMissingSessionId: boolean;
}): SessionKeyResolution {
const sessionCfg = opts.cfg.session;
const scope = sessionCfg?.scope ?? "per-sender";
const mainKey = normalizeMainKey(sessionCfg?.mainKey);
const requestedAgentId = opts.agentId?.trim() ? normalizeAgentId(opts.agentId) : undefined;
const requestedSessionId = opts.sessionId?.trim() || undefined;
const requestedSessionKey = opts.sessionKey?.trim() || undefined;
const toSessionKey =
!requestedSessionKey && !requestedSessionId && classifySessionKeyShape(opts.to) === "agent"
? opts.to?.trim()
: undefined;
const explicitSessionKey =
requestedSessionKey ||
toSessionKey ||
(!requestedSessionId
? resolveExplicitAgentSessionKey({
cfg: opts.cfg,
agentId: requestedAgentId,
})
: undefined);
const scopedSessionAgentId = parseAgentSessionKey(explicitSessionKey)?.agentId;
const explicitKeyStoreOwner = resolvePersistedSessionStoreOwnerForKey(
opts.cfg,
explicitSessionKey,
);
if (
explicitKeyStoreOwner.kind === "configured" &&
requestedAgentId &&
requestedAgentId !== explicitKeyStoreOwner.agentId
) {
throw new AgentSelectionRequiredError(listAgentIds(opts.cfg), {
surface: `session key "${explicitSessionKey}"`,
hint: `The shared fixed-store row belongs to agent "${explicitKeyStoreOwner.agentId}", not --agent "${requestedAgentId}".`,
});
}
if (explicitKeyStoreOwner.kind === "retired") {
throw new AgentSelectionRequiredError(listAgentIds(opts.cfg), {
surface: `session key "${explicitSessionKey}"`,
hint: `The shared fixed-store row belongs to retired agent "${explicitKeyStoreOwner.agentId}".`,
});
}
const knownAgentId =
requestedAgentId ??
scopedSessionAgentId ??
(explicitKeyStoreOwner.kind === "configured" ? explicitKeyStoreOwner.agentId : undefined) ??
tryResolveLegacyCompatibilityAgentId(opts.cfg);
const unownedBareSessionKey = Boolean(
requestedSessionId &&
explicitSessionKey &&
classifySessionKeyShape(explicitSessionKey) === "legacy_or_alias" &&
!knownAgentId,
);
// A session id is already an explicit target: seed only its store scan from a live roster owner.
// The anchor is not resolved ownership and must never escape through the returned resolution.
const sessionIdScanAnchor = requestedSessionId
? (knownAgentId ?? listAgentIds(opts.cfg)[0])
: undefined;
const defaultAgentId = knownAgentId
? normalizeAgentId(knownAgentId)
: requestedSessionId
? undefined
: normalizeAgentId(
resolveDefaultAgentId(opts.cfg, {
surface: "agent command session routing",
hint: "Pass --agent <id> or an agent-prefixed --session-key.",
}),
);
const storeAgentId = explicitSessionKey
? unownedBareSessionKey
? sessionIdScanAnchor
: isUnscopedSessionKeySentinel(explicitSessionKey)
? (requestedAgentId ?? defaultAgentId)
: resolveAgentIdFromSessionKey(explicitSessionKey, defaultAgentId)
: (requestedAgentId ?? defaultAgentId ?? sessionIdScanAnchor);
if (!storeAgentId) {
throw new AgentSelectionRequiredError(listAgentIds(opts.cfg), {
surface: "agent command session routing",
hint: "Pass --agent <id> or an agent-prefixed --session-key.",
});
}
const storePath = resolveSessionStorePathCore(sessionCfg?.store, {
agentId: storeAgentId,
});
const ctx: MsgContext | undefined = opts.to?.trim() ? { From: opts.to } : undefined;
let sessionKey: string | undefined =
(!unownedBareSessionKey && explicitSessionKey
? canonicalizeMainSessionAlias({
cfg: opts.cfg,
agentId: storeAgentId,
sessionKey: explicitSessionKey,
})
: undefined) ??
(!unownedBareSessionKey && ctx
? resolveSessionKey(scope, ctx, mainKey, storeAgentId)
: undefined);
// Command preparation needs one owned entry. Exact reads preserve the SQLite target and
// Doctor guards without enumerating the agent store or exposing hidden run-owned rows.
// Exclusion and lookup share the persisted locator; routing keeps the request key.
const storeSessionKey = sessionKey ? normalizeStoreSessionKey(sessionKey) : undefined;
const sessionEntry =
storeSessionKey && !isInternalSessionEffectsKey(storeSessionKey)
? loadExactSessionEntryReadOnly({
agentId: storeAgentId,
storePath,
sessionKey: storeSessionKey,
})?.entry
: undefined;
// If a session id was provided, prefer to re-use its existing entry (by id) even when no key was
// derived. When duplicates exist across agent stores, pick the same deterministic best match used
// by the shared gateway/session resolver helpers instead of whichever store happens to be scanned
// first.
if (
requestedSessionId &&
(!explicitSessionKey || unownedBareSessionKey) &&
(!sessionKey || sessionEntry?.sessionId !== requestedSessionId)
) {
const { candidates, ownerConflict } = collectSessionIdMatchesForRequest({
cfg: opts.cfg,
sessionEntries: loadCommandSessionEntries({ storePath, agentId: storeAgentId }),
storePath,
storeAgentId,
sessionId: requestedSessionId,
searchOtherAgentStores: requestedAgentId === undefined,
});
const selectedMatch = selectSessionIdMatchCandidate(
candidates.filter((candidate) => candidate.resolution.agentId !== undefined),
requestedSessionId,
);
if (selectedMatch) {
return {
...selectedMatch.resolution,
sessionEntry: structuredClone(selectedMatch.entry),
};
}
if (ownerConflict) {
throw new AgentSelectionRequiredError(listAgentIds(opts.cfg), {
surface: `session id "${requestedSessionId}"`,
hint: requestedAgentId
? `The matching session belongs to a different agent than --agent "${requestedAgentId}".`
: "The matching unscoped session belongs to a retired fixed-store owner.",
});
}
}
if (requestedSessionId && !sessionKey && opts.createMissingSessionId) {
const explicitSessionAgentId =
requestedAgentId ??
tryResolveLegacyCompatibilityAgentId(opts.cfg) ??
resolveDefaultAgentId(opts.cfg, {
surface: "agent command session creation",
hint: "Pass --agent <id> when creating a session from --session-id.",
});
sessionKey = buildExplicitSessionIdSessionKey({
sessionId: requestedSessionId,
agentId: explicitSessionAgentId,
});
return {
agentId: explicitSessionAgentId,
sessionKey,
storePath,
};
}
return { agentId: storeAgentId, sessionKey, sessionEntry, storePath };
}
/** Resolves an existing session-id row across agent stores without creating a fallback key. */
export function resolveExistingSessionKeyForRequest(opts: {
cfg: OpenClawConfig;
sessionId: string;
agentId?: string;
}): SessionKeyResolution {
return resolveSessionKeyForRequestInternal({ ...opts, createMissingSessionId: false });
}
/** Resolves the session key/store targeted by one command request. */
function resolveSessionKeyForRequest(opts: {
cfg: OpenClawConfig;
to?: string;
sessionId?: string;
sessionKey?: string;
agentId?: string;
}): SessionKeyResolution {
return resolveSessionKeyForRequestInternal({ ...opts, createMissingSessionId: true });
}
/** Core alias retained for runtime owners that bypass the public library facade. */
export function resolveSessionKeyForRequestCore(
opts: Parameters<typeof resolveSessionKeyForRequest>[0],
): SessionKeyResolution {
return resolveSessionKeyForRequest(opts);
}
/** Resolves or creates the session used by one agent command request. */
export function resolveSession(opts: {
cfg: OpenClawConfig;
to?: string;
sessionId?: string;
sessionKey?: string;
agentId?: string;
}): SessionResolution {
const sessionCfg = opts.cfg.session;
const {
agentId: resolvedAgentId,
sessionKey,
sessionEntry,
storePath,
} = resolveSessionKeyForRequestCore({
cfg: opts.cfg,
to: opts.to,
sessionId: opts.sessionId,
sessionKey: opts.sessionKey,
agentId: opts.agentId,
});
const now = Date.now();
const sessionAgentId =
(opts.agentId?.trim() ? normalizeAgentId(opts.agentId) : undefined) ??
resolvedAgentId ??
parseAgentSessionKey(sessionKey)?.agentId ??
tryResolveLegacyCompatibilityAgentId(opts.cfg) ??
resolveDefaultAgentId(opts.cfg, {
surface: "agent command session ownership",
hint: "Pass --agent <id> or an agent-prefixed --session-key.",
});
const resetType = resolveSessionResetType({ sessionKey });
const channelReset = resolveChannelResetConfig({
sessionCfg,
channel: sessionDeliveryChannel(sessionEntry),
});
const resetPolicy = resolveSessionResetPolicy({
sessionCfg,
resetType,
resetOverride: channelReset,
});
const requestedSessionId = opts.sessionId?.trim() || undefined;
const terminalMainTranscriptNewerThanRegistry =
sessionEntry && !requestedSessionId
? hasTerminalMainSessionTranscriptNewerThanRegistrySync({
entry: sessionEntry,
sessionScope: sessionCfg?.scope,
sessionKey,
agentId: sessionAgentId,
mainKey: sessionCfg?.mainKey,
storePath,
})
: false;
const lockedModelSelection = isModelSelectionLocked(sessionEntry);
const skipImplicitExpiry =
resetPolicy.configured !== true && hasProviderOwnedSession(sessionEntry);
const fresh = sessionEntry
? lockedModelSelection ||
(!terminalMainTranscriptNewerThanRegistry &&
(skipImplicitExpiry ||
evaluateSessionFreshness({
updatedAt: sessionEntry.updatedAt,
...resolveSessionLifecycleTimestamps({
entry: sessionEntry,
agentId: sessionAgentId,
sessionKey,
storePath,
}),
now,
policy: resetPolicy,
}).fresh))
: false;
const sessionId =
requestedSessionId || (fresh ? sessionEntry?.sessionId : undefined) || crypto.randomUUID();
const isNewSession = !fresh && !requestedSessionId;
const resolvedSessionEntry =
isNewSession && sessionEntry ? clearRotatedSessionMetadata(sessionEntry) : sessionEntry;
clearBootstrapSnapshotOnSessionRollover({
sessionKey,
previousSessionId: isNewSession ? sessionEntry?.sessionId : undefined,
});
// Behavior overrides belong to the logical session, not one transcript id.
// Carry them across every rollover; explicit `default` directives clear them.
const persistedThinking = sessionEntry?.thinkingLevel
? normalizeThinkLevel(sessionEntry.thinkingLevel)
: undefined;
const persistedVerbose = sessionEntry?.verboseLevel
? normalizeVerboseLevel(sessionEntry.verboseLevel)
: undefined;
return {
sessionId,
sessionKey,
sessionEntry: resolvedSessionEntry,
storePath,
isNewSession,
previousSessionId: isNewSession ? sessionEntry?.sessionId : undefined,
persistedThinking,
persistedVerbose,
};
}