| import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; |
| import { resolveTrustedGroupId } from "../../agents/agent-tools.policy.js"; |
| import { clearAllCliSessions } from "../../agents/cli-session.js"; |
| import { buildMainSessionRecoveryClearPatch } from "../../agents/main-session-recovery/main-session-recovery-clear.js"; |
| import { |
| evaluateSessionFreshness, |
| hasTerminalMainSessionTranscriptNewerThanRegistrySync, |
| resolveSessionLifecycleTimestamps, |
| type SessionFreshness, |
| } from "../../config/sessions.js"; |
| import { hasProviderOwnedSession } from "../../config/sessions/entry-freshness.js"; |
| import { resolveSessionEntryAccessTarget } from "../../config/sessions/session-accessor.js"; |
| import { isRecoverableTerminalSessionStatus } from "../../config/sessions/terminal-status.js"; |
| import type { InternalSessionEntry as SessionEntry } from "../../config/sessions/types.js"; |
| import type { OpenClawConfig } from "../../config/types.openclaw.js"; |
| import { |
| isAcpSessionKey, |
| isCronSessionKey, |
| isSubagentSessionKey, |
| parseAgentSessionKey, |
| } from "../../routing/session-key.js"; |
| import { |
| deliveryContextFromSession, |
| mergeDeliveryContext, |
| normalizeSessionDeliveryState, |
| sessionDeliveryOrigin, |
| sessionDeliveryRoute, |
| type DeliveryContext, |
| } from "../../utils/delivery-context.shared.js"; |
| import { resolveSessionStoreKey } from "../session-store-key.js"; |
| import { |
| normalizeTrustedGroupMetadata, |
| requestGroupMatchesTrusted, |
| resolveTrustedGroupMetadata, |
| type TrustedGroupMetadata, |
| } from "./agent-task-tracking.js"; |
|
|
| export type AgentSessionPatchBuild = { |
| patch: Partial<SessionEntry>; |
| spawnedBy: string | undefined; |
| groupId: string | undefined; |
| groupChannel: string | undefined; |
| groupSpace: string | undefined; |
| freshSessionRotatedSinceLoad: boolean; |
| isNewSession: boolean; |
| rotatedSessionId: boolean; |
| usableRequestedSessionId: string | undefined; |
| freshness: SessionFreshness | undefined; |
| }; |
|
|
| type AgentSessionReuseInput = { |
| freshEntry: SessionEntry | undefined; |
| cfg: OpenClawConfig; |
| sessionAgentId: string; |
| canonicalSessionKey: string; |
| storePath: string; |
| expectedExistingSessionId?: string; |
| hasRestoredCronContinuation: boolean; |
| resetPolicy: ReturnType<typeof import("../../config/sessions.js").resolveSessionResetPolicy>; |
| now: number; |
| requestedSessionId?: string; |
| isSystemGatewayRun: boolean; |
| visibleRequest: boolean; |
| failedSessionTranscriptMissing: (entry: SessionEntry | undefined) => boolean; |
| }; |
|
|
| |
| export function evaluateAgentSessionReuse(params: AgentSessionReuseInput) { |
| const lifecycleTimestamps = params.freshEntry |
| ? resolveSessionLifecycleTimestamps({ |
| entry: params.freshEntry, |
| storePath: params.storePath, |
| agentId: params.sessionAgentId, |
| sessionKey: params.canonicalSessionKey, |
| }) |
| : undefined; |
| const skipImplicitExpiry = |
| params.expectedExistingSessionId !== undefined || |
| params.hasRestoredCronContinuation || |
| params.freshEntry?.modelSelectionLocked === true || |
| (params.resetPolicy.configured !== true && hasProviderOwnedSession(params.freshEntry)); |
| const freshness = params.freshEntry |
| ? skipImplicitExpiry |
| ? ({ fresh: true } satisfies SessionFreshness) |
| : evaluateSessionFreshness({ |
| updatedAt: params.freshEntry.updatedAt, |
| ...lifecycleTimestamps, |
| now: params.now, |
| policy: params.resetPolicy, |
| }) |
| : undefined; |
| const requestedSessionMatchesEntry = Boolean( |
| params.requestedSessionId && params.freshEntry?.sessionId?.trim() === params.requestedSessionId, |
| ); |
| const terminalMainTranscriptNewerThanRegistry = |
| params.isSystemGatewayRun || requestedSessionMatchesEntry |
| ? false |
| : hasTerminalMainSessionTranscriptNewerThanRegistrySync({ |
| entry: params.freshEntry, |
| sessionScope: params.cfg.session?.scope, |
| sessionKey: params.canonicalSessionKey, |
| agentId: params.sessionAgentId, |
| mainKey: params.cfg.session?.mainKey, |
| storePath: params.storePath, |
| }); |
| const recoverableTerminalSession = |
| Boolean(params.freshEntry?.sessionId) && |
| params.visibleRequest && |
| isRecoverableTerminalSessionStatus(params.freshEntry?.status); |
| const canReuseSession = |
| Boolean(params.freshEntry?.sessionId) && |
| ((freshness?.fresh ?? false) || recoverableTerminalSession) && |
| !params.failedSessionTranscriptMissing(params.freshEntry) && |
| !terminalMainTranscriptNewerThanRegistry; |
| const usableRequestedSessionId = |
| params.requestedSessionId && (!params.freshEntry?.sessionId || canReuseSession) |
| ? params.requestedSessionId |
| : undefined; |
| const sessionId = |
| usableRequestedSessionId ?? (canReuseSession ? params.freshEntry?.sessionId : undefined); |
| const isNewSession = |
| !params.freshEntry || |
| (!canReuseSession && !usableRequestedSessionId) || |
| Boolean(usableRequestedSessionId && params.freshEntry?.sessionId !== usableRequestedSessionId); |
| return { |
| freshness, |
| recoverableTerminalSession, |
| canReuseSession, |
| usableRequestedSessionId, |
| sessionId, |
| isNewSession, |
| }; |
| } |
|
|
| export function buildAgentSessionPatch( |
| params: AgentSessionReuseInput & { |
| initialEntry: SessionEntry | undefined; |
| normalizedSpawned: { groupId?: string; groupChannel?: string; groupSpace?: string }; |
| requestDeliveryHint: DeliveryContext | undefined; |
| requestLabel?: string; |
| explicitSessionKey?: string; |
| pluginOwnerId?: string; |
| fallbackSessionId: string; |
| touchInteraction: boolean; |
| }, |
| ): AgentSessionPatchBuild { |
| const storedSpawnedBy = normalizeOptionalString(params.freshEntry?.spawnedBy); |
| const freshSpawnedBy = storedSpawnedBy |
| ? resolveSessionStoreKey({ |
| cfg: params.cfg, |
| sessionKey: storedSpawnedBy, |
| storeAgentId: params.sessionAgentId, |
| }) |
| : undefined; |
| const storedGroup = normalizeTrustedGroupMetadata(params.freshEntry); |
| let inheritedGroup: TrustedGroupMetadata | undefined; |
| if ( |
| freshSpawnedBy && |
| (!storedGroup.groupId || !storedGroup.groupChannel || !storedGroup.groupSpace) |
| ) { |
| try { |
| const parentEntry = resolveSessionEntryAccessTarget({ |
| cfg: params.cfg, |
| sessionKey: freshSpawnedBy, |
| }).entry; |
| inheritedGroup = normalizeTrustedGroupMetadata({ |
| groupId: parentEntry?.groupId, |
| groupChannel: parentEntry?.groupChannel, |
| groupSpace: parentEntry?.space, |
| }); |
| } catch (error) { |
| if ((error as { code?: unknown })?.code === "SESSION_CANONICAL_KEY_MIGRATION_REQUIRED") { |
| throw error; |
| } |
| inheritedGroup = undefined; |
| } |
| } |
| const trustedGroup = resolveTrustedGroupMetadata({ |
| sessionKey: params.canonicalSessionKey, |
| spawnedBy: freshSpawnedBy, |
| stored: storedGroup, |
| inherited: inheritedGroup, |
| }); |
| const validatedGroup = trustedGroup.groupId |
| ? resolveTrustedGroupId({ |
| groupId: trustedGroup.groupId, |
| sessionKey: params.canonicalSessionKey, |
| spawnedBy: freshSpawnedBy, |
| }) |
| : undefined; |
| const trustRequestSelectors = |
| Boolean(trustedGroup.groupId) && |
| requestGroupMatchesTrusted({ |
| requestGroupId: params.normalizedSpawned.groupId, |
| trustedGroupId: trustedGroup.groupId, |
| }); |
| const nextGroup = validatedGroup?.dropped |
| ? { groupId: undefined, groupChannel: undefined, groupSpace: undefined } |
| : { |
| groupId: trustedGroup.groupId, |
| groupChannel: |
| trustedGroup.groupChannel ?? |
| (trustRequestSelectors ? params.normalizedSpawned.groupChannel : undefined), |
| groupSpace: |
| trustedGroup.groupSpace ?? |
| (trustRequestSelectors ? params.normalizedSpawned.groupSpace : undefined), |
| }; |
|
|
| const effectiveDelivery = mergeDeliveryContext( |
| deliveryContextFromSession(params.freshEntry), |
| params.requestDeliveryHint, |
| ); |
| const delivery = normalizeSessionDeliveryState({ |
| route: sessionDeliveryRoute(params.freshEntry), |
| context: effectiveDelivery, |
| origin: sessionDeliveryOrigin(params.freshEntry), |
| }); |
| const labelValue = normalizeOptionalString(params.requestLabel) || params.freshEntry?.label; |
| const explicitSessionDisplayName = |
| params.freshEntry === undefined && |
| params.visibleRequest && |
| normalizeOptionalString(params.explicitSessionKey) && |
| !labelValue && |
| !isCronSessionKey(params.canonicalSessionKey) && |
| !isSubagentSessionKey(params.canonicalSessionKey) && |
| !isAcpSessionKey(params.canonicalSessionKey) |
| ? parseAgentSessionKey(params.canonicalSessionKey)?.rest.trim() |
| : undefined; |
| const freshSessionRotatedSinceLoad = Boolean( |
| params.initialEntry?.sessionId && |
| params.freshEntry?.sessionId && |
| params.freshEntry.sessionId !== params.initialEntry.sessionId, |
| ); |
| const reuse = evaluateAgentSessionReuse(params); |
| const freshSessionId = reuse.sessionId ?? params.fallbackSessionId; |
| const freshRotatedSessionId = Boolean( |
| params.freshEntry?.sessionId && params.freshEntry.sessionId !== freshSessionId, |
| ); |
| const patchSessionId = freshSessionRotatedSinceLoad |
| ? params.freshEntry?.sessionId |
| : freshSessionId; |
| const shouldClearRotatedState = freshRotatedSessionId && !freshSessionRotatedSinceLoad; |
| const shouldClearTerminalState = |
| reuse.canReuseSession && |
| reuse.recoverableTerminalSession && |
| !freshSessionRotatedSinceLoad && |
| patchSessionId === params.freshEntry?.sessionId; |
| const automaticRecoveryClearPatch = shouldClearRotatedState |
| ? buildMainSessionRecoveryClearPatch(params.freshEntry) |
| : {}; |
| const patch: Partial<SessionEntry> = { |
| sessionId: patchSessionId, |
| updatedAt: params.now, |
| ...(reuse.isNewSession && !freshSessionRotatedSinceLoad |
| ? { sessionStartedAt: params.now } |
| : {}), |
| ...(params.touchInteraction |
| ? { |
| lastInteractionAt: params.now, |
| |
| |
| agentStatus: undefined, |
| } |
| : {}), |
| ...automaticRecoveryClearPatch, |
| delivery, |
| ...(labelValue ? { label: labelValue } : {}), |
| |
| |
| ...(explicitSessionDisplayName ? { displayName: explicitSessionDisplayName } : {}), |
| ...(freshSpawnedBy ? { spawnedBy: freshSpawnedBy } : {}), |
| groupId: nextGroup.groupId, |
| groupChannel: nextGroup.groupChannel, |
| space: nextGroup.groupSpace, |
| |
| ...(params.freshEntry === undefined && params.pluginOwnerId |
| ? { pluginOwnerId: params.pluginOwnerId } |
| : {}), |
| ...(shouldClearRotatedState || shouldClearTerminalState |
| ? { |
| status: undefined, |
| lifecycleRunId: undefined, |
| lastRunId: undefined, |
| startedAt: undefined, |
| endedAt: undefined, |
| runtimeMs: undefined, |
| abortedLastRun: undefined, |
| } |
| : {}), |
| }; |
| if (shouldClearRotatedState) { |
| clearAllCliSessions(patch); |
| } |
| return { |
| patch, |
| spawnedBy: freshSpawnedBy, |
| groupId: nextGroup.groupId, |
| groupChannel: nextGroup.groupChannel, |
| groupSpace: nextGroup.groupSpace, |
| freshSessionRotatedSinceLoad, |
| isNewSession: reuse.isNewSession, |
| rotatedSessionId: freshRotatedSessionId, |
| usableRequestedSessionId: reuse.usableRequestedSessionId, |
| freshness: reuse.freshness, |
| }; |
| } |
|
|