From 4dbfdf1a9a9d9238fb84022ab78ed9f5c73eb5dc Mon Sep 17 00:00:00 2001 From: 0xLLVEL Date: Mon, 19 Oct 2026 13:36:26 +0700 Subject: [PATCH] fix(update): retain FreeBuff when ZCode reads the shared fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maintenance deduplicated clients on the path each inspection read, so ZCode's fallback view of ~/.agents/mcp.json marked that file "covered" and dropped FreeBuff from the scope. ZCode setup writes ~/.zcode/cli/config.json and deliberately leaves the fallback untouched (docs/client-configuration.md), so the emitted command refreshed only ZCode while the shared row both products load stayed pinned to the old package version. Each setup action rewrites only its own configuration path and no other action rewrites the shared row, so every selected client keeps its own maintenance action; the read-path covered/deduped filter is removed. Covers the Codex P2 on UpdateMaintenance.ts:104. Validation: the new regression fails on e2fdec384 with {clients:["zcode"]} and passes with {clients:["freebuff","zcode"]}; updateMaintenance 31/31, setupClientConfigCleanupOwnership, cliUpdateAlias, updateOutput, cliStartupImports green; tsc --noEmit, oxlint (0 errors) and oxfmt --check clean; clientRegistrationStatus keeps the same 4 symlink/stale-vs-aligned failures this Windows host has on base e2fdec384 (verified by stashing the change). 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- src/application/UpdateMaintenance.ts | 37 +++++-------------- .../process-global/updateMaintenance.test.ts | 28 +++++++++++++- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/application/UpdateMaintenance.ts b/src/application/UpdateMaintenance.ts index a5e06ef2ec..2dec719567 100644 --- a/src/application/UpdateMaintenance.ts +++ b/src/application/UpdateMaintenance.ts @@ -3,7 +3,6 @@ import { RegularFileCleanupFailure } from "./RegularFileRead.js"; import { classifyClientRegistrationCommand } from "./ClientRegistrationIdentity.js"; import { readClientRegistrationInspections } from "./ClientRegistrationStatus.js"; -import { supportedClients } from "./SupportedClients.js"; import { existingSkillDestinations } from "./SetupSkill.js"; import type { SetupAction } from "./SetupTypes.js"; import type { Result } from "../domain/result.js"; @@ -77,32 +76,14 @@ export const existingMaintenanceScope = async ( continue; clients.push(registration.client); } - // One shared file is one maintenance decision: a shared-registration client - // defers to another selected client covering the same file, so the owned - // row is refreshed once through the applicable product instead of twice - // through two views of the same registration. - const shared = new Map( - supportedClients(home, process.platform, environment).map((client) => [ - client.name, - client.sharedRegistration === true, - ]), - ); - const covered = new Set( - registrations - .filter( - ({ status }) => - clients.includes(status.client) && shared.get(status.client) !== true, - ) - .map(({ status }) => status.config_path), - ); - const deduped = clients.filter( - (client) => - shared.get(client) !== true || - !registrations.some( - ({ status }) => - status.client === client && covered.has(status.config_path), - ), - ); + // Each setup action rewrites only that client's own configuration path, so + // maintenance keeps every selected client instead of deferring one view of + // a registration to another client's inspection: ZCode writes + // ~/.zcode/cli/config.json and deliberately leaves the ~/.agents/mcp.json + // fallback untouched (docs/client-configuration.md), while FreeBuff writes + // the shared file itself. Dropping FreeBuff because ZCode reads that + // fallback would leave the row both products load pinned to the old + // version. const skillDestinations = await existingSkillDestinations( home, environment, @@ -110,7 +91,7 @@ export const existingMaintenanceScope = async ( resources, ); return { - clients: deduped, + clients, skill: skillDestinations.length > 0, skillDestinations, }; diff --git a/tests/process-global/updateMaintenance.test.ts b/tests/process-global/updateMaintenance.test.ts index f25770f9a3..5c44093386 100644 --- a/tests/process-global/updateMaintenance.test.ts +++ b/tests/process-global/updateMaintenance.test.ts @@ -158,9 +158,11 @@ describe("maintenance activation and layered configuration", () => { }), ); + // The fallback row is also FreeBuff's registration, and FreeBuff is the + // setup action that rewrites it, so both views stay in maintenance scope. await expect( existingMaintenanceScope(home, entryPoint, process.env), - ).resolves.toMatchObject({ clients: ["zcode"] }); + ).resolves.toMatchObject({ clients: ["freebuff", "zcode"] }); }); it("selects an enabled registration inherited through layered OpenCode files", async () => { @@ -355,6 +357,30 @@ describe("shared registration maintenance", () => { }); }); + it("retains FreeBuff when ZCode reads the shared fallback", async () => { + // Both products load the one shared row, but their setup actions write + // different files: ZCode writes ~/.zcode/cli/config.json and deliberately + // leaves ~/.agents/mcp.json untouched (docs/client-configuration.md), so + // its fallback view cannot cover FreeBuff's registration. The shared row + // would stay pinned to the old version without FreeBuff's own action. + await writeClient("zcode", JSON.stringify({ mcp: { servers: {} } })); + await mkdir(join(home, ".config", "freebuff-desktop"), { + recursive: true, + }); + await writeClient( + "freebuff", + JSON.stringify({ + mcpServers: { + rea: { command: "npx", args: ["-y", "rea-agents@0.1.0", "mcp"] }, + }, + }), + ); + + await expect( + existingMaintenanceScope(home, entryPoint, process.env), + ).resolves.toMatchObject({ clients: ["freebuff", "zcode"] }); + }); + it("leaves a foreign shared registration out of maintenance", async () => { await writeClient( "freebuff",