Multiworkspace tasks could not complete due to two independent bugs:
1. task.workspaceWorktrees had no SQLite column / rowToTask mapping, so
fn_acquire_repo_worktree's updateTask write was dropped on every persist
(applyTaskPatch writes the DB-round-tripped task back to task.json). Every
later getTask returned undefined, so fn_task_done's scope verifier read {}
and blocked with "acquired no sub-repo worktrees", and isWorkspaceTask()
consumers misfired. Persist it mirroring mergeDetails (schema column + v129
migration + db-migrate + defineTaskColumn + TaskRow + rowToTask).
2. In workspace mode every task ran rooted at the shared browse-only root, and
setActiveSession registered that path keyed only by path — so a second
concurrent workspace task was rejected by the foreign-task guard
("active-session path ... is held by ..."). Give each task a task-scoped
synthetic session key (sessionRegistryPath), applied at all register and
unregister sites; the in-memory worktree Set still holds the real root.
Regression tests assert the persistence invariant across getTask/listTasks/
store-reopen and concurrent session registration across all three session
surfaces; both verified to fail without the fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>