diff --git a/scripts/check-session-accessor-boundary.mjs b/scripts/check-session-accessor-boundary.mjs index a13c2791f0dc..a36008dd232f 100644 --- a/scripts/check-session-accessor-boundary.mjs +++ b/scripts/check-session-accessor-boundary.mjs @@ -78,6 +78,8 @@ export const allowedSessionStoreRuntimeFileBackedCompatExports = new Set([ export const migratedSessionAccessorFiles = new Set([ "packages/memory-host-sdk/src/host/session-files.ts", + "src/acp/control-plane/manager.background-task.ts", + "src/acp/control-plane/manager.core.ts", "src/acp/runtime/session-meta.ts", "src/agents/acp-spawn.ts", "src/agents/auth-profiles/session-override.ts", @@ -165,6 +167,7 @@ export const migratedSessionAccessorWriteFiles = new Set([ "src/agents/embedded-agent-runner/run/attempt.ts", "src/agents/live-model-switch.ts", "src/agents/main-session-restart-recovery.ts", + "src/agents/session-suspension.ts", "src/auto-reply/reply/abort.ts", "src/agents/subagent-control.ts", "src/agents/subagent-registry-helpers.ts", diff --git a/scripts/lib/session-accessor-debt-baseline.json b/scripts/lib/session-accessor-debt-baseline.json index 6220f8a40e85..6321303d3aaf 100644 --- a/scripts/lib/session-accessor-debt-baseline.json +++ b/scripts/lib/session-accessor-debt-baseline.json @@ -2,8 +2,6 @@ "embeddedAgentSessionTarget": {}, "memoryHostSessionCorpus": {}, "sessionAccessorRead": { - "src/acp/control-plane/manager.background-task.ts": 2, - "src/acp/control-plane/manager.core.ts": 1, "src/agents/subagent-announce-output.ts": 3, "src/agents/subagent-announce.test-support.ts": 2, "src/agents/tools/transcripts-tool.ts": 2, @@ -19,7 +17,6 @@ "src/config/sessions/transcript.ts": 2 }, "sessionAccessorWrite": { - "src/agents/session-suspension.ts": 2, "src/agents/subagent-spawn.test-helpers.ts": 1, "src/commands/doctor-heartbeat-main-session-repair.ts": 2, "src/commands/doctor-session-snapshots.ts": 2, diff --git a/src/acp/control-plane/manager.background-task.test.ts b/src/acp/control-plane/manager.background-task.test.ts index 76684d5cda70..7731163f7413 100644 --- a/src/acp/control-plane/manager.background-task.test.ts +++ b/src/acp/control-plane/manager.background-task.test.ts @@ -13,11 +13,11 @@ const LOBSTER = "🦞"; const HIGH_SURROGATE_WITHOUT_LOW = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])/; function fakeDeps(): AcpSessionManagerDeps { - const readSessionEntry = (params: { sessionKey: string }) => + const loadSessionEntry = (params: { sessionKey: string }) => params.sessionKey === "child-session" ? { entry: { spawnedBy: "requester-session" } } : { entry: {} }; - return { readSessionEntry } as unknown as AcpSessionManagerDeps; + return { loadSessionEntry } as unknown as AcpSessionManagerDeps; } describe("appendBackgroundTaskProgressSummary", () => { diff --git a/src/acp/control-plane/manager.background-task.ts b/src/acp/control-plane/manager.background-task.ts index 6669d331c8a0..d2d46e192239 100644 --- a/src/acp/control-plane/manager.background-task.ts +++ b/src/acp/control-plane/manager.background-task.ts @@ -102,7 +102,7 @@ export function resolveBackgroundTaskContext(params: { requestId: string; text: string; }): BackgroundTaskContext | null { - const childEntry = params.deps.readSessionEntry({ + const childEntry = params.deps.loadSessionEntry({ cfg: params.cfg, sessionKey: params.sessionKey, })?.entry; @@ -111,7 +111,7 @@ export function resolveBackgroundTaskContext(params: { if (!requesterSessionKey) { return null; } - const parentEntry = params.deps.readSessionEntry({ + const parentEntry = params.deps.loadSessionEntry({ cfg: params.cfg, sessionKey: requesterSessionKey, })?.entry; diff --git a/src/acp/control-plane/manager.core.ts b/src/acp/control-plane/manager.core.ts index 3672461b59ae..64dff4ee98d4 100644 --- a/src/acp/control-plane/manager.core.ts +++ b/src/acp/control-plane/manager.core.ts @@ -87,7 +87,7 @@ export class AcpSessionManager { sessionKey, }; } - const stored = this.deps.readSessionEntry({ + const stored = this.deps.loadSessionEntry({ cfg: params.cfg, sessionKey, clone: false, diff --git a/src/acp/control-plane/manager.types.ts b/src/acp/control-plane/manager.types.ts index da9c35f43d13..cd40daf851cd 100644 --- a/src/acp/control-plane/manager.types.ts +++ b/src/acp/control-plane/manager.types.ts @@ -150,7 +150,7 @@ export type TurnLatencyStats = { export type AcpSessionManagerDeps = { listAcpSessions: typeof listAcpSessionEntries; - readSessionEntry: typeof readAcpSessionEntry; + loadSessionEntry: typeof readAcpSessionEntry; upsertSessionMeta: typeof upsertAcpSessionMeta; getRuntimeBackend: typeof getAcpRuntimeBackend; requireRuntimeBackend: typeof requireAcpRuntimeBackend; @@ -205,7 +205,7 @@ export type WithManagerSessionActor = (sessionKey: string, op: () => Promise< export const DEFAULT_DEPS: AcpSessionManagerDeps = { listAcpSessions: listAcpSessionEntries, - readSessionEntry: readAcpSessionEntry, + loadSessionEntry: readAcpSessionEntry, upsertSessionMeta: upsertAcpSessionMeta, getRuntimeBackend: getAcpRuntimeBackend, requireRuntimeBackend: requireAcpRuntimeBackend, diff --git a/src/agents/session-suspension.test.ts b/src/agents/session-suspension.test.ts index 8510ed9b79f9..9aae75290f1e 100644 --- a/src/agents/session-suspension.test.ts +++ b/src/agents/session-suspension.test.ts @@ -5,15 +5,15 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { CommandLane } from "../process/lanes.js"; import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; -const sessionStoreMocks = vi.hoisted(() => ({ - applySessionStoreEntryPatch: vi.fn(), +const sessionAccessorMocks = vi.hoisted(() => ({ + patchSessionEntry: vi.fn(), })); const commandQueueMocks = vi.hoisted(() => ({ setCommandLaneConcurrency: vi.fn(), })); -vi.mock("../config/sessions.js", () => sessionStoreMocks); +vi.mock("../config/sessions/session-accessor.js", () => sessionAccessorMocks); vi.mock("../process/command-queue.js", () => commandQueueMocks); @@ -45,7 +45,7 @@ describe("session suspension", () => { vi.clearAllTimers(); } vi.useRealTimers(); - sessionStoreMocks.applySessionStoreEntryPatch.mockClear(); + sessionAccessorMocks.patchSessionEntry.mockClear(); commandQueueMocks.setCommandLaneConcurrency.mockClear(); }); @@ -114,9 +114,10 @@ describe("session suspension", () => { await suspendLane(Number.MAX_SAFE_INTEGER, {} as OpenClawConfig, CommandLane.Main); expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS); - const patch = sessionStoreMocks.applySessionStoreEntryPatch.mock.calls[0]?.[0].patch as { + const buildPatch = sessionAccessorMocks.patchSessionEntry.mock.calls[0]?.[1] as () => { quotaSuspension?: { expectedResumeBy?: number }; }; + const patch = buildPatch(); expect(patch.quotaSuspension?.expectedResumeBy).toBe(1_000 + MAX_TIMER_TIMEOUT_MS); }); diff --git a/src/agents/session-suspension.ts b/src/agents/session-suspension.ts index 9eecfc3c780d..a54f38a6a1a6 100644 --- a/src/agents/session-suspension.ts +++ b/src/agents/session-suspension.ts @@ -7,7 +7,7 @@ import { AsyncLocalStorage } from "node:async_hooks"; import path from "node:path"; import { resolveAgentMaxConcurrent, resolveSubagentMaxConcurrent } from "../config/agent-limits.js"; import { resolveCronMaxConcurrentRuns } from "../config/cron-limits.js"; -import { applySessionStoreEntryPatch } from "../config/sessions.js"; +import { patchSessionEntry } from "../config/sessions/session-accessor.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { setCommandLaneConcurrency } from "../process/command-queue.js"; @@ -127,12 +127,9 @@ export async function suspendSession(params: SessionSuspensionParams) { const expectedResumeBy = resolveExpiresAtMsFromDurationMs(ttlMs, { nowMs: now }) ?? now; try { - await applySessionStoreEntryPatch({ - storePath, - sessionKey, - skipMaintenance: true, - takeCacheOwnership: true, - patch: { + await patchSessionEntry( + { storePath, sessionKey }, + () => ({ quotaSuspension: { schemaVersion: 1, suspendedAt: now, @@ -144,8 +141,9 @@ export async function suspendSession(params: SessionSuspensionParams) { expectedResumeBy, state: "suspended", }, - }, - }); + }), + { skipMaintenance: true, takeCacheOwnership: true }, + ); } catch (err) { log.warn("failed to persist quota suspension; not throttling lane", { sessionId: params.sessionId, diff --git a/test/scripts/check-session-accessor-boundary.test.ts b/test/scripts/check-session-accessor-boundary.test.ts index 72aae9b3060f..ca32debe864b 100644 --- a/test/scripts/check-session-accessor-boundary.test.ts +++ b/test/scripts/check-session-accessor-boundary.test.ts @@ -28,6 +28,8 @@ describe("session accessor boundary guard", () => { expect(migratedSessionAccessorFiles).toEqual( new Set([ "packages/memory-host-sdk/src/host/session-files.ts", + "src/acp/control-plane/manager.background-task.ts", + "src/acp/control-plane/manager.core.ts", "src/acp/runtime/session-meta.ts", "src/agents/acp-spawn.ts", "src/agents/auth-profiles/session-override.ts", @@ -125,6 +127,7 @@ describe("session accessor boundary guard", () => { "src/agents/embedded-agent-subscribe.handlers.compaction.runtime.ts", "src/agents/live-model-switch.ts", "src/agents/main-session-restart-recovery.ts", + "src/agents/session-suspension.ts", "src/auto-reply/reply/abort.ts", "src/agents/subagent-control.ts", "src/agents/subagent-registry-helpers.ts",