Context
Operator feedback on 2026-06-06 flagged the lifecycle idle-threshold pattern as an ADR 0019 shape error: the threshold should be an AiConfig leaf, not local process.env parsing. I V-B-A'd the current source after the #12605 review loop: PR #12605 already routes its Memory Core drift threshold through activeSessionIdleThresholdMs, but the wake/lifecycle scripts still carry separate direct env reads and hardcoded defaults.
Live latest-open sweep: checked the latest 20 open issues on 2026-06-06 via gh issue list --state open --limit 20 --json number,title,author,labels,url. Nearby open issues included #12612, #12609, #12565, #12536, #12479, #12456, #12450, #12435, #12402, #12401, #12190, #12186, #12153, #12123, #12075, #12073, #12065, #12008, #11976, and #11909; none owns this residual. Exact duplicate searches found only closed or adjacent tickets: IDLE_THRESHOLD_MS surfaced closed #10633; TRIO_WAKE_COOLDOWN_SECONDS surfaced closed #10626; NEO_TRIO_IDENTITIES surfaced closed #12438 and #10636; lifecycle idle threshold AiConfig found no issue. The Knowledge Base semantic path was attempted first but is currently unavailable (knowledgeBase collection missing), so this ticket is grounded in live GitHub plus local source evidence.
The Problem
ADR 0019 says AiConfig / child providers are the reactive Provider SSOT: leaf(default, env, type) owns env override, defaulting, and type decoding; consumers read resolved leaves at the use site. The wake lifecycle scripts still bypass that model:
ai/scripts/lifecycle/checkSunsetted.mjs:72 parses process.env.IDLE_THRESHOLD_MS and falls back to 10 * 60 * 1000.
ai/scripts/lifecycle/checkAllAgentIdle.mjs:59 parses process.env.NEO_TRIO_IDENTITIES with a hardcoded Neo-agent roster fallback.
ai/scripts/lifecycle/checkAllAgentIdle.mjs:62 parses process.env.IDLE_THRESHOLD_MS again.
ai/scripts/lifecycle/trioWakeCooldown.mjs:38 parses process.env.TRIO_WAKE_COOLDOWN_SECONDS and falls back to 600.
- Unit tests in
checkAllAgentIdle.spec.mjs and trioWakeCooldown.spec.mjs set those raw env vars directly, which keeps the env names load-bearing outside the Provider surface.
This is not the same as MCP tool argument validation: MCP tools already have OpenAPI/Zod tool shapes. The residual here is lifecycle-script configuration, so the right validation/defaulting primitive is the AiConfig Provider leaf set.
The Architectural Reality
- ADR 0019 is the authority: read resolved leaves at use sites; never re-implement env resolution around the Provider SSOT.
ai/config.template.mjs already owns Tier-1 wake/orchestrator configuration, including wakeDaemonHeartbeatAlivePath, orchestrator.intervals.swarmHeartbeatMs, and orchestrator.swarmHeartbeat.{targetSource,targets}.
ai/daemons/orchestrator/Orchestrator.mjs already reads AiConfig.orchestrator.swarmHeartbeat.targets / targetSource and injects them into SwarmHeartbeatService.
ai/daemons/orchestrator/scheduling/swarmHeartbeat.mjs already contains the deployment-portable target resolver for self, active-local-team, active-subscribers, active-a2a-participants, and disabled.
checkAllAgentIdle.mjs currently has a second identity source via NEO_TRIO_IDENTITIES, which can drift from the resolver-backed heartbeat target contract.
The Fix
Implement a single, non-micro cleanup slice for the lifecycle wake knobs:
- Add one owning AiConfig leaf set for wake lifecycle policy values. Recommended owner:
AiConfig.orchestrator.swarmHeartbeat or a sibling wake policy group in Tier-1 config, because these scripts are consumed by the orchestrator heartbeat lane. Do not duplicate the same value in both Tier-1 and Memory Core child config.
- Move the idle threshold used by
checkSunsetted.mjs and checkAllAgentIdle.mjs to one resolved leaf. Preserve existing operator compatibility either by binding the leaf to the legacy env name IDLE_THRESHOLD_MS or by adding a documented deprecation bridge; do not keep direct use-site env parsing.
- Route all-agent identity selection through the existing swarm-heartbeat target contract where practical. If
checkAllAgentIdle.mjs still needs an explicit override, make it a Provider-owned leaf and explain why it is distinct from orchestrator.swarmHeartbeat.targets / targetSource.
- Move trio wake cooldown TTL to an AiConfig leaf, preserving legacy
TRIO_WAKE_COOLDOWN_SECONDS behavior or documenting a deprecation bridge.
- Update focused unit coverage so tests exercise Provider/env resolution, not raw script-local parsing. Keep subprocess coverage for the script entrypoints.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
| Lifecycle idle threshold |
ADR 0019; checkSunsetted.mjs; checkAllAgentIdle.mjs |
One AiConfig leaf owns the default and env override; both scripts read the resolved value at use site |
Invalid/missing env falls back through the Provider leaf decoder/default, not local parseInt logic |
Leaf JSDoc in ai/config.template.mjs |
Unit tests prove env override + default; subprocess tests still pass |
| All-agent idle target set |
Epic #11829 AC2; orchestrator.swarmHeartbeat config; resolveSwarmHeartbeatTargets() |
Use the existing resolver-backed target contract or one Provider-owned explicit override with clear distinction |
External/fork profile must not silently fan out to Neo maintainer identities |
Leaf/helper JSDoc if a new explicit override remains |
Unit tests prove default, explicit override, and no hardcoded Neo roster fallback |
| Trio wake cooldown TTL |
#10626 precedent; ADR 0019 |
trioWakeCooldown.mjs reads a resolved AiConfig leaf for TTL seconds/ms |
Existing env behavior is preserved via leaf binding or explicit deprecation bridge |
Leaf JSDoc |
trioWakeCooldown.spec.mjs proves TTL default + override via Provider-owned config |
Decision Record impact
aligned-with ADR 0019. This does not amend ADR 0019; it applies the sanctioned Provider-SSOT pattern to wake lifecycle policy knobs that #12438 left out of scope.
Acceptance Criteria
Out of Scope
- Changing MCP
add_message / tool schemas; those already have tool-shape validation and are not the source of this residual.
- Reworking the whole wake-subscription or bridge-daemon delivery path.
- Closing #12456; this is one residual leaf under that epic.
- Reopening #12438 or #12461; both are closed and this ticket covers the lifecycle-script residual they did not finish.
Avoided Traps
- Do not create an 8-line PR that only swaps one threshold in one file; this ticket intentionally batches the related lifecycle wake knobs.
- Do not add a second config source of truth in Memory Core if Tier-1 orchestrator wake config owns the behavior.
- Do not preserve direct
parseInt(process.env.X) at the use site behind a renamed helper; the leaf owns env/default/type decoding.
- Do not silently hardcode Neo maintainer identities into a public default; preserve the deployment-portable resolver discipline from #11829.
Related
Parent epic: #12456
Wake substrate authority: #11829, #10626
Adjacent closed AiConfig cleanup: #12438, #12461
Recent precedent: PR #12605 routes a Memory Core drift threshold through AiConfig instead of service-local env parsing.
Handoff Retrieval Hint: AiConfig lifecycle wake knobs IDLE_THRESHOLD_MS NEO_TRIO_IDENTITIES TRIO_WAKE_COOLDOWN_SECONDS checkAllAgentIdle checkSunsetted trioWakeCooldown
Context
Operator feedback on 2026-06-06 flagged the lifecycle idle-threshold pattern as an ADR 0019 shape error: the threshold should be an AiConfig leaf, not local
process.envparsing. I V-B-A'd the current source after the #12605 review loop: PR #12605 already routes its Memory Core drift threshold throughactiveSessionIdleThresholdMs, but the wake/lifecycle scripts still carry separate direct env reads and hardcoded defaults.Live latest-open sweep: checked the latest 20 open issues on 2026-06-06 via
gh issue list --state open --limit 20 --json number,title,author,labels,url. Nearby open issues included #12612, #12609, #12565, #12536, #12479, #12456, #12450, #12435, #12402, #12401, #12190, #12186, #12153, #12123, #12075, #12073, #12065, #12008, #11976, and #11909; none owns this residual. Exact duplicate searches found only closed or adjacent tickets:IDLE_THRESHOLD_MSsurfaced closed #10633;TRIO_WAKE_COOLDOWN_SECONDSsurfaced closed #10626;NEO_TRIO_IDENTITIESsurfaced closed #12438 and #10636;lifecycle idle threshold AiConfigfound no issue. The Knowledge Base semantic path was attempted first but is currently unavailable (knowledgeBasecollection missing), so this ticket is grounded in live GitHub plus local source evidence.The Problem
ADR 0019 says
AiConfig/ child providers are the reactive Provider SSOT:leaf(default, env, type)owns env override, defaulting, and type decoding; consumers read resolved leaves at the use site. The wake lifecycle scripts still bypass that model:ai/scripts/lifecycle/checkSunsetted.mjs:72parsesprocess.env.IDLE_THRESHOLD_MSand falls back to10 * 60 * 1000.ai/scripts/lifecycle/checkAllAgentIdle.mjs:59parsesprocess.env.NEO_TRIO_IDENTITIESwith a hardcoded Neo-agent roster fallback.ai/scripts/lifecycle/checkAllAgentIdle.mjs:62parsesprocess.env.IDLE_THRESHOLD_MSagain.ai/scripts/lifecycle/trioWakeCooldown.mjs:38parsesprocess.env.TRIO_WAKE_COOLDOWN_SECONDSand falls back to600.checkAllAgentIdle.spec.mjsandtrioWakeCooldown.spec.mjsset those raw env vars directly, which keeps the env names load-bearing outside the Provider surface.This is not the same as MCP tool argument validation: MCP tools already have OpenAPI/Zod tool shapes. The residual here is lifecycle-script configuration, so the right validation/defaulting primitive is the AiConfig Provider leaf set.
The Architectural Reality
ai/config.template.mjsalready owns Tier-1 wake/orchestrator configuration, includingwakeDaemonHeartbeatAlivePath,orchestrator.intervals.swarmHeartbeatMs, andorchestrator.swarmHeartbeat.{targetSource,targets}.ai/daemons/orchestrator/Orchestrator.mjsalready readsAiConfig.orchestrator.swarmHeartbeat.targets/targetSourceand injects them intoSwarmHeartbeatService.ai/daemons/orchestrator/scheduling/swarmHeartbeat.mjsalready contains the deployment-portable target resolver forself,active-local-team,active-subscribers,active-a2a-participants, anddisabled.checkAllAgentIdle.mjscurrently has a second identity source viaNEO_TRIO_IDENTITIES, which can drift from the resolver-backed heartbeat target contract.The Fix
Implement a single, non-micro cleanup slice for the lifecycle wake knobs:
AiConfig.orchestrator.swarmHeartbeator a sibling wake policy group in Tier-1 config, because these scripts are consumed by the orchestrator heartbeat lane. Do not duplicate the same value in both Tier-1 and Memory Core child config.checkSunsetted.mjsandcheckAllAgentIdle.mjsto one resolved leaf. Preserve existing operator compatibility either by binding the leaf to the legacy env nameIDLE_THRESHOLD_MSor by adding a documented deprecation bridge; do not keep direct use-site env parsing.checkAllAgentIdle.mjsstill needs an explicit override, make it a Provider-owned leaf and explain why it is distinct fromorchestrator.swarmHeartbeat.targets/targetSource.TRIO_WAKE_COOLDOWN_SECONDSbehavior or documenting a deprecation bridge.Contract Ledger Matrix
checkSunsetted.mjs;checkAllAgentIdle.mjsparseIntlogicai/config.template.mjsorchestrator.swarmHeartbeatconfig;resolveSwarmHeartbeatTargets()trioWakeCooldown.mjsreads a resolved AiConfig leaf for TTL seconds/mstrioWakeCooldown.spec.mjsproves TTL default + override via Provider-owned configDecision Record impact
aligned-with ADR 0019. This does not amend ADR 0019; it applies the sanctioned Provider-SSOT pattern to wake lifecycle policy knobs that #12438 left out of scope.
Acceptance Criteria
process.env.IDLE_THRESHOLD_MS,process.env.NEO_TRIO_IDENTITIES, orprocess.env.TRIO_WAKE_COOLDOWN_SECONDSparsing remains inai/scripts/lifecycle/*.mjsuse sites.checkSunsetted.mjsandcheckAllAgentIdle.mjsread the same resolved AiConfig idle threshold leaf.checkAllAgentIdle.mjsno longer carries a hardcoded Neo-maintainer roster fallback; it uses the existing heartbeat target contract or a Provider-owned explicit override with documented separation.trioWakeCooldown.mjsreads a resolved AiConfig cooldown leaf.Out of Scope
add_message/ tool schemas; those already have tool-shape validation and are not the source of this residual.Avoided Traps
parseInt(process.env.X)at the use site behind a renamed helper; the leaf owns env/default/type decoding.Related
Parent epic: #12456
Wake substrate authority: #11829, #10626
Adjacent closed AiConfig cleanup: #12438, #12461
Recent precedent: PR #12605 routes a Memory Core drift threshold through AiConfig instead of service-local env parsing.
Handoff Retrieval Hint:
AiConfig lifecycle wake knobs IDLE_THRESHOLD_MS NEO_TRIO_IDENTITIES TRIO_WAKE_COOLDOWN_SECONDS checkAllAgentIdle checkSunsetted trioWakeCooldown