← reports

#2290 · compactThread returns 409 for acp-omp although the provider-acp bridge already implements /compact

Bug Priority: Low Effort: small (per issue; no project field set) omp providers provider-acp open on GitHub 2026-08-24 · base 494f66526

Verdict: REPRODUCED · Root-cause confidence: high

Linked PR: #2293 — verdict REQUEST CHANGES (unmergeable as written: it edits a declaration that no longer lives in the file it patches, and it un-gates a path that reports a false "Context compacted" on omp; details in section 7).

1. TL;DR

On an idle thread whose provider is acp-omp (the omp CLI driven over ACP by the first-party provider-acp plugin), POST /api/v1/threads/:id/compact answers 409 Provider "acp-omp" does not support manual context compaction, and the composer's / menu has no compact command for omp threads. The server refuses before any provider code runs: it asks the provider registry whether acp-omp declared supportsManualCompaction, and the shipped agent table (plugins/provider-acp/src/known-agents.ts) simply omits the flag for omp while setting it for opencode. The bridge itself already handles the compaction request for any ACP agent by sending the agent's own /compact as a prompt, and omp 16.3.10 really does execute its builtin /compact from ACP prompt text. So the issue is accurate: the static declaration is stale and the fix is one property on the omp entry.

One important caveat the issue and PR both miss: when I lifted the gate and compacted a small omp session live, omp answered "Compaction failed: Nothing to compact (session too small)" but still ended the prompt with stopReason: "end_turn", so the bridge emitted thread/compacted and the UI showed a "Context compacted" banner directly above the failure text. Flipping the flag alone therefore trades a 409 for a silently wrong success signal on small sessions (the pi plugin already special-cases exactly this message; see section 5).

2. Claims vs findings

Claim from the issueStatusEvidence
POST /api/v1/threads/<id>/compact on an idle acp-omp thread returns 409 Provider "acp-omp" does not support manual context compactionVerifiedLive on my dev instance (section 4, step 4) and by unit test repro-2290-acp-omp-compaction.test.ts (fails with exactly that body on base).
The composer hides/disables the /compact affordance for omp threadsVerifiedGET /projects/:id/commands?provider=acp-omp returns no builtin compact; provider=pi and provider=acp-opencode do. Screenshot 2290-omp-slash-menu-base.png shows the menu with only Skills.
The rejection happens in the server's capability gate before the bridge is consultedVerifiedcompactThreadContext in apps/server/src/routes/threads/actions.ts#L136-L146 throws before sendThreadMessage; the unit test's host RPC responder sees zero turn.submit commands on base.
Only the static per-agent capability declaration is wrong; acp-opencode overrides the same flagVerified (file moved)At base the declaration is no longer in plugins/provider-acp/server.ts (the issue's links point at fff3ae8). Since #2325 (e42a4ef48, 2026-08-23) it is plugins/provider-acp/src/known-agents.ts: opencode has supportsManualCompaction: true (L128), omp has nothing (L152-L190); src/declaration.ts#L122-L124 only sets the capability when the agent says true.
The provider-acp bridge already implements manual compaction end-to-end by sending /compact via session/promptVerifiedpackages/provider-bridge-acp/src/bridge/bridge.ts#L2291-L2330 (startCompaction) and #L2812-L2818 (dispatch on isStandaloneBuiltinCompactCommand). Live: after the flip the POST returned 200 and the event log shows item/started contextCompaction → thread/compacted → turn/completed.
omp executes builtin slash commands from ACP prompt text, so /compact worksVerified, with a catchomp 16.3.10 bundle: the ACP prompt handler calls executeAcpBuiltinSlashCommand on the prompt text; compact is in ACP_BUILTIN_SLASH_COMMANDS with acpDescription: "Compact the conversation" and is advertised in available_commands_update. The catch: every consumed builtin command resolves the prompt with stopReason: "end_turn", including when omp's /compact handler printed Compaction failed: ….
"The bridge fails the turn legibly rather than pretending success when an agent lacks the command" / flipping the flag does not weaken per-agent honestyRefuted for ompLive after the flip on a 2-message session: omp emitted the agent message Compaction failed: Nothing to compact (session too small), then the bridge emitted thread/compacted and the timeline rendered "Context compacted". Screenshot 2290-omp-thread-after-compact-fixed.png; events in events-after-compact.json.
The same call succeeds on acp-opencode, codex, pi, claude-codeVerified at the gate levelregistry.supportsManualCompaction("acp-opencode") is true in the unit test; the existing public-thread-compaction.test.ts covers pi returning 200. I did not run live opencode/codex/claude turns.
Production evidence (issue comment): a queued /compact on a busy acp-omp thread (OMP 17.4.1) completed with thread/compactedUnverifiedI cannot see thread thr_4tm7g4ie78. It is consistent with the code path, but note that per the finding above a thread/compacted event from omp does not by itself prove the context shrank.
Not a duplicate of #1103 / #1152; #1152 excluded ACP providers deliberatelyVerified#1152 (merged 2026-08-11) added manual compaction for codex/claude-code/pi; ACP's /compact maintenance prompt and the opencode override came later (#1640, #1879). The ACP tier default remains false in src/declaration.ts#L24.

3. Environment

4. Minimal reproduction

A. Unit test (no omp needed, 3 seconds)

Save repro-2290-acp-omp-compaction.test.ts as apps/server/test/public/repro-2290-acp-omp-compaction.test.ts and run it from apps/server. The test harness registers the real first-party provider plugins, seeds an idle acp-omp thread with a fake host session, and POSTs the compact route.

cd apps/server && pnpm exec vitest run test/public/repro-2290-acp-omp-compaction.test.ts

expected: 200 and one turn.submit carrying createStandaloneBuiltinCompactCommandInput()
actual (base 494f66526):
 FAIL  test/public/repro-2290-acp-omp-compaction.test.ts > #2290 manual compaction on acp-omp > POST /threads/:id/compact on an idle acp-omp thread dispatches the /compact turn (expected 200)
AssertionError: {"code":"invalid_request","message":"Provider \"acp-omp\" does not support manual context compaction"}: expected 409 to be 200
 ✓ documents the current gate: registry answers false for acp-omp, true for acp-opencode

With the one-line fix from section 6 applied the first case passes and the second (which asserts the current broken value) flips, as expected (tests-ported.txt).

/**
 * Repro for get-bb/bb#2290: POST /api/v1/threads/:id/compact on an idle
 * `acp-omp` thread answers 409 "does not support manual context compaction"
 * even though the provider-acp bridge implements /compact for every ACP
 * agent. The gate is the static capability declaration in
 * plugins/provider-acp/src/known-agents.ts (acp-omp omits
 * `supportsManualCompaction: true`; acp-opencode sets it).
 */
import { describe, expect, it } from "vitest";
import { createStandaloneBuiltinCompactCommandInput } from "@bb/domain";
import { registerHostRpcResponder, type HostRpcHandlerResult } from "../helpers/host-rpc.js";
import { readJson } from "../helpers/json.js";
import { seedEnvironment, seedHostSession, seedProjectWithSource, seedThread, seedThreadRuntimeState } from "../helpers/seed.js";
import { withTestHarness, type TestAppHarness } from "../helpers/test-app.js";

function seedIdleThread(harness: TestAppHarness, providerId: string) {
  const { host, session } = seedHostSession(harness.deps);
  const { project } = seedProjectWithSource(harness.deps, { hostId: host.id });
  const environment = seedEnvironment(harness.deps, { hostId: host.id, projectId: project.id });
  const thread = seedThread(harness.deps, { environmentId: environment.id, projectId: project.id, providerId, status: "idle" });
  seedThreadRuntimeState(harness.deps, { environmentId: environment.id, providerThreadId: `provider-thread-${providerId}`, threadId: thread.id });
  const responder = registerHostRpcResponder(harness, {
    hostId: host.id, sessionId: session.id,
    handle: ({ command }): HostRpcHandlerResult => {
      if (command.type === "host.list_files") return { ok: true, result: { files: [], truncated: false } };
      if (command.type === "host.read_file") return { ok: false, errorCode: "ENOENT", errorMessage: `Path does not exist: ${command.path}` };
      return { ok: true, result: { appliedAs: "new-turn" } };
    },
  });
  return { thread, responder };
}

describe("#2290 manual compaction on acp-omp", () => {
  it("POST /threads/:id/compact on an idle acp-omp thread dispatches the /compact turn (expected 200)", async () => {
    await withTestHarness(async (harness) => {
      const { thread, responder } = seedIdleThread(harness, "acp-omp");
      const response = await harness.app.request(`/api/v1/threads/${thread.id}/compact`, { method: "POST" });
      const body = await readJson(response.clone());
      expect(response.status, JSON.stringify(body)).toBe(200);   // <-- 409 on base
      const turnSubmits = responder.requests.filter(({ command }) => command.type === "turn.submit");
      expect(turnSubmits).toHaveLength(1);
      expect(turnSubmits[0]?.command).toMatchObject({
        type: "turn.submit", threadId: thread.id,
        input: createStandaloneBuiltinCompactCommandInput(),
        resumeContext: { providerId: "acp-omp" },
      });
    });
  });

  it("documents the current gate: registry answers false for acp-omp, true for acp-opencode", async () => {
    await withTestHarness(async (harness) => {
      const registry = harness.deps.providerRegistry;
      expect(registry.get("acp-omp")).not.toBeNull();
      expect(registry.supportsManualCompaction("acp-opencode")).toBe(true);
      expect(registry.supportsManualCompaction("acp-omp")).toBe(false);
    });
  });
});

B. Live, against a running bb with omp installed

  1. Start a dev instance from the checkout and load its environment (scripts in 2290/repro/ do every step below):
    scripts/bb-dev-app current            # prints App/Server/Host daemon URLs + data dir
    eval "$(scripts/bb-dev-app env)"      # BB_SERVER_URL=http://localhost:21279 for my run
  2. Confirm omp is listed as a provider (it is only listed when the omp binary is on PATH) and create a project on a scratch git repo (03-create-project.sh):
    curl -s $BB_SERVER_URL/api/v1/system/providers | jq -r '.[].id'
    codex claude-code pi acp-cursor acp-opencode acp-omp acp-grok acp-hermes-agent
    HOST_ID=$(curl -s $BB_SERVER_URL/api/v1/hosts | jq -r '.[0].id')
    curl -s -X POST $BB_SERVER_URL/api/v1/projects -H 'content-type: application/json' \
      -d "{\"name\":\"qa-2290\",\"source\":{\"type\":\"local_path\",\"path\":\"/tmp/bb-2290-qa\",\"hostId\":\"$HOST_ID\"}}"
    {"id":"proj_nz8rp57u3c", ...}
  3. Spawn one omp thread with a tiny prompt and wait until it is idle (04-spawn-and-compact.sh; the omp turn took ~80 s here):
    pnpm bb:dev thread spawn --project proj_nz8rp57u3c --provider acp-omp --permission-mode accept-edits --title "2290 repro" --prompt "Reply only with ok." --json
    { "id": "thr_5krwcmy3pc", "providerId": "acp-omp", ... }
    curl -s $BB_SERVER_URL/api/v1/threads/thr_5krwcmy3pc | jq '{providerId,status}'
    {"providerId": "acp-omp", "status": "idle"}
  4. Ask for manual compaction:
    curl -s -i -X POST $BB_SERVER_URL/api/v1/threads/thr_5krwcmy3pc/compact
    
    expected: HTTP/1.1 200 OK  {"ok":true}  followed by a contextCompaction turn on the thread
    actual:   HTTP/1.1 409 Conflict
    {"code":"invalid_request","message":"Provider \"acp-omp\" does not support manual context compaction"}
  5. The same gate hides the composer command. The command list the composer fetches has no builtin compact for omp but does for pi and opencode:
    curl -s "$BB_SERVER_URL/api/v1/projects/proj_nz8rp57u3c/commands?provider=acp-omp"      | jq '[.commands[]|select(.name=="compact")|.name]'   → []
    curl -s "$BB_SERVER_URL/api/v1/projects/proj_nz8rp57u3c/commands?provider=pi"           | jq '...'                                            → ["compact"]
    curl -s "$BB_SERVER_URL/api/v1/projects/proj_nz8rp57u3c/commands?provider=acp-opencode" | jq '...'                                            → ["compact"]
Composer slash menu on the omp thread at base: only a Skills section, no Commands/compact entry
Base build, omp thread, / typed in the composer: the menu opens straight at Skills; there is no Commands → compact row. (The "Context compacted" banner visible in the timeline is from the experiment in section 5, run before this capture on the same thread.)
Composer slash menu on the omp thread with the fix: a Commands section with compact — Compact context
Same thread after adding supportsManualCompaction: true to the omp entry (dev server hot-reloaded the plugin): a Commands → compact · Compact context row appears above Skills.

Repro files: 2290/repro/ · raw outputs: 04-spawn-and-compact.txt, repro-test-base.txt.

5. Root cause

Mechanism. The compact route is a pure policy gate on a static capability:

// apps/server/src/routes/threads/actions.ts#L136-L146
async function compactThreadContext(deps: AppDeps, thread: Thread): Promise<void> {
  ensureThreadIsWritable(thread);
  if (!deps.providerRegistry.supportsManualCompaction(thread.providerId)) {
    throw new ApiError(409, "invalid_request",
      `Provider "${thread.providerId}" does not support manual context compaction`);
  }
  ...
  await sendThreadMessage(deps, { ..., payload: { input: createStandaloneBuiltinCompactCommandInput(), mode: "start" } });
}

actions.ts#L136-L146. The registry answers from the plugin registration's serverCapabilities (provider-registry.ts#L416-L422), which is copied verbatim from the declared capabilities at registration (plugin-provider-registration.ts#L254). The same accessor decides whether the composer's command list includes the builtin /compact (projects.ts#L748-L750, includeBuiltinCompact), which is why the affordance disappears too.

Since #2325 the ACP plugin builds each declaration from an agent definition. The tier default is false and the builder only upgrades it when the agent definition says true:

// plugins/provider-acp/src/declaration.ts#L21-L30 and #L117-L126
const ACP_BASE_CAPABILITIES: PluginProviderCapabilities = { ..., supportsManualCompaction: false, ... };
...
    capabilities: {
      ...ACP_BASE_CAPABILITIES,
      fork: agent.fork ?? DEFAULT_FORK,
      permissionModes: [...ACP_BASE_CAPABILITIES.permissionModes],
      ...(agent.supportsManualCompaction === true ? { supportsManualCompaction: true } : {}),
      ...

declaration.ts#L117-L126. In the shipped table, opencode sets the flag and omp does not:

// plugins/provider-acp/src/known-agents.ts
  { id: "acp-opencode", ..., visibility: "installed", supportsManualCompaction: true, fork: "tip", ... },   // L120-L151
  { id: "acp-omp",      ..., visibility: "installed", /* no supportsManualCompaction */ fork: "tip", ... }, // L152-L190

known-agents.ts#L120-L190. Nothing downstream distinguishes omp from opencode: the bridge dispatches the standalone builtin /compact mention to startCompaction for every agent (bridge.ts#L2812-L2818), which sends session/prompt with text /compact (bridge.ts#L2291-L2330). The declaration is the only thing that says no. The issue's source links (to server.ts and ACP_BASE_CAPABILITIES at fff3ae8) describe the same defect in its pre-#2325 location.

Is the declaration actually wrong for omp? Yes. In the omp 16.3.10 bundle the ACP prompt handler first tries executeAcpBuiltinSlashCommand(text); the compact command has an ACP handle (calls session.compact(...), then prints Compaction complete. Tokens: A -> B) and an acpDescription, and omp lists it in available_commands_update. Live, with the flag flipped, the POST returned 200 and omp ran the command (section 5b).

5b. Deeper issue: omp's failed compaction is reported as thread/compacted

The bridge's honesty rule is "only an end_turn prompt counts as compacted" (bridge.ts#L2312-L2325; the translator emits context.compacted only for status: "completed", delta-translation.ts#L963-L979). omp does not honor that contract: its ACP agent resolves every consumed builtin slash command with stopReason: "end_turn", and its /compact handler reports failure as an agent message (m1("Compaction failed: …") → {consumed: true}), not as a prompt error.

Observed live after applying the one-line fix, on the 2-message omp thread from section 4 (05-compact-with-fix.txt, events-after-compact.json):

POST /api/v1/threads/thr_5krwcmy3pc/compact  →  HTTP/1.1 200 OK  {"ok":true}

client/turn/requested   source=tell  input=[/compact builtin mention]
turn/started
item/started            {"type":"contextCompaction", label {pending:"Compacting context", completed:"Compacted context"}}
turn/input/accepted
item/started            {"type":"agentMessage","id":"dac1263510-i2"}
item/agentMessage/delta "Compaction failed: Nothing to compact (session too small)"
item/completed          agentMessage "Compaction failed: Nothing to compact (session too small)"
thread/compacted        ← false: nothing was compacted
turn/completed          {"status":"completed"}
Timeline showing a /compact turn, a 'Context compacted' banner and the text 'Compaction failed: Nothing to compact (session too small)'
With the gate lifted: the timeline shows the /compact request, then a Context compacted lifecycle banner, immediately followed by omp's own Compaction failed: Nothing to compact (session too small). A second attempt produced the identical sequence.

The pi plugin hit the same provider message in #1721 and now classifies "Compaction failed: Nothing to compact (session too small)" and "Compaction failed: Already compacted" as a no-op: it emits a provider.warning (category: "compaction-skipped") and a completed turn boundary with no context.compacted (provider-pi/src/delta-translation.ts#L183-L196, #L736-L753). omp is a pi fork and prints the identical strings, so the ACP bridge needs the equivalent before thread/compacted from omp can be trusted (it affects the timeline banner and anything keyed on thread/compacted, e.g. the fork-history cut list in thread-fork-history.ts#L240 and auto-compaction plugins deciding the thread is now small).

6. Proposed fix (first principles)

  1. Flip the declaration where it lives now (this is all #2293 intends; ported diff in ported-fix.diff):
    --- a/plugins/provider-acp/src/known-agents.ts
    +++ b/plugins/provider-acp/src/known-agents.ts
    @@ -157,6 +157,11 @@ export const KNOWN_ACP_AGENTS
         signInCommand: "omp login",
         installUrl: "https://github.com/can1357/omp",
         visibility: "installed",
    +    // omp runs its builtin `/compact` from ACP prompt text
    +    // (executeAcpBuiltinSlashCommand) and advertises it in
    +    // available_commands_update, so the bridge's /compact maintenance turn
    +    // works (#2290).
    +    supportsManualCompaction: true,
         // Unverified; the ACP tier's value (see acp-opencode).
         fork: "tip",
    Update the expectation table in apps/server/test/services/plugins/first-party-provider-plugins.test.ts (acp-omp → supportsManualCompaction: true) and add the acp-omp 200-path case to apps/server/test/public/public-thread-compaction.test.ts. No wire shape changes (the flag already exists in the declaration schema and SystemProviderInfo does not carry it), so no HOST_DAEMON_PROTOCOL_VERSION bump is needed. Verified: with this diff the PR's two test files and my repro pass (tests-ported.txt), typecheck of bb-plugin-provider-acp is green, and the live POST returns 200.
  2. Make the bridge honest about omp's no-op/failed compaction (same change or an immediate follow-up, otherwise step 1 ships a false "Context compacted"). In startCompaction (packages/provider-bridge-acp/src/bridge/bridge.ts), while session.activePromptKind === "compaction", watch the agent message text the agent streams during the maintenance prompt; if the completed text starts with Compaction failed:, finish with { status: "failed", error: text } — or, mirroring pi, treat the two known no-op strings ("Nothing to compact (session too small)", "Already compacted") as a skipped compaction: completed turn boundary plus a provider.warning with category: "compaction-skipped", and no context.compacted. Cover it in bridge.test.ts with the fake ACP agent answering /compact with that agent message and end_turn. Risk: string matching on a vendor message; pi already accepts that risk for the same strings, and the fallback (reporting success) is worse than a missed match.

7. PR review — #2293 "fix(provider-acp): declare manual compaction support for omp"

What it changes. Branch fix/acp-compaction-409, one commit e1e0599bd on merge base fff3ae8: adds supportsManualCompaction: true to the acp-omp entry of ACP_PROVIDERS in plugins/provider-acp/server.ts; flips the acp-omp row in the first-party capability table test; adds a public-route test asserting 200 plus one turn.submit with the builtin compact input for an acp-omp thread; reformats two unrelated statements in the same test file.

Does it address the root cause? Conceptually yes: the only defect is the declaration and the PR flips it, matching the opencode precedent. Mechanically no longer: GitHub reports the PR CONFLICTING, and it is not a trivial textual conflict.

Findings

#SeverityWhereFinding
1blockingplugins/provider-acp/server.ts (PR hunk at L134-L140)#2325 (e42a4ef48, merged the day after this PR was opened) deleted ACP_PROVIDERS and ACP_BASE_CAPABILITIES from server.ts; the agent facts now live in src/known-agents.ts and the capability builder in src/declaration.ts. git merge-tree 494f66526 pr-2293 → CONFLICT (content): Merge conflict in plugins/provider-acp/server.ts (conflict diff, 700 lines). A naive "take theirs" would resurrect the old file. The change must be re-expressed as one property on the omp entry in known-agents.ts (section 6, step 1).
2majorpackages/provider-bridge-acp/src/bridge/bridge.ts#L2312-L2325, PR description "fails the turn legibly rather than pretending success"The PR un-gates a path whose success signal is not trustworthy for omp. omp resolves every consumed builtin slash command with end_turn, including a /compact that printed Compaction failed: Nothing to compact (session too small); the bridge then emits thread/compacted and the UI shows "Context compacted" (section 5b, reproduced twice live). Flipping the flag converts a loud 409 into a silent false positive for every small omp session — exactly the class of bug #1721 fixed for pi. Either add the no-op/failure detection in the same PR or state the limitation and file the follow-up before merging; the PR description currently claims the opposite.
3minorapps/server/test/public/public-thread-compaction.test.ts new caseThe test asserts the server dispatches a turn.submit with the compact input and resumeContext.providerId "acp-omp". That is a faithful regression test for the gate (it fails on base with the 409) but it proves nothing about the bridge or omp; the description's claim that it "exercises the same path production exercises" overstates it. Fine to keep, but the bridge-side behavior for omp (finding 2) has no test at all.
4minorsame test file, hunks at L1-L5 and L277-L287Unrelated reformatting of an import and an expect.poll chain (oxfmt output on a stale base). Harmless but adds conflict surface; drop when rebasing.
5okprotocol / layeringNo wire, daemon, or CLI surface changes; the flag is consumed only inside the server, so no HOST_DAEMON_PROTOCOL_VERSION bump is required. Server/daemon boundary is respected (policy stays in the declaration + server gate).
6ok"How you verified"Claims of fail-before/pass-after were made at fff3ae8. I re-ran the equivalent at base by cherry-picking the PR, resolving the conflict to base's server.ts and adding the property in known-agents.ts: the PR's two test files and my repro pass (8/9, the 9th being my intentionally inverted "documents the gate" case), turbo typecheck --filter=bb-plugin-provider-acp green.

Tests run: pnpm exec vitest run test/public/repro-2290-acp-omp-compaction.test.ts test/public/public-thread-compaction.test.ts test/services/plugins/first-party-provider-plugins.test.ts on branch pr-2293-ported (log); pnpm exec turbo run typecheck --filter=bb-plugin-provider-acp; live compaction on a real omp thread with the ported change (section 5b).

Verdict: REQUEST CHANGES. Rebase onto main and move the one property to plugins/provider-acp/src/known-agents.ts (finding 1); address or explicitly scope out the false thread/compacted on omp's "Nothing to compact"/"Already compacted" replies and correct the description (finding 2). With those two done it is a safe, small merge.

8. Related issues

9. Appendix

Artifacts

Note on the experiment order

The dev server hot-reloads the provider-acp plugin on source change ("plugin provider-acp: 1 file changed · rebuilt host in 1282ms · reloaded provider-acp" in dev.log), so the "fixed" captures were taken while the one-line change was in the worktree and the "base" captures after reverting it (07-recapture-base.sh confirms the command list is empty and the POST is 409 again before the screenshot). Step 06's second thread tell did not produce a new provider turn (no events were appended before the second compaction), so both compaction attempts ran against the same 2-message session; the production comment on the issue reports a successful compaction on a 73k-token session, which I did not reproduce.

omp 16.3.10 ACP internals consulted (minified bundle @oh-my-pi/pi-coding-agent/dist/cli.js)

// ACP prompt handler: builtin slash commands run before the model sees the text
async#h(f,W,Y){ if(await this.#v(f,W))return; let Z=await mH0(W,{session:f.session,...});
  if(Z!==!1){ if("prompt"in Z){await f.session.prompt(Z.prompt,{images:Y});return}
    this.#C(f,{stopReason:"end_turn", usage:...}); return }          // <-- end_turn for any consumed command
  ... await f.session.prompt(W,{images:Y}) ... }
// executeAcpBuiltinSlashCommand
async function mH0(f,W){let Y=$S0(f); if(!Y)return!1; let q=lg1(Y.name); if(!q?.handle)return!1;
  let Z=await q.handle(Y,W); if(Z===void 0)return{consumed:!0}; return Z}
// /compact ACP handle
{name:"compact", acpDescription:"Compact the conversation", handle:async(f,W)=>{ ...
  try{await W.session.compact(Y.instructions, ...)}catch(w){return m1(`Compaction failed: ${W6(w)}`,W)}   // m1 = output + {consumed:true}
  ... await W.output(`Compaction complete. Tokens: ${Z} -> ${J} (saved ${w}).`); return D1() }}
// session.compact(): throw Error("Nothing to compact (session too small)") / Error("Already compacted")

Commands run (abridged)

gh issue view 2290 --comments; gh pr view 2293; gh pr diff 2293
pnpm install --frozen-lockfile --prefer-offline; pnpm exec turbo run build
git fetch origin main; git log 494f66526..origin/main --oneline -- plugins/provider-acp apps/server/src   # empty
scripts/bb-dev-app current; eval "$(scripts/bb-dev-app env)"
bash 2290/repro/01-list-providers.sh; bash 03-create-project.sh; bash 04-spawn-and-compact.sh
cd apps/server && pnpm exec vitest run test/public/repro-2290-acp-omp-compaction.test.ts        # 409 on base
git fetch origin pull/2293/head:pr-2293; git merge-tree --write-tree 494f66526 pr-2293            # CONFLICT server.ts
git checkout -b pr-2293-ported 494f66526; git cherry-pick pr-2293; git checkout 494f66526 -- plugins/provider-acp/server.ts
(edit plugins/provider-acp/src/known-agents.ts: supportsManualCompaction: true on acp-omp); git commit
pnpm exec turbo run build --filter=bb-plugin-provider-acp; pnpm exec vitest run <3 test files>; pnpm exec turbo run typecheck --filter=bb-plugin-provider-acp
bash 05-compact-with-fix.sh; bash 06-second-turn-and-compact.sh; doobie --headless < doobie-after-compact.js
git checkout 494f66526 -- plugins/provider-acp/src/known-agents.ts; bash 07-recapture-base.sh
pnpm dev:stop; rm -rf <data dir> /tmp/bb-2290-qa