Skip to content

Commit acc2a0e

Browse files
authored
refactor: route boot session mapping through accessor (#96225)
1 parent 704fc35 commit acc2a0e

4 files changed

Lines changed: 164 additions & 103 deletions

File tree

scripts/check-session-accessor-boundary.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export const migratedSessionAccessorFiles = new Set([
107107
"src/gateway/sessions-history-http.ts",
108108
"src/gateway/session-utils.ts",
109109
"src/gateway/managed-image-attachments.ts",
110+
"src/gateway/boot.ts",
110111
"src/gateway/server-methods/artifacts.ts",
111112
"src/gateway/server-methods/chat.ts",
112113
"src/gateway/sessions-resolve.ts",
@@ -163,6 +164,7 @@ export const migratedSessionAccessorWriteFiles = new Set([
163164
"src/auto-reply/reply/session-usage.ts",
164165
"src/commands/tasks.ts",
165166
"src/config/sessions/cleanup-service.ts",
167+
"src/gateway/boot.ts",
166168
"src/gateway/server-node-events.ts",
167169
"src/gateway/session-compaction-checkpoints.ts",
168170
"src/plugins/host-hook-cleanup.ts",

src/config/sessions/session-accessor.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,46 @@ export type RestoreSessionFromCompactionCheckpointParams = {
541541
storePath: string;
542542
};
543543

544+
export type TemporarySessionMappingPreservationResult<T> = {
545+
/** Result returned by the operation while the temporary mapping may exist. */
546+
result: T;
547+
/** Snapshot failure; callers may continue when temporary cleanup is best-effort. */
548+
snapshotFailure?: string;
549+
/** Restore/delete failure for the original temporary mapping state. */
550+
restoreFailure?: string;
551+
};
552+
553+
type TemporarySessionMappingSnapshot =
554+
| {
555+
canRestore: false;
556+
sessionKey: string;
557+
snapshotFailure: string;
558+
storePath: string;
559+
}
560+
| {
561+
canRestore: true;
562+
hadEntry: false;
563+
sessionKey: string;
564+
storePath: string;
565+
}
566+
| {
567+
canRestore: true;
568+
entry: SessionEntry;
569+
hadEntry: true;
570+
sessionKey: string;
571+
storePath: string;
572+
};
573+
574+
type TemporarySessionMappingOperationResult<T> =
575+
| {
576+
ok: true;
577+
result: T;
578+
}
579+
| {
580+
error: unknown;
581+
ok: false;
582+
};
583+
544584
export type SessionEntryCreateWithTranscriptContext = {
545585
/** Current entry under the requested key before creation, if any. */
546586
existingEntry?: SessionEntry;
@@ -1393,6 +1433,37 @@ export async function applyRestartRecoveryLifecycle<T>(params: {
13931433
return writerResult.result;
13941434
}
13951435

1436+
/**
1437+
* Runs an operation while preserving one temporary session mapping.
1438+
* The storage backend snapshots exactly the named key before the operation and
1439+
* restores that entry, or deletes it when it did not previously exist, after
1440+
* the operation finishes. SQLite backends can implement the same named
1441+
* preservation lifecycle without exposing mutable store access to callers.
1442+
*/
1443+
export async function preserveTemporarySessionMapping<T>(
1444+
scope: SessionAccessScope,
1445+
operation: () => Promise<T> | T,
1446+
): Promise<TemporarySessionMappingPreservationResult<T>> {
1447+
const snapshot = snapshotTemporarySessionMapping(scope);
1448+
let operationResult: TemporarySessionMappingOperationResult<T>;
1449+
try {
1450+
operationResult = { ok: true, result: await operation() };
1451+
} catch (err) {
1452+
operationResult = { error: err, ok: false };
1453+
}
1454+
1455+
const restoreFailure = await restoreTemporarySessionMapping(snapshot);
1456+
if (!operationResult.ok) {
1457+
throw operationResult.error;
1458+
}
1459+
1460+
return {
1461+
result: operationResult.result,
1462+
...(snapshot.canRestore ? {} : { snapshotFailure: snapshot.snapshotFailure }),
1463+
...(restoreFailure ? { restoreFailure } : {}),
1464+
};
1465+
}
1466+
13961467
/** Removes entries and orphan transcript artifacts owned by a named session lifecycle. */
13971468
export async function cleanupSessionLifecycleArtifacts(
13981469
params: SessionLifecycleArtifactCleanupParams,
@@ -2515,6 +2586,53 @@ function createFallbackSessionEntry(patch: Partial<SessionEntry>): SessionEntry
25152586
};
25162587
}
25172588

2589+
function snapshotTemporarySessionMapping(
2590+
scope: SessionAccessScope,
2591+
): TemporarySessionMappingSnapshot {
2592+
const storePath = resolveAccessStorePath(scope);
2593+
try {
2594+
const store = loadSessionStore(storePath, { skipCache: true });
2595+
const entry = store[scope.sessionKey];
2596+
return {
2597+
canRestore: true,
2598+
...(entry ? { entry: structuredClone(entry), hadEntry: true } : { hadEntry: false }),
2599+
sessionKey: scope.sessionKey,
2600+
storePath,
2601+
};
2602+
} catch (err) {
2603+
return {
2604+
canRestore: false,
2605+
sessionKey: scope.sessionKey,
2606+
snapshotFailure: formatErrorMessage(err),
2607+
storePath,
2608+
};
2609+
}
2610+
}
2611+
2612+
async function restoreTemporarySessionMapping(
2613+
snapshot: TemporarySessionMappingSnapshot,
2614+
): Promise<string | undefined> {
2615+
if (!snapshot.canRestore) {
2616+
return undefined;
2617+
}
2618+
try {
2619+
await updateSessionStore(
2620+
snapshot.storePath,
2621+
(store) => {
2622+
if (snapshot.hadEntry) {
2623+
store[snapshot.sessionKey] = structuredClone(snapshot.entry);
2624+
return;
2625+
}
2626+
delete store[snapshot.sessionKey];
2627+
},
2628+
{ activeSessionKey: snapshot.sessionKey },
2629+
);
2630+
return undefined;
2631+
} catch (err) {
2632+
return formatErrorMessage(err);
2633+
}
2634+
}
2635+
25182636
function cleanupPreviousResetTranscripts(params: {
25192637
agentId: string;
25202638
previousEntry: SessionEntry;

src/gateway/boot.ts

Lines changed: 42 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ import {
1818
resolveMainSessionKey,
1919
} from "../config/sessions/main-session.js";
2020
import { resolveStorePath } from "../config/sessions/paths.js";
21-
import { loadSessionStore, updateSessionStore } from "../config/sessions/store.js";
22-
import type { SessionEntry } from "../config/sessions/types.js";
21+
import { preserveTemporarySessionMapping } from "../config/sessions/session-accessor.js";
2322
import type { OpenClawConfig } from "../config/types.openclaw.js";
2423
import { formatErrorMessage } from "../infra/errors.js";
2524
import { createSubsystemLogger } from "../logging/subsystem.js";
@@ -33,14 +32,6 @@ function generateBootSessionId(): string {
3332
return `boot-${ts}-${suffix}`;
3433
}
3534

36-
type SessionMappingSnapshot = {
37-
storePath: string;
38-
sessionKey: string;
39-
canRestore: boolean;
40-
hadEntry: boolean;
41-
entry?: SessionEntry;
42-
};
43-
4435
const log = createSubsystemLogger("gateway/boot");
4536
const BOOT_FILENAME = "BOOT.md";
4637

@@ -101,68 +92,6 @@ async function loadBootFile(
10192
}
10293
}
10394

104-
function snapshotSessionMapping(params: {
105-
cfg: OpenClawConfig;
106-
sessionKey: string;
107-
}): SessionMappingSnapshot {
108-
const agentId = resolveAgentIdFromSessionKey(params.sessionKey);
109-
const storePath = resolveStorePath(params.cfg.session?.store, { agentId });
110-
try {
111-
const store = loadSessionStore(storePath, { skipCache: true });
112-
const entry = store[params.sessionKey];
113-
if (!entry) {
114-
return {
115-
storePath,
116-
sessionKey: params.sessionKey,
117-
canRestore: true,
118-
hadEntry: false,
119-
};
120-
}
121-
return {
122-
storePath,
123-
sessionKey: params.sessionKey,
124-
canRestore: true,
125-
hadEntry: true,
126-
entry: structuredClone(entry),
127-
};
128-
} catch (err) {
129-
log.debug("boot: could not snapshot session mapping", {
130-
sessionKey: params.sessionKey,
131-
error: String(err),
132-
});
133-
return {
134-
storePath,
135-
sessionKey: params.sessionKey,
136-
canRestore: false,
137-
hadEntry: false,
138-
};
139-
}
140-
}
141-
142-
async function restoreSessionMapping(
143-
snapshot: SessionMappingSnapshot,
144-
): Promise<string | undefined> {
145-
if (!snapshot.canRestore) {
146-
return undefined;
147-
}
148-
try {
149-
await updateSessionStore(
150-
snapshot.storePath,
151-
(store) => {
152-
if (snapshot.hadEntry && snapshot.entry) {
153-
store[snapshot.sessionKey] = snapshot.entry;
154-
return;
155-
}
156-
delete store[snapshot.sessionKey];
157-
},
158-
{ activeSessionKey: snapshot.sessionKey },
159-
);
160-
return undefined;
161-
} catch (err) {
162-
return formatErrorMessage(err);
163-
}
164-
}
165-
16695
export async function runBootOnce(params: {
16796
cfg: OpenClawConfig;
16897
deps: CliDeps;
@@ -193,39 +122,49 @@ export async function runBootOnce(params: {
193122
const sessionKey = resolveBootSessionKey(mainSessionKey);
194123
const message = buildBootPrompt(result.content ?? "");
195124
const sessionId = generateBootSessionId();
196-
const mappingSnapshot = snapshotSessionMapping({
197-
cfg: params.cfg,
198-
sessionKey,
199-
});
125+
const agentId = resolveAgentIdFromSessionKey(sessionKey);
126+
const storePath = resolveStorePath(params.cfg.session?.store, { agentId });
200127

201-
// Register the boot prompt for the message-tool echo guard so the
202-
// tool layer can drop fallback-model echoes that copy substantial
203-
// BOOT.md content without preserving the wrapper markers above.
204-
// Always cleared in finally so a failed run does not leave a stale
205-
// entry that mis-fires on an unrelated subsequent run reusing the
206-
// same session key. Refs #53732.
207-
setBootEchoContextForSession(sessionKey, message);
208-
let agentFailure: string | undefined;
209-
try {
210-
await agentCommand(
211-
{
212-
message,
213-
sessionKey,
214-
sessionId,
215-
deliver: false,
216-
suppressPromptPersistence: true,
217-
},
218-
bootRuntime,
219-
params.deps,
220-
);
221-
} catch (err) {
222-
agentFailure = formatErrorMessage(err);
223-
log.error(`boot: agent run failed: ${agentFailure}`);
224-
} finally {
225-
clearBootEchoContextForSession(sessionKey);
128+
const mappingPreservation = await preserveTemporarySessionMapping(
129+
{ storePath, sessionKey },
130+
async () => {
131+
// Register the boot prompt for the message-tool echo guard so the
132+
// tool layer can drop fallback-model echoes that copy substantial
133+
// BOOT.md content without preserving the wrapper markers above.
134+
// Always cleared in finally so a failed run does not leave a stale
135+
// entry that mis-fires on an unrelated subsequent run reusing the
136+
// same session key. Refs #53732.
137+
setBootEchoContextForSession(sessionKey, message);
138+
try {
139+
await agentCommand(
140+
{
141+
message,
142+
sessionKey,
143+
sessionId,
144+
deliver: false,
145+
suppressPromptPersistence: true,
146+
},
147+
bootRuntime,
148+
params.deps,
149+
);
150+
return undefined;
151+
} catch (err) {
152+
const failure = formatErrorMessage(err);
153+
log.error(`boot: agent run failed: ${failure}`);
154+
return failure;
155+
} finally {
156+
clearBootEchoContextForSession(sessionKey);
157+
}
158+
},
159+
);
160+
const agentFailure = mappingPreservation.result;
161+
if (mappingPreservation.snapshotFailure) {
162+
log.debug("boot: could not snapshot session mapping", {
163+
sessionKey,
164+
error: mappingPreservation.snapshotFailure,
165+
});
226166
}
227-
228-
const mappingRestoreFailure = await restoreSessionMapping(mappingSnapshot);
167+
const mappingRestoreFailure = mappingPreservation.restoreFailure;
229168
if (mappingRestoreFailure) {
230169
log.error(`boot: failed to restore session mapping: ${mappingRestoreFailure}`);
231170
}

test/scripts/check-session-accessor-boundary.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ describe("session accessor boundary guard", () => {
5757
"src/gateway/sessions-history-http.ts",
5858
"src/gateway/session-utils.ts",
5959
"src/gateway/managed-image-attachments.ts",
60+
"src/gateway/boot.ts",
6061
"src/gateway/server-methods/artifacts.ts",
6162
"src/gateway/server-methods/chat.ts",
6263
"src/gateway/sessions-resolve.ts",
@@ -121,6 +122,7 @@ describe("session accessor boundary guard", () => {
121122
"src/auto-reply/reply/session-usage.ts",
122123
"src/commands/tasks.ts",
123124
"src/config/sessions/cleanup-service.ts",
125+
"src/gateway/boot.ts",
124126
"src/gateway/server-node-events.ts",
125127
"src/gateway/session-compaction-checkpoints.ts",
126128
"src/plugins/host-hook-cleanup.ts",

0 commit comments

Comments
 (0)