diff --git a/server/src/__tests__/adapter-registry.test.ts b/server/src/__tests__/adapter-registry.test.ts index ec5c1988..8567d028 100644 --- a/server/src/__tests__/adapter-registry.test.ts +++ b/server/src/__tests__/adapter-registry.test.ts @@ -32,7 +32,10 @@ import { requireServerAdapter, unregisterServerAdapter, } from "../adapters/index.js"; -import { setOverridePaused } from "../adapters/registry.js"; +import { + resolveExternalAdapterRegistration, + setOverridePaused, +} from "../adapters/registry.js"; const externalAdapter: ServerAdapterModule = { type: "external_test", @@ -384,3 +387,71 @@ describe("server adapter registry", () => { expect(patchedCtx.agent.adapterConfig.env.PAPERCLIP_API_KEY).toBe("agent-run-jwt"); }); }); + +describe("resolveExternalAdapterRegistration", () => { + it("preserves module-provided sessionManagement", () => { + const sessionManagement = { + supportsSessionResume: true, + nativeContextManagement: "unknown" as const, + defaultSessionCompaction: { + enabled: true, + maxSessionRuns: 200, + maxRawInputTokens: 2_000_000, + maxSessionAgeHours: 72, + }, + }; + const adapter: ServerAdapterModule = { + type: "external_session_test", + execute: async () => ({ exitCode: 0, signal: null, timedOut: false }), + testEnvironment: async () => ({ + adapterType: "external_session_test", + status: "pass", + checks: [], + testedAt: new Date(0).toISOString(), + }), + sessionManagement, + }; + + const resolved = resolveExternalAdapterRegistration(adapter); + + expect(resolved.sessionManagement).toBe(sessionManagement); + }); + + it("falls back to the hardcoded registry when the module omits sessionManagement", () => { + // An external that overrides a built-in type should inherit the built-in's + // sessionManagement when it does not provide its own. + const adapter: ServerAdapterModule = { + type: "claude_local", + execute: async () => ({ exitCode: 0, signal: null, timedOut: false }), + testEnvironment: async () => ({ + adapterType: "claude_local", + status: "pass", + checks: [], + testedAt: new Date(0).toISOString(), + }), + }; + + const resolved = resolveExternalAdapterRegistration(adapter); + + expect(resolved.sessionManagement).toBeDefined(); + expect(resolved.sessionManagement?.supportsSessionResume).toBe(true); + expect(resolved.sessionManagement?.nativeContextManagement).toBe("confirmed"); + }); + + it("leaves sessionManagement undefined when neither module nor registry provides one", () => { + const adapter: ServerAdapterModule = { + type: "external_unknown_test", + execute: async () => ({ exitCode: 0, signal: null, timedOut: false }), + testEnvironment: async () => ({ + adapterType: "external_unknown_test", + status: "pass", + checks: [], + testedAt: new Date(0).toISOString(), + }), + }; + + const resolved = resolveExternalAdapterRegistration(adapter); + + expect(resolved.sessionManagement).toBeUndefined(); + }); +}); diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index 9ee10d13..40202edc 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -331,7 +331,13 @@ registerBuiltInAdapters(); // Load external adapter plugins (e.g. droid_local) // // External adapter packages export createServerAdapter() which returns a -// ServerAdapterModule. The host fills in sessionManagement. +// ServerAdapterModule. When the module provides its own sessionManagement +// it is preserved; otherwise the host falls back to the built-in registry +// lookup (so externals that override a built-in type inherit the builtin's +// policy). This brings init-time registration to at-least-as-good behavior +// as the hot-install path (routes/adapters.ts:179 -> registerServerAdapter): +// both preserve module-provided sessionManagement, and init-time additionally +// applies the registry fallback for externals overriding a built-in type. // --------------------------------------------------------------------------- /** Cached sync wrapper — the store is a simple JSON file read, safe to call frequently. */ @@ -339,6 +345,29 @@ function getDisabledAdapterTypesFromStore(): string[] { return getDisabledAdapterTypes(); } +/** + * Merge an external adapter module with host-provided session management. + * + * Module-provided `sessionManagement` takes precedence. When absent, fall + * back to the hardcoded registry keyed by adapter type (so externals that + * override a built-in — same `type` — inherit the builtin's policy). If + * neither is available, `sessionManagement` remains `undefined`. + * + * Exported for unit tests; runtime callers use the IIFE below, which + * applies this transformation during the external-adapter load pass. + */ +export function resolveExternalAdapterRegistration( + externalAdapter: ServerAdapterModule, +): ServerAdapterModule { + return { + ...externalAdapter, + sessionManagement: + externalAdapter.sessionManagement + ?? getAdapterSessionManagement(externalAdapter.type) + ?? undefined, + }; +} + /** * Load external adapters from the plugin store and hardcoded sources. * Called once at module initialization. The promise is exported so that @@ -362,10 +391,7 @@ const externalAdaptersReady: Promise = (async () => { } adaptersByType.set( externalAdapter.type, - { - ...externalAdapter, - sessionManagement: getAdapterSessionManagement(externalAdapter.type) ?? undefined, - }, + resolveExternalAdapterRegistration(externalAdapter), ); } } catch (err) {