Skip to content

v0.0.32

Choose a tag to compare

@savant0x savant0x released this 18 Sep 19:32
· 616 commits to main since this release

0.0.32 — 2026-09-18

Free-compute harvester probes run through a bounded pool (FID-2026-0918-001)

  • The daily harvester (scripts/providers/harvest-freeairouter.ts) probed
    every tracked host serially — one host at a time, up to 3 network
    round-trips × 10s timeout each — and 41 of 71 tracked hosts sit at
    boundary-unverifiable, re-probed every run by design. The probe loop
    dominated the run's wall clock.
  • New scripts/providers/lib/probe-pool.ts: runWithConcurrency worker
    pool with PROBE_CONCURRENCY = 6 (named constant beside the timeout it
    overlaps). Both probe sites wired through it: the daily probe-merge phase
    and the Stage-E health loop — the latter split into its own module
    scripts/providers/lib/health-probe.ts (move-only seam split) to hold
    harvest-report-phase.ts under the 300-line ceiling. Per-host etiquette
    untouched: hosts are distinct vendor endpoints, still one probe set each.
  • RED-first: 9-pin suite written before the implementation (cap, ordering,
    empty, oversized limit, serial degradation, rejection propagation,
    constant pin, pooled health ×2). LIVE evidence: full --probe harvest
    exit 0 in 13.7s (214 records, ~43 probes) — the serial bound was minutes.
  • Gates: probe-pool 9/0; providers tree 110/0; eslint 0 warnings;
    quality:report PASS; lint:md + prettier clean; fid:verify 5/5 gates PASS,
    receipt stamped. Commit 69f6e7a7. Closed + archived 2026-09-18.

Unverifiable hosts re-probe on a 3-day cadence (FID-2026-0918-002)

  • The harvester re-probed all 41 boundary-unverifiable hosts every daily
    run even though a boundary verdict only needs re-measurement after the
    lapse window (PROBE_LAPSED_AFTER_DAYS = 3). New
    shouldReprobeUnverifiable() gate skips hosts with a fresh
    lastProbeAttemptUtc (< 3 days) and fail-opens on stale/never/malformed
    values — worst case one extra probe, never lost evidence. Every attempted
    probe stamps withProbeAttempt() so failed measurements don't re-fire
    early.
  • State layer: optional lastProbeAttemptUtc on CandidateState with
    defensive parse — plus an explicit carry-forward line in diffCandidates,
    which rebuilds every re-sighted host record from a field literal and
    would otherwise have silently dropped the field on every run (a pin
    proves a re-sight preserves it).
  • LIVE double-run evidence: run 1 stamped 40 hosts (14.0s); same-day run 2
    skipped every freshly-stamped host (identical host sets, zero timestamp
    diffs) in 1.13s. Combined with FID-2026-0918-001 the daily scan drops
    from ~14s to ~1.1s typical wall clock, evidence freshness bounded at
    3 days by construction.
  • Gates: probe-cadence 9/0; providers tree 119/0; typecheck common exit 0;
    quality PASS; fid:verify 5/5 gates PASS. Commit 69f6e7a7. Closed +
    archived 2026-09-18.

/model picker now activates the selected provider's routing (FID-2026-0917-005)

  • Selecting a model in the /model picker persisted the model's provider to
    settings.json but never activated it in the runtime environment.
    process.env.DIRECT_PROVIDER and process.env.INFERENCE_BASE_URL stayed
    empty, so isDirectProviderMode() returned false — the usage monitor
    queried the SavantCode backend (402 → "Out of credits. Please add credits
    at https://savant-code.com/usage") and the inference request itself routed
    to getWebsiteUrl() (savant-code.com) instead of the selected provider.
    The UI said the provider was selected, but requests silently passthrough-ed
    to the SavantCode website. A different layer from the OpenRouter key fixes
    (FID-2026-0917-003/-004) with the same user-visible symptom; the /provider
    picker avoided the bug because it already called activateConfiguredProvider.
  • The selection logic is extracted into applyModelPickerSelection(model) in
    cli/src/utils/provider-setup.ts (Law 13 — one function, one truth: resolves
    model.provider ?? 'openrouter', persists the model preference, and
    activates via activateConfiguredProvider, the same guarded seam the
    /provider picker uses). handleModelPickerSelect is now a one-line
    delegation, so the regression suite exercises the real production seam
    rather than a copy of its logic. The provider preference is persisted only
    when activation declines (unkeyed provider) — no duplicated write on the
    happy path.
  • Fail-closed preserved: an unkeyed provider declines activation and leaves an
    explicit INFERENCE_BASE_URL untouched — the SDK surfaces a clear
    missing-key error at send time rather than sending a stale credential.
  • Gates: typecheck cli exit 0; eslint 0/0 on all three files; prettier clean;
    model-picker-activation + provider-setup 25 pass / 0 fail (77 expects);
    receipt 3/3 PASS re-stamped live at the archived path; independent Verifier
    AUDIT PASS on all functional criteria (two findings fixed en route: a
    tautological test target resolved by the extraction, and a redundant persist
    call removed). Production call-graph confirmed: use-chat-pickers.ts →
    build-chat-layout-props.ts:181 (onModelPickerSelect) → production chat
    UI. Commits 91daaf93 (fix + test) + 6b326d61 (FID record). Closed +
    archived 2026-09-17.

Rejected OpenRouter master key now fails closed, not silently (FID-2026-0917-004)

  • resolveOpenRouterApiKey() treated any failed master-key exchange as a
    soft failure: log a warning, then fall through to OPENROUTER_API_KEY.
    When that fallback held a stale or revoked key, the CLI silently sent a
    dead credential and the operator saw the vendor 401 User not found. at
    chat-completions time — an error naming neither the dead key nor the failed
    exchange that selected it. Second occurrence of this exact anti-pattern
    (FID-2026-0917-001 added the diagnostics but kept the fallthrough).
  • A 401/403 from the /api/v1/keys exchange is now terminal: it
    negative-caches (cachedKey = null) and returns undefined instead of
    substituting a different credential for the one the operator configured.
    Because openrouter is a registered kind: 'gateway' provider
    (common/src/providers/registry.ts:23-25), getModelForRequest's
    active-provider guard throws the templated missing-key error before
    createDefaultInferenceModel is reached — so the operator gets an accurate
    message, not a dead key sent upstream. Transient failures (429, 5xx,
    network) keep the soft fallthrough, since a still-valid regular key remains
    legitimately usable while the exchange endpoint is unavailable.
  • Returning undefined preserves the existing string | undefined contract
    — no new throw sites, no caller breakage. The negative cache reuses the
    existing environment-signature invalidation and
    resetOpenRouterApiKeyCache(), so a fresh key stored via /provider
    recovers without a restart.
  • Gates: sdk typecheck exit 0; resolver suite 13 pass / 0 fail (5 new);
    eslint 0 warnings; prettier clean; lint:md exit 0; independent Verifier
    AUDIT — 4 PASS / 1 FAIL refuted with registry evidence (the flagged
    ?? apiKey fallback is unreachable on the live DIRECT_PROVIDER=openrouter
    path because the gateway guard throws first). Shipped to the rebuilt
    sdk/dist the running CLI loads. Closed + archived 2026-09-17.

OpenRouter key shadowed by Bun dotenv auto-loader in dev boot (FID-2026-0917-003)

  • Every OpenRouter call in local dev failed with User not found. because
    Bun's dotenv auto-loader is not disabled by the --cwd .. flag in the
    dev script, contrary to the comment at load-dev-env.ts:5-6. With
    cwd=cli/, Bun pre-seeded process.env.OR_MASTER_KEY from the stale
    cli/.env.local before load-dev-env.ts ran; the "existing env wins" rule
    then permanently locked in that value, so the good repo-root key was
    always skipped.
  • Three-part fix: --no-env-file added to the dev script
    (cli/package.json:17) so load-dev-env.ts is the sole deterministic
    loader; the false comment replaced with the real mechanism; the divergent
    stale OR_MASTER_KEY removed from cli/.env.local (gitignored — a
    local-machine fix). The stale OPENROUTER_API_KEY was also cleared from
    HKCU\Environment.
  • Bidirectional probe, identical command, one flag different: stale key →
    401 → User not found.; root key → exchange OK → HTTP 200. Gates:
    typecheck cli exit 0; probe exit 0; quality exit 0. Closed + archived
    2026-09-17.

EHEL Law 3 gate no longer deadlocks writes on a lint-failing doc (FID-2026-0917-002)

  • The EHEL pre-write Law 3 gate blocked every write tool call — including
    the exact str_replace that would have fixed the violation — because a
    markdown doc (dev/handoff.md) sat in dirtyFiles with a failing
    markdownlint (MD040) run keeping it out of verifiedFiles. The violation
    blocked its own remedy: a write/verify ordering deadlock. Root cause was a
    policy enforced at one lifecycle point and not another: classifyFileKind
    already classifies *.md as docs and evaluateWritesAtStepBoundary
    already declares "documentation artifacts gate on markdownlint, never on
    Law 3" — but the pre-write gate checked only the isExemptWritePath prefix
    list (dev/fids/, dev/nova/, dev/scratchpad/), omitting docs/,
    dev/handoff.md, dev/session-summaries/, and README.md.
  • One-predicate fix reusing the existing classifier (Law 13 — one function,
    one truth): unverifiedDirty now filters to classifyFileKind(f) === 'code'. Code verification is not weakened — a dirty code file still
    hard-blocks, and a mixed doc+code set still blocks while naming only the
    code file (pinned by a regression test). The Law 15 advisory scanner is
    intentionally left unguarded: it reports ALL unverified files, docs
    included, as an advisory rather than a block.
  • Second independent blocker fixed: dev/wiki/patterns/*.md (MD013,
    machine-generated 286-char lines) kept repo-wide lint:md at exit 1, so no
    doc could ever earn verification credit. Exempted in .markdownlintignore
    on the same precedent as dev/scratchpad/** and
    dev/provider-candidates/**; repo-wide lint:md flipped exit 1 → 0.
  • Shipped to the built artifact: the fix lives in source AND the rebuilt
    sdk/dist the running CLI loads (node_modules/@savant-code/sdk symlinks
    to sdk/, whose exports map to dist/index.mjs). After bun run build, the
    guard appears in exactly one place per bundle — runPreWriteGates at
    dist/index.cjs:48430 + dist/index.mjs:48334 — with the Law 15 scanner
    correctly unguarded. A runtime probe extracted the shipped function body
    from the built index.cjs and executed it: docs-only dirty unblocks,
    code-only dirty blocks, mixed doc+code blocks naming only the code file.
  • Gates: typecheck packages/agent-runtime exit 0; law3 suite 8 pass / 0 fail
    (3 new); eslint 0; prettier clean; lint:md repo-wide exit 0 (was exit 1);
    quality:report PASS; receipt re-stamped 3/3 LIVE at the archived path;
    independent Verifier AUDIT (2 FAIL + 2 NEEDS-REVIEW, all discharged).
    Commits f8d46ee2 (gate + tests), 9fb99351 (fences + exemption),
    be9b71ec (FID + summary), 2394ac2f (handoff refresh). Closed +
    archived 2026-09-17.

OpenRouter 401 fixed: dev env split + observable key exchange (FID-2026-0917-001)

  • OpenRouter calls failed with the vendor 401 User not found. Two root
    causes, neither in the OpenRouter client (header, base URL, and model id
    proven correct by a live probe replaying the exact request path):
    dev-mode .env.local discovery used first-match findUp and silently
    skipped the repo-root file (a root OPENROUTER_API_KEY never reached
    process.env), and the resolver's OR_MASTER_KEY exchange fell through
    on failure with zero diagnostics — a management key lists models (200)
    but cannot run inference.
  • cli/src/pre-init/load-dev-env.ts now discovers every .env.local
    from the repo root down (findAllUp + applyOneEnvLocal), applying
    outermost-first so deeper files override; existing process.env values
    still win.
  • sdk/src/impl/openrouter-key-resolver.ts logs the exchange response
    status/body (capped 200 chars) on non-2xx before falling through to the
    regular-key fallback — a bad management key now says 401 Invalid API key instead of vanishing. No key material is logged (Law 12).
  • Evidence: live probe — the regular key streams chat-completions 200
    through the unchanged client; the management key reproduces the exact
    User not found. 401. Gates: typecheck cli+sdk exit 0; eslint 0;
    prettier clean; load-dev-env.test.ts 4/0;
    openrouter-key-resolver.test.ts 8/0; quality PASS; receipt 5/5 LIVE;
    independent Verifier AUDIT PASS. Closed + archived 2026-09-17.

Compaction signal no longer pins; report excerpt folds (FID-2026-0916-008)

  • The in-stream "✓ Compaction complete (−N tokens) — P% of window" panel no
    longer stays pinned below every later message. onNewUserMessage — the
    canonical pre-run zeroing path — now clears compactionStatus,
    compactionEvents, and lastCompactionReport (preserving compactionCount,
    the session sidebar stat). The signal shows for the rest of the run that
    compacted, then retires on the next message.
  • The report excerpt is now collapsed by default behind a ▾ expand
    affordance with a 160-char preview, instead of dumping the full
    96K-char summary with no toggle. Reuses the existing Button/
    CollapseButton pattern from CompactionSummaryBlock (Law 11).
  • The excerpt renders through a new presentational CompactionReportExcerpt
    sub-component that takes the fold state as a prop (same shape as
    CompactionSummaryBlock's isCollapsed), making both fold states testable
    under the static-render harness.
  • Gates: typecheck cli exit 0; eslint 0; prettier clean; markdownlint clean;
    chat-store-compaction.test.ts 10/0; compaction-signal.test.tsx 13/0.

Atria gateway provider added (FID-2026-0916-005)

  • Atria AI (https://api.atria-asi.ai/v1) is a built-in provider exposing
    the single Atria-Dawn-Preview model (256K context, vendor-published;
    pinned at 262,144 in the fallback table, not derived from the family
    heuristic). Follows the bazaarlink one-model static-catalog pattern
    (FID-2026-0915-006) exactly: the vendor's /v1/models endpoint is
    key-protected, so a static one-model map is the known-checked-in set.
  • /provider atria selects it and offers the masked ATRIA_API_KEY entry
    (setupAvailable: true — derived, matching every other built-in gateway);
    /model atria/Atria-Dawn-Preview routes through the generic
    createProviderModel factory (protocol: 'openai', idTransform: 'strip').
  • Six derived surfaces wired: registry entry (registry-partitioned.ts),
    atriaModels map + AtriaModel type (gateway-catalogs.ts),
    MODEL_CATALOGS registration, fallback-table row + NAME_CATALOG_MODEL_IDS,
    cli fetchAtriaModels + ATRIA_NAMES (merged in the gateway combine), and
    the re-export. Provider docs regenerated (README / cli-release /
    .env.example tables + docs/index.md + docs/sdk-overview.md).
  • Gates: typecheck ×3 (common, cli, sdk) exit 0; RED-first pin suite 4/4
    (single model cataloged, cli fetcher serves vendor window, fallback-table
    pin, registry setupAvailable + ATRIA_API_KEY); openrouter-models family
    58/0; Law 4 reachability greps land at gateway.ts:62/:210,
    openrouter-models.ts:54, static-catalogs-gateways.ts:211; eslint 0;
    prettier + lint:md clean. Closed + archived 2026-09-16.

LEARNINGS.md Safe Core restructure; retire-tool defects fixed (FID-2026-0916-007)

  • dev/LEARNINGS.md no longer truncates at boot: 1,610 lines / 102,520 chars
    → 810 lines / 45,409 chars
    . The 27 legacy ## Session narrative blocks
    (2026-07-25..2026-08-10) were moved verbatim to a new move-only archive
    dev/LEARNINGS-RETIRED.md (byte-identity proven against git show HEAD: —
    exact substring, normalized equal).
  • Boundary inversion fixed: the insertion marker sat at EOF, below the
    legacy boundary, so every new lesson landed in ungoverned space and
    learnings:check printed a false PASS (16) over a 32-lesson file. The
    marker now sits at line 3 (top of governed space) so appends validate.
    markerIssues in scripts/learnings-validation.ts enforces marker-above-
    boundary and rejects prose trailing after the boundary.
  • Retire-tool defects fixed in scripts/learnings-retire.ts: an absent
    --cap parsed argv[0] → NaN, which is not nullish, so the loop retired
    every entry; and applyRetirement rebuilt the file from lesson blocks
    alone, silently deleting all prose and markers. parseCap now rejects
    non-finite values; removeLessonSpans removes only retired spans.
  • Two-tier governance by design: the 16 pre-schema narrative lessons are
    preserved below the boundary byte-identically and deliberately unvalidated —
    conforming them would require fabricating Evidence/Verification claims
    (Law 5). learnings:check output now names both populations so the count
    can never be misread as "total lessons."
  • Regression pins: 8 tests in scripts/__tests__/learnings-retire.test.ts
    (NaN cap, prose/marker preservation, final-block marker re-emission,
    append-only archive) + 2 inversion tests in scripts/learnings.test.ts.

Compaction-summary contamination guards (FID-2026-0916-006)

  • The /compact standing summary no longer transcribes harness machinery as
    operator dialogue. Four root causes fixed at source: ECHO_REFRESH protocol
    refreshes are excluded from summary intake (summary-parsing.ts,
    structured-summary.ts); user-turn transcription strips harness framing
    (<system> dumps, <compaction-notice>, <user_message> wrappers,
    <think>) via stripHarnessFraming (summarize-messages.ts,
    buildStandingFacts); the existing ≥40% tag-density infrastructure detector
    is now applied to every standing-fact candidate line; and substance floors
    reject fragment "decisions" (./hosts/ok) and greeting-spam goals
    (hey), with operator-ruled greeting-run coalescing (MQ3).
  • All guards are pure functions in agents/context-pruner/contamination-guards.ts
    (300-line-ceiling split from structured-summary.ts, 299 ≤ 300), embedded
    into the generated pruner scope via the existing .toString() pipeline —
    serialization contract verified.
  • RED-first pin suite reproduces the LIVE pasted-artifact shapes exactly
    (8 fail → 11/0 GREEN); pruner contract suites 35/0; receipt 6/6 LIVE via
    fid:verify.

Gateway catalogs re-aligned to vendor truth; OpenCode zen/go removed (FID-2026-0916-004)

  • commandcode catalog re-aligned to the vendor's renormalized roster:
    dashed versions (claude-sonnet-4.6 → claude-sonnet-4-6), vendor
    prefixes dropped (openai/gpt-5.6-sol → gpt-5.6-sol),
    zai-org/GLM-5.2 + canonical Kimi casing, plus coding-relevant
    additions only (GLM-5.3, grok-4.6, Qwen3.8 family,
    deepseek-v4.1-flash class) — no bulk roster dumps. The COMMANDCODE_PROTOCOLS
    map was re-keyed to the new spellings.
  • tokenharbor dead ids removed: gemini-3.6-flash, minimax-m3, and
    kimi-k3:free (the free channel was a launch promo that has ended —
    confirmed LIVE by 429).
  • opencode-zen and opencode-go removed from Savant entirely. The zen
    free-tier gate ("free tier can only be used in OpenCode") is
    account-type-based, not UA-spoofable, so zen cannot serve Savant.
    Registry entries, catalogs, OPENCODE_ZEN_PROTOCOLS/OPENCODE_GO_PROTOCOLS,
    the zen/go cli modules and gateway wiring, context-window rows, the sdk
    opencode-key-resolver chain and isOpenCodeGoModel, the
    getOpenCodeGoApiKeyFromEnv getter, and exception-manifest entries are
    all gone; test fixtures re-pointed to surviving gateways.
  • RED-first pin module observed 9 fail / 3 pass against the pre-change
    catalogs; receipt stamped 10/10 LIVE (typecheck common/cli/sdk, six
    test suites, repo-wide quality). The free-mode cyclic-tool regression
    suite (FID-2026-0905-004) was re-pointed to TokenRouter's per-model
    protocol ids, preserving all four wire-protocol coverage legs.

Receipt stamping fixed for gates-runs-to-EOF FIDs (FID-2026-0916-003)

  • EOF-stamped receipts no longer read as stale. A FID whose
    ## Verification Gates section runs to EOF (no heading after it) got a
    receipt that never validated: the stamp's EOF branches rewrite the
    document tail (trimEnd + separator reinsertion) while the pre-stamp
    fingerprint and the validator's receipt-span removal disagreed on the
    trailing-newline shape — a sha256 mismatch, fail-closed. The fix
    tail-normalizes the hashed view to exactly one trailing newline inside
    computeFidFingerprint on both paths (the single hashing authority);
    stampReceipt and buildReceipt are unchanged, and freshness is
    preserved — any content edit still invalidates the fingerprint.
  • Verified LIVE end-to-end: a gates-to-EOF fixture stamped via the real
    fid:verify --write chain now passes fid:verify --check (single file
    and repo-wide); the fixture was destroyed after the proof. Contract
    suites 34/0 + executor suites 35/0; the new fingerprint test module
    also carries an EOF stamp-path identity pin alongside the existing
    insert-path pin (FID-2026-0907-010).

Catalog window truth for the pre-program static providers (FID-2026-0916-002)

  • The four static-catalog providers that predate the context-window
    program — tokenrouter, tokenharbor, commandcode, opencode-go — now
    resolve windows from vendor truth:
    94 exact-id rows added to
    CONTEXT_WINDOW_FALLBACKS from keyed LIVE rosters (bazaarlink +
    orcarouter, OpenRouter tiebreak; 2-of-3 vendor votes marked), and all
    four CLI catalog fetchers switched from the inferContextLength family
    heuristic to getContextWindowFallback(id). Fixes the operator-reported
    tokenharbor/deepseek-v4-flash 131k display (vendor-published:
    1,048,576). A closed-world coverage invariant pins every curated static
    id to a table row so this gap class cannot silently reopen.
  • TokenRouter dead channels removed (catalog 35 → 31): both free GLMs
    • mirothinker (keyed 503 "No available channel") and image-only
      seedream. MiniMax-M3 kept — keyed HTTP 200 despite roster absence
      (roster absence ≠ dead, verified by call).
  • Per-model protocol dispatch: new TOKENROUTER_PROTOCOLS map (vendor
    supported_endpoint_types) + registry protocol: 'multi' wiring — 6
    Responses-only, 7 Anthropic-only, and 1 Gemini-only ids were
    mis-dispatched as chat before (codex → HTTP 404 "Use the
    v1/responses endpoint instead"). OrcaRouter free-tier cap + OpenRouter
    key-rotation notes documented. 39/39 tests across 3 suites (825
    expects); LIVE window-truth probe 17/17 vs the keyed rosters.

Pipeline integrity hardening (FID-2026-0916-001)

  • Denylist round-trip fixed: mergeDenylist normalizes both persisted
    shapes (the {_meta, entries} object it writes AND legacy bare arrays)
    and preserves firstSeenUtc while bumping lastSeenUtc — the denylist
    no longer resets every run. LIVE proof: 118 → 163 entries across three
    harvests with all pre-FID rows keeping their original firstSeenUtc;
    flagged rows persisted for the first time.
  • Silent-class audit trail: stage-0 exclusions beyond
    risky/down/free-relay (unconfirmed category, monitor-directory,
    free-product, no-probe-data, unreachable) now render counted report
    groups + a summary row via a new pure buildExclusionAuditRows builder
    (legacy three classes byte-identical) — the report's "every gate
    decision, with reason" claim now covers the whole feed.
  • providers:propose fail-closed tracking guard: a host absent from
    the tracked candidate set refuses to scaffold (exit 1 + remediation:
    run harvest first / use an operator seed); tracked hosts keep the
    DO-NOT-CURATE warning path.
  • Probe trust boundary designed, not accidental: probe-endpoint
    admits only https URLs with public hosts — static checks reject
    loopback/private/link-local literals and non-canonical integer/hex/octal
    host forms, and a resolving DNS check (injectable lookupImpl) closes
    the hostname-based SSRF gap; the operator-stamped Stage-E health path
    passes allowPrivate: true (local Ollama custom providers remain
    health-checkable). Suite 85 → 101 (16 new pins), 411 expects, 0 fail;
    LIVE harvest receipt 5/5.

BazaarLink gateway provider (FID-2026-0915-006)

  • 19th provider, static 12-model catalog: the identity-audited genuine
    free channel bazaarlink/qwen/qwen3.7-flash:free (1M ctx, keyed gauntlet:
    consistent EN/ZH self-ID, zero injection class) + the 11 gateway-available
    top-coding paid models (BenchLM SWE-bench Pro leaderboard dated
    2026-09-15 ∩ the LIVE keyed roster; fable-5.1/fable-5/opus-5/opus-4.8/
    opus-4.7/sonnet-5 @ 1M, gpt-5.6-sol/terra/luna @ 1.05M, grok-4.5 @ 500k,
    qwen3.8-max @ 1M). Paid channels added untested per operator ruling;
    serving-name disclosure (responses echo the routed model) is the carried
    honesty control.
  • Excluded by audit evidence, not omission: deepseek :free channels
    (EN self-IDs claimed GPT-4 and Claude Opus 4.1; ~60–70 hidden injected
    tokens) and auto:free (429 storm, 120–265s latency) — reinstating them
    requires a new operator decision plus a gauntlet re-run.
  • Full integration chain: registry entry (openai-compatible, strip
    transform, BAZAARLINK_API_KEY), MODEL_CATALOGS ref, shim export,
    vendor fallback table, CLI picker fetcher + gateway merge, 8 docs
    surfaces; Step-5 LIVE keyed round-trip through the production chain
    (exact echo, no injection class); closed-world pins widened to 19
    providers / 17 setup.

Operator seed intake: 16 operator-authorized hosts enter the discovery pipeline (FID-2026-0915-003)

  • New scripts/providers/lib/seed-hosts.ts: the operator's free-provider
    expansion list (16 hosts after rulings) as typed SeedCard data, merged
    into the stage-0 candidate stream each harvest run with operator
    precedence
    — an operator-authorized seed overrides the feed's card for
    the same host (the original feed-precedence design was corrected after
    the first LIVE run exposed it silently defeating two rulings).
  • Identical gate chain, no bypass: seeds pass stage-0, the typosquat
    screen, registry dedupe, and the LIVE unauthenticated-generation probe
    exactly like feed hosts — which rejected 2 of the 16 (b.ai,
    platform.experientiallabs.ai: open relays, LLMjacking class) with
    evidence recorded in the denylist and report.
  • LIVE result: 16/16 seeds tracked in candidates.json (8 boundary-ok,
    6 boundary-unverifiable pending deeper discovery, 2 rejected); 14
    first-sight provenance rows in the report audit trail; seed pins 6/126
    (pipeline suite 82/342, 0 fail); removal = delete the seed entry.
  • File-cap split program closed (FID-2026-0915-002): all 12 over-cap
    files split move-only on existing seams; quality:report PASS (1498
    files); archived with verification receipt.

File-cap split program: all 12 over-cap files split on existing seams (FID-2026-0915-002)

  • bun run quality:report now PASSes with 0 violations (1498 baselined
    files): every file above the max_file_lines: 300 absolute ceiling was
    split move-only along a seam that already existed — no behavior change, no
    import-path churn for consumers (re-export facades / same-name exports).
  • Splits: discovery-state.ts → intelligence + io + facade; harvest
    main → candidates-io + phases + report-phase; report.ts → format +
    sections; harvest-core / intelligence-layer test suites split on
    describe boundaries with exact assertion parity (76/216); the
    context-pruner trio → preserved-state-merge / summary-sections /
    summarize-messages-entries via the embeddedHelpers Batch B pattern
    (registered in handle-steps.ts; serialization tests + bundle
    regeneration green); right-sidebar → header component; run-results →
    gates module; spawn-agent-inline → pruner-guards module;
    provider-wizard-steps → step-sections module.
  • Verification: root typecheck exit 0 (all workspaces), root test gate
    421 pass / 5737 expect(), LIVE providers:harvest --probe rerun
    shape-identical (all report sections render; candidates.json v2).
  • Operator visibility: commit series 67e77d37 → 9fbe5364; FID
    resolution records the measured seams and the two recorded deviations.

Free-compute intelligence layer: stability · model index · churn · nudges · fallback hints · quality gauntlet (FID-2026-0915-001)

  • Six workstreams over data the pipeline already collects (operator:
    "so all 6"; MQ1–MQ4 ruled — all four recommendations accepted: 14-day
    ring, <½-latency nudge, 8-prompt rubric, gitignored denylist):
    • W1 stability ring — every host accumulates a 14-sample daily history
      in candidates.json (state v2, zero-blip migration; same-day re-runs
      replace, oldest dropped); the report's candidates tables gain an
      Uptime (14d) column (100% · 67ms avg).
    • W2 model-first index — family normalization joins vendor ids
      (meta/llama-3.3-70b-instruct), feed prose (Llama 3.1/3.3 family),
      and bare ids (llama-3.3-70b); the report gains a readiness-ordered
      Model availability index (LIVE: deepseek ×23, qwen ×20, llama ×16),
      and a host silently dropping a model family is flagged in the audit
      trail (dropout detection).
    • W3 churn analytics + denylist — firstSeenUtc is set on first
      sight and never mutated; the report gains an Ecosystem churn
      section (7-day new-host window, 72h-rule lapses, median lifespan of
      dead hosts); denylist.json accumulates every rejection/flag ever
      recorded with dates (machine-readable, gitignored).
    • W4 better-default nudges — when a health-tracked pipeline provider
      degrades AND a boundary-ok candidate is more than 2× faster with ≥1
      model, the sanitized agent announce block gains ONE capped suggest-line
      (agent suggests, never switches).
    • W5 429 fallback hints — on a rate-limit failure the CLI offers up
      to 2 OTHER boundary-ok hosts serving the same model family
      (fail-silent: missing state ⇒ banner exactly as before; once per model
      per session; the failing host is excluded via the error URL; wired
      through both the throw path and the error-output completion path).
    • W6 quality gauntlet (operator-run) — bun run providers:quality -- <host> runs a fixed 8-prompt coding rubric against one candidate with
      the operator's key (env at invocation, used in headers only, never
      stored or logged, refused BEFORE any network call); scores land in
      quality.json and render in the report's Quality (operator-run)
      section. Never scheduled; not a merge gate.
  • State layer moved to common
    (common/src/providers/discovery-state.ts — Law 13 single truth for the
    report, the wizard prefill, and the W5 CLI seam; scripts keep a
    re-export shim). Invariants unchanged: zero auto-merge/switch, zero key
    storage, agent has no destructive tool, artifacts gitignored.
  • Verification: 34 new pin tests (RED-first per workstream; 131 tests
    across touched suites, 0 fail); typecheck ×4; eslint --max-warnings 0;
    lint:md; prettier; LIVE harvest gate — v2 migration zero-blip (55/55
    unchanged), ring/index/churn/quality sections rendered, chutes.ai
    still RELAY ✗ with history. W6 validated live (orcarouter.ai,
    operator key, 8/8 · 1607ms): caught a fetch POST→GET redirect defect
    (apex→www 301) — gauntlet now normalizes /v1 and re-POSTs manually with
    auth intact, and probeEndpoint was hardened the same way (RED-first
    pins; the harvester re-probes boundary-unverifiable hosts so stale
    measurement artifacts self-correct — LIVE: orcarouter.ai corrected from
    boundary-unverifiable to boundary-ok (401, 389ms)).

Free-compute discovery pipeline: daily harvest → probe → propose → accept → track → remove (FID-2026-0914-003)

  • Daily pipeline (bun run providers:harvest [--probe]): fetches the
    freeairouter open feed once (transparent UA, 30s timeout, once daily),
    applies the stage-0 filter (verified + reachable + confirmed
    first-party-free/commercial-aggregator — 55/214 viable on the live
    payload), runs the two-tier typosquat screen (tier-1 auto-reject with a
    legitimate-vendor allowlist — caught api.celebras.ai; api.z.ai
    survives), diffs against persisted state + the built-in registry (72-hour
    lapse prune via downStreak), and — with --probe — probes new
    candidates against their own vendor surfaces (GET /v1/models +
    401-boundary check: unauthenticated 2xx = open relay = auto-reject; no
    keys ever) and health-probes pipeline-sourced custom providers.
  • Stable data-backed report (MQ8): dev/provider-candidates/report.md
    (gitignored) is replaced in place every run — full candidate table,
    complete audit trail (every exclusion/rejection carries a reason),
    new/changed/lapsed sections, Stage-E health section. candidates.json
    carries the rolling diff state; agent-context.txt carries the announce
    payload.
  • Stage C (bun run providers:propose -- <host>): writes an
    FID-001-shaped curation scaffold into dev/scratchpad/provider-proposals/
    (write-guarded to dev/); open-relay hosts get a DO-NOT-CURATE banner.
  • Loop-5 production seams: CustomProviderConfig gains a fail-closed-
    validated provenance stamp (source: 'discovery-pipeline' +
    acceptedAt, preserved by parseCustomProviders — single validation
    truth); the CLI boot-check runs the harvest in the background when the
    report is >24h old (repo-gated via isDiscoveryRepo, fail-silent, never
    blocks the TUI, IS_SAVANT_FREE exempt); session bootstrap surfaces the
    sanitized announce block once per candidate set (host/count/verdict/
    report-path facts only — never feed prose; prompt-injection surface zero
    by construction).
  • MQ6 one-step add: /provider add <host> for a probe-passed candidate
    opens the existing wizard pre-filled (id/label/baseUrl/envVar derived
    from the host; Enter adopts, typing replaces — the consent gesture exists
    only on discovery sessions); the finalized record is stamped; open-relay
    hosts refuse to prefill (enforced after the LIVE run caught a real one).
  • Stage E lifecycle close: stamped providers are re-probed daily
    (liveness, /v1/models shape, 401-boundary drift = compromise
    signal
    ); degradation surfaces in the report + agent block with the
    exact remediation — the shipped /provider remove <id>. The agent never
    receives a destructive tool.
  • LIVE gate: first real run — 214 feed records → 55 candidates → a
    genuine open relay (chutes.ai) auto-rejected in the wild; 7 tier-2
    flags with exact distance evidence. 54 new pin tests (RED-first for the
    wizard seam); suites green: cli 3696 / common 724 / sdk 611, typecheck
    ×4, eslint --max-warnings 0, lint:md, prettier. Day-two verification
    runs caught and fixed probe-evidence erosion (standing boundary verdicts
    now carry forward across unchanged re-sights — re-verified live, 55/55
    verdicts survive; zero classification drift and a silent no-nag announce
    on unchanged feeds).
  • Report/state restructure (operator review: "professionally structured,
    properly organized models"):
    the report gained an executive Summary
    table, a "This run's changes" section (only when non-empty), candidates
    grouped first-party/aggregator and sorted by readiness (verified
    boundary → model count → latency, relays last), boundary display as
    ok/unverified/RELAY ✗ from the STANDING verdict, and a capped
    per-host model roster; the audit trail became counted reason-groups
    (4 sections replace ~140 bullets; every host still listed). The Models
    column no longer conflates "list hidden" with "no models": probed count
    plain, feed-listed count with a † marker, else —. The fingerprint is
    MODELS-ONLY (the feed's LLM-rewritten quota prose caused false
    changed classifications; live proof: the prose-hash run misflagged 2
    hosts the models-hash run correctly ignores), stored as an 8-hex
    FNV-1a hash in a _meta+hosts candidates.json wrapper (~44% smaller,
    one-glance records) with bit-identical legacy migration — zero
    re-classification blip. Renderer/serializer logic moved out of the
    driver into tested lib modules; 62 pipeline tests green.

Context-compaction rebuild: LLM-semantic summaries + window-truth audit (FID-2026-0914-002)

  • The compaction artifact the operator hit (Progress note: fragment
    storms, [digest] bytes=11 HEAD: [compacted] re-digest spam, punctuation
    "decisions", error-log self-play) was fully diagnosed: the summary was
    authored by a deterministic TypeScript transcriber
    (agents/context-pruner/*, embedded model-less via .toString()), not
    by a model. All 21 reference compact systems reviewed (openclaw, kimi,
    hermes, codex, cline, …) use a model call.
  • LLM semantic writer (new Layer 3): the session model rewrites the
    deterministic excerpt into a first-person handoff (kimi pattern:
    settled-vs-open decisions, exact commands/paths/results, named unknowns,
    forward plan, evidence honesty) at the spawn boundary — the only seam
    with model access. Output is validated (800–6000 tokens, no tool-call
    leakage, secrets [REDACTED]); user aborts propagate; every other
    failure degrades to the deterministic excerpt verbatim. Session model
    only — no override knob (operator ruling).
  • Deterministic writer hardened (kept as fallback + input material):
    consecutive streamed assistant fragments coalesce into one entry (the
    2-char-line storm is dead), identical consecutive user messages
    coalesce with an (×N) marker, [compacted] sentinels and
    {compacted:true} JSON placeholders are never re-digested, the
    error-log section is removed from the structured block, preserved-state
    paths are normalized repo-relative from the threaded projectRoot and
    recency-capped at 12 newest-first (was 25, absolute C:\... noise), and
    the first-user-turn pin skips protocol dumps. Edits land in
    agents/context-pruner/* and regenerate the bundled artifact — the
    generated file is never hand-edited.
  • Context-window truth: both substring heuristics (name-table guess
    • inferContextLength fallbacks, including two live in the gateway
      catalogs found by the audit) are retired to a vendor-published
      CONTEXT_WINDOW_FALLBACKS table; resolveContextWindowForModel
      returns { window, provenance } and the sidebar now shows a window
      provenance badge (catalog / gateway / vendor-table / default). Unmatched
      models get the conservative default — never a wrong family guess.
  • Envelope preservation is pinned across every compaction form
    (<conversation_summary> → <historical_memory> →
    <compaction-summary>, plus the bare literal) so emergency truncation
    can never drop the preserved state. A LIVE gate drove the production
    runContextPrunerMain over a 190-message fragment-storm history:
    150 fragments → 2 entries, 11 resumes → 1 coalesced entry, zero
    placeholder re-digests, goal + first turn pinned verbatim (7/7).
  • Gates: typecheck × 4, agents/agent-runtime/sdk/cli suites (all
    pre-existing vendored-resource and cross-suite-pollution failures
    proven pre-existing by stash A/B), eslint --max-warnings 0,
    prettier, lint:md, regenerated-bundle verification, LIVE compaction
    gate.

Infron + UnoRouter gateway providers added (FID-2026-0914-001)

  • Two new built-in gateway providers follow the static-allowlist template
    (FID-2026-0913-001): Infron (catalog api.infron.ai/v1/models,
    inference https://llm.onerouter.pro/v1; 4 :free + 5 top-coding
    models) and UnoRouter (inference https://api.unorouter.com/v1; 12
    curated :free + 5 top-coding models, 5 of them live-verified HTTP-200
    this session). Codex/Responses-API-only models are excluded by operator
    ruling (chat-only). 26 curated channels total, all context windows
    vendor-published; picker derivation, setup derivation, and model prefixes
    all derive from the two registry entries with no new machinery.
  • The CLI picker's static-catalog module split (283/300 cap discipline):
    the new providers' maps + fetchers live in
    cli/src/utils/openrouter-models/static-catalogs-gateways.ts, leaving
    static-catalogs.ts untouched. Test-suite splits mirror the Task 45
    pattern (provider-registry-gateways.test.ts,
    model-provider-free-mode-gateways.test.ts); closed-world pins widened
    16→18 registry keys / 14→16 setup ids.
  • Latent harness defect fixed: the SDK's one-shot .env.local bootstrap
    ("Using environment: dev") fires lazily on the first transitive import,
    re-injecting real keys AFTER a test harness deletes them in
    beforeEach — key-missing fail-closed legs must warm the module graph
    first. The fix also cured a pre-existing test-order flake in the sdk
    family (proven by stash A/B: failures 6→5, errors 4→3).
  • Docs synced across all 8 hand-maintained surfaces the drift guard pins
    (README ×2, docs ×4, release README, .env.example);
    generate:provider-docs:check green.
  • Known boundaries: Infron's free tier requires team balance ≥ $5, so its
    LIVE keyed 200 remains NEEDS-REVIEW until the account is funded
    (OrcaRouter precedent — the integration itself is verified end-to-end
    through the production chain); UnoRouter documents 429s at peak hours
    and multi-supplier failover (personal-use posture, not a community
    default).

OrcaRouter gateway provider closed — keyed live acceptance green (FID-2026-0911-002)

  • The final Step-4 closure gate passed: with a fresh post-linkage key,
    the keyed chat round-trip returned HTTP 200 "OK" on
    orcarouter/free (catalog 195/195 via the production chain, no-key
    401 fail-closed). No code changes in this closure — the integration
    had been complete since 2026-09-11; the residue was an
    account-entitlement state. Root cause of the two-day 429
    err_free_access_denied era: OrcaRouter binds free-tier entitlement
    at KEY-MINT time, so the pre-linkage key kept failing after the
    GitHub linkage registered (hypothesis (a) confirmed by
    discriminator). FID closed + archived with the receipt re-stamped
    live at the archived path (8/8 gates).

Provider docs synced to the 16-provider registry + drift guard (FID-2026-0913-005)

  • Post-0.0.31 sweep: the top-level README provider table (9 rows against a
    16-provider registry — missing KiosAPI, APInex, OrcaRouter, B.AI, HCNSec,
    TokenBom), the zh-CN mirror, docs/index.md, docs/sdk-overview.md, and
    the shipped npm README's shell examples (still teaching the legacy
    OPENCODE_GO_API_KEY) are all synced to the registry; 4 dead
    docs/design/Adding New Providers.md links re-pointed to
    docs/archive/design/.
  • The generator now owns a THIRD GENERATED surface — the main README's
    provider table — and --check additionally runs
    scripts/provider-docs-drift.ts: presence-token pins over every
    hand-maintained surface (8 windowed + 5 whole-file checks), so the next
    provider that lands without its doc sync fails validate:repository,
    pre-push, and the release gates with the exact missing token (69 findings
    pre-fix → 0).
  • RED-first test scripts/__tests__/provider-docs-drift.test.ts (4 tests /
    45 expects) pins the current tree, a stale fixture, and missing anchors.
  • Latent defect fixed en route: GENERATED markers between a markdown
    table's delimiter row and body rows terminate the table token — the
    release README had shipped that broken shape since the generator's
    introduction, masked by its line-1 file-wide MD013 disable. The generator
    now renders the ENTIRE contiguous table inside the markers
    (renderProviderTable).