Fence the two coordinator-side stops nobody had leased
F65, found live on run 20. A plan mismatch asking for a human decision recorded its observation, then failed to block the task: Store.Append fences every lifecycle event on a leased task against the current owner and epoch, and this TaskBlocked carried neither. The task kept implementing while the contradiction sat durable in the log, and the agent was told its report was refused. The trajectory gate had the same omission. The human-decision path already did this correctly and explained why in a comment. That comment is now a helper all three call. The tests could not have caught it. planWith never leased its task, so every plan test ran in a state no agent can be in, which is exactly what the lease helper's own comment warns against. It leases now, and the mismatch block test fails without the fence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
This commit is contained in:
@@ -58,18 +58,27 @@ func RequestHumanDecision(s *store.Store, project registry.Project, taskID strin
|
||||
return blockTask(s, t, domain.BlockReasonHumanDecision, req.Render(), &req)
|
||||
}
|
||||
|
||||
// fenceToLease binds a lifecycle event to the lease that is producing it.
|
||||
// Store.Append fences every lifecycle event on a leased task against the
|
||||
// current owner and epoch, so an event that omits them is a conflict rather
|
||||
// than a block. Every coordinator-side stop goes through here: two of them did
|
||||
// not, and both failed silently against a live lease (F65). A task stops for a
|
||||
// human only while some session is running, so the leased case is the only one
|
||||
// that ever mattered.
|
||||
func fenceToLease(payload map[string]any, t domain.Task) {
|
||||
if t.Lease == nil {
|
||||
return
|
||||
}
|
||||
payload["harness_id"] = t.Lease.HarnessID
|
||||
payload["lease_epoch"] = t.Lease.Epoch
|
||||
}
|
||||
|
||||
func blockTask(s *store.Store, t domain.Task, reason domain.BlockReason, blocker string, req *domain.DecisionRequest) (domain.Event, error) {
|
||||
payload := map[string]any{
|
||||
"blocker": blocker, "block_reason": string(reason),
|
||||
"lifecycle_phase": "awaiting_human",
|
||||
}
|
||||
if t.Lease != nil {
|
||||
// Store.Append fences every lifecycle event on a leased task against
|
||||
// the current owner and epoch. A question from a session that no
|
||||
// longer owns the task is a conflict, not a block.
|
||||
payload["harness_id"] = t.Lease.HarnessID
|
||||
payload["lease_epoch"] = t.Lease.Epoch
|
||||
}
|
||||
fenceToLease(payload, t)
|
||||
if req != nil {
|
||||
payload["decision_request"] = req
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user