Parent Epic
#12065 — Sub 6 of 9. Depends on Sub 3 (executeRemCycle calls into this substrate).
Premise
Per Discussion #12062 §2.3 gaps 3-4: orchestrator's dream task currently has NO provider-readiness gate (only the partial in-DreamService probe which is skipped for gemini/ollama paths) and only emits e.message via recordTaskOutcome instead of a rich diagnostic payload. This is a silent-failure enabler + observability gap.
Per OQ4 [RESOLVED_TO_AC]: GraphService.decayGlobalTopology() placement inside unified REM method as cycle-finalization step (Algorithmic Lock handles cadence). MUST honor §2.10 control-plane safety.
Cycle-1 RESHAPE (operator-VETO 2026-05-27 on prior extraction approach)
The prior Part A prescription — "extract waitForProvider + helpers from runSandman.mjs into a new ai/services/graph/providerReadiness.mjs module" — was the WRONG SHAPE. The extraction encoded contract-violating defaults:
DEFAULT_OPENAI_COMPATIBLE_HOST / DEFAULT_OLLAMA_HOST module-level constants
config.X?.host || DEFAULT_X_HOST fallback patterns (9 instances)
- Module-level magic timeout/retry constants (
PROVIDER_READY_ATTEMPTS=30, RETRY_MS=1000, TIMEOUT_MS=3000)
These violate the operator's foundational contract enforced at PR #12061 cycle-4: NO HIDDEN DEFAULT VALUE FALLBACKS IN CODE (substrate-banked at feedback_no_hidden_default_fallbacks.md).
The Part A intent — "orchestrator needs provider-readiness capability" — is VALID. The extraction-to-shared-module-with-defaults interpretation is NOT. Reworked prescription below.
Prescription (cycle-1 reshape)
Part A — Orchestrator-side provider-readiness gate + rich diagnostic (config-as-SSOT):
Configurable values live in config templates with concrete defaults — not as module-level constants:
Orchestrator.executeRemCycle() gates on provider readiness inline before delegating to DreamService:
- Use existing
runSandman.mjs:getGraphProviderReadinessTarget() (post-cycle-4b: config-verbatim, fail-loud) — already substrate-correct
- Use existing
runSandman.mjs:waitForProvider() if it's clean (post-cycle-4b audit pending); OR inline the probe in Orchestrator
- On failure → emit rich diagnostic via
createProviderFailureDiagnostic (or equivalent — see schema below) into healthService.recordTaskOutcome + REM run state model (Sub 2)
- On unsupported provider → fail-loud (no fallback)
The diagnostic payload accepts config-shaped input verbatim — fields surface as null/undefined when source config is absent (fail-loud); NO || null / || '' coercion in the envelope builder.
If extraction-to-shared-helper is needed for symmetry with Sub 5 (refreshGoldenPath): the helper module accepts config as parameter, fails loud on missing required fields, uses NO module-level default constants. Placement TBD based on actual call-graph (Orchestrator-internal preferred over ai/services/graph/ if only orchestrator consumes it).
Part B — decayGlobalTopology() placement (unchanged from original):
- Add
includeDecay flag to executeRemCycle({...}) signature
- When
true, calls GraphService.decayGlobalTopology() as cycle-finalization step inside the lease
- 24-hour Algorithmic Lock handles cadence (already in
GraphService.mjs — function self-skips when hoursElapsed < 24)
- MUST enforce §2.10 active-control-plane safety: decay operation MUST allowlist projection-layer edge-types (NOT WAKE_SUBSCRIPTION / mailbox-routing edges)
Contract Ledger
Per ticket-intake-workflow.md §7 — backfilled per @neo-gpt intake-readiness handoff 2026-05-27T05:53Z + reshaped 2026-05-27T~12:30Z for contract compliance.
Diagnostic envelope schema (unchanged from prior — schema was correct, construction was the issue)
| Field |
Type |
Source |
Stability |
event |
'provider_readiness_timeout' | 'unsupported_graph_provider' |
derived from reason |
STABLE |
reason |
enum: 'PROVIDER_READINESS_TIMEOUT' | 'UNSUPPORTED_GRAPH_PROVIDER' |
caller-passed |
STABLE |
provider |
string |
resolved graph provider |
STABLE |
graphProvider |
string |
alias of provider |
STABLE |
modelProvider |
string | undefined |
config.modelProvider raw (NO || null coercion — surface undefined if absent) |
STABLE |
host |
string | undefined |
provider host from config; undefined if unsupported (NO || null coercion) |
STABLE |
endpoint |
string | undefined |
/api/tags (Ollama) or /v1/models (OpenAI-compat) |
STABLE |
url |
string | undefined |
host + endpoint; undefined if host or endpoint absent |
STABLE |
supported |
boolean |
true/false per GRAPH_MODEL_PROVIDERS allowlist |
STABLE |
model |
string | undefined |
provider model name from config; undefined if absent |
STABLE |
embeddingModel |
string | undefined |
provider embedding model from config; undefined if absent |
STABLE |
attempts |
number | undefined |
from waitResult |
STABLE |
elapsedMs |
number | undefined |
from waitResult |
STABLE |
timeoutMs |
number | undefined |
from waitResult |
STABLE |
lifecycleStatus |
Object | undefined |
consumer-sourced |
STABLE |
nextAction |
string |
actionable English (always present) |
STABLE shape — English may evolve |
Redaction rules (unchanged)
apiKey from config.openAiCompatible.apiKey MUST NOT appear in diagnostic envelope. Envelope carries only host, model, embeddingModel, NEVER apiKey.
lifecycleStatus is consumer-sourced; consumers MUST NOT pass objects containing credentials.
Contract-compliance constraints (NEW — for implementer)
The implementer MUST audit the diff with the operator's diagnostic command before opening the PR:
rg -n 'cycle-[0-9]|Lane [A-Z]|AC[0-9]|#[0-9]{4,5}|\.mjs:[0-9]+' <changed-source-paths>Per AGENTS_ATLAS.md §15.6 Source-comment intent filter:
- NO ticket/PR/lane/AC/cycle/line-number anchors in source-file comments (test.describe labels are the documented exception)
- NO module-level
DEFAULT_* constants substituting for absent config
- NO
|| / ?? fallback patterns; config IS SSOT, fail-loud on missing required fields
- NO magic numbers in module scope; configurable values go in config templates
Part B contracts (DEFERRED to Sub 3 #12069)
includeDecay flag + decayGlobalTopology() cycle-finalization placement: contracts deferred to Sub 3's executeRemCycle({...}) design since the integration point IS that signature. Sub 6 Part B remains blocked-on-Sub-3.
Acceptance Criteria
Part B ACs deferred to Sub 3 #12069:
- AC9 (decay placement) + AC10 (control-plane safety) move to Sub 3 since integration point is
executeRemCycle({...}) signature
Avoided Traps
- ❌ Extract helpers WITH
DEFAULT_* constants — operator-VETO precedent (PR #12061 cycle-4 + PR #12084 reject)
- ❌
|| fallback on absent config — violates NO HIDDEN DEFAULTS contract
- ❌ Magic numbers in module scope — configurable values belong in config templates
- ❌ Source-file comments referencing tickets/ACs/lanes — violates §15.6
- ❌ Skip the rich diagnostic — cloud-deploy post-mortem loses substrate (per Discussion §2.3 gap 4)
- ❌ Decay outside the lease — graph mutation outside concurrency protection
- ❌ Decay without control-plane allowlist — empirical anchor: bridge-cursor failure (Discussion §2.10)
Related
- Epic #12065
- Discussion #12062 §2.3 (gaps 3+4+8) + §2.10 (control-plane safety) + OQ4 [RESOLVED_TO_AC]
- Sub 3 (executeRemCycle consumer), Sub 5 (refreshGoldenPath consumer)
- Contract anchor:
feedback_no_hidden_default_fallbacks.md (PR #12061 cycle-4)
- Discipline anchor:
AGENTS_ATLAS.md §15.6 Source-comment intent filter
- VETO precedent: PR #12084 rejected 2026-05-27 (9 contract violations + pre-cycle-4b regression)
Parent Epic
#12065 — Sub 6 of 9. Depends on Sub 3 (executeRemCycle calls into this substrate).
Premise
Per Discussion #12062 §2.3 gaps 3-4: orchestrator's
dreamtask currently has NO provider-readiness gate (only the partial in-DreamService probe which is skipped for gemini/ollama paths) and only emitse.messageviarecordTaskOutcomeinstead of a rich diagnostic payload. This is a silent-failure enabler + observability gap.Per OQ4 [RESOLVED_TO_AC]:
GraphService.decayGlobalTopology()placement inside unified REM method as cycle-finalization step (Algorithmic Lock handles cadence). MUST honor §2.10 control-plane safety.Cycle-1 RESHAPE (operator-VETO 2026-05-27 on prior extraction approach)
The prior Part A prescription — "extract
waitForProvider+ helpers fromrunSandman.mjsinto a newai/services/graph/providerReadiness.mjsmodule" — was the WRONG SHAPE. The extraction encoded contract-violating defaults:DEFAULT_OPENAI_COMPATIBLE_HOST/DEFAULT_OLLAMA_HOSTmodule-level constantsconfig.X?.host || DEFAULT_X_HOSTfallback patterns (9 instances)PROVIDER_READY_ATTEMPTS=30,RETRY_MS=1000,TIMEOUT_MS=3000)These violate the operator's foundational contract enforced at PR #12061 cycle-4: NO HIDDEN DEFAULT VALUE FALLBACKS IN CODE (substrate-banked at
feedback_no_hidden_default_fallbacks.md).The Part A intent — "orchestrator needs provider-readiness capability" — is VALID. The extraction-to-shared-module-with-defaults interpretation is NOT. Reworked prescription below.
Prescription (cycle-1 reshape)
Part A — Orchestrator-side provider-readiness gate + rich diagnostic (config-as-SSOT):
Configurable values live in config templates with concrete defaults — not as module-level constants:
ai/config.template.mjsadds anorchestrator.providerReadinessblock:orchestrator: { ..., providerReadiness: { attempts : 30, delayMs : 1000, timeoutMs: 3000 } }AiConfig.orchestrator.providerReadiness)NEO_ORCHESTRATOR_PROVIDER_READY_ATTEMPTSetc.)ollama.host/openAiCompatible.hostalready exist as concrete config — re-use them; do NOT introduceDEFAULT_*constants anywhereOrchestrator.executeRemCycle()gates on provider readiness inline before delegating to DreamService:runSandman.mjs:getGraphProviderReadinessTarget()(post-cycle-4b: config-verbatim, fail-loud) — already substrate-correctrunSandman.mjs:waitForProvider()if it's clean (post-cycle-4b audit pending); OR inline the probe in OrchestratorcreateProviderFailureDiagnostic(or equivalent — see schema below) intohealthService.recordTaskOutcome+ REM run state model (Sub 2)The diagnostic payload accepts config-shaped input verbatim — fields surface as null/undefined when source config is absent (fail-loud); NO
|| null/|| ''coercion in the envelope builder.If extraction-to-shared-helper is needed for symmetry with Sub 5 (
refreshGoldenPath): the helper module accepts config as parameter, fails loud on missing required fields, uses NO module-level default constants. Placement TBD based on actual call-graph (Orchestrator-internal preferred overai/services/graph/if only orchestrator consumes it).Part B —
decayGlobalTopology()placement (unchanged from original):includeDecayflag toexecuteRemCycle({...})signaturetrue, callsGraphService.decayGlobalTopology()as cycle-finalization step inside the leaseGraphService.mjs— function self-skips whenhoursElapsed < 24)Contract Ledger
Per
ticket-intake-workflow.md §7— backfilled per @neo-gpt intake-readiness handoff 2026-05-27T05:53Z + reshaped 2026-05-27T~12:30Z for contract compliance.Diagnostic envelope schema (unchanged from prior — schema was correct, construction was the issue)
event'provider_readiness_timeout' | 'unsupported_graph_provider'reasonreason'PROVIDER_READINESS_TIMEOUT' | 'UNSUPPORTED_GRAPH_PROVIDER'providerstringgraphProviderstringprovidermodelProviderstring | undefinedconfig.modelProviderraw (NO|| nullcoercion — surface undefined if absent)hoststring | undefined|| nullcoercion)endpointstring | undefined/api/tags(Ollama) or/v1/models(OpenAI-compat)urlstring | undefinedhost + endpoint; undefined if host or endpoint absentsupportedbooleanGRAPH_MODEL_PROVIDERSallowlistmodelstring | undefinedembeddingModelstring | undefinedattemptsnumber | undefinedwaitResultelapsedMsnumber | undefinedwaitResulttimeoutMsnumber | undefinedwaitResultlifecycleStatusObject | undefinednextActionstringRedaction rules (unchanged)
apiKeyfromconfig.openAiCompatible.apiKeyMUST NOT appear in diagnostic envelope. Envelope carries onlyhost,model,embeddingModel, NEVERapiKey.lifecycleStatusis consumer-sourced; consumers MUST NOT pass objects containing credentials.Contract-compliance constraints (NEW — for implementer)
The implementer MUST audit the diff with the operator's diagnostic command before opening the PR:
rg -n 'cycle-[0-9]|Lane [A-Z]|AC[0-9]|#[0-9]{4,5}|\.mjs:[0-9]+' <changed-source-paths>Per
AGENTS_ATLAS.md §15.6 Source-comment intent filter:DEFAULT_*constants substituting for absent config||/??fallback patterns; config IS SSOT, fail-loud on missing required fieldsPart B contracts (DEFERRED to Sub 3 #12069)
includeDecayflag +decayGlobalTopology()cycle-finalization placement: contracts deferred to Sub 3'sexecuteRemCycle({...})design since the integration point IS that signature. Sub 6 Part B remains blocked-on-Sub-3.Acceptance Criteria
ai/config.template.mjsunderorchestrator.providerReadiness; MC overlay inherits verbatimOrchestratorgatesdreamtask on provider readiness before invoking DreamService; uses config-verbatim — NO||fallbacks, NODEFAULT_*constantse.message) viahealthService.recordTaskOutcome+ REM run state model (Sub 2)|| null/|| ''coercion)apiKeynever appears in diagnostic envelope (unit test asserts negative-assertion:expect(diagnostic.apiKey).toBeUndefined()+ envelope keys do not includeapiKey)AGENTS_ATLAS.md §15.6diagnostic command — no ticket/AC/line-number anchors in source commentsrefreshGoldenPath()(when filed) reuses the same provider-readiness gate — symmetric consumptionPart B ACs deferred to Sub 3 #12069:
executeRemCycle({...})signatureAvoided Traps
DEFAULT_*constants — operator-VETO precedent (PR #12061 cycle-4 + PR #12084 reject)||fallback on absent config — violates NO HIDDEN DEFAULTS contractRelated
feedback_no_hidden_default_fallbacks.md(PR #12061 cycle-4)AGENTS_ATLAS.md §15.6 Source-comment intent filter