Skip to content

fix(hooks): suppress system event injection when deliver is false#49234

Closed
BrennerSpear wants to merge 3 commits into
openclaw:mainfrom
BrennerSpear:fix/hook-deliver-false-silent-v2
Closed

fix(hooks): suppress system event injection when deliver is false#49234
BrennerSpear wants to merge 3 commits into
openclaw:mainfrom
BrennerSpear:fix/hook-deliver-false-silent-v2

Conversation

@BrennerSpear

Copy link
Copy Markdown
Contributor

Summary / What changed

Builds on the fix from #36332 by @cioclawcode, rebased onto current main with test coverage added.

Fix (commits 1-2 by @cioclawcode)

  • src/gateway/server/hooks.ts: Guard enqueueSystemEvent with value.deliver !== false so hooks with deliver: false complete silently — no delivery, no system event injected into the main session.
  • Error path intentionally still surfaces to main session (silent failures are harder to debug than noisy ones). This asymmetry is documented with an inline comment.

Tests (commit 3)

Addresses the Greptile 3/5 review on the original PR:

  • suppresses system event when mapping sets deliver: false — verifies no system event is enqueued when a mapping completes successfully with deliver: false
  • still surfaces errors to main session even when deliver: false — confirms the intentional error-path asymmetry is working as designed
  • direct /hooks/agent with deliver: false suppresses system event — covers the /hooks/agent direct endpoint path (not just mappings)

Why

When deliver: false is set on a hook mapping (e.g. for silent email triage), the hook run summary was still injected as a system event into the main session, which then got announced to the last active channel (Telegram, Discord, etc.). This made deliver: false ineffective for truly silent background automations.

Issue

Fixes #36325
Supersedes #36332

Co-authored-by: @cioclawcode (original fix + docs commits preserved with authorship)

ciosnippybot and others added 3 commits March 17, 2026 16:42
When a hook mapping sets `deliver: false`, the delivery announcement is
correctly suppressed, but a system event ("Hook <name>: <summary>") is
still injected into the main session. This happens because the guard
only checks `!result.delivered` — which is always false when delivery
was never attempted.

Add `value.deliver !== false` to the guard so that hooks with explicit
`deliver: false` complete silently without notifying the main session.

Fixes openclaw#36325
The error path intentionally always surfaces to the main session even
when deliver is false — silent hook failures are harder to debug.
Add a comment making this asymmetry explicit per review feedback.
Addresses Greptile review feedback on openclaw#36332:

- Test that deliver: false mapping suppresses system event injection
- Test that errors still surface to main session (intentional asymmetry)
- Test that /hooks/agent direct endpoint also respects deliver: false

Co-authored-by: cioclawcode <[email protected]>
@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime size: S labels Mar 17, 2026
@BrennerSpear
BrennerSpear marked this pull request as draft March 17, 2026 20:45
@greptile-apps

greptile-apps Bot commented Mar 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug where hooks configured with deliver: false were still injecting a system event into the main session after a successful run, causing the result to be announced on the last active channel (Telegram, Discord, etc.) — defeating the purpose of silent background automations.

The fix is a minimal one-line guard in dispatchAgentHook (src/gateway/server/hooks.ts): the condition for calling enqueueSystemEvent on the success path is tightened from !result.delivered to !result.delivered && value.deliver !== false. The error path intentionally still surfaces to the main session (documented with an inline comment), preserving debuggability.

Three new integration tests are added to server.hooks.test.ts covering the key scenarios:

  • Success path with deliver: false via a hook mapping (no system event injected)
  • Error path with deliver: false (error still surfaced to main session)
  • Direct /hooks/agent endpoint with deliver: false (no system event injected)

Notes:

  • The fix is correct and well-scoped. The strict !== false comparison is appropriate — deliver: undefined preserves the existing behaviour.
  • Tests 1 and 3 use setTimeout(200) for negative assertions (no system event), which is a common pattern for fire-and-forget async but carries a small risk of flakiness under CI load. See inline comment for details.

Confidence Score: 4/5

  • Safe to merge — fix is minimal, correct, and well-tested; only risk is minor test flakiness in slow CI environments.
  • The production code change is a single, well-reasoned guard that cleanly addresses the root cause without affecting any other code paths. Error surfacing is preserved as documented. Three new tests cover all stated scenarios. The slight deduction is for the setTimeout(200)-based negative assertions in two tests, which could produce non-deterministic results under CI resource contention.
  • No files require special attention; the setTimeout(200) pattern in server.hooks.test.ts (lines 547 and 607) is worth monitoring in CI for intermittent failures.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/gateway/server.hooks.test.ts
Line: 546-547

Comment:
**Fixed-delay wait for negative assertions may be flaky in slow CI**

Both the suppression tests (for mapping and direct `/hooks/agent`) rely on a bare `setTimeout(200)` to allow the `void` async dispatch to complete before asserting `peekSystemEvents(...).length === 0`. With an immediately-resolving mock this is almost always fine, but under heavy CPU contention in CI the microtask queue may not have fully drained by the time the timer fires, which could produce a spurious pass (the system event simply hadn't arrived yet rather than being truly suppressed).

Consider a lightweight poll-until-idle helper, or — since `cronIsolatedRun` is already asserted to have been called — awaiting the completion of the underlying task more deterministically:

```typescript
// One option: poll until the mock has been called, with a shorter timeout
const deadline = Date.now() + 2000;
while (cronIsolatedRun.mock.calls.length === 0 && Date.now() < deadline) {
  await new Promise((r) => setTimeout(r, 10));
}
```

At minimum, adding a comment explaining why 200 ms is sufficient (mock resolves synchronously in the next microtask) would help future readers understand the intent.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: b0edd79

Comment on lines +546 to +547
// Give the async dispatch time to complete.
await new Promise((resolve) => setTimeout(resolve, 200));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Fixed-delay wait for negative assertions may be flaky in slow CI

Both the suppression tests (for mapping and direct /hooks/agent) rely on a bare setTimeout(200) to allow the void async dispatch to complete before asserting peekSystemEvents(...).length === 0. With an immediately-resolving mock this is almost always fine, but under heavy CPU contention in CI the microtask queue may not have fully drained by the time the timer fires, which could produce a spurious pass (the system event simply hadn't arrived yet rather than being truly suppressed).

Consider a lightweight poll-until-idle helper, or — since cronIsolatedRun is already asserted to have been called — awaiting the completion of the underlying task more deterministically:

// One option: poll until the mock has been called, with a shorter timeout
const deadline = Date.now() + 2000;
while (cronIsolatedRun.mock.calls.length === 0 && Date.now() < deadline) {
  await new Promise((r) => setTimeout(r, 10));
}

At minimum, adding a comment explaining why 200 ms is sufficient (mock resolves synchronously in the next microtask) would help future readers understand the intent.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/server.hooks.test.ts
Line: 546-547

Comment:
**Fixed-delay wait for negative assertions may be flaky in slow CI**

Both the suppression tests (for mapping and direct `/hooks/agent`) rely on a bare `setTimeout(200)` to allow the `void` async dispatch to complete before asserting `peekSystemEvents(...).length === 0`. With an immediately-resolving mock this is almost always fine, but under heavy CPU contention in CI the microtask queue may not have fully drained by the time the timer fires, which could produce a spurious pass (the system event simply hadn't arrived yet rather than being truly suppressed).

Consider a lightweight poll-until-idle helper, or — since `cronIsolatedRun` is already asserted to have been called — awaiting the completion of the underlying task more deterministically:

```typescript
// One option: poll until the mock has been called, with a shorter timeout
const deadline = Date.now() + 2000;
while (cronIsolatedRun.mock.calls.length === 0 && Date.now() < deadline) {
  await new Promise((r) => setTimeout(r, 10));
}
```

At minimum, adding a comment explaining why 200 ms is sufficient (mock resolves synchronously in the next microtask) would help future readers understand the intent.

How can I resolve this? If you propose a fix, please make it concise.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b0edd79967

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// delivery *and* main-session notification. Only inject a system
// event when delivery was requested but did not succeed (e.g. the
// target channel could not be resolved).
if (!result.delivered && value.deliver !== false) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Surface run-level errors for deliver-false hooks

This condition suppresses all hook system events when deliver is false, but many failures from runCronIsolatedAgentTurn are returned as status: "error" results (not thrown exceptions). In those cases the catch path never runs, so deliver: false hooks silently drop real execution errors (timeouts, model/config errors, error payload outcomes), making broken automations invisible to operators.

Useful? React with 👍 / 👎.

@BrennerSpear
BrennerSpear marked this pull request as ready for review March 17, 2026 21:43

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b0edd79967

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// delivery *and* main-session notification. Only inject a system
// event when delivery was requested but did not succeed (e.g. the
// target channel could not be resolved).
if (!result.delivered && value.deliver !== false) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Surface non-throw hook failures when deliver is false

The new value.deliver !== false guard suppresses all undelivered outcomes for silent hooks, but many real failures from runCronIsolatedAgentTurn are returned as status: "error" results instead of thrown exceptions (for example model/config/payload error paths), so the catch block is never reached. In those cases a deliver: false hook now fails silently with no main-session system event, which breaks observability for background automations even though the code comment says errors should still be surfaced.

Useful? React with 👍 / 👎.

@BrennerSpear

Copy link
Copy Markdown
Contributor Author

@vincentkoc Mind taking a look at this one when you get a chance? It’s a very small hooks fix + tests, and it unblocks a pretty practical Gmail webhook automation case: deliver: false currently still leaks a system event into the main session, so “silent” email labeling flows still notify the user. This patch makes silent hooks actually silent while preserving error surfacing. If it looks good, would love a review/merge.

@clawsweeper

clawsweeper Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Closing this as duplicate or superseded after Codex automated review.

Close #49234 as superseded. Current main still has the original one-bit !result.delivered hook fallback, but #49234's narrow guard was flagged for hiding non-throw status:"error" hook failures. Open PR #55761 now tracks the same remaining deliver:false leak with the broader announcement-policy fix and tests.

Best possible solution:

Close #49234 as superseded and keep the remaining hooks work on #55761, where the fix should separate main-session announcement policy from delivery state, keep successful deliver:false hooks silent, preserve returned error visibility, and land with deterministic regression tests.

What I checked:

So I’m closing this here and keeping the remaining discussion on the canonical linked item.

Codex Review notes: model gpt-5.5, reasoning high; reviewed against 54f8e4145e43.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hook deliver: false still injects system event into main session

2 participants