Bug-hunt round 34: Ctrl+C self-exit misdiagnosis, dead API, doc gaps - #51
Merged
Conversation
record's <-ctx.Done() shutdown branch called finaliseOutputs on every
child unconditionally, unlike the sibling anyExit branch (fixed in
round 32), which excludes a self-exited child from the sweep. When a
recorder's done channel closed in the sub-millisecond window before
the interrupt reached the select, that recorder fell through to
finaliseOutputs and was either misdiagnosed via classifyMissingOutput
("stayed blocked on the permission prompt") when it captured nothing,
or silently exited 0 with a truncated recording presented as a clean
session when it left partial data.
Factor the pre-stopAll early-exit sampling into a shared
sampleEarlyExits helper used by both select cases, so a recorder that
exits on its own gets the same classifyRecorderExit diagnosis no
matter which case observes the exit.
Assisted-by: Claude:claude-sonnet-5
Three gaps against the actual code and fixture: the words omission-reasons list did not mention that empty, whitespace-only, or invisible-only text is dropped (transcribe.go already documents this for segment text one row up); the timeline.jsonl field table had no Required column and did not state t's (and payload t1's) ±1e9-second bound, which ReadEntries enforces; and the utt-003 example trimmed the utterance text to start at "Now" while keeping t0 and the real word times from the untrimmed fixture, leaving the first shown word 1.6s after its own t0. Restore the example's opening words so t0 matches its first word again. Assisted-by: Claude:claude-sonnet-5
Validate had zero callers anywhere in the module — ingest uses the unexported validate/indexTimeline pair directly, and internal/review calls analyze.Load, not Validate. Being under internal/, it cannot have external consumers either. Drop it along with the now-unused errors import. Assisted-by: Claude:claude-sonnet-5
Assisted-by: Claude:claude-sonnet-5
TestCtrlCDiagnosesRecorderThatSelfExitedWithPartialOutput cancelled the context after the child was already reaped, making both select cases in Run ready at once. Go's select picks between ready cases uniformly at random, not deterministically favouring ctx.Done(): when anyExit wins instead, the diagnosis is returned as the error rather than written to opts.Log. The test asserted on the log alone, so it failed whenever the non-ctxEarly path fired (confirmed flaky on CI, 0/6000 locally but reproducible under load). Check both the log and the returned error. Assisted-by: Claude:claude-sonnet-5
atPositions' sole caller was the Validate export removed in this
branch's earlier commit; Ingest builds positioned{} literals inline
and never called it. Its doc comment also named Validate, now a
dangling reference to a deleted symbol.
Assisted-by: Claude:claude-sonnet-5
The timeline.jsonl example's word-level "Typing" entry read
{"w":"Typing","t":16.0}, but real merge output emits {"w":"Typing","t":16}
— transcript.jsonl's rounded literal, not Go's default float
formatting, which drops the trailing zero. The t field's bound
description also named ReadEntries, the internal function that first
enforces it, where the rest of the page and the sibling src row name
the user-facing commands (report, analyze) that refuse.
Assisted-by: Claude:claude-sonnet-5
The sibling entry for round 32's equivalent fix on the recorder-exit path already exists; this branch's Ctrl+C-path fix is a matching user-visible behaviour change and needs its own line, following the same precedent round 31 used to back-fill round 30's. Assisted-by: Claude:claude-sonnet-5
Two inaccuracies an adversarial review caught in the draft entry: it claimed the timeline.jsonl/emit.go HTML-escaping asymmetry pre-dates round 32 via session.SafeText, when round 32 demonstrably introduced that specific asymmetry (both sides escaped before it) — the defensible claim is narrower, that EmitRequest's own output is unaffected because it re-decodes and re-marshals regardless. It also framed round 31's split-discard of the words-row finding as resting on a different question than this round's re-examination, when one of round 31's two refuters already argued the surviving rationale this round's refuters reached again. Also cites round 30's prior, narrower refutation of a utt-003 finding and distinguishes it from this round's, and records the atPositions/CHANGELOG follow-ups. Assisted-by: Claude:claude-sonnet-5
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.
Round 34 of the autonomous bug-hunt loop (state: issue #24).
This round's hunt ran concurrently with another session's round 33 (merged as #49), both starting from the same last-recorded-round read of the state issue. Rebased onto #49's merge and renumbered to 34; the one overlapping nitpick (an
AGENTS.mdgate-list wording fix) is dropped below as a duplicate of #49's own fix.Confirmed substantive (1)
record's<-ctx.Done()shutdown branch misdiagnosed (or silently swallowed) a recorder that self-exited just before the interrupt.internal/record/record.go's<-ctx.Done()case calledfinaliseOutputson every child unconditionally, unlike the siblinganyExitbranch (fixed in round 32, PR #48), which excludes a self-exited child from the sweep beforestopAllruns. When a recorder'sdonechannel closed in the (empirically measured, sub-millisecond) window before the interrupt reached the select, that recorder fell through tofinaliseOutputsexactly like a normally-stopped one:classifyMissingOutput's "stayed blocked on the permission prompt" narrative — disproved by its own exit, with the real exit status never surfaced (internal/record/tcc.go:116-129);audio.wav),finaliseOutputssaw usable data, appended no problem, andRunreturnednil— a truncated recording silently presented as a clean, complete session.Both failure shapes were empirically reproduced during verification (200/200 and 60/60 trials against the pre-fix code), and the defect directly contradicts the documented contract at
docs/reference/cli.md:127-130and the code's own stated invariant (internal/record/record.go, the comment above theanyExitbranch's exclusion logic).Fix: factored the pre-
stopAllearly-exit sampling both branches need into a sharedsampleEarlyExitshelper (internal/record/record.go), used by both select cases, so a recorder that exits on its own gets the sameclassifyRecorderExitdiagnosis regardless of which case observes the exit. Two new regression tests (internal/record/record_test.go:TestCtrlCDiagnosesRecorderThatSelfExitedWithNoOutput,TestCtrlCDiagnosesRecorderThatSelfExitedWithPartialOutput) were watched to fail against the pre-fix code and pass after.CHANGELOG.mdgains the matching entry, the sibling of round 32's for the recorder-exit path.An adversarial CI failure (and a matching post-hoc review) caught that the partial-output test made both
selectcases ready by construction, so it asserted onopts.Logalone when Go's non-deterministic case choice can route the diagnosis to the returned error instead — fixed to check both. The same review caught that removinganalyze.Validate(below) had orphaned its sole caller-side helper,atPositions; removed that too, along with its now-dangling comment namingValidate.Confirmed nitpicks (4)
docs/reference/session-directory.md'swordsomission-reasons row didn't mention that a word with empty, whitespace-only, or invisible-only text is also dropped (internal/transcribe/transcribe.go:529-537), unlike the equivalenttext-row rule documented one row up. Round 31 split-discarded the identical finding: one refuter called the drop rule pre-existing with no staleness fresh from round 30, the other found it survives on the row's own precedent of documenting the equivalenttext-row cause. This round's two independent refuters, examining it fresh, both reached the surviving refuter's conclusion.timeline.jsonlfield table had noRequiredcolumn and did not statet's (or a speech payload'st1's) ±1e9-second magnitude bound, whichinternal/timeline/timeline.goenforces (refused byreport/analyze) — the same bound thetranscript.jsonltable documents fort0/t1three sections above.utt-003example trimmed the illustrated utterance text to start mid-sentence ("Now I expect…") while keeping the untrimmed fixture'st0and word times, leaving the first shown word 1.6s after its ownt0. Round 30 refuted a claim about this same example's abbreviated text against the fuller fixture sentence (the page's examples are established illustrative reductions, and nofindings.jsonlquote citesutt-003) — but that reasoning doesn't reach this narrower defect: the abbreviation itself is fine, the self-contradiction between the shownt0and the shown first word's time is the actual gap. Restored the opening words so they agree witht0again, matching realmergeoutput's integer formatting ("t":16, not"t":16.0) rather than transcript.jsonl's rounded literal.internal/analyze/validate.go's exportedValidatefunction had zero callers anywhere in the module —Ingestuses the unexportedvalidate/indexTimelinepair directly, andinternal/reviewcallsanalyze.Load, notValidate. Being underinternal/, it cannot have external consumers either. Removed, along with the now-unusederrorsimport and the orphanedatPositionshelper noted above.Considered and refuted
timeline.jsonlfromanalyze's emitted analysis request via HTML-escaping (internal/session/session.go'sjsonlEncodervsinternal/analyze/emit.go's plainjson.Marshal): round 32'sjsonlEncoder(SetEscapeHTML(false)) did newly desynchronise the two artefacts' own on-disk/in-source encoding of<,>, and&— before it,WriteJSONLescaped likeemit.gostill does. ButEmitRequestre-decodes the timeline and re-marshals, soWriteJSONL's encoder choice never reaches the request an operator or model is actually shown: one refuter built HEAD and HEAD~1 and provedEmitRequest's output byte-identical across round 32's change. Split verdict, discarded.whisperx.go/whispercpp.goacceptingend < start): the downstreamt1→t0clamp is a documented, deliberate fallback (docs/reference/session-directory.md:70,internal/timeline/timeline.go), so no real misbehaviour survives..github/workflows/ci.yml's comment vs its actualBuildstep flags): real discrepancy, no behavioural consequence — the repo has no cgo-conditional source.docs/reference/cli.md:152's "ingest reads timeline.jsonl only" phrasing, read against ingest's ownfindings.jsonlverdict-guard scan: the same paragraph states that guard explicitly four sentences later; no reader is misled.docs/reference/session-directory.md'smanifest.jsonexample omitting optional fields present in the real fixture: both omitted fields are marked optional in the table directly above; not misleading.AGENTS.md'sgo test ./...gate-list wording — this round's own hunt independently found and would have fixed the same issue Bug-hunt round 33: unbounded total-size JSONL reads, two nitpicks #49 already fixed; dropped as a duplicate during the rebase rather than re-fixed.Verification
Every finding faced two independent adversarial refuters before being included above; only findings both refuters failed to kill were fixed. All four sweep dimensions (code, docs-vs-functionality, infrastructure, internal doc consistency) also surfaced several items that survived one but not both refuters — those are omitted per the loop's "when in doubt, discard" rule and not itemised here. Two full rounds of post-fix adversarial review (correctness; docs accuracy) also ran against this PR's own diff, catching and fixing the flaky-test/dead-code issues above and an earlier draft of this body's own inaccuracies.
Gates
go build,gofmt -l .,go vet ./...,go test ./...,go test -race ./...(including 1000+ stress iterations of the two new regression tests), the pipeline smoke (merge+reportagainstexamples/sample-session), andsh -n install.sh && bash -n install.shall pass on the branch head, rebased onto #49's merge with no merge conflicts..abcd/work/DECISIONS.mdandCHANGELOG.mdgain the round's entries in this PR.