## What happened
FN-8004's implementation work finished and passed review. The auto-merge
then failed with `Grok ACP turn failed: Internal error` — a ~20 second
provider blip — and the task was parked `status: "failed"` with 8 files
of complete, reviewed work stranded on its branch.
The park is the interesting part: `status: "failed"` is precisely what
tells recovery to stop. So a misclassification here isn't a missed
retry, it's **terminal**. Both recovery paths were disabled by the same
wrong verdict:
- `maybeRetryTransientMerge` (inline, 3 retries w/ backoff) — never
fired once (`mergeTransientRetryCount: 0`).
- `recoverTransientMergeFailures` (self-healing sweep, exists exactly to
rescue parked in-review tasks) — skipped it, gated on the same
classifier.
## Three defects fixed
**1. No AI-provider failure class existed.** The AI merge drives a real
LLM turn, but `classifyTransientMergeError` only modeled git/lease/spawn
faults. Adds `ai-provider-turn-failure`.
**2. ACP dropped the error detail.** `promptAcpSession` rethrew the SDK
error unchanged, discarding the JSON-RPC `code`/`data` — the only
evidence the fault was provider-side. ("Internal error" is just the
standard text for `-32603`.) It now preserves them, keeping the original
as `cause`:
```
Internal error (acp rpc code -32603, retryable)
```
Classification anchors on that envelope, **not** on the bare `"Internal
error"` — matching that unanchored would disguise genuine application
defects as retryable blips. Only provider-fault codes (`-32603`,
`-32000`..`-32003`) are retryable; caller-fault codes
(`-32600`..`-32602`) stay permanent, since retrying just repeats the
failing call.
**3. Sweep/inline asymmetry** (found while tracing; latent and
unreported). The inline gate accepted `isTransientError(msg) ||
classify(msg)`, but the sweep consulted **only** the classifier. So
`ECONNRESET` / `socket hang up` during a merge earned inline retries and
then went **invisible to the sweep** once parked — stranded forever. The
classifier now delegates to `isTransientError`, so both gates agree by
construction.
To keep that delegation from importing the detector's
`usage-limit-detector → logger` chain (the chain FN-5627 split the
classifier out to avoid, which would break
`notification-service.test.ts`'s partial `vi.mock`), the pure predicates
moved to the import-free leaf `transient-error-patterns.ts`, re-exported
from `transient-error-detector.ts`. All 13 exports preserved, verified
programmatically.
## Loosened budgets
Per request, so more self-heals. Both apply **only** to errors already
proven transient; the ceiling and
`merger:transient-failure-budget-exhausted` audit path remain.
| Budget | Before | After |
|---|---|---|
| `MAX_AUTO_MERGE_TRANSIENT_RETRIES` | 3 | 5 (backoff
5s/10s/20s/40s/80s) |
| `MAX_TRANSIENT_MERGE_RECOVERIES` | 2 | 5 |
The bump broke two suites that had hardcoded the old `3`. Rather than
swap in another magic number, both now derive the cap from the constant
so future tuning doesn't re-break them.
## Verification
- `pnpm test:gate` green · `pnpm lint` clean · engine + ACP typecheck
clean · `pnpm verify:fast` PASS (5/5)
- ACP plugin 230 tests green · Grok plugin 64 green · engine
transient/merge suites 136 green
- Regression tests assert the **invariant across every surface** (per
*Fix the Invariant, Not the Repro*), not just the reported Grok string:
both ACP runtime prefixes, all retryable/non-retryable rpc codes, both
SDK error shapes, network delegation, class-ordering, and negative cases
proving bare `"Internal error"` and real defects stay permanent.
- A test caught a genuine bug in my own code mid-review (nested-shape
message shadowing), now fixed.
- `notifier.test.ts > "awaiting approval"` fails — **confirmed
pre-existing on clean main**, unrelated.
## Note
FN-8004's own branch (`fusion/fn-8004`) is still unmerged and its work
looks complete. Once this lands, its merge should be retried separately.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@fusion-plugin-examples/acp-runtime
A Fusion runtime plugin that drives any external Agent Client Protocol (ACP) agent over JSON-RPC/stdio. One integration unlocks every ACP-compatible agent (Gemini CLI, the Claude Code ACP adapter, and any future agent that speaks the protocol) through the standard protocol instead of a bespoke per-CLI integration.
Selected via runtimeId: "acp". Installed on demand (experimental) — see the
Fusion plugin catalog (fn plugin install fusion-plugin-acp-runtime).
Security posture
The ACP agent is an untrusted subprocess that calls back into Fusion for permissions and filesystem access. This plugin enforces a defense-in-depth floor:
- Per-category permission gating. Each
session/request_permissionis classified by tool kind into a Fusion action category and checked against the live permission policy — never a preset shortcut.allow_onceonly (never a persisted blanket grant). Unmappable kinds and missing policy default-deny. - Unrestricted-risk acknowledgement (
acpAllowUnrestricted). Because the shipped default policy isunrestricted(allow-all), a blanketallowon a sensitive category is escalated to approval unless the user explicitly setsacpAllowUnrestricted: true. Prefer running the ACP runtime under anapproval-requiredpolicy. - Filesystem jail.
fs/read_text_file/fs/write_text_fileare opt-in (acpFsRead/acpFsWrite, writes default OFF), confined to the sessioncwdby a real symlink-resolving jail (realpath +O_NOFOLLOW), with a deny-list for secrets (.env,*.pem, …) and git internals (.git/**). Writes are gated through thefile_write_deletepermission category. - Untrusted-input bounds. Streamed output is sanitized (ANSI/control strip) and bounded (per-turn + per-chunk caps; bounded tool-call correlation map).
- Subprocess isolation. The agent env is built from an allow-list
(
acpEnvAllowList) — inheritedprocess.envis not forwarded.
Not sandboxed in v1: the agent's own process/network syscalls run with Fusion's user privileges (OS-level sandboxing is recommended future work).
Settings
| Key | Default | Meaning |
|---|---|---|
acpBinaryPath |
acp-agent |
Agent binary to spawn |
acpArgs |
[] |
Args that launch the agent in ACP/stdio mode (e.g. ["--acp"]) |
acpModel |
— | Optional model identifier reported via describeModel |
acpFsRead |
false |
Advertise/register fs/read_text_file |
acpFsWrite |
false |
Advertise/register fs/write_text_file (gated) |
acpEnvAllowList |
[] |
Env var names forwarded to the agent subprocess |
acpAllowUnrestricted |
false |
Acknowledge the untrusted-agent risk under an allow-all policy |
Upstream / third-party integration evidence
Per AGENTS.md (External-integration evidence):
- Protocol homepage / docs: https://agentclientprotocol.com
- Upstream protocol repo: https://github.com/agentclientprotocol/agent-client-protocol
- TypeScript SDK repo: https://github.com/agentclientprotocol/typescript-sdk
- Dependency (npm):
@agentclientprotocol/sdk— https://www.npmjs.com/package/@agentclientprotocol/sdk - Pinned release:
0.24.0(Apache-2.0) - Tarball: https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.24.0.tgz
- Integrity (sha512):
sha512-vvu9appvGvfYstBj19C6NCepV6SvUhY5VRv60KUZ4XzhTah/olOYul5Zo4C+x2enyshMSvgB2mm/OEmrsHaSmA== - Agent binaries driven: user-supplied ACP agents (e.g.
gemini --acp) and the bundled Claude bridge below. User-configured agents remainupstream-pending-verificationper agent.
Bundled Claude ACP bridge evidence
- Canonical upstream repo URL: https://github.com/moabualruz/claude-code-cli-acp
- Docs / homepage URL: https://github.com/moabualruz/claude-code-cli-acp#readme
- Release / download URL: npm package
claude-code-cli-acp(version0.1.1) — https://www.npmjs.com/package/claude-code-cli-acp - Binary / CLI name:
claude-code-cli-acp - Checksum:
sha512-qpfRGOXkOs9mqI7oumsGistWisyXcCC0r7ng7wdLvGMIORdzHjmUUa+94Jftgr/NYAVnAUe6N7kimD8PaO3D5g==(frompnpm-lock.yamlforclaude-code-cli-acp@0.1.1) - Pinned-commit spot-review: tag
v0.1.1points to commitc93f4f4ca449f451d9f3b7db536caf4060883da9(annotated tagca33404fc1128d6a88a55b248f042f70b4bc9f9a, unsigned). License: Apache-2.0. Behavior reviewed for this integration: runsclaudethrough a PTY, reads transcript JSONL, exposes an ACP server over stdio, and requires@anthropic-ai/claude-codeinstalled + authenticated.
See docs/acp-contract.md for the launch/readiness contract and failure taxonomy.