Skip to content

v1.9.0 — Degraded multi-AI runs are visible and no longer STRONG BUY

Choose a tag to compare

@furic furic released this 07 Aug 03:08
· 37 commits to main since this release

What's New

One theme: a safeguard you can silently lose is not a safeguard.

Degraded multi-AI runs are now visible — and no longer produce STRONG BUY

Since v1.7, STRONG BUY has had to clear the unanimity rule: every configured provider must independently vote STRONG BUY, or the consensus caps at BUY. That rule is the entire justification for running two models.

It was possible to lose it without noticing.

When a provider fails mid-run — quota exhausted, network blip — the surviving provider's recommendations hit this short-circuit in computeConsensusAction:

if (scores.length === 0) return "HOLD";
if (scores.length === 1) return scores[0].action;   // straight through
// ... tally, tiebreak, then the unanimity rule

One provider, so the action passes through untouched. No unanimity check, because unanimity among one model is trivially satisfied. And the output looked identical to a verified consensus: a bare confidence number, no agreement badge, nothing anywhere saying a provider was missing.

That is how MSFT reached the 2026-06-23 brief as a STRONG BUY 91% on Claude's vote alone, with Gemini quota-exhausted. The strongest single signal this system has produced, and it silently bypassed the check that gives STRONG BUY its meaning. It was acted on. The gap was only discovered six weeks later, by accident.

What changes now. applyDegradedProviderPolicy() marks every recommendation of a degraded run:

degradation: { configured: 2, answered: 1, missing: ["Google Gemini"] }

and, by default, caps STRONG BUY at BUY — because cross-provider agreement is part of the STRONG BUY criteria and it demonstrably did not happen. The reason string is annotated so the text matches the downgraded action, following the existing guard convention.

Renderers surface it either way:

Surface Rendering
Email ⚠ 1/2 AI badge, missing providers in the tooltip
Telegram ⚠ 1/2 AI tag on the signal line

Both show even in single-provider rendering — that is precisely the case being flagged, and the old code hid the degradation exactly where it mattered most.

Had the MSFT run gone through this, it would have arrived as BUY 91% ⚠ 1/2 AI.

Opting out

"ai": {
  "strongBuyRequiresAllProviders": true
}

Set false to keep the survivor's STRONG BUY. The degradation badge shows regardless — capping is policy, visibility is not.

What is deliberately not affected

A single-provider setup is not a degraded run. If you only ever set one API key, that configuration never promised unanimity, so nothing changes for you: no cap, no badge, byte-identical behaviour to v1.8. This has its own negative test, because getting it wrong would silently downgrade every single-key user's recommendations.

The policy only fires when 2+ providers were configured and some failed to answer. It also covers the partial case — 3 configured, 2 answered — which still aggregates normally but is still degraded.

Fixes

The test script silently skipped test files

- "test": "node --import=tsx/esm --test test/util.test.ts test/social.test.ts test/intradayCompare.test.ts"
+ "test": "node --import=tsx/esm --test test/*.test.ts"

The runner enumerated three filenames by hand, so a newly added test file was never executed — and the suite reported a confident pass without it. A test that isn't run looks exactly like a test that passed, which is the same failure mode as the provider bug above, one layer down in the tooling.

Discovered while adding this release's tests: the suite reported 104 passing with 9 of them not running. Real total is now 113 tests, all passing.

Upgrading

Nothing required. ai.strongBuyRequiresAllProviders defaults to true; omit the block entirely and you get the safe behaviour.

If you run both providers, expect an occasional ⚠ 1/2 AI badge and correspondingly fewer STRONG BUYs on days a provider is rate-limited. That is the fix working — those were never verified STRONG BUYs to begin with.

Full changelog: v1.8.1...v1.9.0