## 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>
Grok advertises promptCapabilities.image=false and ignores ACP image
ContentBlocks (live probe: NO_IMAGE). Path-based vision works when the
agent is given an absolute file path. Include path hints in chat prompts
from .fusion/chat-attachments and carry path on ChatImageContent for
file:// uris.
AcpRuntimeAdapter.promptWithFallback ignored options, so dashboard chat
images never became ACP ContentBlock image entries. Extract images from
prompt options and pass them through buildPromptBlocks for both acp-runtime
and the Grok vendored client.
Grok emits `_x.ai/session_notification` / `_x.ai/session/update` for
hook_execution status. The ACP SDK routes those to Client.extNotification;
without it, every successful post_tool_use hook logged -32601 Method not
found. Implement no-op extMethod/extNotification on default and bridging
handlers in acp-runtime and the Grok vendored copy.
Replace one-shot grok -p JSON with native grok agent stdio (ACP) for realtime
streaming, tool visibility, and multi-turn sessions. Vendor the ACP client
into fusion-plugin-grok-runtime, forward Fusion fn_* tools and operator MCP,
stage Fusion skills via --plugin-dir, authenticate per xAI headless docs, and
align project chat manager store resolution so Grok chat sessions can send.
- Greptile P2: `acp.active` now reflects the ACTUAL dispatch determinant
(FUSION_CLAUDE_ACP, which includes the operator force-override), not the
experimental flag alone — so the status isn't misleading when forced on/off.
- CodeRabbit/Greptile P2: add FNXC:ClaudeAcp comments to the new code blocks
per the AGENTS.md greppable-comment convention.
Already fixed in the prior commit (daa37d08c): the P1 "sticky env" / latch
(applyClaudeAcpEnable now recomputes each call + FUSION_CLAUDE_ACP_FORCE
override) and the enable->disable-on-same-env regression test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two-reviewer pass (security + architecture) on KTD10 + the full Route A
increment: no code defects, no P0, merge-ready as a dormant increment. Applying
the P1 follow-ups:
- Add the feature changeset (@runfusion/fusion minor) — the one convention gap.
- KTD10 tests: fail-closed (bridge not resolved -> env stays unset -> -p) and
idempotency (second onLoad keeps the first published path).
- Document the two intentional, parallel MCP-forwarding paths (U10 engine-adapter
vs U11 provider-driver) so nobody double-forwards, and the known
ACP-path-token-usage=0 residual so U12 doesn't treat it as a bug.
Reviewers confirmed: dormancy invariant holds end-to-end (nothing sets
FUSION_CLAUDE_ACP=1; both flag+path required; -p is the default); OAuth pi path
untouched. 206/206 plugin tests, 333/333 pi-claude-cli tests, typecheck clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The acp-runtime plugin's onLoad now publishes the identity-pinned bundled
claude-code-cli-acp path to FUSION_CLAUDE_ACP_BRIDGE (when unset), so the
pi-claude-cli kill-switch resolves the bridge WITHOUT a manual env var — no
engine->plugin static coupling. Publishes the path only; the ACP transport stays
OFF until an operator sets FUSION_CLAUDE_ACP=1 (rollout gate). Explicit env
override wins; resolver is pinned to the plugin's node_modules/.bin shim.
204/204 plugin tests green (3 new KTD10 tests); typecheck clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Route A unblock + the first Route A code increment.
- U9 verdict recorded (plan OQ1 + docs/acp-contract.md): in an authenticated
interactive session the pinned claude-code-cli-acp 0.1.1 bridge forwards
session/new mcpServers to Claude, Claude invokes the forwarded Fusion tool,
and the call traverses the ACP permission gate (session/request_permission).
Both security-critical answers resolve positively — overturns the headless
NOT-GO chain (FN-6466/6467/6473/6476), whose only blocker was running
detached from the login keychain session.
- R17 (daemon auth) recorded and closed for the supported setup: creds are
macOS Keychain-only; the user's login-session fn daemon has keychain access
(the existing claude -p provider authenticates there), so the bridge does too.
- U10: thread an optional mcpServers list through the ACP runtime contract.
newAcpSession now forwards it (was hardcoded []); AgentRuntimeOptions (engine
+ plugin-local copy) gains the field; defaults to [] to preserve Route B's
read-only ask posture. Tool calls still route through the U5 permission floor.
Plugin typechecks clean; provider-session tests 12/12 (incl. 2 new forwarding
tests). U11-U13 (provider transport swap, picker/auth, workflow verify) remain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Restore mobile auto-merge dashboard stability and broaden regression coverage.
- share the mobile media query between viewport detection and board stabilization so landscape phones stay on the mobile path
- add an integration regression suite for toggling auto-merge across mobile, tablet, desktop, rollback, and task review surfaces
- expand dashboard and CLI tests, keep the new published changeset, and preserve related vitest coverage lists and safety comments
Files changed:
$(git diff --cached --stat)
Fusion-Task-Id: FN-5972
Fusion-Task-Lineage: f3a3bbae-21c2-451b-b24f-848cc12d0542
- ci-test-shard: timing outputFile is now RELATIVE — one pnpm invocation
fans out to several packages whose vitests all received the same
absolute path, so every package overwrote the same timings file (last
writer wins). Each package now writes <pkgDir>/.timings/; discovery
(discoverWorkspaceTimingFiles) and the CI artifact globs scan the tree
- acp event-bridge-bounds: 20s timeout on the CPU-bound plan-flood test
(timed out at default 5s under loaded CI shard, passes in isolation)
- acp process-manager: port-4040-allowlist marker for its doc comments
(main-side; local guard flagged it after merging main)
handlePlan charged the budget but never checked the ceiling or set the
flag, so a plan-ONLY stream kept emitting after crossing the cap (caught by
both review bots). It now flags + truncates exactly like text/thinking.
Adds the plan-only flood regression test (185 total) and the category
frontmatter field to the new solutions doc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two valid P1s from PR review threads:
- event-bridge: plan output bypassed the per-turn output cap — entry size was
bounded but entry COUNT wasn't (1000 entries ~ 64MB through onThinking).
Plans are now suppressed once the cap flags, capped at MAX_PLAN_ENTRIES=100
with a truncation marker, bounded, and charged to the budget. +2 tests.
- control-handler: with pauseForApproval but no findApprovalByDedupeKey, a
human approval was silently discarded (unreadable status -> deny). HITL now
requires BOTH closures upfront and default-denies before creating a request,
so no approval is wasted and no pending record orphaned.
184 tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Check pauseForApproval BEFORE createApprovalRequest so a gate with
create-but-no-pause default-denies without orphaning a pending approval
record (greptile P1).
- prompt-builder: whitespace-only prompt yields no text block (code/comment
mismatch) + regression test.
- onLoad logs arg count, not raw args (args can carry inline tokens).
- Document that engine-driven session resume (loadAcpSession) is deferred v1.
- Strengthen tests: eviction path observed end-to-end, id-normalization
asserted via differing raw forms, loadSession receives the normalized id.
Skipped with reasons (recorded in review thread reply): exports-to-dist,
README title (package name is correct), Surface Enumeration boilerplate,
heavy-lift streaming-read/path-jail rework, and two suggestions that would
weaken the default-deny floor. 182 tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The echo-agent fixture's prompt handler awaits a sessionUpdate write before
registering the cancellable hang; the cancel notification is dispatched
concurrently and could land first on loaded CI shards, no-op, and leave the
prompt hanging forever (5s test timeout on shard 2). The fixture now records a
pending cancel so prompt() resolves 'cancelled' immediately regardless of
arrival order. Test-fixture-only change; verified 5x locally.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tier-2 code review fixes:
- P1 correctness: EventBridge per-turn state never reset — once the per-turn
output cap tripped, all later turns were silently suppressed and tool/accum
state bled across turns. Surface resetTurn() and call it per prompt turn.
- P1: plan_update read a non-existent .entries field (wrong SDK shape) and
wiped the displayed plan — now a documented no-op (full 'plan' is source of
truth).
- P1 security: write-path TOCTOU — open without O_TRUNC, re-validate realpath,
then truncate, so an intermediate-symlink-swapped escaped target is never
truncated before rejection.
- DoS: fs read stat-gates and bounded-reads oversized files instead of loading
them fully before the ceiling.
- Security: stderr redaction now spans chunk boundaries; secret deny-list adds
.git-credentials/*.p12/*.pfx/*.keystore/.pgpass/.htpasswd/etc.
- Reliability: cancelAcpSession bounded by a timeout so a blocked stdin can't
delay the registry SIGKILL.
- Maintainability: drop dead ACP_NOT_IMPLEMENTED export; type agentCapabilities
via the SDK AgentCapabilities; strengthen the S1 write-denial assertion.
+4 tests (181 total); typecheck + eslint clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Post-implementation /simplify cleanup. Registers the plan-specified
process.on('exit', killAllProcesses) safety hook in index.ts (was missing —
closes an orphan-subprocess gap on hard exit). Removes the unwired idle-timer
(engine StuckTaskDetector + dispose()/registry teardown is authoritative per
KTD4a) and its tests. Fixes a stale dispositionFor doc comment, removes a
redundant identifier re-normalization in the event bridge, and clarifies why
the FusionCategory type keeps git_write/task_agent_mutation. Behavior-
preserving; 177 tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the ACP runtime plugin into the published CLI (RUNTIME_PLUGIN_IDS in
tsup.config) and the on-demand BUILTIN_PLUGINS catalog (experimental), matching
the untrusted-subprocess security posture. Adds the Risk S1 default-policy
safety: an acpAllowUnrestricted acknowledgement (default false) — without it, a
blanket allow on a sensitive category is escalated to approval rather than
auto-approved under the allow-all default policy, applied in both the permission
floor and fs write gating. Adds docs/acp-contract.md (launch/readiness +
failure taxonomy), a README with the AGENTS.md-required upstream evidence
(SDK repo/docs/release/integrity), a bundle-output test for the staged plugin,
and a @runfusion/fusion minor changeset. Package green at 179 tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>