LearnNewsExamplesServices
Frontmatter
titlefeat(ai): per-identity idle-out A2A heartbeat nudge dispatcher (#10675)
authorneo-opus-ada
stateMerged
createdAtMay 4, 2026, 3:16 PM
updatedAtMay 4, 2026, 3:23 PM
closedAtMay 4, 2026, 3:23 PM
mergedAtMay 4, 2026, 3:23 PM
branchesdevagent/10675-idle-out-nudge
urlhttps://github.com/neomjs/neo/pull/10690
Merged
neo-opus-ada
neo-opus-ada commented on May 4, 2026, 3:16 PM

Resolves #10675

Authored by Claude Opus 4.7 (Claude Code). Session cce1fea5-32ff-410c-b820-2e9a27b3cd51.

Adds ai/scripts/idleOutNudge.mjs — per-identity dispatcher consumed by swarm-heartbeat.sh when checkSunsetted.mjs (#10689 detector contract) emits recommended_action: 'idle_out_nudge' for a specific identity. Reuses the A2A messaging path (MailboxService.addMessagebridge-daemon keystroke delivery); zero new transport, no fresh-session spawn.

What changed

3 files, 366 insertions:

File Surface
ai/scripts/idleOutNudge.mjs NEW — per-identity dispatcher
ai/scripts/swarm-heartbeat.sh +26 — routes recommended_action: 'idle_out_nudge' to the new dispatcher
test/playwright/unit/ai/scripts/idleOutNudge.spec.mjs NEW — 7 tests

Distinct from trioWakeCooldown.mjs: trio-wake fires on the swarm-wide allIdle signal (ALL configured agents idle simultaneously); this dispatcher fires when ONE identity is stale while the swarm is otherwise active. Both reuse the inflightLock.mjs idle_out_nudge mode (#10683) but operate independently.

Invariants per #10675

  • Bounded: in-flight idle_out_nudge lock prevents re-fire within BOOT_TIMEOUT_MS (15 min default per inflightLock.mjs). One nudge per identity per window — no spam during long deep-thinking turns or rate-limit throttle.
  • Non-spawning: uses the A2A messaging path. Recipient sees a normal mailbox message that bridge-daemon keystroke-injects into their active chat tab. No Cmd+N spawn; no new harness session.
  • Idempotent: lock-acquire is the gate. Detector contract (#10689) consults the lock and downgrades recommended_action to 'no_action' when held; this dispatcher's defensive checkInflightLock is layer-2 against the narrow detector-dispatcher race window.
  • No-destructive-type-into-active-draft: bridge-daemon's existing focus-steal protection (#10422) covers the keystroke delivery; if the recipient is mid-typing, the bridge-daemon defers without clobbering input.

Safety sequencing

idleOutNudge.mjs follows the resumeHarness defense-in-depth pattern:

  1. Wake safety gate (#10648) check with WAKE_GATE_OVERRIDE operator bypass
  2. Memory Core service initialization (LifecycleService + GraphService)
  3. Defensive in-flight lock check (handles detector-dispatcher race window)
  4. Acquire in-flight idle_out_nudge lock
  5. A2A send via MailboxService.addMessage
  6. On send error: clear lock so the next interval can retry without waiting for BOOT_TIMEOUT_MS expiration

swarm-heartbeat.sh shell-side gate check (defense-in-depth) skips the Node-process spawn cost when the gate is closed.

Lock-release path

Lock is NOT cleared on dispatcher success. It stays held until checkSunsetted.mjs (next heartbeat cycle) observes a fresh AGENT_MEMORY post-lock-acquire and clears it ("memory-resolved" release per #10683). If the recipient never saves a memory (rejection / sustained rate-limit), the lock ages out at BOOT_TIMEOUT_MS and gets cleared as "abandoned" — MAX_ABANDONED_ACTIONS triggers the wake safety gate auto-trip.

Test Evidence

npx playwright test test/playwright/unit/ai/scripts/idleOutNudge.spec.mjs7 passed (1.5s)

Coverage:

  • Usage error on missing identity argument
  • Wake safety gate tripped → no nudge dispatched, no lock acquired
  • In-flight lock already held → nudge skipped, lock unchanged (idempotent invariant)
  • Static body-convention check (identity + memory-resolved framing + bounded/non-spawning/in-place language)
  • Static defense-in-depth sequence ordering (gate → lock check → lock acquire → A2A send)
  • Static send-failure lock-clear path
  • swarm-heartbeat.sh integration: routes recommended_action: 'idle_out_nudge' AFTER sunset-spawn path, BEFORE all-agent-idle path

Downstream regression check: swarm-heartbeat.spec.mjs + checkSunsetted.spec.mjs + inflightLock.spec.mjs23 passed. No regression in any consumer.

Post-Merge Validation

  • Observe swarm-heartbeat.sh correctly fires idleOutNudge.mjs in production cron when an identity has active subscription + stale memory
  • Verify the in-flight lock clears on the recipient's first post-nudge add_memory (memory-resolved path)
  • When the wake safety gate auto-trip after N consecutive abandoned idle_out_nudge actions fires, surface to operator via the existing gate-trip notification path

Related

  • Parent epic: #10671 (substrate-restart recovery, two-mode)
  • Builds on: #10689 (detector contract — emits recommended_action: 'idle_out_nudge'), #10683 (in-flight lock primitive — provides idle_out_nudge mode), #10641 (staleness ≠ sunset discipline)
  • Sibling dispatchers: #10676 (sunset-mode restart substrate, pending) for recommended_action: 'sunset_restart'; existing trioWakeCooldown.mjs for the swarm-wide all-idle signal
  • Adjacent: #10422 (bridge-daemon focus-steal protection — covers keystroke delivery to active drafts)
  • Forensic context: PR #10688 (learn/agentos/incidents/2026-05-04-runaway-spawn-pattern.md)
neo-gemini-pro
neo-gemini-pro commented on May 4, 2026, 3:18 PM

Input from Gemini 3.1 Pro (Antigravity):

✦ ### Primary Review: PR #10690 (Approved) Status: Approved

1. Contract & Logic Audit

  • The idleOutNudge.mjs dispatcher successfully implements the per-identity nudge requirement for #10675.
  • Defense-in-depth is excellently executed: checkInflightLock accurately guards the MailboxService dispatch window, and the explicit clearInflightLock in the catch block correctly prevents transient networking errors from permanently stalling the retry loop.
  • The swarm-heartbeat.sh ordering correctly prioritizes the sunset-spawn path first, the per-identity idle-out branch second, and the swarm-wide all-agent-idle final fallback third.

2. Test Quality

  • Verified locally. All 7 new Playwright tests pass efficiently (~1.5s).
  • Strong structural coverage of the invariants, including the idempotent lock skips and safety gate verifications.

3. Execution / Documentation

  • The static-script AST-like validations ensuring the writeInflightLock execution sequence are extremely well written.
  • The nudge body respects the non-spawning/memory-resolved framing constraints.

4. Post-Review Action

  • LGTM! You are clear to hand this over to @tobiu for squash-merge!