Skip to content

Commit 8195b5e

Browse files
yuyutaotaoclaude
andauthored
chore(core): refactor session persistence to execution store pattern (#2144)
* refactor(core): convert SessionStore from object literal to class SessionStore was a module-level singleton object with static-like methods. Convert it to a proper class so consumers can instantiate their own stores if needed. Export both the class (`SessionStore`) and a default shared instance (`sessionStore`) for backward compatibility. https://claude.ai/code/session_018UrEFfDUNYhJAtFQydjpnG * refactor(core): rename SessionStore to AgentDumpStore and let Agent own it - Rename session-store.ts → dump-store.ts, class SessionStore → AgentDumpStore - Rename session-report.ts → dump-report.ts - Agent now creates and holds its own AgentDumpStore instance (this.dumpStore) instead of using a module-level global - exportSessionReport() accepts an optional store parameter so Agent can pass its own instance; MCP tool handler creates a fresh one on demand - Rename types: PersistedSession → PersistedAgentDump, EnsureSessionInput → EnsureDumpSessionInput - Rename buildSessionDump() → buildDump() for clarity https://claude.ai/code/session_018UrEFfDUNYhJAtFQydjpnG * refactor(core): rename AgentDumpStore to ExecutionStore Align naming with existing codebase terminology (ExecutionDump, ExecutionTask): - AgentDumpStore → ExecutionStore (file: execution-store.ts) - dump-report.ts → execution-report.ts - PersistedAgentDump → ExecutionSession - EnsureDumpSessionInput → EnsureExecutionSessionInput - buildDump() → buildGroupedDump() (matches IGroupedActionDump return type) - Agent field: dumpStore → executionStore https://claude.ai/code/session_018UrEFfDUNYhJAtFQydjpnG --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1d2676a commit 8195b5e

File tree

5 files changed

+111
-89
lines changed

5 files changed

+111
-89
lines changed

packages/core/src/agent/agent.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ import {
5252
import { existsSync } from 'node:fs';
5353
import { resolve } from 'node:path';
5454
import type { AbstractInterface } from '@/device';
55-
import { exportSessionReport } from '@/session-report';
56-
import { SessionStore } from '@/session-store';
55+
import { exportSessionReport } from '@/execution-report';
56+
import { ExecutionStore } from '@/execution-store';
5757
import type { TaskRunner } from '@/task-runner';
5858
import {
5959
type IModelConfig,
@@ -152,6 +152,8 @@ export class Agent<
152152

153153
service: Service;
154154

155+
executionStore: ExecutionStore;
156+
155157
dump: GroupedActionDump;
156158

157159
reportFile?: string | null;
@@ -347,6 +349,7 @@ export class Agent<
347349
},
348350
},
349351
});
352+
this.executionStore = new ExecutionStore();
350353
this.dump = this.resetDump();
351354
this.reportFileName =
352355
opts?.reportFileName ||
@@ -419,7 +422,7 @@ export class Agent<
419422
}
420423

421424
private syncSessionMetadata(): void {
422-
SessionStore.ensureSession({
425+
this.executionStore.ensureSession({
423426
sessionId: this.opts.sessionId!,
424427
platform: this.interface.interfaceType,
425428
groupName: this.opts.groupName,
@@ -436,12 +439,15 @@ export class Agent<
436439
): void {
437440
let order = this.sessionExecutionOrders[executionIndex];
438441
if (order === undefined) {
439-
order = SessionStore.appendExecution(this.opts.sessionId!, execution);
442+
order = this.executionStore.appendExecution(
443+
this.opts.sessionId!,
444+
execution,
445+
);
440446
this.sessionExecutionOrders[executionIndex] = order;
441447
return;
442448
}
443449

444-
SessionStore.updateExecution(this.opts.sessionId!, order, execution);
450+
this.executionStore.updateExecution(this.opts.sessionId!, order, execution);
445451
}
446452

447453
appendExecutionDump(execution: ExecutionDump, runner?: TaskRunner): number {
@@ -489,7 +495,7 @@ export class Agent<
489495
}
490496

491497
writeOutActionDumps() {
492-
// No-op: persistence is handled by SessionStore in persistSessionDump().
498+
// No-op: persistence is handled by ExecutionStore in persistSessionDump().
493499
// Report is generated at destroy() time via exportSessionReport().
494500
}
495501

@@ -1301,7 +1307,10 @@ export class Agent<
13011307
// Generate final report from session data
13021308
if (this.opts.generateReport !== false) {
13031309
try {
1304-
this.reportFile = exportSessionReport(this.opts.sessionId!);
1310+
this.reportFile = exportSessionReport(
1311+
this.opts.sessionId!,
1312+
this.executionStore,
1313+
);
13051314
} catch (error) {
13061315
debug('Failed to generate session report:', error);
13071316
}
Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@ import {
55
} from '@midscene/shared/env';
66
import { logMsg } from '@midscene/shared/utils';
77
import { z } from 'zod';
8-
import { SessionStore } from './session-store';
8+
import { ExecutionStore } from './execution-store';
99
import { reportHTMLContent } from './utils';
1010

11-
export function exportSessionReport(sessionId: string): string {
12-
const dump = SessionStore.buildSessionDump(sessionId);
13-
const reportPath = join(SessionStore.reportDir(sessionId), 'index.html');
11+
export function exportSessionReport(
12+
sessionId: string,
13+
store: ExecutionStore = new ExecutionStore(),
14+
): string {
15+
const dump = store.buildGroupedDump(sessionId);
16+
const reportPath = join(store.reportDir(sessionId), 'index.html');
1417

1518
reportHTMLContent(JSON.stringify(dump), reportPath, false);
16-
SessionStore.markReportGenerated(sessionId, reportPath);
19+
store.markReportGenerated(sessionId, reportPath);
1720

1821
if (!globalConfigManager.getEnvConfigInBoolean(MIDSCENE_REPORT_QUIET)) {
1922
logMsg(`Midscene - report generated: ${reportPath}`);
Lines changed: 64 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ import { getMidsceneRunSubDir } from '@midscene/shared/common';
1313
import type { AgentOpt, IExecutionDump, IGroupedActionDump } from './types';
1414
import { ExecutionDump } from './types';
1515

16-
const sessionLockRetryDelayMs = 20;
17-
const sessionLockTimeoutMs = 30_000;
18-
const sessionLockStaleMs = 5 * 60_000;
19-
const sessionLockSleepArray = new Int32Array(new SharedArrayBuffer(4));
16+
const lockRetryDelayMs = 20;
17+
const lockTimeoutMs = 30_000;
18+
const lockStaleMs = 5 * 60_000;
19+
const lockSleepArray = new Int32Array(new SharedArrayBuffer(4));
2020

21-
export interface PersistedSession {
21+
export interface ExecutionSession {
2222
sessionId: string;
2323
platform: string;
2424
groupName: string;
@@ -32,7 +32,7 @@ export interface PersistedSession {
3232
reportFilePath?: string;
3333
}
3434

35-
interface EnsureSessionInput {
35+
export interface EnsureExecutionSessionInput {
3636
sessionId: string;
3737
platform: string;
3838
groupName?: string;
@@ -47,11 +47,11 @@ function defaultGroupName(platform: string, sessionId: string): string {
4747
}
4848

4949
function normalizeSessionRecord(
50-
session: Partial<PersistedSession> & {
50+
session: Partial<ExecutionSession> & {
5151
sessionId: string;
5252
platform: string;
5353
},
54-
): PersistedSession {
54+
): ExecutionSession {
5555
return {
5656
sessionId: session.sessionId,
5757
platform: session.platform,
@@ -82,7 +82,7 @@ function orderedRootExecutionFiles(dir: string): string[] {
8282
}
8383

8484
function sleepSync(ms: number): void {
85-
Atomics.wait(sessionLockSleepArray, 0, 0, ms);
85+
Atomics.wait(lockSleepArray, 0, 0, ms);
8686
}
8787

8888
function validateSessionId(sessionId: string): void {
@@ -114,10 +114,10 @@ function isAlreadyExistsError(error: unknown): error is NodeJS.ErrnoException {
114114
);
115115
}
116116

117-
function withSessionLock<T>(sessionId: string, fn: () => T): T {
117+
function withLock<T>(sessionId: string, fn: () => T): T {
118118
const dir = sessionDirPath(sessionId);
119119
const lockDir = sessionLockDir(sessionId);
120-
const lockDeadline = Date.now() + sessionLockTimeoutMs;
120+
const lockDeadline = Date.now() + lockTimeoutMs;
121121

122122
mkdirSync(dir, { recursive: true });
123123

@@ -131,7 +131,7 @@ function withSessionLock<T>(sessionId: string, fn: () => T): T {
131131
}
132132

133133
try {
134-
if (Date.now() - statSync(lockDir).mtimeMs > sessionLockStaleMs) {
134+
if (Date.now() - statSync(lockDir).mtimeMs > lockStaleMs) {
135135
rmSync(lockDir, { recursive: true, force: true });
136136
continue;
137137
}
@@ -143,7 +143,7 @@ function withSessionLock<T>(sessionId: string, fn: () => T): T {
143143
throw new Error(`Timed out waiting for session lock: ${sessionId}`);
144144
}
145145

146-
sleepSync(sessionLockRetryDelayMs);
146+
sleepSync(lockRetryDelayMs);
147147
}
148148
}
149149

@@ -160,61 +160,65 @@ function writeTextFileAtomic(filePath: string, content: string): void {
160160
renameSync(tempFilePath, filePath);
161161
}
162162

163-
export const SessionStore = {
163+
/**
164+
* Persists agent execution dumps to the filesystem, grouped by session ID.
165+
* Each agent should own its own instance.
166+
*/
167+
export class ExecutionStore {
164168
rootDir(): string {
165169
return getMidsceneRunSubDir('session');
166-
},
170+
}
167171

168172
sessionDir(sessionId: string): string {
169173
return sessionDirPath(sessionId);
170-
},
174+
}
171175

172176
agentFilePath(sessionId: string): string {
173-
return join(SessionStore.sessionDir(sessionId), 'agent.json');
174-
},
177+
return join(this.sessionDir(sessionId), 'agent.json');
178+
}
175179

176180
executionBasePath(sessionId: string, order: number): string {
177-
return join(SessionStore.sessionDir(sessionId), `${order}.json`);
178-
},
181+
return join(this.sessionDir(sessionId), `${order}.json`);
182+
}
179183

180184
reportDir(sessionId: string): string {
181-
const dir = join(SessionStore.sessionDir(sessionId), 'report');
185+
const dir = join(this.sessionDir(sessionId), 'report');
182186
mkdirSync(dir, { recursive: true });
183187
return dir;
184-
},
188+
}
185189

186-
load(sessionId: string): PersistedSession {
187-
const filePath = SessionStore.agentFilePath(sessionId);
190+
load(sessionId: string): ExecutionSession {
191+
const filePath = this.agentFilePath(sessionId);
188192

189193
if (!existsSync(filePath)) {
190194
throw new Error(`Session not found: ${sessionId}`);
191195
}
192196

193197
return normalizeSessionRecord(
194-
JSON.parse(readFileSync(filePath, 'utf-8')) as PersistedSession,
198+
JSON.parse(readFileSync(filePath, 'utf-8')) as ExecutionSession,
195199
);
196-
},
200+
}
197201

198-
save(session: PersistedSession): PersistedSession {
199-
mkdirSync(SessionStore.sessionDir(session.sessionId), { recursive: true });
202+
save(session: ExecutionSession): ExecutionSession {
203+
mkdirSync(this.sessionDir(session.sessionId), { recursive: true });
200204
const normalized = normalizeSessionRecord(session);
201205
writeTextFileAtomic(
202-
SessionStore.agentFilePath(normalized.sessionId),
206+
this.agentFilePath(normalized.sessionId),
203207
JSON.stringify(normalized, null, 2),
204208
);
205209
return normalized;
206-
},
210+
}
207211

208-
ensureSession(input: EnsureSessionInput): PersistedSession {
209-
return withSessionLock(input.sessionId, () => {
212+
ensureSession(input: EnsureExecutionSessionInput): ExecutionSession {
213+
return withLock(input.sessionId, () => {
210214
const now = Date.now();
211-
const filePath = SessionStore.agentFilePath(input.sessionId);
215+
const filePath = this.agentFilePath(input.sessionId);
212216

213217
if (existsSync(filePath)) {
214-
const existing = SessionStore.load(input.sessionId);
218+
const existing = this.load(input.sessionId);
215219
const mergedModelBriefs = new Set(existing.modelBriefs);
216220
input.modelBriefs?.forEach((brief) => mergedModelBriefs.add(brief));
217-
const next: PersistedSession = {
221+
const next: ExecutionSession = {
218222
...existing,
219223
platform: input.platform ?? existing.platform,
220224
groupName:
@@ -228,10 +232,10 @@ export const SessionStore = {
228232
input.deviceType ?? existing.deviceType ?? existing.platform,
229233
updatedAt: now,
230234
};
231-
return SessionStore.save(next);
235+
return this.save(next);
232236
}
233237

234-
return SessionStore.save({
238+
return this.save({
235239
sessionId: input.sessionId,
236240
platform: input.platform,
237241
groupName:
@@ -245,64 +249,64 @@ export const SessionStore = {
245249
executionCount: 0,
246250
});
247251
});
248-
},
252+
}
249253

250254
markReportGenerated(
251255
sessionId: string,
252256
reportFilePath: string,
253-
): PersistedSession {
254-
return withSessionLock(sessionId, () => {
255-
const session = SessionStore.load(sessionId);
256-
return SessionStore.save({
257+
): ExecutionSession {
258+
return withLock(sessionId, () => {
259+
const session = this.load(sessionId);
260+
return this.save({
257261
...session,
258262
reportFilePath,
259263
updatedAt: Date.now(),
260264
});
261265
});
262-
},
266+
}
263267

264268
appendExecution(sessionId: string, execution: ExecutionDump): number {
265-
return withSessionLock(sessionId, () => {
266-
const session = SessionStore.load(sessionId);
269+
return withLock(sessionId, () => {
270+
const session = this.load(sessionId);
267271
const order = session.executionCount + 1;
268-
const basePath = SessionStore.executionBasePath(sessionId, order);
272+
const basePath = this.executionBasePath(sessionId, order);
269273

270274
ExecutionDump.cleanupFiles(basePath);
271275
execution.serializeToFiles(basePath);
272-
SessionStore.save({
276+
this.save({
273277
...session,
274278
executionCount: order,
275279
updatedAt: Date.now(),
276280
});
277281

278282
return order;
279283
});
280-
},
284+
}
281285

282286
updateExecution(
283287
sessionId: string,
284288
order: number,
285289
execution: ExecutionDump,
286290
): void {
287-
withSessionLock(sessionId, () => {
288-
const session = SessionStore.load(sessionId);
289-
const basePath = SessionStore.executionBasePath(sessionId, order);
291+
withLock(sessionId, () => {
292+
const session = this.load(sessionId);
293+
const basePath = this.executionBasePath(sessionId, order);
290294

291295
ExecutionDump.cleanupFiles(basePath);
292296
execution.serializeToFiles(basePath);
293-
SessionStore.save({
297+
this.save({
294298
...session,
295299
executionCount: Math.max(session.executionCount, order),
296300
updatedAt: Date.now(),
297301
});
298302
});
299-
},
303+
}
300304

301-
buildSessionDump(sessionId: string): IGroupedActionDump {
302-
return withSessionLock(sessionId, () => {
303-
const session = SessionStore.load(sessionId);
305+
buildGroupedDump(sessionId: string): IGroupedActionDump {
306+
return withLock(sessionId, () => {
307+
const session = this.load(sessionId);
304308
const rootExecutionFiles = orderedRootExecutionFiles(
305-
SessionStore.sessionDir(sessionId),
309+
this.sessionDir(sessionId),
306310
);
307311

308312
if (!rootExecutionFiles.length) {
@@ -311,7 +315,7 @@ export const SessionStore = {
311315

312316
const executions: IExecutionDump[] = [];
313317
for (const fileName of rootExecutionFiles) {
314-
const basePath = join(SessionStore.sessionDir(sessionId), fileName);
318+
const basePath = join(this.sessionDir(sessionId), fileName);
315319
const inlineJson = ExecutionDump.fromFilesAsInlineJson(basePath);
316320
executions.push(JSON.parse(inlineJson) as IExecutionDump);
317321
}
@@ -325,8 +329,8 @@ export const SessionStore = {
325329
deviceType: session.deviceType ?? session.platform,
326330
};
327331
});
328-
},
329-
};
332+
}
333+
}
330334

331335
export function createSessionAgentOptions(input: {
332336
sessionId?: string;

0 commit comments

Comments
 (0)