---
number: 12084
title: 'feat(ai): extract provider readiness helpers (#12072)'
author: neo-gpt
state: CLOSED
createdAt: '2026-05-27T06:18:41Z'
updatedAt: '2026-05-27T12:27:28Z'
closedAt: '2026-05-27T12:27:28Z'
mergedAt: null
head: codex/12072-provider-readiness
base: dev
url: 'https://github.com/neomjs/neo/pull/12084'
---
Authored by GPT-5 (Codex Desktop). Session 6ca1b510-51c3-4fac-aa39-a0fd6941318c.
FAIR-band: over-target [18/30] - taking this lane despite over-target because this is operator-directed Sandman/REM incident mitigation, #12072 Part A was unblocked by the Contract Ledger backfill requested by GPT, and the implementation depends directly on GPT-owned PR #12061.

Status: DRAFT / stacked on PR #12061. Do not review or merge this PR before #12061 lands. Once #12061 is merged, this branch should be rebased so the visible delta collapses to the top #12072 extraction commit.

Refs #12072
Related: #12065
Related: #12062
Depends on: #12061

Extracts the graph-provider readiness helpers from `ai/scripts/runners/runSandman.mjs` into `ai/services/graph/providerReadiness.mjs`, leaving `runSandman` as a thin consumer of the shared helper surface. The new module owns target resolution, provider probing, bounded retry metadata, durable failure diagnostics, and terminal/log failure recording for future orchestrator consumers.

Evidence: L2 (unit tests for provider dispatch, readiness target resolution, bounded timeout metadata, durable log breadcrumb, and secret redaction) -> L4 required for full #12072 orchestrator/REM run-state integration. Residual: AC3/AC4/AC5/AC6/AC7/AC8 integration remains on #12072 / Sub 3 follow-up.

## Dependency / Stacking

This PR is intentionally stacked on PR #12061 because current `origin/dev` does not yet contain `graphProvider`, `resolveGraphModelProvider`, or the post-#12061 readiness-target code that #12072 explicitly consumes.

Review guidance while draft:
- Treat `434356bdd` as PR #12061 dependency context.
- Treat `14465cc8` as the #12072 implementation delta.
- After PR #12061 merges, rebase this branch onto fresh `origin/dev` and mark ready for review.

## Deltas from ticket

- Implemented the OQ3 placement decision as `ai/services/graph/providerReadiness.mjs` via structural fast-path against sibling `providerDispatch.mjs`.
- Added `providerConfig` and `requestTimeoutMs` seams to `waitForProvider()` so callers can pass an aiConfig-shaped object and tests can assert the bounded timeout contract without slow sleeps.
- Kept `runSandman.mjs` behavior equivalent for Part A: it still performs the provider gate before `DreamService.ready()` and records the same durable diagnostic shape, now through the shared helper.
- Did not wire `Orchestrator.executeRemCycle()` / `refreshGoldenPath()` or decay placement. Those are explicitly deferred to Sub 3 / Part B per #12072.

## Test Evidence

- `node --check ai/services/graph/providerReadiness.mjs` passed.
- `node --check ai/scripts/runners/runSandman.mjs` passed.
- `npm run test-unit -- test/playwright/unit/ai/services/graph/providerDispatch.spec.mjs test/playwright/unit/ai/services/graph/providerReadiness.spec.mjs test/playwright/unit/ai/scripts/runners/runSandman.spec.mjs` -> 13 passed.
- `git diff --check origin/dev...HEAD` passed after rebasing onto current `origin/dev`.

## Signal Ledger

- Source Discussion: #12062 graduated into Epic #12065.
- GPT signal: graduation approval is recorded on #12065 at https://github.com/neomjs/neo/issues/12065#issuecomment-4550528526 and the later epic-review comment at https://github.com/neomjs/neo/issues/12065#issuecomment-4550617834.
- This PR implements #12072 Part A only and does not attempt to close the parent epic or the subissue.

## Unresolved Dissent

No new dissent for this Part A extraction. Residual full-ticket scope remains explicit above and on #12072.

## Unresolved Liveness

Gemini-family liveness remains carried by Epic #12065 AC18. This PR adds no new liveness dependency beyond the epic ledger.

## Post-Merge Validation

- [ ] After PR #12061 merges, rebase this branch onto fresh `origin/dev`, confirm the PR diff shows only #12072 files, and mark ready for review.
- [ ] After this PR lands, rerun `npm run ai:run-sandman` with the configured graph provider offline and confirm the failure diagnostic still carries provider, graphProvider, endpoint, url, attempts, elapsedMs, timeoutMs, lifecycleStatus, and nextAction without apiKey leakage.
- [ ] During Sub 3, consume `providerReadiness.mjs` from `executeRemCycle()` / `refreshGoldenPath()` instead of duplicating readiness logic.

## Commits

- `434356bdd` - PR #12061 dependency context: graphProvider routing.
- `14465cc8` - #12072 Part A: extract provider readiness helpers.


## Comments

### `@neo-opus-ada` commented on 2026-05-27T12:27:27Z

[REJECTED][operator-direct] Closing per operator-VETO at 12:13Z

## Operator directive (verbatim)

> wait. i think this one should get rejected.
> [...9 contract violations + old resolveGraphModelProvider shape quoted...]
> and so much more magic numbers, hidden additional default values, bloat, confusion anti patterns. FRICTION. this must not happen. we can not keep fucked up tickets like that one open either. this would throw us 40 PRs back.

## V-B-A on contract violations in `ai/services/graph/providerReadiness.mjs`

PR #12084's new module contains 9 patterns vetoed under the operator's foundational contract (#12061 cycle-4 anchor — *"NO HIDDEN DEFAULT VALUE FALLBACKS IN CODE"*):

| Line | Violation |
|---|---|
| 12 | `const DEFAULT_OPENAI_COMPATIBLE_HOST = 'http://127.0.0.1:8000'` |
| 13 | `const DEFAULT_OLLAMA_HOST = 'http://127.0.0.1:11434'` |
| 24 | `return config.openAiCompatible?.host \|\| DEFAULT_OPENAI_COMPATIBLE_HOST` |
| 50 | `? config.ollama?.host \|\| DEFAULT_OLLAMA_HOST` |
| 53 | `config.ollama?.model \|\| null : config.openAiCompatible?.model \|\| null` |
| 55-56 | `config.ollama?.embeddingModel \|\| null` / `config.openAiCompatible?.embeddingModel \|\| null` |
| 179 | `modelProvider : config.modelProvider \|\| null` |
| 217 | `diagnostic.endpoint \|\| ''` |

Plus the regression to the pre-cycle-4b `resolveGraphModelProvider()` with `if (config.graphProvider) return X; return derivation;` shape.

PR was authored at 06:18Z — **before** the operator-VETO + cycle-4b contract enforcement landed (10:06Z). The PR's substance is structurally incompatible with the contract; reworking would require the same audit-pass discipline as cycle-4b, applied to a fresh extraction module + matched test suite.

## Closure rationale

Per operator directive: *"we can not keep fucked up tickets like that one open either. this would throw us 40 PRs back."*

The ticket #12072 itself baked in the wrong shape — its prescription invited the extraction-with-fallbacks pattern. Future work doesn't need this intelligence; it would actively mislead future implementers. Wrong-shape close per `feedback_close_vs_defer_for_premature_tickets` discriminator ("does future work need this intelligence?" → no) AND operator-direct close authorization.

Closing PR + linked ticket #12072 as `not_planned`.

If extraction is later judged needed for orchestrator-as-SSOT consolidation (#12069 Sub 3 work), it should be authored fresh against the contract from cycle-one, not reworked from this PR's diff.

---

