Skip to content

fix(hooks): suppress silent shared-hook fallback while preserving error surfacing#55761

Closed
EffortlessSteven wants to merge 8 commits into
openclaw:mainfrom
EffortlessSteven:fix/hooks-announce-policy-upstream
Closed

fix(hooks): suppress silent shared-hook fallback while preserving error surfacing#55761
EffortlessSteven wants to merge 8 commits into
openclaw:mainfrom
EffortlessSteven:fix/hooks-announce-policy-upstream

Conversation

@EffortlessSteven

@EffortlessSteven EffortlessSteven commented Mar 27, 2026

Copy link
Copy Markdown

Fixes #55704

Summary

Shared hook fallback was using the wrong signal.

The old gateway behavior treated !result.delivered as enough reason to post a hook summary into the resolved main-session destination. That is too coarse. A successful silent hook, a handled async delivery path, and a real hook error are different outcomes. They need different fallback behavior.

This PR replaces that one-bit check with an explicit announcement policy.

The new policy:

  1. result.status !== "ok" -> surface the hook result.
  2. explicit announceToMain -> honor it.
  3. successful deliver:false -> stay silent.
  4. result.delivered === true -> avoid duplicate fallback.
  5. result.deliveryAttempted === true -> avoid handled-path fallback.
  6. otherwise -> preserve legacy fallback.

It also fixes the fallback label so default or already-prefixed hook names do not render as Hook Hook.

Current main integration

This branch has been refreshed onto current main, including #73228's target-agent hook completion/error routing.

That means the final behavior is split cleanly:

  • shouldAnnounceHookResultToMain(...) decides whether a hook result should surface.
  • resolveHookEventSessionKey(...) decides which resolved main-session destination receives it.

For explicit-agent hooks, that destination may be the target agent's main session rather than the default main session.

What changed

Production code

  • Added shouldAnnounceHookResultToMain(...) in src/gateway/server/hooks.ts.
  • Replaced the old if (!result.delivered) fallback gate with the ordered policy above.
  • Added formatHookPrefix(...) and use it in both success and catch paths.
  • Added announceToMain?: boolean to RunCronAgentTurnResult.
  • Updated announceToMain wording to refer to the hook's resolved main-session destination.
  • Preserved current main's lazy runCronIsolatedAgentTurn import shape.
  • Preserved current main's target-agent hook event routing.

Tests

Added or updated coverage for:

  • direct /hooks/agent + deliver:false success -> no fallback
  • mapped hook + deliver:false success -> no fallback
  • deliver:false + non-throw status:"error" -> still surfaces
  • deliveryAttempted:true + delivered:false -> no handled-path fallback
  • explicit announceToMain:true -> fallback still works when requested
  • explicit announceToMain:false + narration/trailing NO_REPLY -> suppressed
  • explicit announceToMain:false + status:"error" -> error still surfaces
  • default hook name -> Hook: ..., not Hook Hook: ...
  • catch path -> Hook (error): ..., not Hook Hook (error): ...
  • explicit-agent announceToMain:true -> routes to the target agent main session, not default main

Reviewer map

Suggested path:

  1. src/gateway/server/hooks.ts
    Check the policy order, prefix formatting, and composition with resolveHookEventSessionKey(...).

  2. src/cron/isolated-agent/run.types.ts
    Check that announceToMain?: boolean is additive and that the wording matches resolved-destination routing.

  3. src/gateway/server.hooks.test.ts
    Check the announcement-policy matrix and explicit-agent routing composition case.

  4. src/gateway/server/hooks.agent-trust.test.ts
    Check that surfaced hook events stay trusted:false and explicit-agent events route to the target agent main session.

  5. src/gateway/test-helpers.runtime-state.ts
    Check that test helper typing uses RunCronAgentTurnResult without importing runtime code.

Validation

Validated locally in a clean detached worktree at:

b84a19d3dfbb81cdf9028df42ca9b7fb8ecb03d2

  • git diff --check: pass
  • targeted oxfmt: pass
  • pnpm test src/gateway/server/hooks.agent-trust.test.ts: pass, 4 tests
  • pnpm test src/gateway/server.hooks.test.ts -t "hook announcement policy": pass, 10 tests
  • pnpm test src/gateway/server.hooks.test.ts -t "target agent": pass, 2 tests
  • pnpm test src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts: pass, 48 tests
  • pnpm check:changed: pass
  • pnpm build: pass
  • [INEFFECTIVE_DYNAMIC_IMPORT]: not present

Note

The original Windows checkout had a local Rolldown node_modules lock/corruption issue. I validated the exact final head in a clean detached worktree with pnpm install --frozen-lockfile; that clean validation passed.

@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime size: M labels Mar 27, 2026
@EffortlessSteven
EffortlessSteven force-pushed the fix/hooks-announce-policy-upstream branch from 7dfcde5 to 27b1e44 Compare March 27, 2026 11:59
@greptile-apps

greptile-apps Bot commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces the coarse !result.delivered fallback gate in dispatchAgentHook with a richer compatibility bridge (shouldAnnounceHookResultToMain) that correctly suppresses silent shared-hook results while always surfacing real run-level errors. It also fixes the long-standing Hook Hook label bug via formatHookPrefix, and adds 8 well-structured tests with a deterministic async flush helper.

Key changes:

  • shouldAnnounceHookResultToMain exports a clean, testable policy function with a well-reasoned priority order: errors always win → explicit announceToMaindeliver:falsedelivered:truedeliveryAttempted:true → legacy fallback
  • formatHookPrefix correctly handles both the default "Hook" name and names that already begin with "Hook ", eliminating the double-prefix symptom in both the success and catch paths
  • announceToMain?: boolean on RunCronAgentTurnResult is a forward-compatible escape hatch that lets the runner override the caller's delivery preference
  • New tests use setImmediate-based flushing instead of fixed-delay sleeps, making negative assertions reliable and fast

Confidence Score: 5/5

Safe to merge — the logic is correct, all branches are tested, and no regressions were introduced.

No P0 or P1 issues found. The priority ordering in shouldAnnounceHookResultToMain is correct and intentional, tests cover every documented branch including the error-overrides-announceToMain:false edge case, and the formatHookPrefix fix is sound for all realistic name inputs. Prior review concerns were already addressed or are explicitly out of scope for this PR.

No files require special attention.

Important Files Changed

Filename Overview
src/gateway/server/hooks.ts Replaces the coarse !result.delivered fallback gate with shouldAnnounceHookResultToMain and fixes the Hook Hook prefix label via formatHookPrefix. Logic is sound and priority ordering is correct.
src/cron/isolated-agent/run.ts Adds the optional announceToMain?: boolean field to RunCronAgentTurnResult. Type-only addition, backward-compatible, well-documented.
src/gateway/server.hooks.test.ts Adds 8 new targeted tests for the announcement policy using a deterministic flushHookDispatch helper. New describe block has its own afterEach with drain + restore. Tests cover all documented branches of shouldAnnounceHookResultToMain.

Reviews (2): Last reviewed commit: "Merge branch 'main' into fix/hooks-annou..." | Re-trigger Greptile

Comment thread src/gateway/server/hooks.ts Outdated
Comment thread src/gateway/server/hooks.ts
Comment thread src/gateway/server.hooks.test.ts
@EffortlessSteven
EffortlessSteven force-pushed the fix/hooks-announce-policy-upstream branch from 27b1e44 to e1af4f8 Compare March 27, 2026 12:02
@EffortlessSteven

Copy link
Copy Markdown
Author

@greptileai please verify that the catch-path label fix, the broadened formatHookPrefix behavior, and the afterEach event drain address the prior review comments.

@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: d88c95c952

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gateway/server/hooks.ts Outdated
return false;
}
// - delivery-attempted hooks should not fallback into main
if (result.deliveryAttempted === true) {

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 Avoid treating deliveryAttempted as a hard no-announce signal

shouldAnnounceHookResultToMain now suppresses main-session fallback whenever result.deliveryAttempted === true, but that flag is also set in non-delivery paths (for example in dispatchCronDelivery when descendant orchestration is still in progress or stale interim text is being suppressed, where no user-visible delivery has actually happened). In those status: "ok" cases, this change drops the only fallback system event for shared hooks, so hook runs can become silently invisible in the main session. This regression is introduced by the new guard and is reproducible for hook flows that return delivered: false, deliveryAttempted: true without an actual outbound send.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I’m keeping the deliveryAttempted guard here.

In src/cron/isolated-agent/delivery-dispatch.ts, deliveryAttempted is already used as a handled/no-fallback signal in non-send status: "ok" paths, not just for outbound-send attempts. The active-descendant orchestration path and the stale-interim suppression path both set deliveryAttempted = true specifically so the timer does not fire redundant enqueueSystemEvent fallback, and the stale-delivery skip paths follow the same contract.

So for shared hooks, treating deliveryAttempted as “do not fallback into main” is intentional and consistent with the existing delivery layer semantics. I clarified that contract in the helper comment and tightened the matching test wording in this PR rather than changing behavior.

@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: 6bc9d7e1af

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gateway/server/hooks.ts Outdated

@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: 6bc9d7e1af

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gateway/server/hooks.ts Outdated
@EffortlessSteven
EffortlessSteven force-pushed the fix/hooks-announce-policy-upstream branch from 6bc9d7e to 4cb0826 Compare March 28, 2026 19:58
@EffortlessSteven
EffortlessSteven force-pushed the fix/hooks-announce-policy-upstream branch from c60c28d to e91c3f8 Compare April 8, 2026 20:03

EffortlessSteven commented Apr 8, 2026

Copy link
Copy Markdown
Author

Refreshed this branch onto current main and force-pushed the PR head.

Kept the shared-hook fallback behavior this PR is meant to land:

  • dispatchAgentHook now keeps current-main summary normalization while still using shouldAnnounceHookResultToMain(...) instead of the old !result.delivered fallback gate
  • successful deliver:false and handled deliveryAttempted:true hook runs stay out of the main session, while real status !== "ok" hook results still surface
  • formatHookPrefix(...) is still used in both success and catch paths, so the default fallback label stays Hook: ... instead of Hook Hook: ...
  • announceToMain?: boolean remains on RunCronAgentTurnResult

One small follow-up was needed after the rebase: src/gateway/test-helpers.runtime-state.ts now types cronIsolatedRun as RunCronAgentTurnResult so the new hook-policy tests typecheck on current main.

Verification on the refreshed branch:

  • pnpm exec vitest run --config vitest.config.ts src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts
  • NODE_OPTIONS=--max-old-space-size=8192 pnpm exec tsc --noEmit

@EffortlessSteven
EffortlessSteven force-pushed the fix/hooks-announce-policy-upstream branch from 92215da to 00b8e3f Compare April 9, 2026 16:22

EffortlessSteven commented Apr 9, 2026

Copy link
Copy Markdown
Author

Refreshed this PR again on top of current main and force-pushed the rebased head.

This rebase kept the intended shared-hook fallback fix while absorbing the latest upstream hook-file shape:

  • src/gateway/server/hooks.ts now keeps current-main CliDeps / OpenClawConfig imports, sanitized safeName, trusted: false, and normalizeOptionalString(...)
  • the PR behavior is still intact there: shouldAnnounceHookResultToMain(...) replaces the old !result.delivered gate, formatHookPrefix(...) replaces the old inline prefix, successful deliver:false / handled deliveryAttempted:true runs stay out of the main session, and real status !== "ok" results still surface
  • src/gateway/test-helpers.runtime-state.ts now keeps both imports the rebase needed: OpenClawConfig from ../config/types.openclaw.js and RunCronAgentTurnResult from ../cron/isolated-agent.js
  • announceToMain?: boolean remains on RunCronAgentTurnResult in src/cron/isolated-agent/run.types.ts
  • the trust-test fixture update remains in place: surfaced-success trust coverage uses announceToMain: true instead of implicitly relying on the old deliver:false fallback behavior

Current verification on the rebased branch:

  • pnpm exec vitest run --config test/vitest/vitest.gateway.config.ts src/gateway/server/hooks.agent-trust.test.ts
  • pnpm exec vitest run --config test/vitest/vitest.cron.config.ts src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts
  • pnpm exec vitest run --config test/vitest/vitest.gateway.config.ts src/gateway/server.hooks.test.ts -t "hook announcement policy" still stalls in this environment, so I am not claiming that block green from local repro here
  • NODE_OPTIONS=--max-old-space-size=8192 pnpm exec tsc --noEmit is still red here, but the remaining failure I see is outside the hook seam:
    • extensions/codex/src/app-server/dynamic-tools.ts

GitHub now shows the PR as mergeable again after the rebase.

@clawsweeper

clawsweeper Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Codex review: keeping this open for maintainer follow-up; there is still a little grit to resolve.

Current main still does not implement this PR's hook announcement policy. The gateway hook dispatcher still falls back to a main-session system event on !result.delivered, RunCronAgentTurnResult still lacks the proposed announceToMain override, and existing tests still encode the old deliver:false fallback behavior. The PR remains an active, recently refreshed implementation candidate rather than obsolete cleanup.

Best possible solution:

Keep this PR open as the active implementation candidate. The best path is to review the announcement policy against the existing cron deliveryAttempted contract, confirm the target-agent routing composition from #73228 remains intact, then land the tested gateway hook fix if CI/Testbox validates the hook and cron lanes.

What I checked:

  • Current main still uses the coarse fallback gate: dispatchAgentHook maps deliver:false to delivery mode none, but after the isolated run it still builds Hook ${safeName} and enqueues a system event whenever !result.delivered; it does not check value.deliver, deliveryAttempted, or an explicit announcement policy. (src/gateway/server/hooks.ts:50, dc541662f815)
  • Default hook naming still permits the Hook Hook label: normalizeAgentPayload defaults the hook name to Hook, while the dispatcher formats successful fallback labels as Hook ${safeName}. With the default name, current main still produces the double prefix this PR is trying to fix. (src/gateway/hooks.test.ts:120, dc541662f815)
  • The proposed result override is absent on main: RunCronAgentTurnResult currently exposes outputText, delivered, deliveryAttempted, and delivery, but no announceToMain?: boolean field. (src/cron/isolated-agent/run.types.ts:3, dc541662f815)
  • Current tests still assert the old no-deliver fallback behavior: The gateway hook trust test builds a hook payload with deliver:false, mocks an isolated result with delivered:false, and expects enqueueSystemEvent to receive a Hook ...: done event. That is the behavior this PR changes to require explicit surfacing for successful silent runs. (src/gateway/server/hooks.agent-trust.test.ts:59, dc541662f815)
  • Delivery-attempted no-fallback semantics already exist in cron: shouldEnqueueCronMainSummary suppresses fallback when deliveryAttempted === true, and the double-announce regression tests document active-subagent and stale-interim paths setting deliveryAttempted=true specifically so the timer skips enqueueSystemEvent. This supports treating the PR's deliveryAttempted behavior as a real maintainer policy point, not as obsolete work. (src/cron/heartbeat-policy.ts:32, dc541662f815)
  • Live refs confirm the PR is still separate from current main: Remote main resolves to dc541662f81503aecc08c193bf9ddf3208a95abb; the PR head resolves to 52cd4bd369a86d17a8e4346c8893d67db6ba81ca. Local searches for shouldAnnounceHookResultToMain and announceToMain on main found no implementation. (dc541662f815)

Remaining risk / open question:

  • The PR changes the visibility contract for successful hook results, especially deliveryAttempted:true with delivered:false; maintainers should explicitly accept that handled-path no-fallback semantics apply to shared hooks.
  • The change touches hook system-event injection, so review should verify silent success stays silent while run-level errors still surface to the resolved target-agent or main-session destination.
  • I did not run tests because this review was required to keep the checkout read-only; rely on CI/Testbox or rerun the targeted gateway and cron tests before merge.

Codex review notes: model gpt-5.5, reasoning high; reviewed against dc541662f815.

@EffortlessSteven

Copy link
Copy Markdown
Author

Refreshed this onto current main and resolved the src/gateway/server/hooks.ts conflict by keeping main’s lazy import/file topology while preserving the hook announcement-policy fix.

Latest pushed head: 86134fc (fix(hooks): align hook announcement fallback with main).

Local validation in a clean detached worktree:

  • pnpm install --frozen-lockfile: pass
  • git diff --check: pass
  • pnpm test src/gateway/server/hooks.agent-trust.test.ts src/gateway/server.hooks.test.ts src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts: pass, 78 tests
  • pnpm check:changed: pass
  • pnpm build: pass
  • [INEFFECTIVE_DYNAMIC_IMPORT]: not present

No intended design change in the rebase-alignment commit.

…or surfacing

The shared-hook fallback in dispatchAgentHook previously keyed off
!result.delivered alone to decide whether to inject a system event into
the main session. This conflates outward delivery state with
main-session announcement policy, causing silent/no-op hooks
(deliver:false) and delivery-attempted hooks to leak fallback summaries
into main.

Replace the single-boolean gate with a compatibility-bridge helper
(shouldAnnounceHookResultToMain) that respects:

- result.status !== "ok" → always surface errors (even if
  announceToMain is false — errors override explicit suppression)
- result.announceToMain (explicit policy, forward-compatible)
- value.deliver === false → silent on success
- result.delivered → no duplicate
- result.deliveryAttempted → no fallback for async-ack paths

Also fix the generic fallback label (both success and catch paths) so
default name "Hook" no longer renders as "Hook Hook".
@EffortlessSteven
EffortlessSteven force-pushed the fix/hooks-announce-policy-upstream branch from 86134fc to b84a19d Compare April 28, 2026 10:16
@EffortlessSteven

Copy link
Copy Markdown
Author

Refreshed again onto current main, including the target-agent hook completion/error routing from #73228, and resolved the src/gateway/server/hooks.ts collision by keeping that routing while preserving this PR's announcement-policy gate and prefix fix.

Latest pushed head: b84a19d3dfbb81cdf9028df42ca9b7fb8ecb03d2.

The resulting composition is:

  • shouldAnnounceHookResultToMain(...) decides whether a hook result should surface.
  • resolveHookEventSessionKey(...) decides which resolved main-session destination receives it.

Local validation in a clean detached worktree:

  • git diff --check: pass
  • targeted oxfmt: pass
  • pnpm test src/gateway/server/hooks.agent-trust.test.ts: pass, 4 tests
  • pnpm test src/gateway/server.hooks.test.ts -t "hook announcement policy": pass, 10 tests
  • pnpm test src/gateway/server.hooks.test.ts -t "target agent": pass, 2 tests
  • pnpm test src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts: pass, 48 tests
  • pnpm check:changed: pass
  • pnpm build: pass
  • [INEFFECTIVE_DYNAMIC_IMPORT]: not present

@openclaw-clownfish

Copy link
Copy Markdown
Contributor

ProjectClownfish could not safely update this branch, so it opened a narrow replacement PR instead.

Replacement PR: #73800
Source PR: #55761
Contributor credit is preserved in the replacement PR body and changelog plan.

@vincentkoc

Copy link
Copy Markdown
Member

Thanks for the original repair work here. The narrow replacement landed in #73800 as 2f31184, preserving the shared-hook announcement-policy fix and source credit. I left the broader hook-policy pieces as follow-up work rather than folding them into this closeout.

@EffortlessSteven

Copy link
Copy Markdown
Author

Awesome. Happy to pull together the follow-up if there's interest.

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

Labels

gateway Gateway runtime size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Shared hook fallback can surface silent hook output into the main session

2 participants