mirror of
https://github.com/paperclipai/paperclip
synced 2026-04-25 17:25:15 +02:00
fix(adapters/routes): apply resolveExternalAdapterRegistration on hot-install (#4324)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - The external adapter plugin system (#2218) lets adapters ship as npm modules loaded via `server/src/adapters/plugin-loader.ts`; since #4296 merged, each `ServerAdapterModule` can declare `sessionManagement` (`supportsSessionResume`, `nativeContextManagement`, `defaultSessionCompaction`) and have it preserved through the init-time load via the new `resolveExternalAdapterRegistration` helper > - #4296 fixed the init-time IIFE path at `server/src/adapters/registry.ts:363-369` but noted that the hot-install path at `server/src/routes/adapters.ts:174 registerWithSessionManagement` still unconditionally overwrites module-provided `sessionManagement` during `POST /api/adapters/install` > - Practical impact today: an external adapter installed via the API needs a Paperclip restart before its declared `sessionManagement` takes effect — the IIFE runs on next boot and preserves it, but until then the hot-install overwrite wins > - This PR closes that parity gap: `registerWithSessionManagement` delegates to the same `resolveExternalAdapterRegistration` helper introduced by #4296, unifying both load paths behind one resolver > - The benefit is consistent behaviour between cold-start and hot-install: no "install then restart" ritual; declared `sessionManagement` on an external module is honoured the moment `POST /api/adapters/install` returns 201 ## What Changed - `server/src/routes/adapters.ts`: `registerWithSessionManagement` delegates to the exported `resolveExternalAdapterRegistration` helper (added in #4296). Honours module-provided `sessionManagement` first, falls back to host registry lookup, defaults `undefined`. Updated the section comment to document the parity-with-IIFE intent. - `server/src/routes/adapters.ts`: dropped the now-unused `getAdapterSessionManagement` import. - `server/src/adapters/registry.ts`: updated the JSDoc on `resolveExternalAdapterRegistration` — previously said "Exported for unit tests; runtime callers use the IIFE below", now says the helper is used by both the init-time IIFE and the hot-install path in `routes/adapters.ts`. Addresses Greptile C1. - `server/src/__tests__/adapter-routes.test.ts`: new integration test — installs a mocked external adapter module carrying a non-trivial `sessionManagement` declaration and asserts `findServerAdapter(type).sessionManagement` preserves it after `POST /api/adapters/install` returns 201. - `server/src/__tests__/adapter-routes.test.ts`: added `findServerAdapter` to the shared test-scope variable set so the new test can inspect post-install registry state. ## Verification Targeted test runs from a clean tree on `fix/external-session-management-hot-install` (rebased onto current `upstream/master` now that #4296 has merged): - `pnpm test server/src/__tests__/adapter-routes.test.ts` — 6 passed (new test + 5 pre-existing) - `pnpm test server/src/__tests__/adapter-registry.test.ts` — 15 passed (ensures the IIFE path from #4296 continues to behave correctly) - `pnpm -w run test` full workspace suite — 1923 passed / 1 skipped (unrelated skip) End-to-end smoke on file: [`@superbiche/cline-paperclip-adapter@0.1.1`](https://www.npmjs.com/package/@superbiche/cline-paperclip-adapter) and [`@superbiche/qwen-paperclip-adapter@0.1.1`](https://www.npmjs.com/package/@superbiche/qwen-paperclip-adapter), both public on npm, both declare `sessionManagement`. With this PR in place, the "restart after install" step disappears — the declared compaction policy is active immediately after the install response. ## Risks - Low risk. The change replaces an inline mutation with a call to a helper that already has dedicated unit coverage (#4296 added three tests for `resolveExternalAdapterRegistration` covering module-provided, registry-fallback, and undefined paths). Behaviour is a strict superset of the prior path — externals that did not declare `sessionManagement` continue to get the hardcoded-registry lookup; externals that did declare it now have those values preserved instead of overwritten. - No migration impact. The stored plugin records (`~/.paperclip/adapter-plugins.json`) are unchanged. Existing hot-installed adapters behave correctly before and after. - No behavioural change for builtin adapters; they hit `registerServerAdapter` directly and never flow through `registerWithSessionManagement`. ## Model Used - Provider and model: Claude (Anthropic) via Claude Code - Model ID: `claude-opus-4-7` (1M context) - Reasoning mode: standard (no extended thinking on this PR) - Tool use: yes — file edits, subprocess invocations for builds/tests/git via the Claude Code harness ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots (N/A — server-only change) - [x] I have updated relevant documentation to reflect my changes (the JSDoc on `resolveExternalAdapterRegistration` and the section comment above `registerWithSessionManagement` now document the parity-with-IIFE intent) - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge
This commit is contained in:
@@ -45,6 +45,7 @@ const overridingConfigSchemaAdapter: ServerAdapterModule = {
|
||||
|
||||
let registerServerAdapter: typeof import("../adapters/registry.js").registerServerAdapter;
|
||||
let unregisterServerAdapter: typeof import("../adapters/registry.js").unregisterServerAdapter;
|
||||
let findServerAdapter: typeof import("../adapters/registry.js").findServerAdapter;
|
||||
let setOverridePaused: typeof import("../adapters/registry.js").setOverridePaused;
|
||||
let adapterRoutes: typeof import("../routes/adapters.js").adapterRoutes;
|
||||
let errorHandler: typeof import("../middleware/index.js").errorHandler;
|
||||
@@ -107,6 +108,7 @@ describe("adapter routes", () => {
|
||||
]);
|
||||
registerServerAdapter = registry.registerServerAdapter;
|
||||
unregisterServerAdapter = registry.unregisterServerAdapter;
|
||||
findServerAdapter = registry.findServerAdapter;
|
||||
setOverridePaused = registry.setOverridePaused;
|
||||
adapterRoutes = routes.adapterRoutes;
|
||||
errorHandler = middleware.errorHandler;
|
||||
@@ -236,4 +238,44 @@ describe("adapter routes", () => {
|
||||
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(403);
|
||||
});
|
||||
|
||||
it("POST /api/adapters/install preserves module-provided sessionManagement (hot-install parity with init-time IIFE)", async () => {
|
||||
const HOT_INSTALL_TYPE = "hot_install_session_test";
|
||||
const declaredSessionManagement = {
|
||||
supportsSessionResume: true,
|
||||
nativeContextManagement: "confirmed" as const,
|
||||
defaultSessionCompaction: {
|
||||
enabled: true,
|
||||
maxSessionRuns: 10,
|
||||
maxRawInputTokens: 100_000,
|
||||
maxSessionAgeHours: 24,
|
||||
},
|
||||
};
|
||||
const externalModule: ServerAdapterModule = {
|
||||
type: HOT_INSTALL_TYPE,
|
||||
execute: async () => ({ exitCode: 0, signal: null, timedOut: false }),
|
||||
testEnvironment: async () => ({
|
||||
adapterType: HOT_INSTALL_TYPE,
|
||||
status: "pass",
|
||||
checks: [],
|
||||
testedAt: new Date(0).toISOString(),
|
||||
}),
|
||||
sessionManagement: declaredSessionManagement,
|
||||
};
|
||||
mockPluginLoader.loadExternalAdapterPackage.mockResolvedValue(externalModule);
|
||||
|
||||
const app = createApp({ isInstanceAdmin: true });
|
||||
const res = await request(app)
|
||||
.post("/api/adapters/install")
|
||||
.send({ packageName: "/tmp/fake-hot-install-adapter", isLocalPath: true });
|
||||
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(201);
|
||||
expect(res.body.type).toBe(HOT_INSTALL_TYPE);
|
||||
|
||||
const registered = findServerAdapter(HOT_INSTALL_TYPE);
|
||||
expect(registered).not.toBeNull();
|
||||
expect(registered?.sessionManagement).toEqual(declaredSessionManagement);
|
||||
|
||||
unregisterServerAdapter(HOT_INSTALL_TYPE);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -353,8 +353,10 @@ function getDisabledAdapterTypesFromStore(): string[] {
|
||||
* 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.
|
||||
* Used by both the init-time IIFE below (external-adapter load pass on
|
||||
* server start) and the hot-install path in `routes/adapters.ts`
|
||||
* (`registerWithSessionManagement`), so the two load paths resolve
|
||||
* `sessionManagement` identically.
|
||||
*/
|
||||
export function resolveExternalAdapterRegistration(
|
||||
externalAdapter: ServerAdapterModule,
|
||||
|
||||
@@ -25,11 +25,11 @@ import {
|
||||
findActiveServerAdapter,
|
||||
listEnabledServerAdapters,
|
||||
registerServerAdapter,
|
||||
resolveExternalAdapterRegistration,
|
||||
unregisterServerAdapter,
|
||||
isOverridePaused,
|
||||
setOverridePaused,
|
||||
} from "../adapters/registry.js";
|
||||
import { getAdapterSessionManagement } from "@paperclipai/adapter-utils";
|
||||
import {
|
||||
listAdapterPlugins,
|
||||
addAdapterPlugin,
|
||||
@@ -168,15 +168,17 @@ async function normalizeLocalPath(rawPath: string): Promise<string> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an adapter module into the server registry, filling in
|
||||
* sessionManagement from the host.
|
||||
* Register an external adapter module into the server registry via the
|
||||
* hot-install path, resolving `sessionManagement` identically to how the
|
||||
* init-time IIFE does. Module-provided `sessionManagement` is honored first,
|
||||
* with fallback to the host registry by type for builtin-type overrides.
|
||||
*
|
||||
* Keeps the hot-install and init-time paths at parity so an adapter installed
|
||||
* via `POST /api/adapters/install` has the same shape in the registry as the
|
||||
* same adapter loaded on the next server restart.
|
||||
*/
|
||||
function registerWithSessionManagement(adapter: ServerAdapterModule): void {
|
||||
const wrapped: ServerAdapterModule = {
|
||||
...adapter,
|
||||
sessionManagement: getAdapterSessionManagement(adapter.type) ?? undefined,
|
||||
};
|
||||
registerServerAdapter(wrapped);
|
||||
registerServerAdapter(resolveExternalAdapterRegistration(adapter));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user