
















@@ -35,6 +35,7 @@ import {
3535} from "../../config/sessions/session-accessor.js";
3636import { resolveSessionKey } from "../../config/sessions/session-key.js";
3737import { resolveMaintenanceConfigFromInput } from "../../config/sessions/store-maintenance.js";
38+import { runExclusiveSessionStoreWrite } from "../../config/sessions/store-writer.js";
3839import { parseSessionThreadInfoFast } from "../../config/sessions/thread-info.js";
3940import {
4041DEFAULT_RESET_TRIGGERS,
@@ -186,6 +187,14 @@ export type InitSessionStateParams = {
186187resumeRequestedSession?: boolean;
187188};
188189190+type InitSessionStateAttemptContext = {
191+agentId: string;
192+conversationBindingContext: ReturnType<typeof resolveSessionConversationBindingContext>;
193+isSystemEvent: boolean;
194+sessionCtxForState: MsgContext;
195+storePath: string;
196+};
197+189198function resolveSessionConversationBindingContext(
190199cfg: OpenClawConfig,
191200ctx: MsgContext,
@@ -244,32 +253,19 @@ function resolveBoundConversationSessionKey(params: {
244253return binding.targetSessionKey;
245254}
246255247-/** Initializes or reuses the reply session state for one inbound turn. */
248-export async function initSessionState(params: InitSessionStateParams): Promise<SessionInitResult> {
249-return await initSessionStateAttempt(params, false);
250-}
251-252-async function initSessionStateAttempt(
256+function resolveInitSessionStateAttemptContext(
253257params: InitSessionStateParams,
254-staleSnapshotRetried: boolean,
255-): Promise<SessionInitResult> {
256-const { ctx, cfg, commandAuthorized } = params;
257-// Heartbeat, cron-event, and exec-event runs should NEVER trigger session
258-// resets or conversation binding retargeting. These are automated system
259-// events, not user interactions that should affect session continuity.
260-// See #58409 for details on silent session reset bug.
258+): InitSessionStateAttemptContext {
259+const { cfg, ctx } = params;
260+// Automated system events must not reset sessions or retarget conversation bindings.
261261const isSystemEvent =
262262ctx.Provider === "heartbeat" || ctx.Provider === "cron-event" || ctx.Provider === "exec-event";
263263const conversationBindingContext = isSystemEvent
264264 ? null
265265 : resolveSessionConversationBindingContext(cfg, ctx);
266-// Native slash commands (Telegram/Discord/Slack) are delivered on a separate
267-// "slash session" key, but should mutate the target chat session.
266+// Slash/menu commands may arrive on a transport session while targeting the chat session.
267+// Prefer explicit command target before binding lookup so command mutations land there.
268268const commandTargetSessionKey = resolveCommandTurnTargetSessionKey(ctx);
269-// Native slash/menu commands can arrive on a transport-specific "slash session"
270-// while explicitly targeting an existing chat session. Honor that explicit target
271-// before any binding lookup so command-side mutations land on the intended session.
272-// Priority: commandTargetSessionKey > boundConversation > route.
273269const targetSessionKey =
274270commandTargetSessionKey ??
275271resolveBoundConversationSessionKey({
@@ -281,20 +277,54 @@ async function initSessionStateAttempt(
281277targetSessionKey && targetSessionKey !== ctx.SessionKey
282278 ? { ...ctx, SessionKey: targetSessionKey }
283279 : ctx;
284-const sessionCfg = cfg.session;
285-const maintenanceConfig = resolveMaintenanceConfigFromInput(sessionCfg?.maintenance);
286-const mainKey = normalizeMainKey(sessionCfg?.mainKey);
287280const agentId = resolveSessionAgentId({
288281sessionKey: sessionCtxForState.SessionKey,
289282config: cfg,
290283fallbackAgentId: sessionCtxForState.AgentId,
291284});
285+return {
286+ agentId,
287+ conversationBindingContext,
288+ isSystemEvent,
289+ sessionCtxForState,
290+storePath: resolveStorePath(cfg.session?.store, { agentId }),
291+};
292+}
293+294+/** Initializes or reuses the reply session state for one inbound turn. */
295+export async function initSessionState(params: InitSessionStateParams): Promise<SessionInitResult> {
296+return await initSessionStateAttempt(params, false);
297+}
298+299+async function initSessionStateAttempt(
300+params: InitSessionStateParams,
301+staleSnapshotRetried: boolean,
302+): Promise<SessionInitResult> {
303+const attemptContext = resolveInitSessionStateAttemptContext(params);
304+// Guarded revision checks only serialize correctly when the snapshot and
305+// commit share the same writer lane.
306+return await runExclusiveSessionStoreWrite(
307+attemptContext.storePath,
308+async () => await initSessionStateAttemptLocked(params, attemptContext, staleSnapshotRetried),
309+);
310+}
311+312+async function initSessionStateAttemptLocked(
313+params: InitSessionStateParams,
314+attemptContext: InitSessionStateAttemptContext,
315+staleSnapshotRetried: boolean,
316+): Promise<SessionInitResult> {
317+const { ctx, cfg, commandAuthorized } = params;
318+const { agentId, conversationBindingContext, isSystemEvent, sessionCtxForState, storePath } =
319+attemptContext;
320+const sessionCfg = cfg.session;
321+const maintenanceConfig = resolveMaintenanceConfigFromInput(sessionCfg?.maintenance);
322+const mainKey = normalizeMainKey(sessionCfg?.mainKey);
292323const groupResolution = resolveGroupSessionKey(sessionCtxForState) ?? undefined;
293324const resetTriggers = sessionCfg?.resetTriggers?.length
294325 ? sessionCfg.resetTriggers
295326 : DEFAULT_RESET_TRIGGERS;
296327const sessionScope = sessionCfg?.scope ?? "per-sender";
297-const storePath = resolveStorePath(sessionCfg?.store, { agentId });
298328const ingressTimingEnabled = process.env.OPENCLAW_DEBUG_INGRESS_TIMING === "1";
299329300330let sessionEntry: SessionEntry;
@@ -858,7 +888,7 @@ async function initSessionStateAttempt(
858888});
859889if (!committed.ok) {
860890if (!staleSnapshotRetried) {
861-return await initSessionStateAttempt(params, true);
891+return await initSessionStateAttemptLocked(params, attemptContext, true);
862892}
863893throw new Error(`reply session initialization conflicted for ${sessionKey}`);
864894}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。