From 6a4d633e422236fc035c61dc4b2e7428c61132b9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 20 Apr 2026 17:14:32 +0100 Subject: [PATCH] perf(test): keep session init thread parsing hot path lazy --- .../.generated/plugin-sdk-api-baseline.sha256 | 4 +- src/auto-reply/reply/session.test.ts | 49 +++++++++++++++++++ src/auto-reply/reply/session.ts | 4 +- src/plugin-sdk/core.ts | 10 +++- 4 files changed, 62 insertions(+), 5 deletions(-) diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index f603f03784f..36aee4f0383 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -cf682f7ed83294f79b72758a9bfa0a720da38f967e249bae5bfa916b2a5f0238 plugin-sdk-api-baseline.json -67c723fb639384f457b2900bc1f2bf2639462ae0d2091e6984b77fd9c5c30251 plugin-sdk-api-baseline.jsonl +15505f02306eb9920e350babf1b290d8fcc1b2338b4fccf65d3525de83d9bbcb plugin-sdk-api-baseline.json +06fc58082be4c2e1df1030fa9e8d965c6d5947984c4aed360c0474c1639a0a84 plugin-sdk-api-baseline.jsonl diff --git a/src/auto-reply/reply/session.test.ts b/src/auto-reply/reply/session.test.ts index 624d73d7fd0..c6eaa7cc6c5 100644 --- a/src/auto-reply/reply/session.test.ts +++ b/src/auto-reply/reply/session.test.ts @@ -26,6 +26,31 @@ import { drainFormattedSystemEvents } from "./session-updates.js"; import { persistSessionUsageUpdate } from "./session-usage.js"; import { initSessionState } from "./session.js"; +const sessionForkMocks = vi.hoisted(() => ({ + forkSessionFromParent: vi.fn(), + nextSessionId: 0, +})); + +type ForkSessionParamsForTest = { + parentEntry: SessionEntry; + sessionsDir: string; +}; + +vi.mock("./session-fork.js", () => ({ + forkSessionFromParent: (...args: [ForkSessionParamsForTest]) => + sessionForkMocks.forkSessionFromParent(...args), + resolveParentForkMaxTokens: (cfg: { session?: { parentForkMaxTokens?: unknown } }) => { + const configured = cfg.session?.parentForkMaxTokens; + return typeof configured === "number" && Number.isFinite(configured) && configured >= 0 + ? Math.floor(configured) + : 100_000; + }, +})); + +vi.mock("../../plugins/hook-runner-global.js", () => ({ + getGlobalHookRunner: () => null, +})); + // Perf: session-store locks are exercised elsewhere; most session tests don't need FS lock files. vi.mock("../../agents/session-write-lock.js", async () => { const actual = await vi.importActual( @@ -216,6 +241,30 @@ function registerCurrentConversationBindingAdapterForTest(params: { beforeEach(() => { sessionBindingTesting.resetSessionBindingAdaptersForTests(); + sessionForkMocks.nextSessionId = 0; + sessionForkMocks.forkSessionFromParent + .mockReset() + .mockImplementation(async ({ parentEntry, sessionsDir }: ForkSessionParamsForTest) => { + if (!parentEntry.sessionFile) { + return null; + } + await fs.mkdir(sessionsDir, { recursive: true }); + const sessionId = `forked-session-${++sessionForkMocks.nextSessionId}`; + const sessionFile = path.join(sessionsDir, `${sessionId}.jsonl`); + await fs.writeFile( + sessionFile, + `${JSON.stringify({ + type: "session", + version: 3, + id: sessionId, + timestamp: new Date().toISOString(), + cwd: process.cwd(), + parentSession: parentEntry.sessionFile, + })}\n`, + "utf-8", + ); + return { sessionId, sessionFile: await fs.realpath(sessionFile) }; + }); }); afterEach(async () => { await sessionMcpTesting.resetSessionMcpRuntimeManager(); diff --git a/src/auto-reply/reply/session.ts b/src/auto-reply/reply/session.ts index f542629c3e8..7a0cdaca4f8 100644 --- a/src/auto-reply/reply/session.ts +++ b/src/auto-reply/reply/session.ts @@ -21,7 +21,7 @@ import { resolveAndPersistSessionFile } from "../../config/sessions/session-file import { resolveSessionKey } from "../../config/sessions/session-key.js"; import { resolveMaintenanceConfigFromInput } from "../../config/sessions/store-maintenance.js"; import { loadSessionStore, updateSessionStore } from "../../config/sessions/store.js"; -import { parseSessionThreadInfo } from "../../config/sessions/thread-info.js"; +import { parseSessionThreadInfoFast } from "../../config/sessions/thread-info.js"; import { DEFAULT_RESET_TRIGGERS, type GroupKeyResolution, @@ -689,7 +689,7 @@ export async function initSessionState(params: { } } } - const threadIdFromSessionKey = parseSessionThreadInfo( + const threadIdFromSessionKey = parseSessionThreadInfoFast( sessionCtxForState.SessionKey ?? sessionKey, ).threadId; const fallbackSessionFile = !sessionEntry.sessionFile diff --git a/src/plugin-sdk/core.ts b/src/plugin-sdk/core.ts index 53b11cd4d33..7cca4a25a37 100644 --- a/src/plugin-sdk/core.ts +++ b/src/plugin-sdk/core.ts @@ -1,3 +1,4 @@ +import type { ResolvedConfiguredAcpBinding } from "../acp/persistent-bindings.types.js"; import { buildChatChannelMetaById } from "../channels/chat-meta-shared.js"; import type { ChatChannelId } from "../channels/ids.js"; import { emptyChannelConfigSchema } from "../channels/plugins/config-schema.js"; @@ -209,9 +210,16 @@ export { export { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; export { isTrustedProxyAddress, resolveClientIp } from "../gateway/net.js"; export { formatZonedTimestamp } from "../infra/format-time/format-datetime.js"; -export { ensureConfiguredAcpBindingReady } from "../acp/persistent-bindings.lifecycle.js"; export { resolveConfiguredAcpBindingRecord } from "../acp/persistent-bindings.resolve.js"; +export async function ensureConfiguredAcpBindingReady(params: { + cfg: OpenClawConfig; + configuredBinding: ResolvedConfiguredAcpBinding | null; +}): Promise<{ ok: true } | { ok: false; error: string }> { + const runtime = await import("../acp/persistent-bindings.lifecycle.js"); + return runtime.ensureConfiguredAcpBindingReady(params); +} + export { resolveTailnetHostWithRunner } from "../shared/tailscale-status.js"; export type { TailscaleStatusCommandResult,