Skip to content

fix(runtime): handle trigger() rejections when waking idle agents#17

Merged
guo-yu merged 1 commit into
mainfrom
claude/fix-unhandled-trigger-rejections
Jun 10, 2026
Merged

fix(runtime): handle trigger() rejections when waking idle agents#17
guo-yu merged 1 commit into
mainfrom
claude/fix-unhandled-trigger-rejections

Conversation

@guo-yu

@guo-yu guo-yu commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Problem

AgentProcess.trigger() is an async method that can reject (it awaits relay.recv, credentialManager.ensureFresh(), runOneShot(), and the preamble/env providers). The supervisor called it fire-and-forget — without await or .catch() — in two places:

  1. Inside the setNewMessageCallback lambda that wakes idle on-demand / idle_cached agents when a message arrives.
  2. Inside wakeUnblockedAgents() when a completed task unblocks dependent agents. The surrounding try/catch there does not cover the promise, since it is never awaited.

Any rejection became an unhandled promise rejection, which can crash the whole supervisor process.

Fix

Both call sites now use:

void agent.trigger().catch((err) => {
  log.error('agent trigger failed', {
    agent: agentName,
    error: err instanceof Error ? err.message : String(err),
  });
});

This matches the existing agent.start().catch(...) error-handling pattern already used in Supervisor.start().

Tests

  • Added two regression tests asserting that no unhandledRejection event is emitted when a woken agent's trigger() rejects, covering both the new-message wake path and the task-unblock wake path. Both tests fail without the fix and pass with it.
  • The tests use plain function mocks rather than vi.fn(), because vitest mocks attach internal handlers to returned promises (for mock.settledResults), which would mask the unhandled rejection.
  • Also tightened an existing loose mock (trigger: vi.fn() returned undefined, which is not a valid Promise<void>).

Full @wanman/runtime suite: 48 files, 716 passed / 8 skipped (pre-existing skips). tsc --noEmit clean.

🤖 Generated with Claude Code

agent.trigger() was called fire-and-forget in two supervisor paths:
the relay new-message callback and wakeUnblockedAgents. Since the
returned promise was never awaited or .catch()ed, any rejection (e.g.
a failed spawn or credential refresh) became an unhandled promise
rejection that could crash the supervisor process — the try-catch in
wakeUnblockedAgents did not cover the async rejection either.

Both call sites now use void agent.trigger().catch(...) and log the
failure via log.error, matching the existing agent.start().catch(...)
pattern in start().

Adds regression tests that assert no unhandledRejection event is
emitted when a woken agent's trigger() rejects (using plain function
mocks, since vi.fn() attaches internal handlers to returned promises
that would mask the unhandled rejection).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@guo-yu guo-yu merged commit d3a8138 into main Jun 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant