Claim-shaped verbs were failing 7/7 (claim_review) and 6/6
(claim_doc_task) as silent 120s FlowVerbTimeout 504s on the NAS: the
per-claim ownership repair walked the whole clone issuing two stat
syscalls per entry (chown_ms 39502 vs git_ms 8 in the live log), several
passes stacked per claim, and the claim transaction held the task row
the whole time — so concurrent writers queued behind it into the 60s
lock_timeout. The walk now does one stat per entry shared by the
chown-skip and chmod-skip checks, and a .git/roboco-owned sentinel
(worktree-aware via _resolve_clone_root, written only after a
zero-failure pass) skips the walk entirely when the tree is already
agent-owned. Every root-side git write invalidates the sentinel BEFORE
its subprocess runs — GitService._run_git for scope != none, plus the
three raw-subprocess paths inside WorkspaceService the adversarial pass
proved bypass it deterministically on the common respawn shape
(_worktree_git for mutating verbs, _fetch_branch_ref,
_fetch_origin_best_effort) — so a live marker can never vouch for files
a root write is about to create.
One of those queued writers was the PM journal-decision auto-record:
its INSERT hit the lock timeout, _ensure_pm_decision's catch-all
swallowed it without rollback, and the poisoned session blew up
escalate_up with PendingRollbackError (live incident). The helper's try
body now runs in a savepoint — one fix covering all seven PM verbs that
route through it — verified empirically against real Postgres in both
directions: the failure path leaves the session healthy and the task
object readable, and create_entry's internal commit inside the savepoint
drains the transactional outbox exactly once.
* fix(orchestrator): task-scoped oscillation breaker for escalate/unblock ping-pong
An escalation ping-pong oscillates a task between two agents (cell PM
escalate_up -> BLOCKED -> main PM unblock -> restored -> respawn ->
escalate again). The per-(agent, task) respawn gate never trips on it:
the restored side is dispatched by _dispatch_claimed_without_agent,
which consults no respawn counter at all, so one side of the round trip
always has fuel regardless of the other's strikes — and even a tripped
main-PM counter only stalls the task silently at blocked instead of
surfacing the oscillation.
- Strikes are counted task-scoped at the unblock() chokepoint
(agent-agnostic; legitimate needs_revision rework never calls
unblock, so it structurally cannot trip this), durable in the
existing orchestration_markers column — no migration.
- Progress between round-trips (commits / revision_count advancing)
resets the count: real forward motion is not an oscillation.
- On trip: the task is blocked with a HUMAN resolver (the budget-breach
posture), both dispatchers stop respawning onto it, further unblock()
refuses until an admin override clears the marker, and the CEO
notification names both agents and the cycle count.
- _notification_has_live_work now treats a HITL-blocked related task as
no live work, closing the same loop for the admin-route escalation
path.
* fix(orchestrator): wire the oscillation trip to the dispatchers and make recovery reachable
- TaskResponse serializes blocker_resolver_type: the dispatchers' HITL-blocked
skip and the notification-path live-work check now actually fire over the
wire instead of only against in-process rows.
- The oscillation marker clears on every human transition out of BLOCKED
(snapshot or not), and the human unblock route treats a tripped task as
the requested intervention: clears the marker and proceeds, while the
agent gateway verb keeps refusing.
- The progress fingerprint includes the terminal-children count, so a
coordination root whose children advanced between escalations resets
instead of accruing toward a false trip.
---------
Co-authored-by: Renn F <rennf93@users.noreply.github.com>