You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
π bug: a hub restart makes every in-flight contributor task unresumable β the agent is interrupted, then handed the same issue back seconds laterΒ #5681
When the hub restarts to take an upgrade, every contributor that is mid-task has its
work thrown away β and then is handed the identical issue back seconds later.
A contributor relay holds one task at a time and keeps working through a brief
disconnect, re-asserting the task when it reconnects. The hub only honors that
re-assertion if it can match a server-issued lease (lookupLease), which is
deliberate: #C4 established that a client must never be able to assert ownership of
work the server did not assign. But leases live only in the hub's memory. A restart
empties that store, so after an upgrade no in-flight resume can ever match, the
relay is told no active lease for this task, and the revoke path interrupts the
agent mid-turn.
The waste is visible in the ordering: the hub revoked the task at 14:24:40 and
re-assigned the same issue to the same relay at 14:24:44. Ownership was never in
question β only the record of it. The agent restarted the identical work from zero,
discarding about two and a half minutes of a turn that was progressing normally.
Blast radius: every contributor holding a task at the moment of any hub restart.
Self-upgrade rolls make this routine rather than rare.
Observed, 2026-09-02
Contributor Danathar, claude backend, relay protocol 1.2, working issue #5617.
Relay console (timestamps local, UTC-4):
[10:22:03] Task assigned: issue kubestellar/hive#5617 β [v5] reviewer lane follow-ups
[10:22:06] Task prompt sent to CLI
[10:24:09] Connection closed (code=1012 service restart: hub restarting for upgrade). Reconnecting in 1000ms...
[10:24:39] Connection closed (code=1006 abnormal closure β no close frame). Reconnecting in 1000ms...
[10:24:40] Connected / Authenticated as c-ca8187f7480c (tier: contributor)
[10:24:40] Reconnected while working on kubestellar/hive#5617 β resuming
[10:24:40] Task revoked: ct-kubestellar/hive-5617-1788358923 β no active lease for this task
[10:24:43] Relaunching claude after task revoke: β¦
[10:24:44] Task assigned: issue kubestellar/hive#5617 β [v5] reviewer lane follow-ups
[10:24:45] Task prompt sent to CLI
Hub activity feed, same window:
2026-09-02T14:24:40Z | joined
2026-09-02T14:24:44Z | picked up | issue kubestellar/hive#5617
The agent's pane at the moment of the revoke:
β I'll start by getting the checkout and reading the issue.
Searched for 9 patterns, read 3 files, listed 3 directories, ran 13 shell commands
βΏ Interrupted Β· What should Claude do instead?
Thirteen shell commands and a fork/clone in, then killed. The task file went from gen=75 to gen=2 β a fresh task record for the same issue.
Note the 1012 is #5390's fix working correctly: the hub now closes cleanly and says
why, instead of dropping a silent 1006. This issue is about what happens after
that clean close.
Why the resume cannot succeed
src/pkg/dashboard/contribute_ws.go β the resume branch of task_progress:
lease:=h.lookupLease(identity, msg.TaskID, canonRepo, msg.Number, msg.TaskGen, time.Now())
iflease==nil {
β¦
_ =sendJSON(conn, WSMessage{Type: "task_revoke", β¦, Reason: "no active lease for this task"})
continue
}
The rejection is correct given what it knows. The gap is upstream: leases are held in
process memory (lastLeaseRenew on the in-memory contributor, expiresAt: now.Add(leaseTTL)), and nothing reconstructs them across a restart. leaseTTL == wsTaskTimeout, so a lease that was minutes from expiry is indistinguishable after a
restart from one that never existed β both look like nil.
#4260 established the contract that a relay reconnecting inside the backoff window
resumes rather than losing its task, and contribute_reconnect_resume_test.go pins it: "It must be resumed, not told 'no
active lease for this task'." That test exercises a reconnect to a live hub, so
it passes while this case fails β the contract simply has no coverage across a
process boundary.
Fix shape
Make the lease survive a restart, without weakening #C4. The relay's claim must still
be checked against a server-issued record; the record just needs to outlive the
process. Options, roughly in order of intrusiveness:
Persist the lease next to the contributor profile, which is already written to
disk on every task transition (saveContributorProfile). On boot, load unexpired
leases so lookupLease can match a legitimate resume. The lease is small and
already has an absolute expiresAt, so a stale one expires on its own.
Reconstruct from the durable run record.appendTaskRun/the task-run log
already persists assignment facts; an assignment with no terminal record and a
timestamp inside leaseTTL is exactly a live lease.
(1) is the smallest change that actually fixes it. (3) is complementary, not a
substitute.
Whatever the mechanism, the observable contract to pin in a test is the one this
incident violated: a relay that reconnects after a hub restart, inside leaseTTL,
with a matching {identity, task_id, repo, number, generation}, resumes its task β
and the hub does not re-offer that issue to anyone, least of all to the relay it just
revoked it from.
Related: #4260 (the resume contract), #5390 (clean close on upgrade), #5391
(self-upgrade roll frequency, which sets how often this fires), #5090.
Summary
When the hub restarts to take an upgrade, every contributor that is mid-task has its
work thrown away β and then is handed the identical issue back seconds later.
A contributor relay holds one task at a time and keeps working through a brief
disconnect, re-asserting the task when it reconnects. The hub only honors that
re-assertion if it can match a server-issued lease (
lookupLease), which isdeliberate: #C4 established that a client must never be able to assert ownership of
work the server did not assign. But leases live only in the hub's memory. A restart
empties that store, so after an upgrade no in-flight resume can ever match, the
relay is told
no active lease for this task, and the revoke path interrupts theagent mid-turn.
The waste is visible in the ordering: the hub revoked the task at 14:24:40 and
re-assigned the same issue to the same relay at 14:24:44. Ownership was never in
question β only the record of it. The agent restarted the identical work from zero,
discarding about two and a half minutes of a turn that was progressing normally.
Blast radius: every contributor holding a task at the moment of any hub restart.
Self-upgrade rolls make this routine rather than rare.
Observed, 2026-09-02
Contributor
Danathar, claude backend, relay protocol 1.2, working issue #5617.Relay console (timestamps local, UTC-4):
Hub activity feed, same window:
The agent's pane at the moment of the revoke:
Thirteen shell commands and a fork/clone in, then killed. The task file went from
gen=75togen=2β a fresh task record for the same issue.Note the
1012is #5390's fix working correctly: the hub now closes cleanly and sayswhy, instead of dropping a silent
1006. This issue is about what happens afterthat clean close.
Why the resume cannot succeed
src/pkg/dashboard/contribute_ws.goβ the resume branch oftask_progress:The rejection is correct given what it knows. The gap is upstream: leases are held in
process memory (
lastLeaseRenewon the in-memory contributor,expiresAt: now.Add(leaseTTL)), and nothing reconstructs them across a restart.leaseTTL == wsTaskTimeout, so a lease that was minutes from expiry is indistinguishable after arestart from one that never existed β both look like
nil.#4260 established the contract that a relay reconnecting inside the backoff window
resumes rather than losing its task, and
contribute_reconnect_resume_test.gopins it: "It must be resumed, not told 'noactive lease for this task'." That test exercises a reconnect to a live hub, so
it passes while this case fails β the contract simply has no coverage across a
process boundary.
Fix shape
Make the lease survive a restart, without weakening #C4. The relay's claim must still
be checked against a server-issued record; the record just needs to outlive the
process. Options, roughly in order of intrusiveness:
disk on every task transition (
saveContributorProfile). On boot, load unexpiredleases so
lookupLeasecan match a legitimate resume. The lease is small andalready has an absolute
expiresAt, so a stale one expires on its own.appendTaskRun/the task-run logalready persists assignment facts; an assignment with no terminal record and a
timestamp inside
leaseTTLis exactly a live lease.in-flight tasks reach a terminal report before the process exits would shrink the
window, though it cannot close it β an upgrade will not wait out a 20-minute turn.
(1) is the smallest change that actually fixes it. (3) is complementary, not a
substitute.
Whatever the mechanism, the observable contract to pin in a test is the one this
incident violated: a relay that reconnects after a hub restart, inside
leaseTTL,with a matching
{identity, task_id, repo, number, generation}, resumes its task βand the hub does not re-offer that issue to anyone, least of all to the relay it just
revoked it from.
Related: #4260 (the resume contract), #5390 (clean close on upgrade), #5391
(self-upgrade roll frequency, which sets how often this fires), #5090.