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>
Addresses the follow-up review on the foundation fixes:
- Re-read the task via store.getTask immediately before merging the per-repo
entry, so a concurrent sibling-repo acquisition that landed since the initial
read isn't clobbered by updateTask's wholesale map replace (narrows the
read-modify-write window to the store lock; a fully atomic per-repo store-level
merge remains a follow-up).
- Normalize the inline FNXC comment to the FNXC:Area yyyy-MM-dd-hh:mm: convention.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- base-commit-capture.ts: shell-quote integration branch with a real
single-quoted POSIX literal instead of JSON.stringify (not shell-safe).
- TaskCard.tsx: memo compares full workspaceWorktrees values, not just key
sets, so a same-key worktreePath/branch change re-renders.
- TaskDetailModal.tsx: gate/render workspace summary off hydrated workingTask.
- worktree-acquisition.ts: null the singular worktree/branch columns in the
workspaceWorktrees write so isWorkspaceTask stays true; wrap non-fatal
post-acquire observability so logEntry/audit can't re-escalate to fatal.
- agent-tools.ts: register sub-repo worktree via onAcquired unconditionally
(idempotent) so a resumed/already-acquired path is tracked after restart.
- executor.ts: DB liveness fallback also checks task.workspaceWorktrees paths.
- executor-workspace.test.ts: root non-git assertion runs in fx.rootDir (".").
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- base-commit-capture: POSIX single-quote integration branch refs instead of
JSON.stringify (double quotes are subject to $-expansion in the shell)
- executor: add per-repo no_commits guard to the workspace verifyWorktreeInvariants
branch (parity with the singular path), gated by the same task-wide no-commit
eligibility
- executor: reviewWorkspacePerRepo failure message now states the per-repo verdict
list is partial (evaluation stops at first failure)
- worktree-acquisition: defensively wrap non-fatal/outer-catch logEntry/audit so a
logging throw cannot promote a non-fatal error to fatal or mask the original error
- docs/plans: add code-fence language tags and fix MD028 blank-line-in-blockquote
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>