From 2d84751d94867303fdfe7f051e9fa7176654a054 Mon Sep 17 00:00:00 2001 From: Sawyer Hood Date: Fri, 21 Aug 2026 05:14:18 +0000 Subject: [PATCH] Stop advertising fork for acp-cursor and acp-grok plugins/provider-acp declared fork: "tip" for every built-in ACP agent, so the registry, POST /threads/fork, and the app all offered fork for Cursor and Grok. Neither cursor-agent nor `grok agent stdio` advertises ACP sessionCapabilities.fork at initialize, so the bridge refused session/fork only after the server had already created and started the fork thread, which then landed in status error ("does not advertise session/fork support"). opencode, omp, and hermes-agent do advertise fork and keep "tip". Declare fork: "none" for acp-cursor and acp-grok so the server rejects the fork up front (HTTP 400, no thread) and the app hides the action. Pin the per-provider fork ladder in the first-party provider plugin golden test. Fixes #1833 Co-Authored-By: Claude --- .../first-party-provider-plugins.test.ts | 30 ++++++++++++++++++- plugins/provider-acp/server.ts | 14 +++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/apps/server/test/services/plugins/first-party-provider-plugins.test.ts b/apps/server/test/services/plugins/first-party-provider-plugins.test.ts index 52d73a83c3..cde4f74c26 100644 --- a/apps/server/test/services/plugins/first-party-provider-plugins.test.ts +++ b/apps/server/test/services/plugins/first-party-provider-plugins.test.ts @@ -26,6 +26,7 @@ const FIRST_PARTY_PROVIDER_DECLARATIONS = [ displayName: "Codex", supportsThreadArchive: true, supportsThreadRename: true, + fork: "checkpoint", supportsManualCompaction: true, supportsUsage: true, visibility: "always", @@ -38,6 +39,7 @@ const FIRST_PARTY_PROVIDER_DECLARATIONS = [ displayName: "Claude Code", supportsThreadArchive: false, supportsThreadRename: false, + fork: "checkpoint", supportsManualCompaction: true, supportsUsage: true, visibility: "always", @@ -50,6 +52,7 @@ const FIRST_PARTY_PROVIDER_DECLARATIONS = [ displayName: "Pi", supportsThreadArchive: false, supportsThreadRename: false, + fork: "checkpoint", supportsManualCompaction: true, supportsUsage: false, visibility: "always", @@ -62,6 +65,7 @@ const FIRST_PARTY_PROVIDER_DECLARATIONS = [ displayName: "Cursor", supportsThreadArchive: false, supportsThreadRename: false, + fork: "none", supportsManualCompaction: false, supportsUsage: true, visibility: "always", @@ -74,6 +78,7 @@ const FIRST_PARTY_PROVIDER_DECLARATIONS = [ displayName: "opencode", supportsThreadArchive: false, supportsThreadRename: false, + fork: "tip", supportsManualCompaction: true, supportsUsage: false, visibility: "installed", @@ -86,6 +91,7 @@ const FIRST_PARTY_PROVIDER_DECLARATIONS = [ displayName: "omp", supportsThreadArchive: false, supportsThreadRename: false, + fork: "tip", supportsManualCompaction: false, supportsUsage: false, visibility: "installed", @@ -98,6 +104,7 @@ const FIRST_PARTY_PROVIDER_DECLARATIONS = [ displayName: "Grok Build", supportsThreadArchive: false, supportsThreadRename: false, + fork: "none", supportsManualCompaction: false, supportsUsage: false, visibility: "installed", @@ -110,6 +117,7 @@ const FIRST_PARTY_PROVIDER_DECLARATIONS = [ displayName: "Hermes Agent", supportsThreadArchive: false, supportsThreadRename: false, + fork: "tip", supportsManualCompaction: false, supportsUsage: false, visibility: "installed", @@ -191,6 +199,16 @@ describe("first-party provider plugins", () => { expect(registry.supportsManualCompaction(plugin.providerId)).toBe( plugin.supportsManualCompaction, ); + // Fork is declared per agent, not per tier: the ACP bridge refuses + // `session/fork` for agents whose `initialize` reply does not + // advertise it (cursor-agent, grok), so a declaration above what the + // agent answers makes POST /threads/fork create a thread that dies + // on start (#1833). The declaration is the server's fork gate and + // the app's fork affordance, so it must match the agent. + expect(registration.serverCapabilities.fork, label).toBe(plugin.fork); + expect(registry.supportsFork(plugin.providerId), label).toBe( + plugin.fork !== "none", + ); expect(registration.info.experimental_providerUsage, label).toBe( plugin.supportsUsage, ); @@ -207,6 +225,15 @@ describe("first-party provider plugins", () => { expect(infos.map((info) => info.logoUrl)).toEqual( ALWAYS_VISIBLE_PROVIDER_IDS.map(expectedLogoUrl), ); + // The client-facing fork flag (the app's "Fork into new thread" + // affordance) agrees with the declaration. + expect( + infos.map((info) => [info.id, info.capabilities.supportsFork]), + ).toEqual( + FIRST_PARTY_PROVIDER_DECLARATIONS.filter( + (plugin) => plugin.visibility === "always", + ).map((plugin) => [plugin.providerId, plugin.fork !== "none"]), + ); }, ); }, 60_000); @@ -327,7 +354,8 @@ describe("first-party provider plugins", () => { supportsServiceTier: true, supportsNativeUserQuestion: false, permissionModes: ["accept-edits", "full"], - supportsFork: true, + // cursor-agent does not advertise ACP session/fork (#1833). + supportsFork: false, supportsSessionRewind: false, }, composerActions: [skills], diff --git a/plugins/provider-acp/server.ts b/plugins/provider-acp/server.ts index d9c06e93b6..a1a615d994 100644 --- a/plugins/provider-acp/server.ts +++ b/plugins/provider-acp/server.ts @@ -9,6 +9,14 @@ const ACP_BASE_CAPABILITIES = { experimental_providerInstallation: false, supportsServiceTier: true, supportsNativeUserQuestion: false, + // ACP session/fork clones a whole session (tip only, no checkpoint rewind), + // and it is an unstable ACP extension that not every agent implements. The + // bridge refuses `session/fork` for an agent whose `initialize` reply does + // not advertise `sessionCapabilities.fork`, but only after the server has + // already created and started the fork thread. So this declaration, which + // is the server's fork gate and the app's fork affordance, must match what + // the agent actually advertises: override it with "none" for agents that + // do not (#1833). fork: "tip" as const, supportsManualCompaction: false, supportsThreadArchive: false, @@ -76,6 +84,9 @@ const ACP_PROVIDERS: readonly PluginProviderDeclaration[] = [ ...ACP_BASE_CAPABILITIES, experimental_providerUsage: true, experimental_providerInstallation: true, + // cursor-agent (2026.08.11) advertises `sessionCapabilities: { list }` + // only; no session/fork. + fork: "none", }, composerActions: [], }, @@ -166,6 +177,9 @@ const ACP_PROVIDERS: readonly PluginProviderDeclaration[] = [ }, capabilities: { ...ACP_BASE_CAPABILITIES, + // `grok agent stdio` advertises `sessionCapabilities: { list, resume, + // close }`; no session/fork. + fork: "none", reasoningLevels: ["low", "medium", "high"], }, composerActions: [],