Skip to content

Resilient assembly-review loop — Reference

Terse reference for the resilient assembly-review loop introduced in v0.9.17-rc1: configuration knobs, coordinator status transitions, emitted events, and the lockout protocol contract. For the conceptual explanation see the deep dive; for the operator experience see the user guide.

Configuration

Config keyDefaultMeaning
Coordinator:StaleLockThresholdSeconds15Age in seconds above which a .git/index.lock file is considered stale and eligible for removal between commit retry attempts. The check also verifies no live git process owns the lock; on any uncertainty the file is left untouched and the fault surfaces as a visible child-turn-failed terminal. Applies per child/revision run's own worktree only. Source: WorktreeManager.ClearStaleIndexLock.

Human review round-trips are no longer capped. The former CoordinatorSteeringDecider.DefaultMaxHumanReviewRoundTrips constant has been removed. A human request-changes at the assembly review gate now unconditionally resets the autonomous steering budget — a supervised, deliberate human request must always be honored, so capping it (and silently re-parking the run) was a dead-end. Each granted round is still bounded by the autonomous DefaultMaxPlanSteeringIterations per steering pass. The WorkPlan.HumanReviewRoundTrips column is retained purely as telemetry (surfaced on the coordinator.steering event's humanReviewRoundTrip field); it no longer gates anything.

Status transitions

The following WorkPlanStatus transitions are new or changed in this release:

Previous behaviorNew behavior
Budget exhausted → AssemblyBlocked (terminal, reason=steering_budget_exhausted) — run parks indefinitely.Budget exhausted → InReview (stage "review") — run escalates to the human-review gate. The assembled work is immediately reviewable.
AssemblyBlocked with steering_budget_exhausted was not eligibility-recoverable.InReview is the existing human-review state; all existing recovery, gate-arm, and crash-resume paths apply unchanged.

Full transition table (assembly phase)

FromEventToNotes
Assembling / AssemblySteeringAutonomous budget exhaustedInReview, stage "review"Guarded CAS via TryEscalateToInReviewAsync. Second replica no-ops.
InReviewHuman approvesAssembling → merge → assembly_completeExisting approve path, unchanged.
InReviewHuman declinesassembly_declined (terminal)Existing decline path, unchanged.
InReviewHuman request-changes (any round-trip)AssemblySteeringBudget unconditionally reset (no cap); autonomous loop restarts under the human's feedback. HumanReviewRoundTrips is incremented as telemetry only.

Emitted events

All existing event types; no new event type is introduced. Fields added to existing events:

EventNew / changed fieldsWhen emitted
coordinator.steering_decisiondecision="proceed", escalation="human_review"When the decider returns Proceed (budget exhausted) and the coordinator escalates instead of terminating.
coordinator.steering_decisiondecision="dispatch_fresh", rationale="single_eligible_agent…" (degrade) or decision="proceed", disposition="rejection", rationale="lockout_no_context…" (escalate)At the lockout gate when no alternate eligible agent exists (#233). With accumulated context the strict lockout degrades to a same-author fresh re-dispatch; without context it escalates to human review. Replaces the pre-#233 lockout_deadlock rationale.
coordinator.assembly_review_requestedgateKind="human-review", reason="steering_budget_exhausted", treeHash, integrationBranch, includedSubtaskIdsImmediately after the durable review-request is written. Same event as the normal happy-path human-review gate.
coordinator.steeringsource, humanReviewRoundTrip=<n>, noteEmitted when a human request-changes is received after escalation. The autonomous budget is always reset (there is no cap); humanReviewRoundTrip is the telemetry-only round count.
coordinator.assembly_implicated_scope_fallbackworkPlanId, source, reviewer, reason (no_target_files_field | target_files_matched_nothing), namedFiles, touchedFiles, contributorIdsEmitted when the #223 implicated-subtask scoping reverts to the broad all-contributors set because the reviewer's structured TARGET_FILES: hint was missing or reverse-mapped to nothing. Makes the fail-safe reversion observable.
coordinator.assembly_changes_requestedworkPlanId, redispatchSubtaskIds, redispatchedSubtaskIds, implicatedSubtaskIds, dependentSubtaskIds, feedbackWhen a gate requests changes. implicatedSubtaskIds are the reviewer-named (lockout-eligible) subtasks; dependentSubtaskIds are their transitive dependents (rebuilt, never locked out); redispatchSubtaskIds is their union.
run.failed (child run)reason=commit_failed_persistent, evidence=<exception summary + lock diagnostics>Emitted when a persistent commit fault exhausts retries in the child pipeline; the child run fails visibly with structured evidence instead of a silent stream drain.

coordinator.assembly_review_requested — accumulated feedback payload

When reason="steering_budget_exhausted", the review-request context includes the accumulated gate feedback from all prior autonomous rounds. This is built by BuildAccumulatedGateFeedbackAsync (bounded to 32 directives, 2000 characters each) and structured by gate source and round number, so the human can see exactly why autonomy could not converge. The same payload is visible in the review card in the web UI.

Implicated-subtask scoping (#223)

A gate request-changes no longer resets/locks out every subtask that touched a file. The reviewer (Rubberduck or Build & Test) may emit an optional structured TARGET_FILES: directive line naming the repo-relative paths it implicated; ReviewTargetFiles.Parse turns it into AssemblyReviewDecision.TargetFiles. The coordinator (AssemblyPlanning.ScopeImplicatedSubtasks) reverse-maps those paths onto the assembly-eligible subtasks that actually committed them, then AssemblyPlanning.TransitiveDependents sweeps their dependents:

SetMembersAuthor lockout?
Implicatedsubtasks that committed a named file (ScopeImplicatedSubtasks)Yes
Re-dispatchimplicated ∪ transitive dependentsdependents: No

If the TARGET_FILES: hint is absent (no_target_files_field) or matches no subtask (target_files_matched_nothing), scoping falls back to the whole contributor set (fail-safe) and emits coordinator.assembly_implicated_scope_fallback. The same helpers back both the live steering path (RouteAssemblyGateThroughSteeringAsync) and the conscious fresh-dispatch executor (RequestChangesAsync).

RAI verdict contract (#231)

The collective-assembly RAI reviewer returns its decision as a machine-readable sentinel, not prose. The prompt requires the last line of the response to be exactly VERDICT: <GREEN|YELLOW|REVISE|RED>; only that line is parsed. Prose (including the verdict legend) is never scanned, which removes the prior heuristic's false-escalation of benign reviews to RED.

AspectContract
Primary parseVERDICT: sentinel line only (TryParseSentinelVerdict). Last sentinel line wins; most-severe token wins on a single ambiguous line. Prose can never yield a passing verdict.
Non-sentinel fallbackUnambiguous emoji only (TryParseEmojiVerdict): 🔴 → blocking RED, 🟡 → advisory YELLOW. Nothing else in prose is consulted.
No parseable verdictExactly one bounded re-ask (ReAskPrompt) demanding only the sentinel; still unparseable → fail safe to blocking RED, reason unparseable_after_reask, emitted as run.rai_error. Never a silent YELLOW/GREEN.
Genuine REDSentinel RED or 🔴 sets ContentSafetyFlaggedRunRaiAsync SafetyFlagged → coordinator RaiBlockAsync (blocking).
Human-facing textExtractRationale / ExtractFeedback strip the sentinel line so surfaced feedback is the explanation, not VERDICT: REVISE.

Source: packages/Agentweaver.AgentRuntime/Workflow/RaiTurnExecutor.cs; apps/Agentweaver.Api/Coordinator/CollectiveAssemblyPipeline.cs.

Operator steering at the review gate (#226)

While the run is parked at the human-review gate (run.status == awaiting_review, coordinator_steerable == true), POST /api/runs/{id}/steer is honored (previously redirect/amend returned queued but were silently dropped). The verbs map as follows:

/steer verb at awaiting_reviewEffectDirective status
redirect / amendDelivered as a request-changes review decision — the same path as POST /assembly/review {request_changes}. The parked loop routes it via RouteAssemblyGateThroughSteeringAsync with #223 scoping + the unconditional budget reset.relayed, or deferred when the gate is armed on another replica
sendAdvisory note on the coordinator timeline; the gate stays armed — no decision, no budget reset.applied

Scoping. A bare redirect/amend has no target files, so it uses the broad all-contributors fallback (identical to /assembly/review with no target_files; emits coordinator.assembly_implicated_scope_fallback). Set targetChildRunId on the steer request to narrow: the targeted subtask's committed files flow through the same ScopeImplicatedSubtasks reverse-map, scoping to that subtask ∪ its co-touching subtasks.

HTTP status. POST /steer returns 201 Created normally, and 202 Accepted when the decision is deferred to another replica's poller (directive deferred) — mirroring the /assembly/review deferred response. It is never left silently queued (the pre-#226 drop bug). The new deferred value joins the pending → queued → relayed → applied steering-directive lifecycle.

Reviewer-rejection lockout protocol

When any gate source issues a request-changes (rejection), the lockout applies to the implicated subtasks only (#223 — never every file-toucher):

StepWhat happens
Lock out the authorThe current subtask author is atomically appended to Subtask.LockedOutAgents (a persisted JSON set).
Select a different agentroster \ LockedOutAgents via CoordinatorOrchestratorExecutor.ResolveRoster + SelectRosterMember.
Dispatch with contextRunOrchestrator.StartChildRevisionHandoffAsync reuses the prior child's worktree + branch; carries the full accumulated feedback bundle; mints a new SDK session (never resumes the locked-out author's session).
Single-eligible domain with context (#233)The implicated subtask's domain has one eligible agent, so RotationSelector.SelectRotationAuthor returns null, but there is accumulated context (hasContext). Degrade the strict lockout to a same-author fresh re-dispatch with full context (ConsciousDispatchFreshFallbackAsync): AssignedAgent kept, LockedOutAgents not mutated, dependents re-dispatched without lockout. Rationale single_eligible_agent.
Single-eligible domain without context (#233)!hasContext — nothing to carry → escalate to the Fix-B human-review gate. Rationale lockout_no_context. Never a terminal.
Recovery-budget boundThe same-author degrade does not reset Subtask.RecoveryAttempts; once MaxRecoveryAttempts (= 3) is hit, CoordinatorSteeringDecider.DecideAsync (SteeringPolicy.Decide, over-budget → Proceed) escalates this gate to human review. Never an infinite loop, never a terminal.

Transitive dependents of the implicated subtasks are re-dispatched to rebuild against the revised contract, but their authors are never locked out (#223 — locking a blameless dependent re-creates the roster-exhaustion deadlock).

Advisory / steer feedback (not a rejection) keeps the same agent in place (StartRevisionAsync); no lockout is applied.

Subtask LockedOutAgents field

Subtask.LockedOutAgents is a JSON-serialized string set that records which agent names are locked out from producing the next revision of that specific subtask artifact. It was a dormant reserved column since the initial 20260617224038_AddCoordinatorWorkPlan migration; v0.9.17-rc1 is the first release to read and write it. No new migration is required.

Stale git-lock diagnostics

WorktreeManager.ClearStaleIndexLock returns an IndexLockClearResult record that is included in the ChildTurnFailedOutput.Evidence string and the run.failed event:

FieldMeaning
lock_presentWhether <gitdir>/index.lock existed at the time of the check.
clearedWhether the lock was deleted.
age_sAge of the lock file in seconds (if present).
live_git_procWhether a live git process was detected (prevents deletion when true).
detailFree-text reason the clear was skipped, if applicable.

The gitdir is resolved from the worktree's .git file for linked worktrees (handles the pointer-file indirection) before checking the lock path, so the diagnostics always refer to the actual index location.

See also