refactor(sessions): reduce accessor boundary debt (#105786)

* refactor(sessions): publish rewrite updates through accessor

* refactor(sessions): patch suspensions through accessor

* refactor(acp): clarify accessor-backed session loads

* test(sessions): ratchet accessor boundary debt

* fix(sessions): preserve file-backed rewrite notifications

* test(sessions): retain file-backed rewrite debt

* fix(sessions): preserve transcript rewrite update identity

* test(sessions): restore rewrite debt baseline
This commit is contained in:
Peter Steinberger
2026-07-12 18:07:55 -07:00
committed by GitHub
parent 6c88469110
commit f9d7aa286e
9 changed files with 26 additions and 24 deletions

View File

@@ -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",

View File

@@ -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,

View File

@@ -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", () => {

View File

@@ -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;

View File

@@ -87,7 +87,7 @@ export class AcpSessionManager {
sessionKey,
};
}
const stored = this.deps.readSessionEntry({
const stored = this.deps.loadSessionEntry({
cfg: params.cfg,
sessionKey,
clone: false,

View File

@@ -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 = <T>(sessionKey: string, op: () => Promise<
export const DEFAULT_DEPS: AcpSessionManagerDeps = {
listAcpSessions: listAcpSessionEntries,
readSessionEntry: readAcpSessionEntry,
loadSessionEntry: readAcpSessionEntry,
upsertSessionMeta: upsertAcpSessionMeta,
getRuntimeBackend: getAcpRuntimeBackend,
requireRuntimeBackend: requireAcpRuntimeBackend,

View File

@@ -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);
});

View File

@@ -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,

View File

@@ -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",