Skip to content

fix(fallback): say whether the primary stalled or failed - #34

Merged
bojieli merged 1 commit into
mainfrom
fix/hedge-log-names-its-trigger
Sep 4, 2026
Merged

fix(fallback): say whether the primary stalled or failed#34
bojieli merged 1 commit into
mainfrom
fix/hedge-log-names-its-trigger

Conversation

@bojieli

@bojieli bojieli commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Follow-up to #33, which is merged; this rebases onto main and stands alone.

What

The hedge logged primary stalled; starting the fallback whichever way it was started, so a backend that had hard-failed a second earlier was reported as a slow one.

Those want opposite responses from whoever reads the log: a stall means wait or raise the delay, a failure means the backend is broken. Chasing a Gemini outage this week the log said "stalled" for a geoblocked endpoint that had been answering HTTP 400 in about a second all morning.

Two spellings now, word-identical on all three ports:

Message Logged when Fields
primary stalled; starting the fallback the delay elapsed with the primary still running primary, fallback, afterMs
primary failed; starting the fallback the primary threw, cutting the delay short primary, fallback

Only the stall waited, so only the stall reports a delay. afterMs=8000 on a handover that happened at 1.7s describes a wait that never took place.

All three ports were different

Worth stating plainly, because I described this as a "three-port string change" when proposing it and that was wrong:

  • Windows logged nothing at all. FallbackTranscriber.cs had no logging calls; the "primary stalled" strings in LoggingTests.cs are formatter fixtures. The hedge fired silently on the one platform whose users cannot fall back to reading a macOS log. It now logs through the same Log class the rest of Core uses.
  • macOS and Android both called a hard failure a stall.
  • macOS spelled the field after and formatted it as a Duration description (8.0 seconds) where Android already used afterMs. Unified on afterMs so the three lines match in fields as well as wording.

Parity handling

Both spellings are asserted in each platform's own suite and recorded in docs/PARITY.md under a new Fallback log messages section, per that document's repeated-verbatim rule. Per its "text checked by diffing, not by reading" rule, the strings were extracted from all three implementations and diffed rather than eyeballed:

swift vs csharp:  identical
swift vs kotlin:  identical

The flake this surfaced, and why it is in this PR

LogRouter is process-global, so a MemoryLogSink captures what the whole process emits, not just what the installing test provoked. The moment Core started logging from FallbackTranscriber, FallbackTranscriberTests began reading the handover line of DictationJourneyTests — which hedges with the same 20 ms delay and names its second backend "fallback":

Assert.Equal() Failure: Strings differ
Expected: "secondary"
Actual:   "fallback"

It failed roughly one full run in five and passed every time its own class ran alone. I first assumed a teardown race between the two sink-installing classes and tried an xUnit collection; that did not fix it, and re-running to find the actual failing assertion is what identified the real cause.

Fixed by serialising the test assembly (AssemblyInfo.cs). Cost is ~2s — 631 tests go from ~1s to ~3s — and it removes the class of failure rather than the one instance. It is in this PR rather than a separate one because this change is what introduces the flake; splitting them would leave a commit that fails one run in five.

Verification

Suite Result
Swift 604 tests, 0 failures
Windows C# 631 tests, 0 failures — 8 consecutive full runs green before rebase, 3 after
Android Kotlin 284 tests, 0 failures
dotnet format whitespace --verify-no-changes clean

🤖 Generated with Claude Code

The hedge logged "primary stalled; starting the fallback" whichever way it was
started, so a backend that had hard-failed a second earlier was reported as a
slow one. Those want opposite responses from whoever reads the log: a stall
means wait or raise the delay, a failure means the backend is broken. Chasing
a Gemini outage this week, the log said "stalled" for a geoblocked endpoint
that had been answering HTTP 400 in about a second all morning.

There are now two spellings, word-identical on all three ports:

    primary stalled; starting the fallback   primary, fallback, afterMs
    primary failed; starting the fallback    primary, fallback

Only the stall waited, so only the stall reports a delay. `afterMs=8000` on a
handover that happened at 1.7s describes a wait that never took place.

All three ports were different, not merely two of them:

- Windows logged nothing at all. `FallbackTranscriber.cs` had no logging, so
  the hedge fired silently on the one platform whose users cannot fall back to
  reading a macOS log. It now logs, via the same `Log` class the rest of Core
  uses.
- macOS and Android both called a hard failure a stall.
- macOS spelled the delay field `after` and formatted it as a `Duration`
  description ("8.0 seconds") where Android already used `afterMs`. Unified on
  `afterMs`, so the three lines match in fields as well as in wording.

Both spellings are asserted in each platform's own suite and recorded in
docs/PARITY.md, per the repeated-verbatim rule there; the strings were checked
by extracting and diffing them across the three files, not by reading.

Serialising the Windows test assembly is part of this change rather than a
separate cleanup: `LogRouter` is process-global, so a MemoryLogSink captures
what the whole process emits. Once Core started logging from
`FallbackTranscriber`, `FallbackTranscriberTests` began reading the handover
line of `DictationJourneyTests`, which hedges with the same 20 ms delay and
names its second backend "fallback". It failed about one full run in five and
passed every time its class ran alone. Serialising costs ~2s (631 tests, ~1s
to ~3s) and made eight consecutive full runs green.

Swift 604 tests, Windows 631, Android 284, all passing; `dotnet format
whitespace --verify-no-changes` clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@bojieli
bojieli merged commit a528f7f into main Sep 4, 2026
13 checks passed
@bojieli
bojieli deleted the fix/hedge-log-names-its-trigger branch September 4, 2026 13:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant