LearnNewsExamplesServices
Frontmatter
id11905
titleSub 1: Layer 2 — heartbeat target resolver (BLOCKING; resolveSwarmHeartbeatTargets contract)
stateClosed
labels
enhancementaiarchitecturemodel-experience
assigneesneo-opus-4-7
createdAtMay 24, 2026, 4:01 PM
updatedAtMay 24, 2026, 5:56 PM
githubUrlhttps://github.com/neomjs/neo/issues/11905
authorneo-opus-4-7
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 24, 2026, 5:56 PM

Sub 1: Layer 2 — heartbeat target resolver (BLOCKING; resolveSwarmHeartbeatTargets contract)

Closedenhancementaiarchitecturemodel-experience
neo-opus-4-7
neo-opus-4-7 commented on May 24, 2026, 4:01 PM

Parent Epic

#11829 — Sub 1 of 5. BLOCKING for cross-family symmetric heartbeat delivery (AC1).

Premise

SwarmHeartbeatService.pulse() currently invokes checkSunsetted + idleOutNudge per this.identity (single-identity binding at ai/daemons/services/SwarmHeartbeatService.mjs:102 + :129-131 + :199 + :224). Per Epic AC1 + AC2: needs deployment-portable resolver so single Orchestrator lane loops over N resolved targets — NOT N service-instances; NOT direct identityRoots iteration.

Prescription

Implement resolveSwarmHeartbeatTargets() with precedence chain per Epic AC2:

  1. Explicit env/config target list
  2. orchestrator.swarmHeartbeat.targetSource ∈ {self | active-local-team | active-subscribers | disabled}
  3. Public default self
  4. Opt-in active-local-team uses ai/graph/identityRoots.mjs filtered on participationStatus === 'active' (Neo team profile only)
  5. Cloud / fork safety per ADR 0014

Wire SwarmHeartbeatService.pulse() to loop over resolved targets, calling checkSunsetted + idleOutNudge per target.

Acceptance Criteria

  • AC1: resolveSwarmHeartbeatTargets() exported pure-function with 5-step precedence chain
  • AC2: pulse() invokes per resolved target (not just this.identity); orchestrator log shows N nudge lines where N = resolver output cardinality
  • AC3: No-config external workspace defaults to self OR disables-with-log — NEVER silently fans out to Neo maintainer identities
  • AC4: Unit tests in test/playwright/unit/ai/daemons/orchestrator/services/SwarmHeartbeatService.spec.mjs prove: no-config-defaults-self / explicit-target-list / active-local-team / unknown-target-fail-closed

Avoided Traps (per Epic)

  • ❌ Iterating directly over identityRoots.mjs IDENTITIES as heartbeat target source — registry is Neo-maintainer-team, NOT framework-portable; external consumers MUST NOT silently inherit Neo identities
  • ❌ N service-instances instead of single-lane multi-target loop
  • ❌ Hardcoding Neo maintainer identities into framework default

Depends on

(none — first sub, no dependencies)

Unblocks

  • Sub 2 (#TBD): Layer 1 wake-content enrichment
  • Sub 3 (#TBD): Layer 3 per-turn anti-pattern surface
  • Sub 4 (#TBD): Layer 5 sunset-handoff pickup queue
  • AC7 (Epic): cross-family symmetric validation — GPT's custom Codex nightshift-lifecycle-driver watchdog retirable once this lands

Related

  • Epic #11829 (parent)
  • Discussion #11823 (graduation source — Cycle-2.7 [GRADUATION_APPROVED])
  • ADR 0014 cloud-deployment-topology profile-default precedent
  • ai/daemons/services/SwarmHeartbeatService.mjs:102/:129-131/:199/:224 — single-identity binding (the substrate gap this sub fixes)

Authored by: Claude Opus 4.7 (Claude Code)

Contract Ledger (added per GPT PR #11913 cycle-1 RA2)

Surface Type Default Valid Values Failure Semantics
NEO_ORCHESTRATOR_SWARM_HEARTBEAT_TARGET_SOURCE env string unset → null self | active-local-team | active-subscribers | disabled invalid value → coerce to null + warn log (resolver then defaults to 'self')
NEO_ORCHESTRATOR_SWARM_HEARTBEAT_TARGETS env CSV string unset → null comma-separated @handle list (handles auto-normalized to canonical @<id> form) empty after normalize → null (falls through to targetSource semantics)
orchestrator.swarmHeartbeat.targetSource (AiConfig) string|null null same as env var above same as env var above
Resolver precedence function explicitTargets > targetSource > 'self' (precedence chain) unknown source → fallback to 'self' + warn log
'self' source enum branch returns [selfIdentity] selfIdentity: string|null null selfIdentity → returns [] + info log naming NEO_AGENT_IDENTITY + targetSource='disabled' knobs (AC3 disables-with-log)
'disabled' source enum branch returns [] + info log (none) always [] + info log
'active-local-team' source enum branch reads identityRoots.IDENTITIES filtered on type==='AgentIdentity' AND properties.participationStatus==='active' (none) empty filter → returns [] (no log; substrate is empty by design)
'active-subscribers' source enum branch unions selfIdentity + injected provider output provider: () => Promise<String[]> missing provider → fallback to 'self' + warn log + info log if selfIdentity is also null
beforeSetIdentity (SwarmHeartbeatService) config-set hook returns null on empty string|null empty/null value → null (Sub 1 #11905 AC3 pivot: removed DEFAULT_IDENTITY fallback to prevent fork-leak)
beforeSetTargetSource (SwarmHeartbeatService) config-set hook returns null on empty/invalid one of VALID_TARGET_SOURCES or null invalid value → coerce to null + warn log (resolver then defaults to 'self')
beforeSetExplicitTargets (SwarmHeartbeatService) config-set hook returns null on empty/non-array string[] non-array OR empty array → null; per-element normalized via normalizeAgentIdentityNodeId

Public-contract surface count: 3 env vars + 1 AiConfig key + 4 resolver enum branches + 3 config-set hooks = 11 named surface points. All 11 covered by tests in test/playwright/unit/ai/daemons/orchestrator/scheduling/swarmHeartbeat.spec.mjs (18 pure-function tests) + test/playwright/unit/ai/daemons/orchestrator/services/SwarmHeartbeatService.spec.mjs (5 integration tests + 1 AC3 fork-safety integration test).

tobiu closed this issue on May 24, 2026, 5:56 PM
tobiu referenced in commit 6bb49d5 - "feat(orchestrator): add resolveSwarmHeartbeatTargets with 5-step precedence chain (#11905) (#11913) on May 24, 2026, 5:56 PM