Le spie che solo un boot spegne, e i giri che nessuno contava - #85
Merged
Merged
Conversation
…per sempre `isChecksHold` leggeva `checks_state === 'running'` come meta' di un OR, e quella riga la spegne un solo punto: `clearStaleChecksRuns`, chiamata alla costruzione della rotta, cioe' al boot. Se `measure()` lancia prima della registrazione finale - `ChecksInterruptedError` del freno swap, `throwIfStopping()`, un'eccezione qualsiasi - il gate cancella la sua chiave e la riga resta «running»: da li' in poi il predicato e' vero per sempre per quella sessione. 89 boot hanno trovato almeno una spia gia' accesa (71 volte 1, 16 volte 2, una 4, una 6), e i due orologi che lo leggono hanno pagato: 1253 riarmi del giudice di stallo e uno spazzino StaleStream che risponde `extend` a ogni turno muto, quindi un turno morto non viene mai finalizzato. Adesso decide il registro VIVO del gate e la riga lo conferma: `clearStaleChecksRuns(isLive)` spegne solo le spie che il gate non conosce, e la passata gira sui 30 secondi che lo spazzino paga gia'. La chiamata nuda del boot resta com'era - li' il registro e' vuoto per costruzione - e una corsa che sta macinando `test:unit` non viene toccata.
…o lo contasse `resumePendingDeliveries` riemette la consegna che il riavvio ha tagliato, e non contava niente: ogni giro ripartiva come il primo. Con TOPICS_SERVER_WATCH=1 il SIGTERM arriva a ogni salvataggio sotto server/ (6 riavvii nell'ora 14/09T23, 4 nell'ora T20, 1-2 all'ora per tutto il 16/09) e un giro con le righe CI costa i comandi locali - typecheck misurato 73-177 s, piu' lint, deadcode e i rail statici - piu' fino a 65 minuti di poll su GitHub. Dopo il #70 una consegna che aspetta solo la CI e' `deliveryOnlyWaitsOnChecks`, quindi e' proprio quella che il cancello del riavvio lascia tagliare: il giro poteva ricominciare all'infinito senza mai un verdetto e senza una riga sulla card. `pending_deliveries.rounds` conta i giri; oltre tre il boot non ne fa un quarto, dimentica la riga e scrive sulla card che serve un commit nuovo o una persona. Un commit diverso azzera il conto, perche' una riconsegna nuova non eredita il debito di quella prima.
… solo l'ora `holdUntilLabel` formattava ora e minuti, e l'ora da sola non distingue un reset di finestra da un piano esaurito. Misurato il 17/09: provider-hold.json teneva un muro Codex scritto il 13/09 alle 17:39 e in scadenza il 19/09 alle 14:45, e il log ha ripetuto «resumes at 14:45» 43 volte fra il 13/09T15:38 e il 16/09T22:28 - ogni riga sembrava «nel pomeriggio». La board ha dispatch_model=gpt-5.6-sol, quindi ci passano tutte le card. Adesso l'etichetta porta la data quando la scadenza non e' oggi, e oltre le 24 ore l'attesa diventa una domanda: il motivo sulla card dice che e' il piano esaurito e non il reset di una finestra, e la riga arriva anche nel thread, una per episodio come fa gia' il tetto di spesa - l'altra attesa che non finisce da sola. La nota va scritta PRIMA del chip: `noteHold` scrive lo stato `queued` senza motivo, e nell'ordine inverso lasciava `dispatch_error` vuoto.
…ici al giorno `planHeldNoted` vive nella closure del dispatcher, quindi nasce vuoto a ogni processo, e l'unica altra difesa era la finestra di dedup di `addComment`: 10 secondi contro riavvii distanti ~35 minuti (44 in 25,7 h su questa macchina). Con 7 card in coda dietro un muro Codex di 6 giorni fanno circa 300 paragrafi identici al giorno nei thread, cioe' esattamente la pila che KANBAN-83 e i 314 commenti «Memoria quasi finita» esistono per chiudere. Non basta `once`: la frase conta i giorni che restano, quindi viene RISCRITTA una volta al giorno. `noteHold` prende ora uno slot opzionale, e chi ha un'attesa che sopravvive a un riavvio passa il suo: `once` per il testo identico, `replaces` per la stessa nota riformulata dal giorno dopo. Provato con tre mutazioni sul test nuovo (cinque boot su tre giorni, stesso DB): senza slot ne' once 3 paragrafi, con il solo once 3, con il solo slot la riga viene riscritta a ogni boot e cambia id - cioe' `updated_at` toccato ogni 35 minuti, la colonna che KANBAN-84 ha gia' trovato a mentire sull'inattivita'.
`isChecksHold` e la passata periodica vivevano in `server.ts`, che non ha file di test: la meta' che decide davvero - il predicato letto dal giudice di stallo (1253 riarmi) e dalla passata StaleStream - era coperta da niente, mentre i test del ramo si fermavano a `clearStaleChecksRuns`. Ora stanno in `services/checks-lights.ts` con le dipendenze iniettate, e sono provati: con la mutazione che rimette `checks_state === 'running'` accanto al registro il file diventa rosso, e cosi' con la mutazione che toglie la guardia «rotta non ancora costruita». E la passata chiama `settleDelivery` su ogni spia che spegne. Senza, le due meta' spingevano in direzioni opposte: T4.3 rende onesta la spia e lascia la card parcheggiata senza giro (in-processo solo un `ChecksInterruptedError` con motivo "swap" fa ripartire la consegna), mentre T4.4 concede ormai tre boot prima di cancellarla. `settleDelivery` arriva a `server.ts` dallo stesso hook che gia' porta il gate, ed e' un no-op per una card senza consegna in volo.
Due difetti nella stessa clausola `ON CONFLICT`, entrambi riprodotti nel file di test nuovo (la tabella non ne aveva nessuno). Primo: il reset pretendeva `pending_deliveries.commit_sha IS NOT NULL`, ma la gamba `interrupted` scrive la riga mentre il server esce, PRIMA che il checkout sia risolto - quindi la riga che accumula giri piu' facilmente e' proprio quella con `commit_sha` NULL. I giri bruciati li ereditava la consegna VERA che arrivava dopo, con un commit suo: abbandonata al primo boot senza averne fatto uno. Un NULL memorizzato vuol dire «non l'abbiamo mai saputo», non «stessa consegna». Secondo: nominare `rounds` nell'`ON CONFLICT` fa lanciare SQLite al PREPARE, e il catch che protegge questa tabella si mangiava la scrittura intera. Su un DB ripristinato da un backup precedente alla migration la riga non veniva mai scritta e ogni riavvio perdeva la sua consegna - il contrario della promessa del docstring. La colonna si sonda una volta per database; senza, si scrive lo statement di prima e il contatore risponde zero, cioe' «riprendi come prima che la tabella contasse».
La verifica avversaria ha misurato che `specs/kanban/spec.md` non aveva nessuna occorrenza di `isChecksHold`, `pending_deliver`, `holdUntilLabel`, `hold` o `running`: T4.3, T4.4 e T4.5 consegnavano una colonna nuova nel DB, un abbandono terminale che parcheggia una card e un commento nuovo sul thread senza uno scenario approvato dietro. KANBAN-86, 87 e 88 li scrivono, con i numeri misurati e i casi che la verifica ha trovato (spia orfana, riga nata senza commit, DB senza la colonna, cinque boot su tre giorni). E KANBAN-84 toccava il contrario di quel che serve: dice che l'inattivita' si misura solo sui segni che il lavoro lascia, ma `taskIdleDays` prende `MAX(created_at) FROM task_comments` senza filtro su autore. Le note di KANBAN-86 e 87 nascono proprio sulle card che nessuno lavora piu', quindi sarebbero loro ad azzerare l'orologio dell'abbandono: il requisito ora lo dice, con il suo scenario. Il codice sta in T2.1, che e' di un'altra tornata.
# Conflicts: # openspec/changes/coda-che-riparte-da-sola/specs/kanban/spec.md # server/services/task-dispatcher.ts
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.
Change approvata:
openspec/changes/coda-che-riparte-da-sola(T4.3, T4.4, T4.5).Cosa cambia
isChecksHoldnon si fida piu' della spiarunningnel DB, che solo un boot spegne. Semeasure()lanciava prima della registrazione finale, il cancello cancellava la sua chiave ma la colonna restavarunning: da li' il giudice di stallo si riarmava all'infinito (1253 riarmi nel log) e lo spazzino StaleStream rispondeva sempreextend, quindi un turno muto non veniva mai finalizzato. 89 boot hanno trovato almeno una spia accesa. Ora decide il registro vivo del cancello, e la logica sta inchecks-lights.tscon i suoi test invece che dentroserver.ts, che di test non ne ha.pending_deliveriesconta i giri: un giro con le righe CI dura i comandi locali piu' fino a 65 minuti di poll, e ogni SIGTERM del watcher lo faceva ripartire da zero senza memoria di cosa fosse gia' verde. Il contatore ora riparte da zero su un commit nuovo, invece di ereditare i giri di una consegna diversa.provider-hold.jsonconteneva un muro Codex messo il 13/09 e in scadenza il 19/09, e il log lo annunciava 43 volte come «resumes at 14:45»: un hold di ore e uno di sei giorni erano indistinguibili. E oltre una soglia l'hold diventa una domanda all'umano invece di un'attesa muta.Verifica
Due giri di refuta avversaria. Il primo ha trovato una raffica di commenti che il fix stesso introduceva:
planHeldNotede' un Set che nasce vuoto a ogni processo, e con 44 riavvii in 25,7 ore e sette card in coda sarebbero stati circa 300 paragrafi identici al giorno — la stessa patologia che questa change esiste per chiudere. Riprodotto su 21 boot in 7 giorni: adesso e' una nota per card, e l'ultima dice «fra 2 giorni», non «fra 6».Il secondo ha confermato ogni blocco chiuso rieseguendo la sequenza che lo provava, e ha verificato che ogni test nuovo e' rosso sul codice di
origin/main. Due residui dichiarati e non nascosti: il cablaggio inserver.tsresta senza test perche'server.tsnon ha file di test, ed e' il buco strutturale che il ramo stesso cita; echeck:spec-coveragenon misura i requisiti di una change ancora aperta, quindi la copertura c'e' ma il cancello non la prova.