The periodic maintenance job `recover-mergeable-review` was silently merging
in-review tasks regardless of `autoMerge` and `mergeStrategy` settings,
defeating the PR-based review flow for users with `autoMerge: false` and
`mergeStrategy: "pull-request"`.
Gate the sweep on `settings.autoMerge` (and globalPause/enginePaused for
consistency with other merge entry points) and route through the engine's
merge queue via the existing `enqueueMerge` callback so `mergeStrategy ===
"pull-request"` is honored. Falls back to the direct `store.mergeTask` path
only when no enqueue callback is wired (standalone/tests).
Closes https://github.com/Runfusion/Fusion/issues/21
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FN-2910 surfaced concurrent reviewer + merger activity on the same task.
Root cause: asymmetric in-flight guards let an unpause-resume kick off a
fresh executor session while a recovery path was already running, and the
auto-merge handoff fired before the executor's finally block finished
cleanup. This sweeps the surrounding lifecycle paths for similar races and
tightens the reviewer pause gate against TOCTOU through runtime setup.
- Symmetric in-flight tracking across `executing`, `recoveringCompleted`,
and `resumingUnpaused`; `recoverCompletedTask` bails when any are set.
- Atomic claim of the recovery slot in the completed-task watchdog before
any awaited work.
- Workflow-rerun bounce returns "bounced" | "skipped-pending" so the
watchdog can no longer log a false-success retry when the original
bounce is still mid-flight.
- Self-healing's completed-task scan re-checks executing IDs inside the
loop instead of trusting a pre-await snapshot.
- 300ms grace period before auto-merge enqueue, giving the executor's
finally block (session disposal, child cleanup) time to drain and
eliminating the residual log-overlap symptom from FN-2910. Test uses
fake timers, no real sleep added.
- New AgentSemaphore.runNested for synchronously nested helper agents
(reviewers): bumps activeCount for honest observability while bypassing
the wait queue, preserving forward-progress fairness for the parent at
low maxConcurrent. Both createReviewStepTool and triage's
createReviewSpecTool now use it.
- New beforeSpawnSession hook on AgentRuntimeOptions/AgentOptions fired
inside createFnAgent immediately before createAgentSession, past every
awaited setup step. Reviewer wires a pause re-check that throws a
sentinel error converted to UNAVAILABLE, closing the TOCTOU window
where pause flipped during runtime resolution or resource loading.
All 2887 engine tests pass; engine + core + cli + dashboard + plugin-sdk
+ pi-claude-cli + desktop typecheck clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a layered recovery cascade to the merger's pre-rebase stage so tasks
no longer get stuck in in-review when their declared dependency was
squash-merged to main and left orphan raw commits in the dependent's
history. Also prevents the orphan situation at the source for new tasks.
Why:
- 13 tasks were stuck in in-review for hours, all hitting the same
pre-merge rebase abort because they shared 6 raw commits inherited
from FN-2729's branch (declared baseBranch). FN-2729 was then
squash-merged to main, turning those raw commits into orphans whose
content is in main but in a different commit shape, conflicting with
later-merged tasks. The merger's `smart-prefer-main` strategy
correctly refused -X ours (which would silently re-introduce main's
deletions), but the only escape hatch was a 30-min cooldown loop
that retried the same impossible rebase forever.
Recovery cascade (merger.ts pre-rebase stage):
- Layer 1: surgical `git rebase --onto <main> <dep-tip> <branch>` when
task.baseBranch is set. Resolves the dep tip from the live branch ref
or recorded baseCommitSha; peels off the dep's inherited commits
cleanly. Captures the squash-merge-of-dep case end-to-end.
- Layer 2: generic patch-id duplicate-content stripping. Walks the last
500 main commits, computes patch-ids, then drops branch commits whose
patch-id matches and cherry-picks the remainder onto main. Captures
manual cherry-picks, double-merges, and any other duplicate-content
variant Layer 1 doesn't see. Restores the branch's pre-mutation SHA
on partial-failure so worst case leaves the worktree no worse than
before the recovery attempt.
- Layer 3: AI arbitration fall-through. If Layers 1+2 fail, log the
situation and proceed to the existing 3-attempt AI merge cascade
instead of throwing. The deterministic post-merge verification
(test + build) gates whatever the AI produces — that gate is what
enforces prefer-main's safety contract under fall-through (no silent
re-introduction of main's deletions).
- Critical: the unsafe `-X ours` Attempt 3 is suppressed under
fall-through. AI Attempts 1+2 are the only paths that can complete
the merge; if both fail and verification rejects them, the task
bounces back to in-progress via the existing engine path rather than
silently merging.
Prevention (executor.ts worktree creation):
- When a task declares a non-main `baseBranch`, branch the worktree
off main (origin/<defaultBranch> when worktreeRebaseBeforeMerge is
enabled and a remote is resolvable; otherwise local rootDir HEAD)
and `git merge --squash` the dep's content as a single import commit.
The dependent branch then carries main's history + 1 commit instead
of inheriting the dep's raw commits, so a future squash-merge of the
dep produces patch-id-matching content that rebases cleanly.
- Honors settings: respects `worktreeRebaseBeforeMerge`,
`worktreeRebaseRemote`, and falls back to local HEAD when no remote
is resolvable. Fully fail-soft: any squash-import error falls back to
the legacy fork-from-dep behavior so worktree creation still works
for setups where the squash flow can't run.
Engine-side last-retry fix (project-engine.ts):
- Changed conflict-retry condition from `currentRetries < MAX` to
`currentRetries + 1 < MAX` so the bounce-to-in-progress code fires
in the same engine tick as the failing attempt, rather than relying
on a setTimeout-scheduled Nth attempt that dies on engine restart.
Without this, a dev-time engine restart between the 3rd and 4th
retry left the task with mergeRetries=MAX and only the 30-min
cooldown sweep could try again.
Tests:
- New "Layer 1 recovery" test asserts the surgical --onto rebase fires
when baseBranch is set and primary rebase aborts, and that Layer 3
fall-through is NOT triggered when Layer 1 succeeds.
- Updated the "no silent fall-through to -X ours" test to cover the
new fall-through path: even after Layers 1+2 fail and the merge
cascade proceeds, -X ours must not run, and the task log must record
both the Layer 3 fall-through entry and the Attempt 3 suppression.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tasks were getting stuck in `in-review` forever when auto-merge could not
resolve conflicts within MAX_AUTO_MERGE_RETRIES. The conflict-exhaustion
branch silently cleared `status` (no error, no log entry, no comment),
and the 30-min cooldown sweep would reset retries and re-attempt the
same impossible merge — looping silently with no user-facing surface.
Why:
- FN-2918 and FN-2903 both spent hours in this loop with no error/comment
visible on the task. The only log evidence was repeated
"Auto-merge retry cooldown elapsed (30m idle)" entries with no
follow-up outcome.
How to apply:
- Every merge failure now writes a `<Manual|Auto>-merge failed: <msg>`
entry to the task log so the dashboard surfaces the reason.
- Conflict-retry exhaustion now bounces the task back to `in-progress`
with a comment + log entry so the executor re-rebases against main
and retries — mirroring the verification-failure-bounce pattern.
- New `mergeConflictBounceCount` task field caps outer bounces
(`MAX_MERGE_CONFLICT_BOUNCES = 2`); past the cap, the task is parked
in `in-review` with `status="failed"` and a follow-up triage task is
created so a human can resolve the conflict manually.
- Non-conflict and non-direct-strategy errors now also set
`status="failed"` so the cooldown sweep can't re-pick them up.
- `canMergeTask` skips tasks with `status="failed"` so terminal
failures (verification cap, bounce cap, non-conflict error) are no
longer eligible for cooldown re-attempts.
Schema migration v52 adds the `mergeConflictBounceCount` column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the AI merge agent reported a build failure via fn_report_build_failure,
executeMergeAttempt immediately ran `git reset --merge` and threw. The catch
handler then spawned an in-merge fix agent on a clean main and called
amendMergeCommitWithFixes, which blindly amended HEAD — the *previous* task's
merge commit — silently dropping the current task's branch and inheriting
the prior task's stats. The dashboard then reported the new task as merged
with completely unrelated files.
- Drop the immediate reset at the build-failure throw site so the squash
state survives for the in-merge fix path.
- Capture preAttemptHeadSha at each mergeAttempt and refuse to amend when
HEAD never moved past it; instead, create a fresh commit from the squash
+ fix changes. If neither HEAD moved nor anything is staged, abort the
merge instead of fabricating success.
- Move the cleanup reset into the mergeAttempt catch handler (with a
labeled resetMergeWithWarn helper) so it still fires when the fix path
is exhausted or disabled.
- Replace the AI-authored commit body with a deterministic body built from
the branch's actual step-commit subjects after every successful AI merge.
Stops the recurring problem of merge messages describing files that are
not in the diff.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
recoverInterruptedMergingTasks searched for landed commits by grepping
commit subjects for the task ID. Users with includeTaskIdInCommit=false
have commit subjects like `feat: ...` (no task ID), so if the merger
crashed after committing but before storing mergeDetails, recovery would
silently fail to find the commit and incorrectly retry the merge.
Three layered defenses:
1. Emit a Fusion-Task-Id: <id> trailer in every Fusion-managed merge
commit body. The 4 fallback commit invocations now include
`-m "Fusion-Task-Id: ..."`. After the AI agent commits, an
idempotent ensureTaskIdTrailerOnHead() amends the trailer in via
`git interpret-trailers` (no-op if already present).
2. findLandedTaskCommit now tries three sources in order:
a. task.mergeDetails.commitSha (if reachable from HEAD)
b. Fusion-Task-Id trailer grep (anchored regex)
c. Subject grep (legacy commits)
3. Trailer grep uses an anchored regex `^Fusion-Task-Id: <id>$` so
it doesn't false-match task IDs appearing as substrings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two cases where mergeDetails.commitSha was wrong:
1. Empty-squash success paths (mergeAttempt + attemptWithSideStrategy
return true when nothing was staged) recorded pre-merge HEAD as the
task's commitSha. That commit had nothing to do with this task —
misleading the dashboard, audit log, and recovery scans.
2. pushAfterMerge can trigger an internal pull --rebase that rewrites
HEAD; mergeDetails was captured before push, so the stored sha
referenced a now-orphaned commit.
Fix:
- Track an empty-merge flag on AiInvocationTracker, set at the three
squashIsEmpty/staged===0 sites. Metadata block omits commitSha when
the flag is set.
- After successful pushAfterMerge, recapture HEAD and update
mergeDetails.commitSha if it changed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pre-merge rebase had the local-base rebase nested inside the
remote-rebase success path, so when no remote resolved (or worktreePath
was missing), the entire block exited without running local-base. That
left smart-prefer-main exposed: with no rebase, the -X ours fallback
would silently re-introduce code main had recently deleted, which is
exactly the case the strategy is meant to prevent.
Restructure so remote rebase (Stage 1) and local-base rebase (Stage 2)
run as independent gates. Local-base rebase still picks up sibling-task
merges that landed locally even when the remote stage was skipped or
disabled, so prefer-main always gets at least one defense.
Also relax the semantic-incompatibility guard: prefer-main now requires
EITHER stage to remain enabled (was: required worktreeRebaseBeforeMerge).
Extracted runLocalBaseRebase() helper to remove duplication between the
two entry points (after Stage 1 vs. standalone) and centralize the
ancestor-check + abort handling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two recovery scans operated on in-review tasks without checking
!task.paused:
- recoverMergedReviewTasks would move a paused task whose merge was
already confirmed to done, against user intent.
- recoverMisclassifiedFailures would clear the error on a paused failed
task, defeating the user's intent to investigate manually.
Add the paused guard to both, matching the pattern used by all other
recovery scans. Completes the pause-vs-stuck audit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Attempt 2 of the merge cascade caught any git merge --squash failure into
mergeExitedWithConflicts=true. If the failure was non-conflict (pre-commit
hook rejection, IO error, locked repo) and produced no U files, the code
fell into the "all conflicts auto-resolved" branch with empty classified
arrays, ran deterministic verification on pre-merge HEAD, and returned
true — recording merge metadata for a merge that never happened.
Distinguish "exit code 1 with U files" (recoverable) from "any other
failure" (real). When a real failure surfaces with no conflicts, raise a
sentinel MergeNonConflictError that the outer mergeAttempt catch propagates
without retrying — retrying just re-runs the same broken command.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
recoverInterruptedMergingTasks was the one self-healing scan that didn't
guard !task.paused. A user who paused a task mid-merge (status=merging,
column=in-review) would still see the recovery scan finalize or unblock
the merge once the stuck timeout elapsed, violating their pause intent.
The other 12 self-healing recovery scans either explicitly skip paused
tasks or operate on terminal/metadata-only state where pause is moot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The smart-prefer-main strategy depends on a successful pre-merge rebase
to honor main's deletions. Previously, three failure modes silently fell
through to the -X ours merge, which would re-introduce code main had
just removed (because branch additions vs main deletions don't textually
conflict and -X ours only resolves content conflicts, not modify/delete).
- Hard-fail when prefer-main is paired with worktreeRebaseBeforeMerge=false
(semantically incoherent combination)
- Hard-fail when the pre-merge rebase starts and aborts (any of the three
rebase paths: remote, nested local-base, or fallback local-only)
- Warn (not throw) on environmental silent skips — no remote resolvable
or no worktreePath — so the gap is observable in logs without breaking
common test/setup environments
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The single "smart" strategy is now two flavors with the new default flipped
to prefer-main. Both share a pre-cascade `git fetch origin <currentBranch>`
+ best-effort fast-forward so a freshly-pushed sibling commit doesn't get
clobbered when the fallback resolves a conflict against a stale base.
- "smart-prefer-main" (new default): -X ours fallback. Protects just-merged
sibling work from being regressed by a concurrent task branch.
- "smart-prefer-branch": -X theirs fallback. Equivalent to legacy "smart".
Legacy "smart" / "prefer-main" enum values are accepted and normalized via
`normalizeMergeConflictStrategy()` so existing settings.json files migrate
seamlessly. The fast-forward step gracefully degrades on fetch failure or
divergent local main (logs and continues).
Updates settings UI dropdown, test helpers, and adds 5 fetch+ff regression
tests + 7 normalize-helper tests. Lint cleanup of two empty catch blocks
in scripts/release.mjs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Reset merge metadata, verification counters, and workflow results when tasks move from in-review/done back to in-progress
- Reopen verification-related steps (or the last step fallback) so re-verification runs from a pending state
- Add execute-time guard to clear stale mergeDetails on in-progress tasks before continuing
- Prevent resumeOrphaned fast-path recovery when completed in-progress tasks still carry merge metadata
- Add targeted executor and TaskForm tests covering FN-2883 regression paths
- Add webhook settings fields and defaults for enablement, URL, format, and event filtering
- Implement WebhookNotificationProvider with payload formatting support for generic, Slack, and Discord endpoints
- Extend NotificationService to manage both ntfy and webhook providers with live settings sync
- Export webhook notification types/providers through engine notification entry points
- Add notification service module with provider abstractions and ntfy provider implementation
- Refactor NtfyNotifier into a compatibility wrapper that delegates task-event delivery to NotificationService
- Initialize and stop NotificationService from ProjectEngine while preserving gridlock notifications via NtfyNotifier
- Export notification APIs from engine index and add focused unit coverage for provider, service, and project-engine wiring
- Add an effective node resolver with task override, project default, and local fallback precedence.
- Wire scheduler dispatch to persist effectiveNodeId/effectiveNodeSource and log resolved node routing.
- Add coverage for effective node resolution and scheduler node routing integration behavior.
- Stabilize workspace test resolution by adding @fusion/core and @fusion/plugin-sdk aliases across Vitest configs.
- Add ProjectEngine memory dreams wiring tests for startup ordering, settings-change resync, and unrelated-setting no-op behavior
- Cover degraded-mode startup and settings-update failure paths to ensure sync errors are logged without stopping the engine
- Extend MemoryView Dream Now tests for success, failure toast handling, and disabled loading-state behavior
- Hermes / OpenClaw plugin index.ts now re-export `probeHermesBinary` /
`probeOpenClawBinary` and their status types so the dashboard's
`runtime-provider-probes.ts` façade can import them via the public
package entry instead of deep paths.
- Dashboard `package.json` adds `@fusion-plugin-examples/hermes-runtime`,
`…/openclaw-runtime`, `…/paperclip-runtime` as workspace deps so
pnpm symlinks them into `packages/dashboard/node_modules/`. Without
these, the new probe imports failed with "Cannot find module" during
`pnpm typecheck`.
This clears 6 of the 9 outstanding typecheck errors. The remaining 3 are
in the in-flight Hermes plugin rewrite (runtime-adapter still imports
from a deleted `./pi-module.js`; the new `index.ts` calls a factory
with the wrong arg type) and should be resolved by the same change set
that landed the rewrite.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Replace Hermes pi module integration with pi-ai session streaming and updated runtime adapter contracts
- Remove legacy engine guard scaffolding and add hermes-stream-client coverage for streaming behavior
- Rewrite plugin and engine e2e tests to align with the new runtime flow and regenerate dist artifacts
- Update Hermes runtime README and package metadata to document pi-ai execution expectations
When the worktree-recycle pool reassigned a path to a new task, the old
task's diff endpoints kept reading the new task's branch state — surfacing
unrelated commits as the original task's "files changed" list.
- Clear task.worktree/branch in the merger after the worktree is released
to the pool or removed, so the path no longer points anywhere.
- Validate the worktree's current branch matches task.branch in the three
worktree-backed diff endpoints; on mismatch return empty rather than
diffing against a foreign branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add end-to-end Hermes runtime test covering PluginStore registration through PluginLoader and PluginRunner resolution
- Verify createResolvedAgentSession uses the Hermes runtime and delegates createFnAgent, promptWithFallback, and describeModel calls
- Add regression test ensuring AgentRuntime-shaped Hermes adapters are reused without compatibility wrapping
- Cover fallback behavior to default pi runtime when Hermes plugin is not installed
- Add engine shell utility to resolve the correct shell per platform
- Update routine runner to execute commands through shared shell selection
- Update cron runner to use the same cross-platform shell behavior
- Apply shared shell handling in dashboard routes for command execution
- Add shell utility tests covering platform-specific selection behavior
- Capture prompt/completion/total token usage from step-scoped sessions in the step session executor
- Persist per-step token usage in executor run context so stats survive across task execution
- Record single-session token usage totals alongside run context stats logging for consistent aggregation
- Expand executor and step-session executor tests to validate token usage persistence and fixture behavior
Capture per-session token usage from pi-coding-agent's getSessionStats()
after each promptWithFallback in the executor and merger paths, so
task.tokenUsage populates live during runs and reflects final totals on
done tasks. Previously the executor never read session usage and only
the heartbeat path bumped agent token totals, leaving task.tokenUsage
undefined even after completion.
Stats panel and done-card timing also now reflect live state: the modal
overlays the SSE-updated task prop on top of the one-shot fullDetail
snapshot, in-progress workflow steps contribute live elapsed to the
Workflow runtime metric, and the done card uses Timed duration (matching
the stats tab) with workflow runtime as fallback. Time indicator labels
coarsened to <1m / Nm / Nh / Nd.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Improve automation startup diagnostics and route handling for manual execution steps
- Add support for full manual automation step execution in dashboard and engine flows
- Expand due-schedule coverage in automation store and dashboard route tests
- Add cron runner regression tests for edge cases and document the automation execution fix via changeset
- Fix triage planning model resolution to fall back through project/global planning settings and default overrides
- Fix reviewer model selection to honor validator-specific settings before default provider/model overrides
- Update merger model resolution to apply default override fallback and align shared task setting types/executor flow
- Add regression coverage for triage, reviewer, and merger fallback behavior and update settings hierarchy documentation
- Skip timer-triggered heartbeat ticks when the target agent is paused
- Add executeHeartbeat pause checks so paused agents do not start task work
- Expand agent-heartbeat tests to cover pause guards across scheduler and execution paths
- Document paused-agent heartbeat behavior in docs and add a patch changeset for @runfusion/fusion
- Add isGitRepository utility in worktree-pool using git rev-parse checks
- Fail fast in TaskExecutor with actionable non-git errors before worktree creation starts
- Classify not-a-git-repository worktree add failures as non-retryable in recovery flows
- Warn from in-process runtime startup when the working directory is not a Git repository
- Expand executor and worktree-pool tests to cover non-git, missing-dir, and conflict-classification paths
- Render a compact token usage indicator in TaskCard footer with accessible labeling and token-aware styling
- Track token usage fields in the TaskCard memo comparator and expose a comparator test helper for regression coverage
- Add TaskCard tests for token usage rendering behavior and comparator invalidation on token usage updates
- Configure runtime plugin Vitest setups with an @fusion/engine source alias for reliable workspace test resolution
- Keep restart integration child_process spawn mocking aligned with execSync-driven merge verification behavior
Three fixes for the worktree-overflow / stuck-task incident:
1. Cap deterministic-verification-failure bounces (fix#2)
Auto-merge previously bounced an in-review task back to in-progress
on every verification failure with no upper bound. A single flaky test
could keep a task ping-ponging in-review→in-progress forever, holding
its worktree and consuming agent slots. Adds verificationFailureCount
on Task (DB migration v48), increments on each bounce, and after 3
failures marks the task failed and creates a follow-up triage task
so a fresh agent can investigate the underlying flake instead of
re-running the same fix loop.
2. Reap unregistered orphan worktree dirs even when recycle is on (fix#3)
cleanupOrphans previously bailed out entirely when recycleWorktrees
was true, leaving stale dirs (clear-hawk-broken, *-bak, leftover
crash debris) on disk forever. New reapUnregisteredOrphans pass
removes only directories that aren't registered git worktrees, so
the recycle pool keeps its warm worktrees but the trash gets cleared.
3. Idempotence guard on activity-log listener wiring (fix#6)
setupActivityLogListeners() was registering handlers on every call.
When init() ran twice, every task:created / task:moved event wrote
N rows to activityLog, producing the duplicate entries visible in
the DB. Added activityListenersWired flag so repeated calls no-op.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9c7c8ee5 switched merger verification from exec to spawn-based
execWithProcessGroup, but restart.integration.test.ts only mocked
execSync/exec. spawn() returned undefined, so the merger crashed before
running the test command and any in-review merge test that hit the
verification path failed with VerificationError.
Adds a spawn mock that funnels through the existing execSync mock so a
single mockedExecSync.mockImplementation continues to control both git
calls and verification command outcomes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 5-minute exec timeout only killed the immediate shell, leaving
vitest/pnpm worker trees alive. Across retries these accumulated and
thrashed the host, starving the engine and TUI. Switch verification to
spawn-based runner with detached process group so timeouts SIGTERM the
whole tree (SIGKILL after 5s grace), and bump the wallclock to 10m for
larger workspaces. Stream-truncate output instead of relying on ENOBUFS.
Also fix two flaky/race-prone dashboard tests that were red on main and
blocking every in-review task at merge verification.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add regression tests across CLI, core, dashboard, and engine for remote access auth, settings parity, and serve/TUI callback wiring
- Expand dashboard route and modal coverage for remote settings/auth flows including node environment behaviors
- Redact provider-switch failure details in tunnel process manager to avoid leaking sensitive provider diagnostics
- Update route registration and engine lifecycle tests to lock in remote-access behavior under real execution paths