---
id: 12003
title: 'Add active-a2a-participants target source for activity-derived pulse candidate discovery (Epic #11993 follow-up)'
state: CLOSED
labels:
  - enhancement
  - ai
  - architecture
  - model-experience
assignees:
  - neo-opus-ada
createdAt: '2026-05-25T23:52:35Z'
updatedAt: '2026-06-07T17:15:45Z'
githubUrl: 'https://github.com/neomjs/neo/issues/12003'
author: neo-opus-ada
commentsCount: 0
parentIssue: null
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-05-26T01:35:00Z'
---
# Add active-a2a-participants target source for activity-derived pulse candidate discovery (Epic #11993 follow-up)

Epic #11993 cycle-3 design intent: the wake substrate models harness readiness **empirically — only activity-derived signals are durable** ([Discussion #11992 §5.1.1](https://github.com/neomjs/neo/discussions/11992)). The 3-signal per-identity decision (`active` + `idle` + `ready`) shipped via Sub-ii #11995. **But the candidate-discovery layer — which identities get into the pulse loop at all — was NOT migrated to activity-derived discovery.** [`ai/daemons/orchestrator/scheduling/swarmHeartbeat.mjs:9-14`](ai/daemons/orchestrator/scheduling/swarmHeartbeat.mjs#L9) preserves the pre-Epic `targetSource` enum: `'self' | 'active-local-team' | 'active-subscribers' | 'disabled'`. `ai/config.template.mjs:209` ships `targetSource: null` (falls through to `'self'`), so every fresh checkout — including operator's local swarm — only pulses the harness-owner identity (`NEO_AGENT_IDENTITY`). Other swarm peers are invisible to the per-identity 3-signal loop entirely.

## Empirical anchor

2026-05-25T23:04Z orchestrator restart on `agent/heartbeat-emit-gate-fix` branch (Epic #11993 substrate live in production). Operator paused `@neo-gpt` for 17min to establish `idle: true`. Multiple pulse cycles fired (`heartbeat.alive` mtime advances every ~15min per cadence). Zero `heartbeat_pulse` GraphLog rows landed because:

1. `targetSource` falls through to `'self'` → `pulseIdentities = ['@neo-opus-ada']`
2. `@neo-opus-ada` continues talking with the operator → `idle: false`
3. `decideWake` correctly returns `wake: false` for the only identity in the loop
4. `@neo-gpt` is never even checked

The substrate is doing exactly what the tracked-template-default `targetSource: 'self'` enum specifies, but that specification does NOT match the Discussion's "activity-derived candidate set" framing.

Operator recollection from Discussion #11992 authoring (confirmed 2026-05-25): *"the env var agent id can not be enough"* + *"new ideas how to detect active participants via the 3h window"*. The intent was for the candidate set to be auto-discovered from A2A activity, not env-var-driven.

The closest existing enum value `'active-subscribers'` discovers via `WAKE_SUBSCRIPTION` node presence (subscription-presence-based, not activity-based) — close but not identical to the Discussion's activity-derived framing. Subscription presence is a stale signal in the marathon-session + context-compaction empirical anchor that motivated cycle-3 (Discussion §5.1.1).

## The Fix

Add `'active-a2a-participants'` as a fifth valid target source and ship it as the tracked default in [`ai/config.template.mjs`](ai/config.template.mjs) so the substrate works correctly for every user out of the box (no operator config-flip required):

1. **Update `VALID_TARGET_SOURCES`** in [`ai/daemons/orchestrator/scheduling/swarmHeartbeat.mjs`](ai/daemons/orchestrator/scheduling/swarmHeartbeat.mjs) to include `'active-a2a-participants'`.

2. **Update `resolveTargets`** to handle the new source: when `targetSource === 'active-a2a-participants'`, invoke a new injected `activeA2aParticipantsProvider` callable (mirrors the existing `activeSubscribersProvider` pattern at line 78). Union the provider result with `selfIdentity` (keeps harness owner in the pulse set, same as `'active-subscribers'`).

3. **Add `SwarmHeartbeatService.getActiveA2aParticipants()`** implementation. Query the graph for distinct `agentIdentity` values where an A2A `MESSAGE` node has been sent OR received within the last 3h (`SENT_TO` / `DELIVERED_TO` edge timestamps adjacent to AgentIdentity nodes). Pattern mirrors `getRecentActivityTimestamps` (same window, but identity-discovery-shape instead of per-identity-timestamps shape).

4. **Wire the provider** in `SwarmHeartbeatService.getPulseIdentities` call to `resolveHeartbeatTargets({..., activeA2aParticipantsProvider: () => this.getActiveA2aParticipants()})`.

5. **Ship as tracked default in `ai/config.template.mjs:209`** — change `targetSource: null` → `targetSource: 'active-a2a-participants'`. Per operator direction 2026-05-25T23:5xZ: "no config flip; this must work for EVERY user; if needed, config.template." Cloud/fork-safety concern that motivated the original `null → 'self'` fallthrough doesn't apply to `'active-a2a-participants'` — it's per-MC-instance derived (each deployment's MC has its own A2A activity), not per-team-registry derived (the `'active-local-team'` source was the team-registry coupling). External workspaces' MCs hold THEIR identities' activity, so candidate discovery is automatically tenant-correct.

6. **Resolver code-side fallback unchanged.** `targetSource: null` (e.g., from external workspaces predating this template update, or operators explicitly setting null) continues to fall through to `'self'` per the resolver's `selfFallback` contract. The fallback is the deployment-portable safety net for the rare case where the template default is bypassed; the canonical tracked default is now `'active-a2a-participants'`.

## Contract Ledger

| Surface | Source of Authority | Proposed Behavior | Fallback / Edge Case | Docs | Evidence |
|---|---|---|---|---|---|
| `VALID_TARGET_SOURCES` const | Discussion #11992 §5.1.1 + this ticket | Adds `'active-a2a-participants'`; existing 4 values preserved | Unknown values continue to fail-closed to `'self'` with warn log (existing behavior) | JSDoc on enum + Discussion #11992 link | Existing unit tests on enum validation continue passing |
| `resolveTargets({targetSource: 'active-a2a-participants', activeA2aParticipantsProvider})` | This ticket | Returns union of `selfIdentity` + provider-discovered identities; deduplicated, normalized | Provider missing/throws → `selfFallback('active-a2a-participants')` (matches existing `active-subscribers` fallback shape at line 78-82) | JSDoc on resolveTargets | Unit test asserts provider invocation + union + dedup |
| `SwarmHeartbeatService.getActiveA2aParticipants()` | This ticket | Returns `Promise<String[]>` of normalized canonical `@<identity>` strings, deduplicated; 3h window | Query failure → empty array + error log (matches `getRecentActivityTimestamps` catch shape) | JSDoc on method | Unit test with mocked GraphService activity rows asserts dedup + 3h cutoff |
| `ai/config.template.mjs` tracked default | Operator direction 2026-05-25 | `swarmHeartbeat.targetSource: 'active-a2a-participants'` (was `null`) | Operators can opt out via overlay `ai/config.mjs` (e.g., back to `'self'` for single-identity tests) | JSDoc on the slot updated to reflect new tracked default + safety rationale | Bootstrap regeneration test (`bootstrapWorktree.mjs` fresh-clone smoke) verifies new template value lands in fresh overlays |

## Acceptance Criteria

- [ ] AC1 — `VALID_TARGET_SOURCES` includes `'active-a2a-participants'`; export unchanged otherwise.
- [ ] AC2 — `resolveTargets({targetSource: 'active-a2a-participants'})` invokes `activeA2aParticipantsProvider`, unions result with `selfIdentity`, deduplicates, normalizes to canonical `@<identity>` form.
- [ ] AC3 — `SwarmHeartbeatService.getActiveA2aParticipants()` queries A2A `MESSAGE` nodes within the last 3h, returns deduplicated canonical agent identities.
- [ ] AC4 — Unit test: `resolveTargets` with `'active-a2a-participants'` source + mock provider returns expected union (covers happy path + provider-missing fallback).
- [ ] AC5 — Unit test: `getActiveA2aParticipants` with a mock GraphService activity row set returns the right identity list (covers 3h cutoff + dedup).
- [ ] AC6 — `ai/config.template.mjs:209` shipped default updated to `'active-a2a-participants'`; JSDoc on the slot reflects new tracked default + clarifies the per-MC-instance safety property (no team-registry coupling).
- [ ] AC7 — Cross-family review per `pull-request §6.1`.
- [ ] AC8 — Post-merge operator-side validation: a fresh checkout / template-respecting clone runs `npm run ai:orchestrator` with `@neo-gpt` idle >15m and no operator overlay; `heartbeat_pulse` GraphLog row lands within first pulse cycle (~15min); Codex Desktop wakes with `[WAKE]` block. This is the AC8 that Epic #11993 deferred to live-validation, now testable end-to-end with the template-default change + sibling lane (gate-fix on `agent/heartbeat-emit-gate-fix`).

## Out of Scope

- Per-identity activity-window override (3h is the Discussion-fixed window; per-call override is speculative).
- Removing the legacy `'self' | 'active-local-team' | 'active-subscribers'` enum values. They remain available; the new value is additive at the enum level even though the tracked default changes to it.
- Integration with the operator-side wake safety gate beyond the existing `pulse()` gate check (which already applies regardless of source).
- The Shape B emit/dispatch gate fix on `agent/heartbeat-emit-gate-fix` (separate lane claimed by @neo-gpt; **two evaluator surfaces** — `WakeSubscriptionService.resync()` + `ai/daemons/bridge/daemon.mjs::evaluateSubscription()` — both must be relaxed per @neo-gpt V-B-A 2026-05-25T23:53Z). This ticket addresses the candidate-discovery half, that lane addresses the route-evaluation half. Both halves must land for Epic #11993 AC8 to pass end-to-end.

## Avoided Traps

- Per-identity activity-window override (speculative).
- Replacing the per-identity `active` check (Sub-ii's `getRecentActivityTimestamps`) — that's already correct for per-identity decision logic. This ticket adds a NEW candidate-discovery layer; per-identity decision is unchanged.
- Making the new provider call `MailboxService.listMessages` per known identity (would require knowing identities upfront — circular). The provider does a graph-level query that DISCOVERS identities from MESSAGE node adjacency.
- Earlier draft of this ticket (cycle-1) said "NOT changing the default" — operator-corrected 2026-05-25T23:5xZ: the substrate must work for every user without requiring a config flip. Template-default change is in scope.
- The original cloud/fork-safety concern (silent fan-out to maintainer identities in external workspaces) applied to `'active-local-team'` which reads `identityRoots.mjs` (Neo team registry). `'active-a2a-participants'` is per-MC-instance derived — no team-registry coupling — so external deployments only ever see their own activity. Safety concern is structurally absent for this source.

## Related

- **Parent Epic:** #11993 (Wake substrate evolution: 3-signal model + Shape B heartbeat pulse)
- **Graduation Discussion:** #11992 §5.1.1 (root-cause framing — "activity-derived signals" durability)
- **Sub-ii:** #11995 / merged PR #11997 (per-identity `active` signal via `getRecentActivityTimestamps` — this ticket's per-identity sibling)
- **Sub-iii:** #11996 / merged PR #11999 (SwarmHeartbeatService 3-signal wire — this ticket adds the missing candidate-discovery layer)
- **Sibling lane:** Shape B route-evaluation gate fix on `agent/heartbeat-emit-gate-fix` (currently claimed by @neo-gpt as Sub-i author per [pr-lane-claim] 2026-05-25T23:45:30Z; per @neo-gpt [sync] 2026-05-25T23:53:19Z the fix must touch both `WakeSubscriptionService` evaluator AND `ai/daemons/bridge/daemon.mjs::evaluateSubscription()`)

Handoff Retrieval Hint: "active-a2a-participants target source candidate discovery 3h Epic #11993 follow-up swarmHeartbeat.mjs resolveTargets template-default".

## Timeline

- 2026-05-25T23:52:36Z @neo-opus-ada assigned to @neo-opus-ada
- 2026-05-25T23:52:36Z @neo-opus-ada added the `enhancement` label
- 2026-05-25T23:52:36Z @neo-opus-ada added the `ai` label
- 2026-05-25T23:52:37Z @neo-opus-ada added the `architecture` label
- 2026-05-25T23:52:37Z @neo-opus-ada added the `model-experience` label
- 2026-05-26T00:01:51Z @neo-gpt cross-referenced by PR #12004
- 2026-05-26T00:12:38Z @neo-opus-ada cross-referenced by #12005
- 2026-05-26T00:17:55Z @neo-gpt cross-referenced by #11993
- 2026-05-26T00:26:55Z @neo-opus-ada cross-referenced by PR #12006
- 2026-05-26T00:59:56Z @neo-opus-ada cross-referenced by #12008
- 2026-05-26T01:07:36Z @neo-opus-ada cross-referenced by PR #12009
- 2026-05-26T01:19:50Z @neo-opus-ada referenced in commit `635e81f` - "fix(orchestrator): DELIVERED_TO branch + template-default coverage + JSDoc — #12003 cycle-2

Cycle-2 response to @neo-gpt cycle-1 CHANGES_REQUESTED on PR #12009:

1. DELIVERED_TO edge branch (broadcast-recipient discovery).
   getActiveA2aParticipants SQL now UNIONs 3 branches instead of 2:
   - SENT_TO       (direct-message recipients, AGENT:* sentinel excluded)
   - DELIVERED_TO  (per-recipient broadcast fan-out — the canonical broadcast target edge)
   - SENT_BY       (message senders)
   Previously only SENT_TO + SENT_BY were covered, so broadcast-only recipients
   (`SENT_TO -> AGENT:*` with per-recipient DELIVERED_TO edges) never entered the
   candidate set. GPT V-B-A against the mailbox delivery model caught this.

2. Edge-taxonomy regression coverage.
   SwarmHeartbeatService.spec test for getActiveA2aParticipants now captures the
   SQL string + parameters and asserts:
   - SENT_TO + DELIVERED_TO + SENT_BY branches all present
   - AGENT:* sentinel exclusion preserved on the SENT_TO branch
   - 3h sentAt cutoff applied (3 identical ISO params per UNION call)
   - MESSAGE label filter on all branches
   Test mocks the DB layer but verifies the SQL contract — proves edge taxonomy
   rather than just returned-row identity list.

3. Template-default regression coverage.
   ai/config.template.spec.mjs now asserts
   `Config.orchestrator.swarmHeartbeat === {targetSource: 'active-a2a-participants'}`
   — direct regression test for the shipped tracked default per #12003 AC6.

4. Resolver JSDoc updated.
   swarmHeartbeat.mjs::resolveTargets JSDoc now documents all 5 enum values per-source,
   notes the tracked template default ('active-a2a-participants'), and adds the
   activeA2aParticipantsProvider parameter. Removes the stale 4-value framing.

Test evidence (commit at this commit):
- config.template.spec.mjs: 5/5 PASS (the new swarmHeartbeat default assertion lives in the existing test block)
- swarmHeartbeat.spec.mjs: 21/21 PASS (unchanged — JSDoc-only update)
- SwarmHeartbeatService.spec.mjs: 32/32 PASS (existing test strengthened to assert SQL edge taxonomy)
- Total: 58/58 PASS"
- 2026-05-26T01:35:00Z @tobiu referenced in commit `68ffafd` - "feat(orchestrator): active-a2a-participants target source for activity-derived pulse candidate discovery (#12003) (#12009)

* feat(orchestrator): active-a2a-participants target source for activity-derived pulse candidate discovery (#12003)

Closes the candidate-discovery half of Epic #11993 cycle-3 "activity-derived signals
are durable" framing — Sub-ii (#11995) shipped per-identity active/idle/ready decision
but pulse-loop candidate set still used the legacy targetSource enum. Default `'self'`
meant orchestrator-started-from-any-harness-owner-repo only ever pulsed that identity;
@neo-gpt + @neo-gemini-3-1-pro never entered the loop. Empirical anchor: operator's
17min @neo-gpt-quiet test on Epic #11993 substrate produced zero heartbeat_pulse rows
because the wrong identities were polled.

Changes:

1. swarmHeartbeat.mjs:
   - VALID_TARGET_SOURCES adds 'active-a2a-participants'
   - resolveTargets gains activeA2aParticipantsProvider injected callable; new case
     unions self with provider output (mirrors active-subscribers shape)

2. SwarmHeartbeatService.mjs:
   - New getActiveA2aParticipants() SQLite query: distinct identities adjacent to
     MESSAGE nodes via SENT_TO + SENT_BY edges with sentAt within last 3h (excludes
     AGENT:* broadcasts which aren't per-identity candidates)
   - Wired into getPulseIdentities() via activeA2aParticipantsProvider

3. config.template.mjs:
   - Default targetSource ships as 'active-a2a-participants' (was null falling
     through to 'self'). Per-MC-instance derived candidate discovery is tenant-safe
     (no team-registry coupling); external workspaces only ever see their own
     MC's activity. Code-side null→self fallback preserved as safety net.

Test evidence:
- swarmHeartbeat.spec.mjs: 21/21 PASS (16 prior + 4 new active-a2a-participants tests + 1 updated VALID_TARGET_SOURCES tuple)
- SwarmHeartbeatService.spec.mjs: 32/32 PASS (29 prior + 1 enum-coverage assertion in beforeSetTargetSource + 2 new getActiveA2aParticipants tests)
- Total: 53/53 PASS

* fix(orchestrator): DELIVERED_TO branch + template-default coverage + JSDoc — #12003 cycle-2

Cycle-2 response to @neo-gpt cycle-1 CHANGES_REQUESTED on PR #12009:

1. DELIVERED_TO edge branch (broadcast-recipient discovery).
   getActiveA2aParticipants SQL now UNIONs 3 branches instead of 2:
   - SENT_TO       (direct-message recipients, AGENT:* sentinel excluded)
   - DELIVERED_TO  (per-recipient broadcast fan-out — the canonical broadcast target edge)
   - SENT_BY       (message senders)
   Previously only SENT_TO + SENT_BY were covered, so broadcast-only recipients
   (`SENT_TO -> AGENT:*` with per-recipient DELIVERED_TO edges) never entered the
   candidate set. GPT V-B-A against the mailbox delivery model caught this.

2. Edge-taxonomy regression coverage.
   SwarmHeartbeatService.spec test for getActiveA2aParticipants now captures the
   SQL string + parameters and asserts:
   - SENT_TO + DELIVERED_TO + SENT_BY branches all present
   - AGENT:* sentinel exclusion preserved on the SENT_TO branch
   - 3h sentAt cutoff applied (3 identical ISO params per UNION call)
   - MESSAGE label filter on all branches
   Test mocks the DB layer but verifies the SQL contract — proves edge taxonomy
   rather than just returned-row identity list.

3. Template-default regression coverage.
   ai/config.template.spec.mjs now asserts
   `Config.orchestrator.swarmHeartbeat === {targetSource: 'active-a2a-participants'}`
   — direct regression test for the shipped tracked default per #12003 AC6.

4. Resolver JSDoc updated.
   swarmHeartbeat.mjs::resolveTargets JSDoc now documents all 5 enum values per-source,
   notes the tracked template default ('active-a2a-participants'), and adds the
   activeA2aParticipantsProvider parameter. Removes the stale 4-value framing.

Test evidence (commit at this commit):
- config.template.spec.mjs: 5/5 PASS (the new swarmHeartbeat default assertion lives in the existing test block)
- swarmHeartbeat.spec.mjs: 21/21 PASS (unchanged — JSDoc-only update)
- SwarmHeartbeatService.spec.mjs: 32/32 PASS (existing test strengthened to assert SQL edge taxonomy)
- Total: 58/58 PASS

---------

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-05-26T01:35:01Z @tobiu closed this issue
- 2026-05-27T05:55:52Z @neo-gpt cross-referenced by PR #12055

