diff --git a/src/commands/doctor/shared/missing-configured-plugin-install.ts b/src/commands/doctor/shared/missing-configured-plugin-install.ts index 36c4e4fad584..30c547c6bb0f 100644 --- a/src/commands/doctor/shared/missing-configured-plugin-install.ts +++ b/src/commands/doctor/shared/missing-configured-plugin-install.ts @@ -787,10 +787,13 @@ async function installCandidate(params: { const existingNpmPackagePath = npmInstallSpec ? resolveExistingCandidateNpmPackagePath({ candidate, npmDir }) : null; + const existingNpmPackageVersion = existingNpmPackagePath + ? await readNpmPackageVersion(existingNpmPackagePath) + : undefined; if ( existingNpmPackagePath && + existingNpmPackageVersion && npmInstallSpec && - clawhubInstallSpec && params.mode !== "update" && isPostCoreConvergencePass(params.env) ) { @@ -800,6 +803,7 @@ async function installCandidate(params: { npmInstallSpec, npmRecordSpec: npmSpecs?.recordSpec ?? npmInstallSpec, packagePath: existingNpmPackagePath, + version: existingNpmPackageVersion, }); } const shouldTryClawHub = @@ -965,12 +969,12 @@ async function adoptExistingNpmPackage(params: { npmInstallSpec: string; npmRecordSpec: string; packagePath: string; + version: string; }): Promise<{ records: Record; changes: string[]; warnings: string[]; }> { - const version = await readNpmPackageVersion(params.packagePath); const npmName = parseRegistryNpmSpec(params.npmInstallSpec)?.name; return { records: { @@ -980,9 +984,10 @@ async function adoptExistingNpmPackage(params: { spec: params.npmRecordSpec, installPath: params.packagePath, installedAt: new Date().toISOString(), - ...(version ? { version, resolvedVersion: version } : {}), + version: params.version, + resolvedVersion: params.version, ...(npmName ? { resolvedName: npmName } : {}), - ...(npmName && version ? { resolvedSpec: `${npmName}@${version}` } : {}), + ...(npmName ? { resolvedSpec: `${npmName}@${params.version}` } : {}), }, }, changes: [ diff --git a/src/commands/doctor/shared/release-configured-plugin-installs.test.ts b/src/commands/doctor/shared/release-configured-plugin-installs.test.ts index 58a8db9ef157..da2699cd9d02 100644 --- a/src/commands/doctor/shared/release-configured-plugin-installs.test.ts +++ b/src/commands/doctor/shared/release-configured-plugin-installs.test.ts @@ -428,7 +428,7 @@ describe("configured plugin install release step", () => { }); }); - it("repairs package-manager plugins for writable legacy parents without explicit deferral", async () => { + it("defers package-manager plugin release completion for writable legacy parents", async () => { mocks.repairMissingPluginInstallsForIds.mockResolvedValue({ changes: ['Installed missing configured plugin "discord".'], warnings: [], @@ -459,8 +459,8 @@ describe("configured plugin install release step", () => { expect(result).toEqual({ changes: ['Installed missing configured plugin "discord".'], warnings: [], - completed: true, - touchedConfig: true, + completed: false, + touchedConfig: false, }); }); diff --git a/src/commands/doctor/shared/update-phase.test.ts b/src/commands/doctor/shared/update-phase.test.ts index edc9005f4ccb..b6058c1ba1c5 100644 --- a/src/commands/doctor/shared/update-phase.test.ts +++ b/src/commands/doctor/shared/update-phase.test.ts @@ -36,7 +36,7 @@ describe("update-phase env helpers", () => { expect(isPostCoreConvergencePass(env)).toBe(true); }); - it("defers configured plugin repair only for explicit update handoffs", () => { + it("defers configured plugin repair for post-core handoffs", () => { expect( shouldDeferConfiguredPluginInstallRepair({ [UPDATE_IN_PROGRESS_ENV]: "1", @@ -53,7 +53,7 @@ describe("update-phase env helpers", () => { [UPDATE_IN_PROGRESS_ENV]: "1", [UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV]: "1", }), - ).toBe(false); + ).toBe(true); expect( shouldDeferConfiguredPluginInstallRepair({ [UPDATE_IN_PROGRESS_ENV]: "1", @@ -80,7 +80,7 @@ describe("update-phase env helpers", () => { [UPDATE_IN_PROGRESS_ENV]: "1", [UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV]: "1", }), - ).toBe(true); + ).toBe(false); expect( isLegacyPackageUpdateDoctorPass({ [UPDATE_IN_PROGRESS_ENV]: "1", diff --git a/src/commands/doctor/shared/update-phase.ts b/src/commands/doctor/shared/update-phase.ts index 9572079b5f97..03da23915ad2 100644 --- a/src/commands/doctor/shared/update-phase.ts +++ b/src/commands/doctor/shared/update-phase.ts @@ -40,14 +40,15 @@ export function isUpdatePackageSwapInProgress(env: NodeJS.ProcessEnv): boolean { /** * True iff configured plugin install repair should be deferred because the * updater guarantees a later post-core convergence pass. Older shipped - * parents may set only the writable-config marker; they still resume after - * the candidate doctor exits, so the candidate must repair payloads before - * control returns to that stale process. + * parents may set only the writable-config marker. Those parents still have a + * post-core handoff, but their in-memory install records are stale after the + * candidate doctor exits, so defer payload repair to the updated child process. */ export function shouldDeferConfiguredPluginInstallRepair(env: NodeJS.ProcessEnv): boolean { return ( isUpdatePackageSwapInProgress(env) && - isTruthyEnvValue(env[UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV]) + (isTruthyEnvValue(env[UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV]) || + isTruthyEnvValue(env[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV])) ); } @@ -67,9 +68,9 @@ export function isLegacyParentWritableUpdateDoctorPass(env: NodeJS.ProcessEnv): } /** - * True iff this newer doctor is running under an older updater. Legacy - * updaters set only `OPENCLAW_UPDATE_IN_PROGRESS`; they do not opt into the - * post-core convergence pass, so configured plugin repair must happen now. + * True iff this newer doctor is running under an older updater that does not + * advertise any post-core handoff marker. Those parents set only + * `OPENCLAW_UPDATE_IN_PROGRESS`, so configured plugin repair must happen now. */ export function isLegacyPackageUpdateDoctorPass(env: NodeJS.ProcessEnv): boolean { return isUpdatePackageSwapInProgress(env) && !shouldDeferConfiguredPluginInstallRepair(env);