Add X-Session-Id and X-Session-Affinity headers to all outbound LLM chat
completion requests so LLM gateways can sticky-route consecutive requests
from the same conversation and observability tools (Langfuse, Arize) can
group stateless API calls into a single multi-turn trace.
The headers carry a stable identifier: the task id when available (stable
across pause/resume), otherwise the pi session id. The implementation wraps
modelRegistry.getApiKeyAndHeaders -- the single chokepoint pi-coding-agent
uses for both the main stream and compaction -- merging routing headers into
the resolved output. This covers all HTTP-based providers (built-in, custom,
and HTTP-streaming extensions) without disturbing auth resolution.
Also propagates taskId to four secondary executor sessions (retry,
verification-fix, workflow-step, child-agent) that previously fell back to
a per-instance pi id, fragmenting per-task observability grouping.
Closes#1675
- Await async spawned child session disposal
- Use own-key iteration for structured tool result previews
- Add FNXC requirement comments for new regression assertions
- merger-ai: resolve+persist concrete landedSha when a sub-repo is recognized
already-landed via the Fusion-Task-Id trailer fallback, so finalize no longer
drops it and mis-finalizes a fully-landed workspace task as a no-op
- project-engine: manual-merge land-lease busy errors reject the resolver without
burning mergeRetries; clear stale busy-reenqueue counter on real partial land;
persist retry count before arming the backoff timer (fail closed on write error)
- cli/dashboard + task: use shared isWorkspaceTask predicate instead of inlining
- base-commit-capture: POSIX single-quote shell escaping for integration ref
- git-repository: validate workspace.json repos elements are strings
- merger-ai: drop dead store param from landOneRepo
- tests: assert the 60s backoff cap across cycles; exercise the real runAiMerge
merge door; fix non-git-root assertion; re-export real workspace error classes
in the merger-ai mock (fixes 24 pre-existing instanceof-undefined failures);
remove generic fake-timer smoke test now covered by the live engine assertion
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wrap the fatal-path acquisition observability writes (logEntry + audit.git)
in safeObserve so a store/audit throw can't replace the original
acquisition error, keeping WorkspaceRepoAcquireBusyError instanceof checks
reliable upstream.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4-persona review of the Phase-D workspace self-healing. The headline: the P0
single-commit-finalize guard had to be applied across ALL surfaces, not just the
one reconciler U1 patched (FN-5893).
Finalize-site audit (A): gated every site where a workspace task could be
single-commit-finalized on one repo's commit — recoverStuckMergeDeadlocks (the
twin of the U1-patched reconciler, reachable via blocked-dependents),
recoverOrphanOnlyScopeViolations, recoverAlreadyMergedReviewTasks,
recoverBranchMisboundInReviewTasks (workspace tasks carry task.branch so the
Boolean(branch) filter didn't exclude them), plus a defensive filter on
finalizeNoOpReviewTasks. recoverMergedReviewTasks confirmed safe (mergeConfirmed
gate). Each is an isWorkspaceTask early-skip; single-repo behavior unchanged.
Reliability/concurrency:
- The partial-land reconciler now captures enqueueMerge's boolean and bounds
re-enqueues (mergeStarvationDrops → fail after N) instead of looping silently
forever on a full queue.
- The phantom-lease reclaim only acts on a terminal owner (null/done/failed) — it
no longer reclaims the lease of an in-progress executing task that registered it
early (shared isWorkspaceOwnerLive predicate).
- A new isMergePending(taskId) = mergeActive ∪ mergeQueue seam (exposed from
ProjectEngine, wired through the runtime) guards both reconcilers against the
merge-queue dispatch window — a task dequeued-but-not-yet-merging is no longer
re-enqueued (which, since a same-task land lease isn't contention, could have
caused a concurrent double-squash).
- FORK-A: a repo whose branch is gone and which isn't landed is parked, not
re-enqueued forever. Orphan-worktree removal failures log.warn + bound.
recoverDoneTaskMergeMetadata skips workspace tasks.
Maintainability: dissolved the self-healing↔merger-ai import cycle by moving
isRepoLanded into a dependency-free workspace-land-predicate.ts; removed a
redundant cast.
Gate green: build, typecheck, lint, test:gate (649+58); self-healing + e2e +
project-engine + merger 724.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A real two-repo workspace lifecycle test in the engine-default lane (describeIfGit,
not the merge gate). Test 1 drives landWorkspaceTask on two acquired sub-repos and
asserts the NO-PUSH invariant directly: each sub-repo gets a real bare origin, and
the test snapshots every origin + remote-tracking ref before/after and asserts
byte-for-byte equality while the local refs/heads advance — a leaked push would
move an origin ref and fail. Plus per-repo landedSha and finalize-exactly-once.
Test 2 forces a repo-B conflict (repo A lands, task not done), then invokes the U1
reconcileWorkspacePartialLands reconciler under fake timers (enqueueMerge wired to
the real in-process route) and asserts recovery completes with no double-land of
repo A (its ref is unchanged from the first pass — proving the isRepoLanded skip).
Engine-default lane confirmed: test:gate stays 649+58 (did not enter engine-core).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Makes the self-healing layer workspace-aware and adds recovery for the states
Phase C introduced. No reconciler may wrongly finalize, skip, or move a workspace
task backward.
Existing reconcilers (the P0/P1 the feasibility check caught):
- recoverInterruptedMergingTasks no longer single-commit-finalizes a workspace
task: a workspace candidate clears the transient "merging" status and re-enqueues
via enqueueMerge (which routes to the idempotent landWorkspaceTask), never
reaching findLandedTaskCommit / moveTask(done) / task:merged — so a partial-landed
task (repo A landedSha, repo B not) is never marked fully merged on one repo's
commit. recoverStaleMergingStatus confirmed single-commit-free.
- recoverMergeableReviewTasks relaxes its Boolean(t.worktree) gate to also admit
isWorkspaceTask(t), so a zero-landed mergeable workspace task (null worktree) is
re-enqueued instead of silently skipped forever.
New reconcilers:
- reconcileWorkspacePartialLands: re-enqueues stuck non-done workspace merges via
enqueueMerge (idempotent skip of landed repos), guarded by allowsAutoMergeProcessing
(FN-5147), user-pause, and a workspace-aware liveness predicate (any sub-repo path
active via pathsForTask+isPathActive — triple-proof isn't workspace-aware). A repo
with its fusion/<id> branch gone AND landedSha unset is parked failed; branch-gone
but landed is skipped. Emits task:reconcile-workspace-partial-land(+-no-action).
- reclaimPhantomWorkspaceLandLeases: enumerates the new activeSessionRegistry
entriesByKind("workspace-repo-land"), age-gated by the FN-6736 floor, and clears a
lease whose owner is terminal/dead (live merging owners untouched). Emits
task:reclaim-phantom-workspace-land-lease.
- reconcileOrphanedWorkspaceWorktrees: removes a done workspace task's recorded
per-repo worktreePaths (isPathActive-guarded) with NO temp-root walk (AGENTS.md).
Emits task:reconcile-orphaned-workspace-worktree.
New activeSessionRegistry.entriesByKind seam; four DatabaseMutationType literals +
the AGENTS.md Run Audit list. Single-repo behavior byte-for-byte unchanged (every
path branches on isWorkspaceTask). 13 new fixture tests; 558 self-healing tests +
test:gate (649+58) green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5-persona review of the Phase-C per-repo merge loop. No P0; the no-push invariant
and retry/park accounting verified clean. Fixed:
Land mechanics (merger-ai.ts / active-session-registry.ts):
- persistRepoLandedSha no longer swallows the DB write: a failed landedSha write
after the ref advanced now escalates to WorkspacePartialLandError so the engine
parks/retries instead of silently re-landing (duplicate squash). isRepoLanded
gains a landedSha-independent fallback — it scans the integration ref for this
task's Fusion-Task-Id trailer (a squash commit is NOT a branch descendant, so a
branch-ancestor check is provably wrong), so an actually-landed repo is skipped
on retry.
- The land lease is now taskId-aware across kinds: any foreign-task holder on a
sub-repo path is contention (a merging task can't run over an executing task's
acquire lease), and registerPath throws ActiveSessionPathHeldByForeignTaskError
instead of silently clobbering a different task's entry.
- The per-repo loop is wrapped in try/finally(setStatus(null)) so the busy/partial
throws can't leave the task stuck 'merging'. WorkspacePartialLandError is a real
exported class (not a .name-mutated Error). finalizeWorkspaceTask re-reads fresh
and no longer swallows the mergeDetails write (TOCTOU). isRepoLanded exported for
Phase D.
Dispatch + doors (project-engine.ts / dashboard.ts / task.ts / @fusion/core):
- getTask-null in the partial-land catch fails closed (park) instead of defaulting
retries to 0 and scheduling an indefinite retry storm.
- The merge-confirmed reachability fast-path skips workspace tasks (its
representative commitSha is a sub-repo squash sha, unreachable in the root cwd —
it was demoting fully-merged tasks); they're verified by per-repo landedSha.
- The CLI/dashboard merge doors now return merged:true on full land (were hardcoded
merged:false). WorkspaceRepoLandBusyError re-enqueues with backoff WITHOUT burning
the mergeRetries quota (bounded busy counter) so contention can't park a healthy
task. Backoff capped at 60s. shouldRetryWorkspacePartialLand folded into
shouldRetryAutoMergeConflict. Catch switched to instanceof. New canonical
isWorkspaceTask predicate in @fusion/core.
Gate green: build, typecheck, lint, test:gate (649+58); workspace-merger + oracle
+ project-engine 174.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>