Context
Sub 1 of 2 under #15031 (the split rationale + AC mapping live in the parent's decision comment). The parent's V-B-A (https://github.com/neomjs/neo/issues/15031#issuecomment-4945488536) confirmed the mechanism at source: construct() auto-fires initAsync() (src/core/Base.mjs:314-317), singletons construct at import (src/Neo.mjs:982-983), and ready() gates on full heavy init — so the production tranche migrates today with ZERO framework changes. The bespoke _initPromise guards make the mixed state safe by construction during the transition (an external call and ready() coexist harmlessly until #15031's sub 2 deletes the guards).
Scope facts from the trace: ~39 production call sites (1 src/ + 38 ai/) of external non-super await X.initAsync(), e.g. the ai/scripts/lifecycle/* boot blocks, SystemLifecycleService.mjs:44-51 (the if (!X._initPromise) await X.initAsync() dance — which then ALSO awaits ready()), ai/agent/AgentOrchestrator.mjs:410.
Proposed Solution
- Migrate every external non-
super await X.initAsync() in src/ + ai/ to await X.ready(), per-site (never mechanical):
- Instantiation-path check per site:
ready() only exists for construct()-ed instances. Neo.create/singleton-import paths are safe; injected-factory seams (e.g. AgentOrchestrator.agentFactory — test doubles must expose ready()) get verified individually.
- Rejection-path classification per service (the error-visibility delta): an external
await initAsync() inside try/catch OBSERVES a rejection; ready() hangs forever when the construct-fired init rejects (isReady never flips). For each service whose initAsync can reject without an internal catch, either adopt the GraphService catch-and-degrade precedent (GraphService.mjs IIFE: internal catch → degraded mode → init resolves) or keep an explicit error surface. ai/scripts/lifecycle/* exit-code consumers are the first check.
- Remove the now-pointless
if (!X._initPromise) production reach-ins in the same pass (SystemLifecycleService, DreamService.mjs:242) — the trailing await X.ready() lines already there become the whole story.
- Production-tree lint (freeze at zero): a grep-class CI check that fails on any NEW external non-
super await *.initAsync() or ._initPromise reach-in scoped to src/ + ai/ (the test tranche stays allowed until sub 2 lands its reset seam and extends the lint repo-wide).
- Do NOT touch: the bespoke
_initPromise guards (deletion is sub 2, blocked on the reset seam), any test/ call site, any core.Base surface.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
~39 production external initAsync() sites |
Base.mjs:601 contract + parent V-B-A |
migrated to await X.ready(), per-site verified |
— |
— |
grep zero in src/ + ai/ |
services whose initAsync can reject |
Base.mjs:314-317 (rejection = ready() hang) |
classified: internal catch-and-degrade OR documented error surface |
GraphService precedent |
service JSDoc |
classification table in the PR body |
| production-tree lint |
this ticket |
CI fails on new external initAsync() await / _initPromise reach-in in src/ + ai/ |
— |
lint header comment |
guard red-test |
Acceptance Criteria
Out of Scope
- Bespoke
_initPromise guard deletion (sub 2 — unsafe until external calls are gone repo-wide AND specs have a reset seam).
- The
test/ tranche (86 call sites — mechanically blocked: specs null _initPromise to force re-init, which ready() cannot express until sub 2's seam exists).
- Any
core.Base / Neo.mjs change.
Related
Parent #15031 · origin PR #15016 · src/core/Base.mjs:266,305,314,601,963 lifecycle · #12597 (CLOSED — the spec-leak history that shaped the test-tranche coupling)
Origin Session ID: ef6b9a4a-54ec-4afb-8438-f89a3ee46ad2
Retrieval Hint: "production sweep external initAsync ready migration rejection-path classification lint src ai tranche"
Context
Sub 1 of 2 under #15031 (the split rationale + AC mapping live in the parent's decision comment). The parent's V-B-A (https://github.com/neomjs/neo/issues/15031#issuecomment-4945488536) confirmed the mechanism at source:
construct()auto-firesinitAsync()(src/core/Base.mjs:314-317), singletons construct at import (src/Neo.mjs:982-983), andready()gates on full heavy init — so the production tranche migrates today with ZERO framework changes. The bespoke_initPromiseguards make the mixed state safe by construction during the transition (an external call andready()coexist harmlessly until #15031's sub 2 deletes the guards).Scope facts from the trace: ~39 production call sites (1
src/+ 38ai/) of external non-superawait X.initAsync(), e.g. theai/scripts/lifecycle/*boot blocks,SystemLifecycleService.mjs:44-51(theif (!X._initPromise) await X.initAsync()dance — which then ALSO awaitsready()),ai/agent/AgentOrchestrator.mjs:410.Proposed Solution
superawait X.initAsync()insrc/+ai/toawait X.ready(), per-site (never mechanical):ready()only exists forconstruct()-ed instances.Neo.create/singleton-import paths are safe; injected-factory seams (e.g.AgentOrchestrator.agentFactory— test doubles must exposeready()) get verified individually.await initAsync()inside try/catch OBSERVES a rejection;ready()hangs forever when the construct-fired init rejects (isReadynever flips). For each service whoseinitAsynccan reject without an internal catch, either adopt theGraphServicecatch-and-degrade precedent (GraphService.mjsIIFE: internal catch → degraded mode → init resolves) or keep an explicit error surface.ai/scripts/lifecycle/*exit-code consumers are the first check.if (!X._initPromise)production reach-ins in the same pass (SystemLifecycleService,DreamService.mjs:242) — the trailingawait X.ready()lines already there become the whole story.superawait *.initAsync()or._initPromisereach-in scoped tosrc/+ai/(the test tranche stays allowed until sub 2 lands its reset seam and extends the lint repo-wide)._initPromiseguards (deletion is sub 2, blocked on the reset seam), anytest/call site, anycore.Basesurface.Contract Ledger Matrix
initAsync()sitesBase.mjs:601contract + parent V-B-Aawait X.ready(), per-site verifiedsrc/+ai/initAsynccan rejectBase.mjs:314-317(rejection =ready()hang)initAsync()await /_initPromisereach-in insrc/+ai/Acceptance Criteria
grepfor external non-superawait *.initAsync()insrc/+ai/returns zero (excludingBase.mjs's framework-internal fire + warning comment).grepfor._initPromisereach-ins (read or write) OUTSIDE the owning services returns zero insrc/+ai/.ai/scripts/lifecycle/*scripts keep their failure semantics (a failed boot still surfaces, never a silent hang).Out of Scope
_initPromiseguard deletion (sub 2 — unsafe until external calls are gone repo-wide AND specs have a reset seam).test/tranche (86 call sites — mechanically blocked: specs null_initPromiseto force re-init, whichready()cannot express until sub 2's seam exists).core.Base/Neo.mjschange.Related
Parent #15031 · origin PR #15016 ·
src/core/Base.mjs:266,305,314,601,963lifecycle · #12597 (CLOSED — the spec-leak history that shaped the test-tranche coupling)Origin Session ID: ef6b9a4a-54ec-4afb-8438-f89a3ee46ad2
Retrieval Hint: "production sweep external initAsync ready migration rejection-path classification lint src ai tranche"