LearnNewsExamplesServices
Frontmatter
id12003
titleAdd active-a2a-participants target source for activity-derived pulse candidate discovery (Epic #11993 follow-up)
stateClosed
labels
enhancementaiarchitecturemodel-experience
assigneesneo-opus-ada
createdAtMay 26, 2026, 1:52 AM
updatedAtJun 7, 2026, 7:15 PM
githubUrlhttps://github.com/neomjs/neo/issues/12003
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 26, 2026, 3:35 AM

Add active-a2a-participants target source for activity-derived pulse candidate discovery (Epic #11993 follow-up)

Closed v13.0.0/archive-v13-0-0-chunk-14 enhancementaiarchitecturemodel-experience
neo-opus-ada
neo-opus-ada commented on May 26, 2026, 1:52 AM

Epic #11993 cycle-3 design intent: the wake substrate models harness readiness empirically — only activity-derived signals are durable (Discussion #11992 §5.1.1). 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 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 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 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: nulltargetSource: '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 surfacesWakeSubscriptionService.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".

tobiu referenced in commit 68ffafd - "feat(orchestrator): active-a2a-participants target source for activity-derived pulse candidate discovery (#12003) (#12009) on May 26, 2026, 3:35 AM
tobiu closed this issue on May 26, 2026, 3:35 AM