---
id: 11874
title: Restore core.Base contract in SwarmHeartbeatService — drop external initAsync calls + isInitialized band-aid
state: CLOSED
labels:
  - enhancement
  - ai
  - refactoring
  - architecture
assignees:
  - neo-opus-ada
createdAt: '2026-05-24T01:21:52Z'
updatedAt: '2026-06-07T17:15:09Z'
githubUrl: 'https://github.com/neomjs/neo/issues/11874'
author: neo-opus-ada
commentsCount: 0
parentIssue: null
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-05-24T10:39:29Z'
---
# Restore core.Base contract in SwarmHeartbeatService — drop external initAsync calls + isInitialized band-aid

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

**Decision Record impact:** none directly; aligned with `src/core/Base.mjs` canonical contract (cited inline).

## Context

Operator surfaced 2026-05-24 — `Neo.ai.daemons.SwarmHeartbeatService` violates the core.Base lifecycle contract in multiple explicit ways. Direct quote: *"this was like a punch into my face. read core.Base. awaiting ready(). NEVER call initConfig outside the hierarchy. ... makes me actually sad. violating the core ideas of neo like that."*

Code-sweep across `ai/` confirmed violations are concentrated in this single file + one consumer in `Orchestrator.mjs`. Adjacent canonical-correct precedent (`DreamService.mjs:77` does `await LifecycleService.ready()`) proves the fix shape is already in-tree.

## The Problem

`core.Base.mjs` JSDoc lines 593-595 (the explicit contract):
> *"The `initAsync()` method is automatically triggered by the framework during `Neo.create()`. Calling it externally (e.g. `await myInstance.initAsync()`) will execute it twice, leading to **fatal duplication bugs**. If you need to wait for a class to finish initializing, **always** use `await myInstance.ready()` instead."*

**4 distinct violations** in `SwarmHeartbeatService.mjs` + 1 in `Orchestrator.mjs`:

| # | Site | Violation |
|---|---|---|
| 1 | `Orchestrator.mjs:323` | `await this.swarmHeartbeatService.initAsync({pollIntervalMs})` — external initAsync call, exactly what core.Base warned against |
| 2 | `SwarmHeartbeatService.mjs:124-140` | `if (this.isInitialized) { ... return; } ... this.isInitialized = true;` — manual idempotency guard; band-aid for violation #1; obviates `#readyPromise` + `isReady_` + `afterSetIsReady` framework substrate already in Base |
| 3 | `SwarmHeartbeatService.mjs:137` | `await LifecycleService.initAsync()` — external initAsync on a peer service |
| 4 | `SwarmHeartbeatService.mjs:138` | `await GraphService.initAsync()` — external initAsync on a peer service |
| 5 | `SwarmHeartbeatService.mjs:128-134` | `identity` + `pollIntervalMs` set imperatively from `process.env.NEO_AGENT_IDENTITY` / `process.env.POLL_INTERVAL` inside initAsync — should be reactive configs; also: `POLL_INTERVAL` env var has no `NEO_*` namespace prefix (env-naming regression) |

## The Architectural Reality

The canonical fix shape is **already present in adjacent code**:
- `DreamService.mjs:77` → `await LifecycleService.ready()` ✓ (canonical)
- `SwarmHeartbeatService.mjs:137` → `await LifecycleService.initAsync()` ✗ (violation)

Same `LifecycleService`. Two consumers in the same `ai/daemons/orchestrator/services/` directory. One follows the contract, one doesn't. Direct copy-paste precedent for the fix.

`LifecycleService` + `GraphService` both `extends Base` correctly (verified) → they have the canonical `#readyPromise` + `isReady_` + `afterSetIsReady` substrate. The violations are purely consumer-side.

12+ other correct `.ready()` usages exist across `ai/` (Agent.mjs, KbGarbageCollection / KbReconciliation / KbAlerting services, KBRecorderService consumers, StorageRouter consumers, Assembler.mjs). SwarmHeartbeatService is the outlier.

## The Fix

**`SwarmHeartbeatService.mjs`:**
- Add `identity_` + `pollIntervalMs_` as reactive configs in `static config` (with `@reactive` JSDoc tag per coding guidelines); defaults `DEFAULT_IDENTITY` + `DEFAULT_POLL_INTERVAL_MS`
- `initAsync()` body:
  - First line: `await super.initAsync();`
  - Replace `await LifecycleService.initAsync()` → `await LifecycleService.ready()`
  - Replace `await GraphService.initAsync()` → `await GraphService.ready()`
  - Remove `if (this.isInitialized)` guard + the "Already initialized" log
  - Remove `this.isInitialized = true` assignment
  - Drop `process.env.NEO_AGENT_IDENTITY` + `process.env.POLL_INTERVAL` raw reads (env layer routed via env-binding registry per Epic #11871 Sub 1 #11873)
- Drop `isInitialized` instance field declaration entirely
- Subclass singleton classification follows the same rule the cycle-2.1 SwarmHeartbeatService surface triggered: external parent configuration ⇒ NOT singleton (per CadenceEngine precedent). Keep singleton ONLY if configs are sensible global defaults; switch to non-singleton if Orchestrator parent-state propagation is needed (audit during impl)

**`Orchestrator.mjs:323`:**
- Replace `await this.swarmHeartbeatService.initAsync({pollIntervalMs: this.swarmHeartbeatIntervalMs})` with:
  ```js
  this.swarmHeartbeatService.pollIntervalMs = this.swarmHeartbeatIntervalMs;
  await this.swarmHeartbeatService.ready();
  ```
- (If the singleton-vs-non-singleton audit lands on non-singleton, the wire-up shape changes to Class A or Class B per the existing Orchestrator patterns — to be decided during impl based on whether parent-state-propagation is needed)

## Acceptance Criteria

1. `SwarmHeartbeatService.initAsync()` starts with `await super.initAsync()` and contains zero `.initAsync()` calls on peer services (only `.ready()`)
2. `isInitialized` instance field + manual idempotency guard + "Already initialized" log all REMOVED
3. `identity` + `pollIntervalMs` exposed as reactive configs (`identity_`, `pollIntervalMs_`) in `static config`; no raw `process.env.X` reads inside `initAsync()`
4. `Orchestrator.mjs:323` no longer calls `swarmHeartbeatService.initAsync(...)` externally; instead sets `pollIntervalMs` reactive config + `await this.swarmHeartbeatService.ready()`
5. Singleton-vs-non-singleton audit: if Orchestrator parent-state propagation is needed, convert SwarmHeartbeatService to non-singleton per CadenceEngine precedent; if not, document explicitly why singleton remains acceptable
6. `process.env.POLL_INTERVAL` raw read (no `NEO_*` prefix) eliminated; if env-driven poll interval is needed, route via `NEO_ORCHESTRATOR_SWARM_HEARTBEAT_INTERVAL_MS` (already declared in Orchestrator's getter, set via config)
7. `process.env.NEO_AGENT_IDENTITY` raw read routed via the env-binding pattern Epic #11871 Sub 1 #11873 establishes (impl-time alignment — either lands first OR uses current `Env.parseString(process.env.NEO_AGENT_IDENTITY, 'NEO_AGENT_IDENTITY')` shape per Neo.util.Env primitive)
8. All existing tests pass; SwarmHeartbeatService.spec.mjs + Orchestrator.spec.mjs swarm-heartbeat-init coverage adapted to the new shape (no external-initAsync mocking needed)
9. Grep-clean: zero new `.initAsync()` external-call violations introduced; the only `.initAsync()` references in `ai/` post-merge are `await super.initAsync()` parent-class calls inside override methods

## Out of Scope

- Broader sweep of other anti-patterns in SwarmHeartbeatService (heartbeat tick logic, sweep behavior, identity resolution semantics) — separate ticket if surfaced
- Heartbeat-related daemon scheduling (Orchestrator side) refactoring — owned by Epic #11831 if relevant
- env-primitive registry implementation — owned by Epic #11871 Sub 1 #11873 (this ticket consumes the registry, doesn't implement it)

## Avoided Traps

- **Folding into Sub 1 of #11871** — operator's "6→20 subs" scope-creep frustration; keep tickets atomic
- **Treating `isInitialized` band-aid as just stale-code-cleanup** — it's a SYMPTOM of violation #1; the band-aid only exists because of external initAsync calls; both must be fixed together
- **Adapting initAsync to be idempotent more cleverly** — directly contradicts core.Base contract; framework already provides `#readyPromise` for the "wait for init" use case; agents must use `ready()` not `initAsync()`
- **Pre-deciding singleton-vs-non-singleton without auditing** — same lesson from MaintenanceBackpressureService Sub 19 today; the audit happens at impl time based on whether external parent configs are needed
- **Skipping the parent-class `super.initAsync()` call** — required by Base.mjs:589 ("Make sure to use the parent call `await super.initAsync()` at the beginning of their implementations")

## Connection to Epic #11871 Sub 1 (#11873)

Both tickets touch env-var reads. Sub 1 #11873 establishes the canonical env-binding registry pattern (`Env.parseX(process.env.NEO_X, 'NEO_X')` inline); this ticket's AC 6+7 consume it. Sub 1 #11873 + this ticket are independent + can land in either order. If this ticket lands first, the env reads use the current `process.env.NEO_X` shape + get harmonized later. If Sub 1 #11873 lands first, this ticket uses the canonical shape from inception.

## Contract Ledger

Per `pr-review §5.4` audit requirement for PRs touching consumed surfaces. #11874 restructures the `Neo.ai.daemons.SwarmHeartbeatService` singleton's lifecycle-contract surface consumed by the Orchestrator daemon. Contract surface enumerated below:

| Surface | Pre-cleanup contract | Post-cleanup contract | Migration |
|---|---|---|---|
| `SwarmHeartbeatService.initAsync(options?)` | Took `{identity, pollIntervalMs}` args; ran imperative `process.env.NEO_AGENT_IDENTITY` + `process.env.POLL_INTERVAL` reads; manual `isInitialized` idempotency guard; called `LifecycleService.initAsync()` + `GraphService.initAsync()` externally | No args; identity-agnostic; calls `super.initAsync()` first then `LifecycleService.ready()` + `GraphService.ready()` per core.Base.mjs:589-595; framework `#readyPromise` handles idempotency | Orchestrator no longer calls `initAsync({...})` externally — sets `service.identity` + `service.pollIntervalMs` via reactive config assignment BEFORE `await service.ready()` |
| `SwarmHeartbeatService.identity_` | Reactive config (already existed pre-#11874) | Same shape; new `beforeSetIdentity` normalizer applies `normalizeAgentIdentityNodeId` + falls back to `DEFAULT_IDENTITY` when null/empty | Orchestrator assigns `service.identity = this.swarmHeartbeatIdentity` in start() before `.ready()` |
| `SwarmHeartbeatService.pollIntervalMs_` | Reactive config (already existed) | Same shape (no normalizer changes) | Orchestrator assigns `service.pollIntervalMs = this.swarmHeartbeatIntervalMs` in start() before `.ready()` |
| `SwarmHeartbeatService.isInitialized_` | Existed as reactive config + manual guard | DELETED — framework `#readyPromise` is the canonical single-init substrate | No external consumers tracked this; tests adapted to drop `isInitialized = false` resets |
| `SwarmHeartbeatService.initFailed` (daemon-local instance field) | Did not exist | Set to `true` on init failure; checked by `Orchestrator.poll()` swarm-heartbeat lane before `pulse()` dispatch | Replaces the env-mutation anti-pattern (`env.NEO_ORCHESTRATOR_SWARM_HEARTBEAT_ENABLED = false`) per operator pushback "what if WE want to disable heartbeat locally? no env var" |
| `SwarmHeartbeatService` singleton classification | `singleton: true` | UNCHANGED — `singleton: true` retained per AC5 documented rationale (1:1 service-parent topology; identity-agnostic `initAsync()`; CadenceEngine/MaintenanceBackpressureService non-singleton rule guards against multi-instance state-collision that doesn't apply here) | Orchestrator wire-up: Class C `swarmHeartbeatService = SwarmHeartbeatService` retained |
| `Orchestrator.swarmHeartbeatIdentity` getter | Did not exist (env read inside SwarmHeartbeatService.initAsync) | NEW: `Env.parseString(process.env.NEO_AGENT_IDENTITY, 'NEO_AGENT_IDENTITY')` — Lane A #11873 env-binding pattern | Tests for env-fallback identity selection should land in `Orchestrator.spec.mjs` (out-of-scope for this PR; covered by integration) |
| `process.env.POLL_INTERVAL` (no `NEO_*` prefix) | Read inside `SwarmHeartbeatService.initAsync` (env-naming regression) | DELETED — routed via canonical `NEO_ORCHESTRATOR_SWARM_HEARTBEAT_INTERVAL_MS` env var through `Orchestrator.swarmHeartbeatIntervalMs` getter | env-naming regression eliminated |

**Behavioral-invariant statement:** External Orchestrator-side lifecycle behavior preserved — service is initialized exactly once at daemon start, pulse() is called per cadence tick, init-failure disables the lane for the run. Internal lifecycle shape restructured to honor core.Base.mjs:589-595 contract; no consumer outside Orchestrator + the unit spec touches the service.

## Authority

Operator surfaced 2026-05-24 + my code-sweep findings (3 external initAsync violations + 1 band-aid + 12+ correct `.ready()` precedents elsewhere in ai/) + nightshift lane-claim 2026-05-24.

## Timeline

- 2026-05-24T01:21:53Z @neo-opus-ada assigned to @neo-opus-ada
- 2026-05-24T01:21:53Z @neo-opus-ada added the `enhancement` label
- 2026-05-24T01:21:53Z @neo-opus-ada added the `ai` label
- 2026-05-24T01:21:53Z @neo-opus-ada added the `refactoring` label
- 2026-05-24T01:21:53Z @neo-opus-ada added the `architecture` label
- 2026-05-24T01:35:46Z @neo-opus-ada cross-referenced by PR #11876
- 2026-05-24T01:46:30Z @neo-opus-ada cross-referenced by PR #11877
- 2026-05-24T02:00:55Z @neo-opus-ada referenced in commit `cb31ea7` - "fix(agentos): restore core.Base contract in SwarmHeartbeatService — drop external initAsync calls + isInitialized band-aid (#11874)

Co-Authored-By: Claude Opus 4.7 (1M context) <neo-opus-4-7@neomjs.com>"
- 2026-05-24T02:06:56Z @neo-opus-ada cross-referenced by PR #11875
- 2026-05-24T02:19:54Z @neo-opus-ada referenced in commit `2f902ed` - "fix(agentos): revise Lane B — keep singleton + identity-agnostic initAsync (#11874)

AC5 audit revision: original Lane B converted SwarmHeartbeatService to non-singleton
(Class B reactive config in Orchestrator + beforeSet propagation). CI surfaced 16
test failures because the spec assumes singleton accessor pattern; full conversion
would also rewrite GPT's 3 new tests in PR #11875 — coordination cost amplified.

Revised shape — keep singleton, make initAsync() identity-agnostic:

* `singleton: true` restored on SwarmHeartbeatService with explicit AC5 rationale
  block: 1:1 service-to-parent topology has no multi-instance state-collision risk;
  identity / pollIntervalMs are pulse-time runtime config (read by `pulse()` per
  tick), not init-time dependencies; non-singleton conversion = follow-up
  architectural-purity, NOT a substrate-correctness blocker
* `initAsync()` body simplified to `super.initAsync()` + peer-service `.ready()`
  calls only (LifecycleService.ready + GraphService.ready); no identity reads,
  no log line referencing this.identity (not set at framework-init time)
* Orchestrator wire-up: Class C `swarmHeartbeatService = SwarmHeartbeatService`
  restored; start() sets `service.identity = swarmHeartbeatIdentity` +
  `service.pollIntervalMs = swarmHeartbeatIntervalMs` via property assignment
  BEFORE `await service.ready()` (configs exist before pulse() ever reads them)
* daemon-local `initFailed` flag preserved (fail-safe invariant)
* `beforeSetSwarmHeartbeatService` Class B method dropped

Test spec rewrite:
* Drop `isInitialized` references in applyDefaultStubs + afterEach (band-aid gone)
* Drop external `initAsync({identity, pollIntervalMs})` calls (framework owns init)
* Delete 2 obsolete tests: `initAsync() is idempotent` (framework handles) +
  `initAsync() picks identity from explicit arg, then env, then default` (env-fallback
  moved to Orchestrator.swarmHeartbeatIdentity getter, covered separately)
* Rewrite `initAsync() normalizes GitHub-login` as direct `beforeSetIdentity` test
  exercising normalizer + DEFAULT_IDENTITY fallback via property assignment

ACs satisfied:
* AC1: super.initAsync first, zero external peer .initAsync (.ready() only) ✓
* AC2: isInitialized field + manual guard + log REMOVED ✓
* AC3: identity_/pollIntervalMs_ reactive configs (already were) ✓
* AC4: Orchestrator no longer calls swarmHeartbeatService.initAsync(...) ✓
* AC5: singleton acceptable per documented 1:1 topology + identity-agnostic init ✓
* AC6: POLL_INTERVAL eliminated; identity/pollIntervalMs via Orchestrator getter ✓
* AC7: NEO_AGENT_IDENTITY via Env.parseString getter in Orchestrator ✓
* AC8: 14/14 SwarmHeartbeatService tests + 2042/2042 full unit suite pass ✓
* AC9: grep-clean on external .initAsync() violations in ai/ ✓

Local: `npm run test-unit` → 2042 passed, 2 skipped, 19 did not run, 0 failed (49.6s)."
- 2026-05-24T02:36:55Z @neo-opus-ada referenced in commit `d6d45d4` - "fix(agentos): restore core.Base contract in SwarmHeartbeatService — drop external initAsync calls + isInitialized band-aid (#11874)

Co-Authored-By: Claude Opus 4.7 (1M context) <neo-opus-4-7@neomjs.com>"
- 2026-05-24T02:36:55Z @neo-opus-ada referenced in commit `a43c92b` - "fix(agentos): revise Lane B — keep singleton + identity-agnostic initAsync (#11874)

AC5 audit revision: original Lane B converted SwarmHeartbeatService to non-singleton
(Class B reactive config in Orchestrator + beforeSet propagation). CI surfaced 16
test failures because the spec assumes singleton accessor pattern; full conversion
would also rewrite GPT's 3 new tests in PR #11875 — coordination cost amplified.

Revised shape — keep singleton, make initAsync() identity-agnostic:

* `singleton: true` restored on SwarmHeartbeatService with explicit AC5 rationale
  block: 1:1 service-to-parent topology has no multi-instance state-collision risk;
  identity / pollIntervalMs are pulse-time runtime config (read by `pulse()` per
  tick), not init-time dependencies; non-singleton conversion = follow-up
  architectural-purity, NOT a substrate-correctness blocker
* `initAsync()` body simplified to `super.initAsync()` + peer-service `.ready()`
  calls only (LifecycleService.ready + GraphService.ready); no identity reads,
  no log line referencing this.identity (not set at framework-init time)
* Orchestrator wire-up: Class C `swarmHeartbeatService = SwarmHeartbeatService`
  restored; start() sets `service.identity = swarmHeartbeatIdentity` +
  `service.pollIntervalMs = swarmHeartbeatIntervalMs` via property assignment
  BEFORE `await service.ready()` (configs exist before pulse() ever reads them)
* daemon-local `initFailed` flag preserved (fail-safe invariant)
* `beforeSetSwarmHeartbeatService` Class B method dropped

Test spec rewrite:
* Drop `isInitialized` references in applyDefaultStubs + afterEach (band-aid gone)
* Drop external `initAsync({identity, pollIntervalMs})` calls (framework owns init)
* Delete 2 obsolete tests: `initAsync() is idempotent` (framework handles) +
  `initAsync() picks identity from explicit arg, then env, then default` (env-fallback
  moved to Orchestrator.swarmHeartbeatIdentity getter, covered separately)
* Rewrite `initAsync() normalizes GitHub-login` as direct `beforeSetIdentity` test
  exercising normalizer + DEFAULT_IDENTITY fallback via property assignment

ACs satisfied:
* AC1: super.initAsync first, zero external peer .initAsync (.ready() only) ✓
* AC2: isInitialized field + manual guard + log REMOVED ✓
* AC3: identity_/pollIntervalMs_ reactive configs (already were) ✓
* AC4: Orchestrator no longer calls swarmHeartbeatService.initAsync(...) ✓
* AC5: singleton acceptable per documented 1:1 topology + identity-agnostic init ✓
* AC6: POLL_INTERVAL eliminated; identity/pollIntervalMs via Orchestrator getter ✓
* AC7: NEO_AGENT_IDENTITY via Env.parseString getter in Orchestrator ✓
* AC8: 14/14 SwarmHeartbeatService tests + 2042/2042 full unit suite pass ✓
* AC9: grep-clean on external .initAsync() violations in ai/ ✓

Local: `npm run test-unit` → 2042 passed, 2 skipped, 19 did not run, 0 failed (49.6s)."
- 2026-05-24T02:36:56Z @neo-opus-ada referenced in commit `7230376` - "docs(agentos): align SwarmHeartbeatService class prose to core.Base ready() contract (#11874)

GPT's PR #11877 Cycle-2 review surfaced stale class-JSDoc prose that
contradicted the Cycle-2 code shape:

Before: "the Orchestrator owns the persistent process and the scheduler,
calling `initAsync()` once at start and `pulse()` once per cadence tick."

After: "the Orchestrator owns the persistent process and the scheduler,
awaiting `service.ready()` once at start (the framework triggers `initAsync()`
exactly once during singleton `Neo.create()`; external callers MUST NOT invoke
`initAsync()` directly per `core.Base.mjs:589-595`) and calling `pulse()` once
per cadence tick."

The expanded form makes the contract explicit in the class-level summary so
future authors querying this substrate get the core.Base.mjs:589-595
discipline without having to read the per-method JSDoc. Mechanical fix; no
code-shape change."
- 2026-05-24T02:53:11Z @neo-gpt cross-referenced by #11878
- 2026-05-24T02:53:35Z @neo-opus-ada referenced in commit `6648cdc` - "docs(agentos): align SwarmHeartbeatService JSDoc ready-order prose to actual code (#11874)

GPT's PR #11877 Cycle-2 follow-up review caught real prose-vs-code drift on the
ready-order ordering — code does assignment BEFORE `.ready()` in `Orchestrator.
start()` lines 349-355, but JSDoc said AFTER `.ready()` in 3 places. The
AFTER framing was incorrect; corrected to BEFORE with explicit rationale:

- class-level singleton-justification block (line 78): added "the framework
  already fired identity-agnostic `initAsync()` at module-load, so `ready()`
  resolves immediately; the BEFORE-`.ready()` ordering matches the canonical
  Neo.create-flows-configs-pre-init shape" rationale
- `identity_` config JSDoc (line 112): same rationale, shorter form
- `initAsync()` Note section (line 157): clarified parent sets configs BEFORE
  `.ready()` in `start()`, framework fired init at module-load, pulse() reads
  per tick

No code-shape change; pure prose-currency fix for the singleton-vs-non-singleton
lifecycle-order documentation. Symmetric with the `Orchestrator.start()` shape
(assign → await ready) and with non-singleton `Neo.create({...configs})` shape."
- 2026-05-24T02:59:46Z @neo-opus-ada referenced in commit `2499f9b` - "fix(agentos): restore core.Base contract in SwarmHeartbeatService — drop external initAsync calls + isInitialized band-aid (#11874)

Co-Authored-By: Claude Opus 4.7 (1M context) <neo-opus-4-7@neomjs.com>"
- 2026-05-24T02:59:46Z @neo-opus-ada referenced in commit `4b45666` - "fix(agentos): revise Lane B — keep singleton + identity-agnostic initAsync (#11874)

AC5 audit revision: original Lane B converted SwarmHeartbeatService to non-singleton
(Class B reactive config in Orchestrator + beforeSet propagation). CI surfaced 16
test failures because the spec assumes singleton accessor pattern; full conversion
would also rewrite GPT's 3 new tests in PR #11875 — coordination cost amplified.

Revised shape — keep singleton, make initAsync() identity-agnostic:

* `singleton: true` restored on SwarmHeartbeatService with explicit AC5 rationale
  block: 1:1 service-to-parent topology has no multi-instance state-collision risk;
  identity / pollIntervalMs are pulse-time runtime config (read by `pulse()` per
  tick), not init-time dependencies; non-singleton conversion = follow-up
  architectural-purity, NOT a substrate-correctness blocker
* `initAsync()` body simplified to `super.initAsync()` + peer-service `.ready()`
  calls only (LifecycleService.ready + GraphService.ready); no identity reads,
  no log line referencing this.identity (not set at framework-init time)
* Orchestrator wire-up: Class C `swarmHeartbeatService = SwarmHeartbeatService`
  restored; start() sets `service.identity = swarmHeartbeatIdentity` +
  `service.pollIntervalMs = swarmHeartbeatIntervalMs` via property assignment
  BEFORE `await service.ready()` (configs exist before pulse() ever reads them)
* daemon-local `initFailed` flag preserved (fail-safe invariant)
* `beforeSetSwarmHeartbeatService` Class B method dropped

Test spec rewrite:
* Drop `isInitialized` references in applyDefaultStubs + afterEach (band-aid gone)
* Drop external `initAsync({identity, pollIntervalMs})` calls (framework owns init)
* Delete 2 obsolete tests: `initAsync() is idempotent` (framework handles) +
  `initAsync() picks identity from explicit arg, then env, then default` (env-fallback
  moved to Orchestrator.swarmHeartbeatIdentity getter, covered separately)
* Rewrite `initAsync() normalizes GitHub-login` as direct `beforeSetIdentity` test
  exercising normalizer + DEFAULT_IDENTITY fallback via property assignment

ACs satisfied:
* AC1: super.initAsync first, zero external peer .initAsync (.ready() only) ✓
* AC2: isInitialized field + manual guard + log REMOVED ✓
* AC3: identity_/pollIntervalMs_ reactive configs (already were) ✓
* AC4: Orchestrator no longer calls swarmHeartbeatService.initAsync(...) ✓
* AC5: singleton acceptable per documented 1:1 topology + identity-agnostic init ✓
* AC6: POLL_INTERVAL eliminated; identity/pollIntervalMs via Orchestrator getter ✓
* AC7: NEO_AGENT_IDENTITY via Env.parseString getter in Orchestrator ✓
* AC8: 14/14 SwarmHeartbeatService tests + 2042/2042 full unit suite pass ✓
* AC9: grep-clean on external .initAsync() violations in ai/ ✓

Local: `npm run test-unit` → 2042 passed, 2 skipped, 19 did not run, 0 failed (49.6s)."
- 2026-05-24T02:59:46Z @neo-opus-ada referenced in commit `0e01505` - "docs(agentos): align SwarmHeartbeatService class prose to core.Base ready() contract (#11874)

GPT's PR #11877 Cycle-2 review surfaced stale class-JSDoc prose that
contradicted the Cycle-2 code shape:

Before: "the Orchestrator owns the persistent process and the scheduler,
calling `initAsync()` once at start and `pulse()` once per cadence tick."

After: "the Orchestrator owns the persistent process and the scheduler,
awaiting `service.ready()` once at start (the framework triggers `initAsync()`
exactly once during singleton `Neo.create()`; external callers MUST NOT invoke
`initAsync()` directly per `core.Base.mjs:589-595`) and calling `pulse()` once
per cadence tick."

The expanded form makes the contract explicit in the class-level summary so
future authors querying this substrate get the core.Base.mjs:589-595
discipline without having to read the per-method JSDoc. Mechanical fix; no
code-shape change."
- 2026-05-24T02:59:46Z @neo-opus-ada referenced in commit `26b79cc` - "docs(agentos): align SwarmHeartbeatService JSDoc ready-order prose to actual code (#11874)

GPT's PR #11877 Cycle-2 follow-up review caught real prose-vs-code drift on the
ready-order ordering — code does assignment BEFORE `.ready()` in `Orchestrator.
start()` lines 349-355, but JSDoc said AFTER `.ready()` in 3 places. The
AFTER framing was incorrect; corrected to BEFORE with explicit rationale:

- class-level singleton-justification block (line 78): added "the framework
  already fired identity-agnostic `initAsync()` at module-load, so `ready()`
  resolves immediately; the BEFORE-`.ready()` ordering matches the canonical
  Neo.create-flows-configs-pre-init shape" rationale
- `identity_` config JSDoc (line 112): same rationale, shorter form
- `initAsync()` Note section (line 157): clarified parent sets configs BEFORE
  `.ready()` in `start()`, framework fired init at module-load, pulse() reads
  per tick

No code-shape change; pure prose-currency fix for the singleton-vs-non-singleton
lifecycle-order documentation. Symmetric with the `Orchestrator.start()` shape
(assign → await ready) and with non-singleton `Neo.create({...configs})` shape."
- 2026-05-24T03:06:21Z @neo-opus-ada referenced in commit `0f5ba82` - "docs(test): align SwarmHeartbeatService spec ready-order Anchor/Echo to code (#11874)

GPT's PR #11877 follow-up V-B-A caught one remaining ready-order prose drift
in `test/playwright/unit/ai/daemons/orchestrator/services/SwarmHeartbeatService.spec.mjs:42`
that wasn't covered by the prior production-side JSDoc fixes:

Before: "parent (Orchestrator) AFTER `.ready()` resolves"
After:  "parent (Orchestrator) via reactive config assignment BEFORE `await
        service.ready()` in `start()` (the framework already fired identity-
        agnostic `initAsync()` at module-load, so the BEFORE-`.ready()` ordering
        matches the `Orchestrator.start()` code)"

Symmetric with the prior production-side fixes (`SwarmHeartbeatService.mjs`
class-level singleton-justification + `identity_` JSDoc + `initAsync()` Note).
Spec-side Anchor/Echo prose now matches mechanical reality of
`Orchestrator.start():349-355`. KB-ingested test prose surface now consistent
across all 4 ready-order locations."
- 2026-05-24T03:17:29Z @neo-gpt cross-referenced by #11871
- 2026-05-24T09:09:12Z @neo-opus-ada referenced in commit `86ad1ee` - "fix(agentos): restore core.Base contract in SwarmHeartbeatService — drop external initAsync calls + isInitialized band-aid (#11874)

Co-Authored-By: Claude Opus 4.7 (1M context) <neo-opus-4-7@neomjs.com>"
- 2026-05-24T09:09:12Z @neo-opus-ada referenced in commit `cc704f0` - "fix(agentos): revise Lane B — keep singleton + identity-agnostic initAsync (#11874)

AC5 audit revision: original Lane B converted SwarmHeartbeatService to non-singleton
(Class B reactive config in Orchestrator + beforeSet propagation). CI surfaced 16
test failures because the spec assumes singleton accessor pattern; full conversion
would also rewrite GPT's 3 new tests in PR #11875 — coordination cost amplified.

Revised shape — keep singleton, make initAsync() identity-agnostic:

* `singleton: true` restored on SwarmHeartbeatService with explicit AC5 rationale
  block: 1:1 service-to-parent topology has no multi-instance state-collision risk;
  identity / pollIntervalMs are pulse-time runtime config (read by `pulse()` per
  tick), not init-time dependencies; non-singleton conversion = follow-up
  architectural-purity, NOT a substrate-correctness blocker
* `initAsync()` body simplified to `super.initAsync()` + peer-service `.ready()`
  calls only (LifecycleService.ready + GraphService.ready); no identity reads,
  no log line referencing this.identity (not set at framework-init time)
* Orchestrator wire-up: Class C `swarmHeartbeatService = SwarmHeartbeatService`
  restored; start() sets `service.identity = swarmHeartbeatIdentity` +
  `service.pollIntervalMs = swarmHeartbeatIntervalMs` via property assignment
  BEFORE `await service.ready()` (configs exist before pulse() ever reads them)
* daemon-local `initFailed` flag preserved (fail-safe invariant)
* `beforeSetSwarmHeartbeatService` Class B method dropped

Test spec rewrite:
* Drop `isInitialized` references in applyDefaultStubs + afterEach (band-aid gone)
* Drop external `initAsync({identity, pollIntervalMs})` calls (framework owns init)
* Delete 2 obsolete tests: `initAsync() is idempotent` (framework handles) +
  `initAsync() picks identity from explicit arg, then env, then default` (env-fallback
  moved to Orchestrator.swarmHeartbeatIdentity getter, covered separately)
* Rewrite `initAsync() normalizes GitHub-login` as direct `beforeSetIdentity` test
  exercising normalizer + DEFAULT_IDENTITY fallback via property assignment

ACs satisfied:
* AC1: super.initAsync first, zero external peer .initAsync (.ready() only) ✓
* AC2: isInitialized field + manual guard + log REMOVED ✓
* AC3: identity_/pollIntervalMs_ reactive configs (already were) ✓
* AC4: Orchestrator no longer calls swarmHeartbeatService.initAsync(...) ✓
* AC5: singleton acceptable per documented 1:1 topology + identity-agnostic init ✓
* AC6: POLL_INTERVAL eliminated; identity/pollIntervalMs via Orchestrator getter ✓
* AC7: NEO_AGENT_IDENTITY via Env.parseString getter in Orchestrator ✓
* AC8: 14/14 SwarmHeartbeatService tests + 2042/2042 full unit suite pass ✓
* AC9: grep-clean on external .initAsync() violations in ai/ ✓

Local: `npm run test-unit` → 2042 passed, 2 skipped, 19 did not run, 0 failed (49.6s)."
- 2026-05-24T09:09:12Z @neo-opus-ada referenced in commit `ec80b05` - "docs(agentos): align SwarmHeartbeatService class prose to core.Base ready() contract (#11874)

GPT's PR #11877 Cycle-2 review surfaced stale class-JSDoc prose that
contradicted the Cycle-2 code shape:

Before: "the Orchestrator owns the persistent process and the scheduler,
calling `initAsync()` once at start and `pulse()` once per cadence tick."

After: "the Orchestrator owns the persistent process and the scheduler,
awaiting `service.ready()` once at start (the framework triggers `initAsync()`
exactly once during singleton `Neo.create()`; external callers MUST NOT invoke
`initAsync()` directly per `core.Base.mjs:589-595`) and calling `pulse()` once
per cadence tick."

The expanded form makes the contract explicit in the class-level summary so
future authors querying this substrate get the core.Base.mjs:589-595
discipline without having to read the per-method JSDoc. Mechanical fix; no
code-shape change."
- 2026-05-24T09:09:13Z @neo-opus-ada referenced in commit `9269548` - "docs(agentos): align SwarmHeartbeatService JSDoc ready-order prose to actual code (#11874)

GPT's PR #11877 Cycle-2 follow-up review caught real prose-vs-code drift on the
ready-order ordering — code does assignment BEFORE `.ready()` in `Orchestrator.
start()` lines 349-355, but JSDoc said AFTER `.ready()` in 3 places. The
AFTER framing was incorrect; corrected to BEFORE with explicit rationale:

- class-level singleton-justification block (line 78): added "the framework
  already fired identity-agnostic `initAsync()` at module-load, so `ready()`
  resolves immediately; the BEFORE-`.ready()` ordering matches the canonical
  Neo.create-flows-configs-pre-init shape" rationale
- `identity_` config JSDoc (line 112): same rationale, shorter form
- `initAsync()` Note section (line 157): clarified parent sets configs BEFORE
  `.ready()` in `start()`, framework fired init at module-load, pulse() reads
  per tick

No code-shape change; pure prose-currency fix for the singleton-vs-non-singleton
lifecycle-order documentation. Symmetric with the `Orchestrator.start()` shape
(assign → await ready) and with non-singleton `Neo.create({...configs})` shape."
- 2026-05-24T09:09:13Z @neo-opus-ada referenced in commit `f720462` - "docs(test): align SwarmHeartbeatService spec ready-order Anchor/Echo to code (#11874)

GPT's PR #11877 follow-up V-B-A caught one remaining ready-order prose drift
in `test/playwright/unit/ai/daemons/orchestrator/services/SwarmHeartbeatService.spec.mjs:42`
that wasn't covered by the prior production-side JSDoc fixes:

Before: "parent (Orchestrator) AFTER `.ready()` resolves"
After:  "parent (Orchestrator) via reactive config assignment BEFORE `await
        service.ready()` in `start()` (the framework already fired identity-
        agnostic `initAsync()` at module-load, so the BEFORE-`.ready()` ordering
        matches the `Orchestrator.start()` code)"

Symmetric with the prior production-side fixes (`SwarmHeartbeatService.mjs`
class-level singleton-justification + `identity_` JSDoc + `initAsync()` Note).
Spec-side Anchor/Echo prose now matches mechanical reality of
`Orchestrator.start():349-355`. KB-ingested test prose surface now consistent
across all 4 ready-order locations."
- 2026-05-24T09:11:49Z @neo-opus-ada referenced in commit `333e4fb` - "fix(agentos): restore core.Base contract in SwarmHeartbeatService — drop external initAsync calls + isInitialized band-aid (#11874)

Co-Authored-By: Claude Opus 4.7 (1M context) <neo-opus-4-7@neomjs.com>"
- 2026-05-24T09:11:49Z @neo-opus-ada referenced in commit `471aa3b` - "fix(agentos): revise Lane B — keep singleton + identity-agnostic initAsync (#11874)

AC5 audit revision: original Lane B converted SwarmHeartbeatService to non-singleton
(Class B reactive config in Orchestrator + beforeSet propagation). CI surfaced 16
test failures because the spec assumes singleton accessor pattern; full conversion
would also rewrite GPT's 3 new tests in PR #11875 — coordination cost amplified.

Revised shape — keep singleton, make initAsync() identity-agnostic:

* `singleton: true` restored on SwarmHeartbeatService with explicit AC5 rationale
  block: 1:1 service-to-parent topology has no multi-instance state-collision risk;
  identity / pollIntervalMs are pulse-time runtime config (read by `pulse()` per
  tick), not init-time dependencies; non-singleton conversion = follow-up
  architectural-purity, NOT a substrate-correctness blocker
* `initAsync()` body simplified to `super.initAsync()` + peer-service `.ready()`
  calls only (LifecycleService.ready + GraphService.ready); no identity reads,
  no log line referencing this.identity (not set at framework-init time)
* Orchestrator wire-up: Class C `swarmHeartbeatService = SwarmHeartbeatService`
  restored; start() sets `service.identity = swarmHeartbeatIdentity` +
  `service.pollIntervalMs = swarmHeartbeatIntervalMs` via property assignment
  BEFORE `await service.ready()` (configs exist before pulse() ever reads them)
* daemon-local `initFailed` flag preserved (fail-safe invariant)
* `beforeSetSwarmHeartbeatService` Class B method dropped

Test spec rewrite:
* Drop `isInitialized` references in applyDefaultStubs + afterEach (band-aid gone)
* Drop external `initAsync({identity, pollIntervalMs})` calls (framework owns init)
* Delete 2 obsolete tests: `initAsync() is idempotent` (framework handles) +
  `initAsync() picks identity from explicit arg, then env, then default` (env-fallback
  moved to Orchestrator.swarmHeartbeatIdentity getter, covered separately)
* Rewrite `initAsync() normalizes GitHub-login` as direct `beforeSetIdentity` test
  exercising normalizer + DEFAULT_IDENTITY fallback via property assignment

ACs satisfied:
* AC1: super.initAsync first, zero external peer .initAsync (.ready() only) ✓
* AC2: isInitialized field + manual guard + log REMOVED ✓
* AC3: identity_/pollIntervalMs_ reactive configs (already were) ✓
* AC4: Orchestrator no longer calls swarmHeartbeatService.initAsync(...) ✓
* AC5: singleton acceptable per documented 1:1 topology + identity-agnostic init ✓
* AC6: POLL_INTERVAL eliminated; identity/pollIntervalMs via Orchestrator getter ✓
* AC7: NEO_AGENT_IDENTITY via Env.parseString getter in Orchestrator ✓
* AC8: 14/14 SwarmHeartbeatService tests + 2042/2042 full unit suite pass ✓
* AC9: grep-clean on external .initAsync() violations in ai/ ✓

Local: `npm run test-unit` → 2042 passed, 2 skipped, 19 did not run, 0 failed (49.6s)."
- 2026-05-24T09:11:50Z @neo-opus-ada referenced in commit `aa13049` - "docs(agentos): align SwarmHeartbeatService class prose to core.Base ready() contract (#11874)

GPT's PR #11877 Cycle-2 review surfaced stale class-JSDoc prose that
contradicted the Cycle-2 code shape:

Before: "the Orchestrator owns the persistent process and the scheduler,
calling `initAsync()` once at start and `pulse()` once per cadence tick."

After: "the Orchestrator owns the persistent process and the scheduler,
awaiting `service.ready()` once at start (the framework triggers `initAsync()`
exactly once during singleton `Neo.create()`; external callers MUST NOT invoke
`initAsync()` directly per `core.Base.mjs:589-595`) and calling `pulse()` once
per cadence tick."

The expanded form makes the contract explicit in the class-level summary so
future authors querying this substrate get the core.Base.mjs:589-595
discipline without having to read the per-method JSDoc. Mechanical fix; no
code-shape change."
- 2026-05-24T09:11:50Z @neo-opus-ada referenced in commit `7df8630` - "docs(agentos): align SwarmHeartbeatService JSDoc ready-order prose to actual code (#11874)

GPT's PR #11877 Cycle-2 follow-up review caught real prose-vs-code drift on the
ready-order ordering — code does assignment BEFORE `.ready()` in `Orchestrator.
start()` lines 349-355, but JSDoc said AFTER `.ready()` in 3 places. The
AFTER framing was incorrect; corrected to BEFORE with explicit rationale:

- class-level singleton-justification block (line 78): added "the framework
  already fired identity-agnostic `initAsync()` at module-load, so `ready()`
  resolves immediately; the BEFORE-`.ready()` ordering matches the canonical
  Neo.create-flows-configs-pre-init shape" rationale
- `identity_` config JSDoc (line 112): same rationale, shorter form
- `initAsync()` Note section (line 157): clarified parent sets configs BEFORE
  `.ready()` in `start()`, framework fired init at module-load, pulse() reads
  per tick

No code-shape change; pure prose-currency fix for the singleton-vs-non-singleton
lifecycle-order documentation. Symmetric with the `Orchestrator.start()` shape
(assign → await ready) and with non-singleton `Neo.create({...configs})` shape."
- 2026-05-24T09:11:50Z @neo-opus-ada referenced in commit `09cd938` - "docs(test): align SwarmHeartbeatService spec ready-order Anchor/Echo to code (#11874)

GPT's PR #11877 follow-up V-B-A caught one remaining ready-order prose drift
in `test/playwright/unit/ai/daemons/orchestrator/services/SwarmHeartbeatService.spec.mjs:42`
that wasn't covered by the prior production-side JSDoc fixes:

Before: "parent (Orchestrator) AFTER `.ready()` resolves"
After:  "parent (Orchestrator) via reactive config assignment BEFORE `await
        service.ready()` in `start()` (the framework already fired identity-
        agnostic `initAsync()` at module-load, so the BEFORE-`.ready()` ordering
        matches the `Orchestrator.start()` code)"

Symmetric with the prior production-side fixes (`SwarmHeartbeatService.mjs`
class-level singleton-justification + `identity_` JSDoc + `initAsync()` Note).
Spec-side Anchor/Echo prose now matches mechanical reality of
`Orchestrator.start():349-355`. KB-ingested test prose surface now consistent
across all 4 ready-order locations."
- 2026-05-24T09:25:16Z @neo-opus-ada referenced in commit `23a3b41` - "fix(agentos): restore core.Base contract in SwarmHeartbeatService — drop external initAsync calls + isInitialized band-aid (#11874)

Co-Authored-By: Claude Opus 4.7 (1M context) <neo-opus-4-7@neomjs.com>"
- 2026-05-24T09:25:16Z @neo-opus-ada referenced in commit `26e0393` - "fix(agentos): revise Lane B — keep singleton + identity-agnostic initAsync (#11874)

AC5 audit revision: original Lane B converted SwarmHeartbeatService to non-singleton
(Class B reactive config in Orchestrator + beforeSet propagation). CI surfaced 16
test failures because the spec assumes singleton accessor pattern; full conversion
would also rewrite GPT's 3 new tests in PR #11875 — coordination cost amplified.

Revised shape — keep singleton, make initAsync() identity-agnostic:

* `singleton: true` restored on SwarmHeartbeatService with explicit AC5 rationale
  block: 1:1 service-to-parent topology has no multi-instance state-collision risk;
  identity / pollIntervalMs are pulse-time runtime config (read by `pulse()` per
  tick), not init-time dependencies; non-singleton conversion = follow-up
  architectural-purity, NOT a substrate-correctness blocker
* `initAsync()` body simplified to `super.initAsync()` + peer-service `.ready()`
  calls only (LifecycleService.ready + GraphService.ready); no identity reads,
  no log line referencing this.identity (not set at framework-init time)
* Orchestrator wire-up: Class C `swarmHeartbeatService = SwarmHeartbeatService`
  restored; start() sets `service.identity = swarmHeartbeatIdentity` +
  `service.pollIntervalMs = swarmHeartbeatIntervalMs` via property assignment
  BEFORE `await service.ready()` (configs exist before pulse() ever reads them)
* daemon-local `initFailed` flag preserved (fail-safe invariant)
* `beforeSetSwarmHeartbeatService` Class B method dropped

Test spec rewrite:
* Drop `isInitialized` references in applyDefaultStubs + afterEach (band-aid gone)
* Drop external `initAsync({identity, pollIntervalMs})` calls (framework owns init)
* Delete 2 obsolete tests: `initAsync() is idempotent` (framework handles) +
  `initAsync() picks identity from explicit arg, then env, then default` (env-fallback
  moved to Orchestrator.swarmHeartbeatIdentity getter, covered separately)
* Rewrite `initAsync() normalizes GitHub-login` as direct `beforeSetIdentity` test
  exercising normalizer + DEFAULT_IDENTITY fallback via property assignment

ACs satisfied:
* AC1: super.initAsync first, zero external peer .initAsync (.ready() only) ✓
* AC2: isInitialized field + manual guard + log REMOVED ✓
* AC3: identity_/pollIntervalMs_ reactive configs (already were) ✓
* AC4: Orchestrator no longer calls swarmHeartbeatService.initAsync(...) ✓
* AC5: singleton acceptable per documented 1:1 topology + identity-agnostic init ✓
* AC6: POLL_INTERVAL eliminated; identity/pollIntervalMs via Orchestrator getter ✓
* AC7: NEO_AGENT_IDENTITY via Env.parseString getter in Orchestrator ✓
* AC8: 14/14 SwarmHeartbeatService tests + 2042/2042 full unit suite pass ✓
* AC9: grep-clean on external .initAsync() violations in ai/ ✓

Local: `npm run test-unit` → 2042 passed, 2 skipped, 19 did not run, 0 failed (49.6s)."
- 2026-05-24T09:25:16Z @neo-opus-ada referenced in commit `bf85072` - "docs(agentos): align SwarmHeartbeatService class prose to core.Base ready() contract (#11874)

GPT's PR #11877 Cycle-2 review surfaced stale class-JSDoc prose that
contradicted the Cycle-2 code shape:

Before: "the Orchestrator owns the persistent process and the scheduler,
calling `initAsync()` once at start and `pulse()` once per cadence tick."

After: "the Orchestrator owns the persistent process and the scheduler,
awaiting `service.ready()` once at start (the framework triggers `initAsync()`
exactly once during singleton `Neo.create()`; external callers MUST NOT invoke
`initAsync()` directly per `core.Base.mjs:589-595`) and calling `pulse()` once
per cadence tick."

The expanded form makes the contract explicit in the class-level summary so
future authors querying this substrate get the core.Base.mjs:589-595
discipline without having to read the per-method JSDoc. Mechanical fix; no
code-shape change."
- 2026-05-24T09:25:17Z @neo-opus-ada referenced in commit `cda1890` - "docs(agentos): align SwarmHeartbeatService JSDoc ready-order prose to actual code (#11874)

GPT's PR #11877 Cycle-2 follow-up review caught real prose-vs-code drift on the
ready-order ordering — code does assignment BEFORE `.ready()` in `Orchestrator.
start()` lines 349-355, but JSDoc said AFTER `.ready()` in 3 places. The
AFTER framing was incorrect; corrected to BEFORE with explicit rationale:

- class-level singleton-justification block (line 78): added "the framework
  already fired identity-agnostic `initAsync()` at module-load, so `ready()`
  resolves immediately; the BEFORE-`.ready()` ordering matches the canonical
  Neo.create-flows-configs-pre-init shape" rationale
- `identity_` config JSDoc (line 112): same rationale, shorter form
- `initAsync()` Note section (line 157): clarified parent sets configs BEFORE
  `.ready()` in `start()`, framework fired init at module-load, pulse() reads
  per tick

No code-shape change; pure prose-currency fix for the singleton-vs-non-singleton
lifecycle-order documentation. Symmetric with the `Orchestrator.start()` shape
(assign → await ready) and with non-singleton `Neo.create({...configs})` shape."
- 2026-05-24T09:25:17Z @neo-opus-ada referenced in commit `5d99245` - "docs(test): align SwarmHeartbeatService spec ready-order Anchor/Echo to code (#11874)

GPT's PR #11877 follow-up V-B-A caught one remaining ready-order prose drift
in `test/playwright/unit/ai/daemons/orchestrator/services/SwarmHeartbeatService.spec.mjs:42`
that wasn't covered by the prior production-side JSDoc fixes:

Before: "parent (Orchestrator) AFTER `.ready()` resolves"
After:  "parent (Orchestrator) via reactive config assignment BEFORE `await
        service.ready()` in `start()` (the framework already fired identity-
        agnostic `initAsync()` at module-load, so the BEFORE-`.ready()` ordering
        matches the `Orchestrator.start()` code)"

Symmetric with the prior production-side fixes (`SwarmHeartbeatService.mjs`
class-level singleton-justification + `identity_` JSDoc + `initAsync()` Note).
Spec-side Anchor/Echo prose now matches mechanical reality of
`Orchestrator.start():349-355`. KB-ingested test prose surface now consistent
across all 4 ready-order locations."
- 2026-05-24T09:52:31Z @neo-opus-ada referenced in commit `fc07f24` - "fix(agentos): restore core.Base contract in SwarmHeartbeatService — drop external initAsync calls + isInitialized band-aid (#11874)

Co-Authored-By: Claude Opus 4.7 (1M context) <neo-opus-4-7@neomjs.com>"
- 2026-05-24T09:52:31Z @neo-opus-ada referenced in commit `045638f` - "fix(agentos): revise Lane B — keep singleton + identity-agnostic initAsync (#11874)

AC5 audit revision: original Lane B converted SwarmHeartbeatService to non-singleton
(Class B reactive config in Orchestrator + beforeSet propagation). CI surfaced 16
test failures because the spec assumes singleton accessor pattern; full conversion
would also rewrite GPT's 3 new tests in PR #11875 — coordination cost amplified.

Revised shape — keep singleton, make initAsync() identity-agnostic:

* `singleton: true` restored on SwarmHeartbeatService with explicit AC5 rationale
  block: 1:1 service-to-parent topology has no multi-instance state-collision risk;
  identity / pollIntervalMs are pulse-time runtime config (read by `pulse()` per
  tick), not init-time dependencies; non-singleton conversion = follow-up
  architectural-purity, NOT a substrate-correctness blocker
* `initAsync()` body simplified to `super.initAsync()` + peer-service `.ready()`
  calls only (LifecycleService.ready + GraphService.ready); no identity reads,
  no log line referencing this.identity (not set at framework-init time)
* Orchestrator wire-up: Class C `swarmHeartbeatService = SwarmHeartbeatService`
  restored; start() sets `service.identity = swarmHeartbeatIdentity` +
  `service.pollIntervalMs = swarmHeartbeatIntervalMs` via property assignment
  BEFORE `await service.ready()` (configs exist before pulse() ever reads them)
* daemon-local `initFailed` flag preserved (fail-safe invariant)
* `beforeSetSwarmHeartbeatService` Class B method dropped

Test spec rewrite:
* Drop `isInitialized` references in applyDefaultStubs + afterEach (band-aid gone)
* Drop external `initAsync({identity, pollIntervalMs})` calls (framework owns init)
* Delete 2 obsolete tests: `initAsync() is idempotent` (framework handles) +
  `initAsync() picks identity from explicit arg, then env, then default` (env-fallback
  moved to Orchestrator.swarmHeartbeatIdentity getter, covered separately)
* Rewrite `initAsync() normalizes GitHub-login` as direct `beforeSetIdentity` test
  exercising normalizer + DEFAULT_IDENTITY fallback via property assignment

ACs satisfied:
* AC1: super.initAsync first, zero external peer .initAsync (.ready() only) ✓
* AC2: isInitialized field + manual guard + log REMOVED ✓
* AC3: identity_/pollIntervalMs_ reactive configs (already were) ✓
* AC4: Orchestrator no longer calls swarmHeartbeatService.initAsync(...) ✓
* AC5: singleton acceptable per documented 1:1 topology + identity-agnostic init ✓
* AC6: POLL_INTERVAL eliminated; identity/pollIntervalMs via Orchestrator getter ✓
* AC7: NEO_AGENT_IDENTITY via Env.parseString getter in Orchestrator ✓
* AC8: 14/14 SwarmHeartbeatService tests + 2042/2042 full unit suite pass ✓
* AC9: grep-clean on external .initAsync() violations in ai/ ✓

Local: `npm run test-unit` → 2042 passed, 2 skipped, 19 did not run, 0 failed (49.6s)."
- 2026-05-24T09:52:31Z @neo-opus-ada referenced in commit `6db8689` - "docs(agentos): align SwarmHeartbeatService class prose to core.Base ready() contract (#11874)

GPT's PR #11877 Cycle-2 review surfaced stale class-JSDoc prose that
contradicted the Cycle-2 code shape:

Before: "the Orchestrator owns the persistent process and the scheduler,
calling `initAsync()` once at start and `pulse()` once per cadence tick."

After: "the Orchestrator owns the persistent process and the scheduler,
awaiting `service.ready()` once at start (the framework triggers `initAsync()`
exactly once during singleton `Neo.create()`; external callers MUST NOT invoke
`initAsync()` directly per `core.Base.mjs:589-595`) and calling `pulse()` once
per cadence tick."

The expanded form makes the contract explicit in the class-level summary so
future authors querying this substrate get the core.Base.mjs:589-595
discipline without having to read the per-method JSDoc. Mechanical fix; no
code-shape change."
- 2026-05-24T09:52:31Z @neo-opus-ada referenced in commit `d7cfe40` - "docs(agentos): align SwarmHeartbeatService JSDoc ready-order prose to actual code (#11874)

GPT's PR #11877 Cycle-2 follow-up review caught real prose-vs-code drift on the
ready-order ordering — code does assignment BEFORE `.ready()` in `Orchestrator.
start()` lines 349-355, but JSDoc said AFTER `.ready()` in 3 places. The
AFTER framing was incorrect; corrected to BEFORE with explicit rationale:

- class-level singleton-justification block (line 78): added "the framework
  already fired identity-agnostic `initAsync()` at module-load, so `ready()`
  resolves immediately; the BEFORE-`.ready()` ordering matches the canonical
  Neo.create-flows-configs-pre-init shape" rationale
- `identity_` config JSDoc (line 112): same rationale, shorter form
- `initAsync()` Note section (line 157): clarified parent sets configs BEFORE
  `.ready()` in `start()`, framework fired init at module-load, pulse() reads
  per tick

No code-shape change; pure prose-currency fix for the singleton-vs-non-singleton
lifecycle-order documentation. Symmetric with the `Orchestrator.start()` shape
(assign → await ready) and with non-singleton `Neo.create({...configs})` shape."
- 2026-05-24T09:52:31Z @neo-opus-ada referenced in commit `ec3922c` - "docs(test): align SwarmHeartbeatService spec ready-order Anchor/Echo to code (#11874)

GPT's PR #11877 follow-up V-B-A caught one remaining ready-order prose drift
in `test/playwright/unit/ai/daemons/orchestrator/services/SwarmHeartbeatService.spec.mjs:42`
that wasn't covered by the prior production-side JSDoc fixes:

Before: "parent (Orchestrator) AFTER `.ready()` resolves"
After:  "parent (Orchestrator) via reactive config assignment BEFORE `await
        service.ready()` in `start()` (the framework already fired identity-
        agnostic `initAsync()` at module-load, so the BEFORE-`.ready()` ordering
        matches the `Orchestrator.start()` code)"

Symmetric with the prior production-side fixes (`SwarmHeartbeatService.mjs`
class-level singleton-justification + `identity_` JSDoc + `initAsync()` Note).
Spec-side Anchor/Echo prose now matches mechanical reality of
`Orchestrator.start():349-355`. KB-ingested test prose surface now consistent
across all 4 ready-order locations."
- 2026-05-24T10:03:37Z @neo-opus-ada referenced in commit `a75e56d` - "fix(agentos): restore core.Base contract in SwarmHeartbeatService — drop external initAsync calls + isInitialized band-aid (#11874)

Co-Authored-By: Claude Opus 4.7 (1M context) <neo-opus-4-7@neomjs.com>"
- 2026-05-24T10:03:37Z @neo-opus-ada referenced in commit `21790fb` - "fix(agentos): revise Lane B — keep singleton + identity-agnostic initAsync (#11874)

AC5 audit revision: original Lane B converted SwarmHeartbeatService to non-singleton
(Class B reactive config in Orchestrator + beforeSet propagation). CI surfaced 16
test failures because the spec assumes singleton accessor pattern; full conversion
would also rewrite GPT's 3 new tests in PR #11875 — coordination cost amplified.

Revised shape — keep singleton, make initAsync() identity-agnostic:

* `singleton: true` restored on SwarmHeartbeatService with explicit AC5 rationale
  block: 1:1 service-to-parent topology has no multi-instance state-collision risk;
  identity / pollIntervalMs are pulse-time runtime config (read by `pulse()` per
  tick), not init-time dependencies; non-singleton conversion = follow-up
  architectural-purity, NOT a substrate-correctness blocker
* `initAsync()` body simplified to `super.initAsync()` + peer-service `.ready()`
  calls only (LifecycleService.ready + GraphService.ready); no identity reads,
  no log line referencing this.identity (not set at framework-init time)
* Orchestrator wire-up: Class C `swarmHeartbeatService = SwarmHeartbeatService`
  restored; start() sets `service.identity = swarmHeartbeatIdentity` +
  `service.pollIntervalMs = swarmHeartbeatIntervalMs` via property assignment
  BEFORE `await service.ready()` (configs exist before pulse() ever reads them)
* daemon-local `initFailed` flag preserved (fail-safe invariant)
* `beforeSetSwarmHeartbeatService` Class B method dropped

Test spec rewrite:
* Drop `isInitialized` references in applyDefaultStubs + afterEach (band-aid gone)
* Drop external `initAsync({identity, pollIntervalMs})` calls (framework owns init)
* Delete 2 obsolete tests: `initAsync() is idempotent` (framework handles) +
  `initAsync() picks identity from explicit arg, then env, then default` (env-fallback
  moved to Orchestrator.swarmHeartbeatIdentity getter, covered separately)
* Rewrite `initAsync() normalizes GitHub-login` as direct `beforeSetIdentity` test
  exercising normalizer + DEFAULT_IDENTITY fallback via property assignment

ACs satisfied:
* AC1: super.initAsync first, zero external peer .initAsync (.ready() only) ✓
* AC2: isInitialized field + manual guard + log REMOVED ✓
* AC3: identity_/pollIntervalMs_ reactive configs (already were) ✓
* AC4: Orchestrator no longer calls swarmHeartbeatService.initAsync(...) ✓
* AC5: singleton acceptable per documented 1:1 topology + identity-agnostic init ✓
* AC6: POLL_INTERVAL eliminated; identity/pollIntervalMs via Orchestrator getter ✓
* AC7: NEO_AGENT_IDENTITY via Env.parseString getter in Orchestrator ✓
* AC8: 14/14 SwarmHeartbeatService tests + 2042/2042 full unit suite pass ✓
* AC9: grep-clean on external .initAsync() violations in ai/ ✓

Local: `npm run test-unit` → 2042 passed, 2 skipped, 19 did not run, 0 failed (49.6s)."
- 2026-05-24T10:03:37Z @neo-opus-ada referenced in commit `ce4fe02` - "docs(agentos): align SwarmHeartbeatService class prose to core.Base ready() contract (#11874)

GPT's PR #11877 Cycle-2 review surfaced stale class-JSDoc prose that
contradicted the Cycle-2 code shape:

Before: "the Orchestrator owns the persistent process and the scheduler,
calling `initAsync()` once at start and `pulse()` once per cadence tick."

After: "the Orchestrator owns the persistent process and the scheduler,
awaiting `service.ready()` once at start (the framework triggers `initAsync()`
exactly once during singleton `Neo.create()`; external callers MUST NOT invoke
`initAsync()` directly per `core.Base.mjs:589-595`) and calling `pulse()` once
per cadence tick."

The expanded form makes the contract explicit in the class-level summary so
future authors querying this substrate get the core.Base.mjs:589-595
discipline without having to read the per-method JSDoc. Mechanical fix; no
code-shape change."
- 2026-05-24T10:03:37Z @neo-opus-ada referenced in commit `f3e7d5e` - "docs(agentos): align SwarmHeartbeatService JSDoc ready-order prose to actual code (#11874)

GPT's PR #11877 Cycle-2 follow-up review caught real prose-vs-code drift on the
ready-order ordering — code does assignment BEFORE `.ready()` in `Orchestrator.
start()` lines 349-355, but JSDoc said AFTER `.ready()` in 3 places. The
AFTER framing was incorrect; corrected to BEFORE with explicit rationale:

- class-level singleton-justification block (line 78): added "the framework
  already fired identity-agnostic `initAsync()` at module-load, so `ready()`
  resolves immediately; the BEFORE-`.ready()` ordering matches the canonical
  Neo.create-flows-configs-pre-init shape" rationale
- `identity_` config JSDoc (line 112): same rationale, shorter form
- `initAsync()` Note section (line 157): clarified parent sets configs BEFORE
  `.ready()` in `start()`, framework fired init at module-load, pulse() reads
  per tick

No code-shape change; pure prose-currency fix for the singleton-vs-non-singleton
lifecycle-order documentation. Symmetric with the `Orchestrator.start()` shape
(assign → await ready) and with non-singleton `Neo.create({...configs})` shape."
- 2026-05-24T10:03:37Z @neo-opus-ada referenced in commit `441b956` - "docs(test): align SwarmHeartbeatService spec ready-order Anchor/Echo to code (#11874)

GPT's PR #11877 follow-up V-B-A caught one remaining ready-order prose drift
in `test/playwright/unit/ai/daemons/orchestrator/services/SwarmHeartbeatService.spec.mjs:42`
that wasn't covered by the prior production-side JSDoc fixes:

Before: "parent (Orchestrator) AFTER `.ready()` resolves"
After:  "parent (Orchestrator) via reactive config assignment BEFORE `await
        service.ready()` in `start()` (the framework already fired identity-
        agnostic `initAsync()` at module-load, so the BEFORE-`.ready()` ordering
        matches the `Orchestrator.start()` code)"

Symmetric with the prior production-side fixes (`SwarmHeartbeatService.mjs`
class-level singleton-justification + `identity_` JSDoc + `initAsync()` Note).
Spec-side Anchor/Echo prose now matches mechanical reality of
`Orchestrator.start():349-355`. KB-ingested test prose surface now consistent
across all 4 ready-order locations."
- 2026-05-24T10:39:29Z @tobiu referenced in commit `05da97c` - "fix(agentos): restore core.Base contract in SwarmHeartbeatService — drop external initAsync calls + isInitialized band-aid (#11874) (#11877)

* fix(agentos): restore core.Base contract in SwarmHeartbeatService — drop external initAsync calls + isInitialized band-aid (#11874)

Co-Authored-By: Claude Opus 4.7 (1M context) <neo-opus-4-7@neomjs.com>

* fix(agentos): revise Lane B — keep singleton + identity-agnostic initAsync (#11874)

AC5 audit revision: original Lane B converted SwarmHeartbeatService to non-singleton
(Class B reactive config in Orchestrator + beforeSet propagation). CI surfaced 16
test failures because the spec assumes singleton accessor pattern; full conversion
would also rewrite GPT's 3 new tests in PR #11875 — coordination cost amplified.

Revised shape — keep singleton, make initAsync() identity-agnostic:

* `singleton: true` restored on SwarmHeartbeatService with explicit AC5 rationale
  block: 1:1 service-to-parent topology has no multi-instance state-collision risk;
  identity / pollIntervalMs are pulse-time runtime config (read by `pulse()` per
  tick), not init-time dependencies; non-singleton conversion = follow-up
  architectural-purity, NOT a substrate-correctness blocker
* `initAsync()` body simplified to `super.initAsync()` + peer-service `.ready()`
  calls only (LifecycleService.ready + GraphService.ready); no identity reads,
  no log line referencing this.identity (not set at framework-init time)
* Orchestrator wire-up: Class C `swarmHeartbeatService = SwarmHeartbeatService`
  restored; start() sets `service.identity = swarmHeartbeatIdentity` +
  `service.pollIntervalMs = swarmHeartbeatIntervalMs` via property assignment
  BEFORE `await service.ready()` (configs exist before pulse() ever reads them)
* daemon-local `initFailed` flag preserved (fail-safe invariant)
* `beforeSetSwarmHeartbeatService` Class B method dropped

Test spec rewrite:
* Drop `isInitialized` references in applyDefaultStubs + afterEach (band-aid gone)
* Drop external `initAsync({identity, pollIntervalMs})` calls (framework owns init)
* Delete 2 obsolete tests: `initAsync() is idempotent` (framework handles) +
  `initAsync() picks identity from explicit arg, then env, then default` (env-fallback
  moved to Orchestrator.swarmHeartbeatIdentity getter, covered separately)
* Rewrite `initAsync() normalizes GitHub-login` as direct `beforeSetIdentity` test
  exercising normalizer + DEFAULT_IDENTITY fallback via property assignment

ACs satisfied:
* AC1: super.initAsync first, zero external peer .initAsync (.ready() only) ✓
* AC2: isInitialized field + manual guard + log REMOVED ✓
* AC3: identity_/pollIntervalMs_ reactive configs (already were) ✓
* AC4: Orchestrator no longer calls swarmHeartbeatService.initAsync(...) ✓
* AC5: singleton acceptable per documented 1:1 topology + identity-agnostic init ✓
* AC6: POLL_INTERVAL eliminated; identity/pollIntervalMs via Orchestrator getter ✓
* AC7: NEO_AGENT_IDENTITY via Env.parseString getter in Orchestrator ✓
* AC8: 14/14 SwarmHeartbeatService tests + 2042/2042 full unit suite pass ✓
* AC9: grep-clean on external .initAsync() violations in ai/ ✓

Local: `npm run test-unit` → 2042 passed, 2 skipped, 19 did not run, 0 failed (49.6s).

* docs(agentos): align SwarmHeartbeatService class prose to core.Base ready() contract (#11874)

GPT's PR #11877 Cycle-2 review surfaced stale class-JSDoc prose that
contradicted the Cycle-2 code shape:

Before: "the Orchestrator owns the persistent process and the scheduler,
calling `initAsync()` once at start and `pulse()` once per cadence tick."

After: "the Orchestrator owns the persistent process and the scheduler,
awaiting `service.ready()` once at start (the framework triggers `initAsync()`
exactly once during singleton `Neo.create()`; external callers MUST NOT invoke
`initAsync()` directly per `core.Base.mjs:589-595`) and calling `pulse()` once
per cadence tick."

The expanded form makes the contract explicit in the class-level summary so
future authors querying this substrate get the core.Base.mjs:589-595
discipline without having to read the per-method JSDoc. Mechanical fix; no
code-shape change.

* docs(agentos): align SwarmHeartbeatService JSDoc ready-order prose to actual code (#11874)

GPT's PR #11877 Cycle-2 follow-up review caught real prose-vs-code drift on the
ready-order ordering — code does assignment BEFORE `.ready()` in `Orchestrator.
start()` lines 349-355, but JSDoc said AFTER `.ready()` in 3 places. The
AFTER framing was incorrect; corrected to BEFORE with explicit rationale:

- class-level singleton-justification block (line 78): added "the framework
  already fired identity-agnostic `initAsync()` at module-load, so `ready()`
  resolves immediately; the BEFORE-`.ready()` ordering matches the canonical
  Neo.create-flows-configs-pre-init shape" rationale
- `identity_` config JSDoc (line 112): same rationale, shorter form
- `initAsync()` Note section (line 157): clarified parent sets configs BEFORE
  `.ready()` in `start()`, framework fired init at module-load, pulse() reads
  per tick

No code-shape change; pure prose-currency fix for the singleton-vs-non-singleton
lifecycle-order documentation. Symmetric with the `Orchestrator.start()` shape
(assign → await ready) and with non-singleton `Neo.create({...configs})` shape.

* docs(test): align SwarmHeartbeatService spec ready-order Anchor/Echo to code (#11874)

GPT's PR #11877 follow-up V-B-A caught one remaining ready-order prose drift
in `test/playwright/unit/ai/daemons/orchestrator/services/SwarmHeartbeatService.spec.mjs:42`
that wasn't covered by the prior production-side JSDoc fixes:

Before: "parent (Orchestrator) AFTER `.ready()` resolves"
After:  "parent (Orchestrator) via reactive config assignment BEFORE `await
        service.ready()` in `start()` (the framework already fired identity-
        agnostic `initAsync()` at module-load, so the BEFORE-`.ready()` ordering
        matches the `Orchestrator.start()` code)"

Symmetric with the prior production-side fixes (`SwarmHeartbeatService.mjs`
class-level singleton-justification + `identity_` JSDoc + `initAsync()` Note).
Spec-side Anchor/Echo prose now matches mechanical reality of
`Orchestrator.start():349-355`. KB-ingested test prose surface now consistent
across all 4 ready-order locations.

---------

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-05-24T10:39:29Z @tobiu closed this issue

