Conversation
…seded
A manual spawnTicket stranded undelivered on an offline host could be
started a second time and reclaimed a third, transiently double-starting
one ticket:
1. committedTicketSpawn (XERK-331) correctly lets a fresh Start through
for an undelivered-on-offline command (reclaim owns that case), so a
new session comes up on a live host.
2. reclaimStrandedTicketSpawns then re-queued the stranded command as a
MANUAL queue entry, and drainTicketQueue dispatches manual entries
unconditionally (the guard-skip that lets the + button ask for a
second session) — a second live session for the one ticket.
Fix: the dispatch memo (ticketDispatchedAt) now records each dispatch's
cmdId, not just its timestamp. rememberDispatch stamps it at every
dispatch site (both Start paths + drainTicketQueue), so the memo always
names the NEWEST dispatch. reclaim consults dispatchSupersedes: when a
stranded command's cmdId is not the newest dispatch (a fresher Start
routed the ticket elsewhere within TICKET_DISPATCH_MEMO_MS), it withdraws
the superseded command WITHOUT re-queueing — cleaning it off the dead host
so it can't start on return, while the fresh dispatch remains the one
session the operator asked for. A genuinely stranded command (its own
dispatch is still the newest) is re-routed exactly as before.
Distinguishes 'operator asked for a second session' from 'reclaim
re-queued a command whose fresh start already happened' without regressing
the manual guard-skip.
Tests: two XERK-540 cases in server.test.js (the superseded-withdraw and
the ordinary-still-reclaimed paths).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fixes XERK-540: a manual
spawnTicketstranded undelivered on an offline host could transiently double-start one ticket.The sequence (all in
turma/server.js's in-memory ticket queue):spawnTicketfor a ticket is stranded undelivered on host A, which goes offline (>OFFLINE_AFTER_MS= 75s silent).committedTicketSpawn(XERK-331) deliberately returns null for an undelivered-on-offline command (reclaim owns that case), so a fresh spawn dispatches to a live host B.reclaimStrandedTicketSpawnswithdrew A's command and re-queued the ticket as a manual entry;drainTicketQueuedispatches manual entries unconditionally (the guard-skip that lets the board's + button ask for a second session) → a second live session for the one ticket.Per the ticket, this is a design call: the fix must tell "operator asked for a second session" (the + button — must keep working) apart from "reclaim re-queued a command whose fresh start already happened," without regressing the manual guard-skip.
Why / how
Took the ticket's suggested route — have reclaim consult the dispatch memo the way the DELETE cancel route does — made precise with a cmdId:
ticketDispatchedAtnow stores{at, cmdId}instead of a bare timestamp.rememberDispatch(site, key, cmdId)records the cmdId at every dispatch site (both Start-route paths +drainTicketQueue), so the memo always names a ticket's newest dispatch.dispatchSupersedes(site, key, cmdId)→ true when the newest dispatch's cmdId differs from a stranded command's own cmdId (withinTICKET_DISPATCH_MEMO_MS= 5 min).reclaimStrandedTicketSpawnschecks it before its repo/free-host/backoff preconditions: a superseded stranded command is withdrawn but not re-queued (cleaning it off the dead host so it can't start on return, turning a transient double-start into a lasting one); a genuinely stranded command — whose own dispatch is still the newest (memo cmdId == its own) — re-routes exactly as before.This is precise: a plain reclaim leaves the memo's cmdId equal to the command's own, so nothing changes; only a genuine second dispatch (a fresh Start) trips the guard. The + button is untouched (it acts when no in-flight command exists →
committedTicketSpawnnull → fresh dispatch). The cancel route'sdispatchedRecentlystill readsrec.atoff the new shape.How verified
XERK-540:cases inserver.test.js(superseded → withdraw-not-requeue; ordinary → still reclaimed). Full turma suite green: 1699 pass, 0 fail, including the wholeXERK-303:reclaim block..claude/rules/turma-ticket-queue.md), under the size cap.GET /api/agents200); ran the suites; mutation-tested the guard (neuteringdispatchSupersedes→false fails the double-start test; dropping thecmdId !==guard fails both ordinary-reclaim and XERK-303 basic reclaim — both caught); adversarial scenarios incl. two sequentially-stranded hosts (the newest dispatch always survives fleet-wide → exactly one session) and a genuine +-button second session (not regressed). Delivered commands never reach the check (it sits after the"deliveredAt" in ccontinue). No defects.Pure server-side queue plumbing (
turma/server.jsonly) — parity-exempt per the Web ⇄ Android contract.