LearnNewsExamplesServices
Frontmatter
id11830
titleLayer 2: implement resolveSwarmHeartbeatTargets() deployment-portable resolver for SwarmHeartbeatService
stateClosed
labels
enhancementaiarchitecturemodel-experience
assigneesneo-opus-4-7
createdAtMay 23, 2026, 12:50 PM
updatedAtMay 24, 2026, 11:36 PM
githubUrlhttps://github.com/neomjs/neo/issues/11830
authorneo-opus-4-7
commentsCount1
parentIssue11829
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 24, 2026, 11:36 PM

Layer 2: implement resolveSwarmHeartbeatTargets() deployment-portable resolver for SwarmHeartbeatService

Closedenhancementaiarchitecturemodel-experience
neo-opus-4-7
neo-opus-4-7 commented on May 23, 2026, 12:50 PM

Parent Epic

#11829 — Sub 1 of 5 (BLOCKING; first in suggested implementation sequence).

Scope

Implement resolveSwarmHeartbeatTargets() contract — deployment-strategy resolver that replaces SwarmHeartbeatService's current single-identity binding pattern (currently bound at initAsync to process.env.NEO_AGENT_IDENTITY || DEFAULT_IDENTITY; only one identity checked per pulse). Single Orchestrator lane loops over RESOLVED targets per deployment profile.

Critical: NOT N service-instances; NOT direct iteration over identityRoots.mjs IDENTITIES (which would leak Neo-maintainer-team assumptions into framework-portable substrate).

Empirical anchor

Operator log evidence:

[idleOutNudge] Sent heartbeat nudge to @neo-opus-4-7

No equivalent log line for @neo-gpt. GPT's "lifecycle-driver watchdog" is a custom Codex-side workaround for a canonical substrate gap, not parallel implementation. The canonical orchestrator nudge path is currently family-asymmetric per SwarmHeartbeatService.mjs:102 / :129-131 / :199 / :224 single-identity binding.

Acceptance Criteria

  1. Resolver precedence chain (per Epic AC2):
    • (1) Explicit env/config target list — NEO_ORCHESTRATOR_SWARM_HEARTBEAT_TARGETS env var (comma-separated) OR orchestrator.swarmHeartbeat.targets config array. Values normalize via existing identity normalization.
    • (2) Config strategy — orchestrator.swarmHeartbeat.targetSource ∈ {self | active-local-team | active-subscribers | disabled}.
    • (3) Public default self — resolved from NEO_AGENT_IDENTITY / bound stdio identity / gh login fallback. If no identity resolves → disable lane with clear config log (NOT silent fallback to Neo maintainer identity).
    • (4) Neo team profile (active-local-team): OPT-IN, uses identityRoots.mjs IDENTITIES filtered on participationStatus === 'active'. Benched identities excluded.
    • (5) Cloud / fork safety per ADR 0014: cloud profile heartbeat-disabled by default; npx neo-app workspaces never receive Neo maintainer wake targets unless explicitly configured.
  2. Single Orchestrator lane loops over resolved targets — output of resolveSwarmHeartbeatTargets(). Per-pulse loops at SwarmHeartbeatService.mjs:199 and :224 consume per-target list, not just this.identity.
  3. Test coverage at test/playwright/unit/ai/daemons/services/SwarmHeartbeatService.spec.mjs:
    • No-config external workspace: heartbeat monitors only current resolved identity OR disables-with-log
    • Explicit target list: one pulse cycle calls checkSunsetted(identity) per configured target, dispatches per-identity recovery independently
    • Neo active-local-team profile: active maintainers included, benched excluded; assertion N active targets → N checkSunsetted calls
    • Unknown target: fail-closed with clear resolver error; no orphan/null mailbox edges
  4. Empirical validation: orchestrator log post-implementation shows N nudge lines per cycle where N = resolveSwarmHeartbeatTargets() output cardinality. Cross-family parity restored (Opus + GPT both receive nudges per cycle).
  5. Backward-compat seam: legacy single-identity initAsync({identity}) path remains for diagnostic mode.

Files (likely touched)

  • ai/daemons/services/SwarmHeartbeatService.mjs (primary surface — refactor single-identity binding to multi-target resolver)
  • ai/daemons/services/resolveSwarmHeartbeatTargets.mjs (NEW — pure resolver function; testable in isolation)
  • ai/config.template.mjs (NEW orchestrator.swarmHeartbeat.{targets, targetSource} config namespace)
  • test/playwright/unit/ai/daemons/services/SwarmHeartbeatService.spec.mjs (extend coverage)
  • test/playwright/unit/ai/daemons/services/resolveSwarmHeartbeatTargets.spec.mjs (NEW — resolver unit tests)

Avoided Traps

  • Iterating directly over identityRoots.mjs IDENTITIES (Cycle-1 source-trace draft I proposed; corrected Cycle-2 by GPT). External consumers (repo forks, npx neo-app, cloud) MUST NOT silently inherit Neo maintainer identities.
  • Spawning N service-instances (one per identity) — single service-lane + per-target loop is the substrate-correct shape.
  • Hardcoding NEO_AGENT_IDENTITY env-var as the only resolution path — the env var is the self strategy's source, not the universal target source.

Related substrate anchors

  • Parent Epic: #11829
  • Originating Discussion: #11823 (closed RESOLVED 2026-05-23T10:49:36Z)
  • ADR 0014 cloud-deployment-topology profile-default precedent: learn/agentos/decisions/0014-cloud-deployment-topology-and-scheduler-task-taxonomy.md
  • ai/scripts/idleOutNudge.mjs (per-identity sender — confirmed correct shape; not the gap)
  • ai/graph/identityRoots.mjs (Opus + GPT active, Gemini operator_benched per ground truth; active-local-team opt-in source)

Authored by: [Claude Opus 4.7] (Claude Code)