fix(auto-reply): restore route replies for exec-event completions#70258
Conversation
Greptile SummaryThis PR fixes async Confidence Score: 5/5Safe to merge; the only finding is a minor test assertion gap that doesn't affect production behaviour. The implementation is correct and consistent with existing patterns. The null guard in No files require special attention. Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/auto-reply/reply/dispatch-from-config.test.ts
Line: 466-472
Comment:
**Missing `accountId` assertion in new test**
The test sets up `deliveryContext.accountId: "acc-1"` and `AccountId: undefined` precisely to exercise the `routeAccountId` fallback — but the `routeReply` expectation never asserts `accountId`. The new `routeAccountId` variable is the other half of this fix (it's passed to four `routeReply` call-sites), so leaving it unverified means the test would still pass even if the account-ID propagation regressed.
```suggestion
expect(mocks.routeReply).toHaveBeenCalledWith(
expect.objectContaining({
channel: "telegram",
to: "telegram:999",
accountId: "acc-1",
}),
);
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(auto-reply): route exec-event replie..." | Re-trigger Greptile |
| expect(dispatcher.sendFinalReply).not.toHaveBeenCalled(); | ||
| expect(mocks.routeReply).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| channel: "telegram", | ||
| to: "telegram:999", | ||
| }), | ||
| ); |
There was a problem hiding this comment.
Missing
accountId assertion in new test
The test sets up deliveryContext.accountId: "acc-1" and AccountId: undefined precisely to exercise the routeAccountId fallback — but the routeReply expectation never asserts accountId. The new routeAccountId variable is the other half of this fix (it's passed to four routeReply call-sites), so leaving it unverified means the test would still pass even if the account-ID propagation regressed.
| expect(dispatcher.sendFinalReply).not.toHaveBeenCalled(); | |
| expect(mocks.routeReply).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| channel: "telegram", | |
| to: "telegram:999", | |
| }), | |
| ); | |
| expect(mocks.routeReply).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| channel: "telegram", | |
| to: "telegram:999", | |
| accountId: "acc-1", | |
| }), | |
| ); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/dispatch-from-config.test.ts
Line: 466-472
Comment:
**Missing `accountId` assertion in new test**
The test sets up `deliveryContext.accountId: "acc-1"` and `AccountId: undefined` precisely to exercise the `routeAccountId` fallback — but the `routeReply` expectation never asserts `accountId`. The new `routeAccountId` variable is the other half of this fix (it's passed to four `routeReply` call-sites), so leaving it unverified means the test would still pass even if the account-ID propagation regressed.
```suggestion
expect(mocks.routeReply).toHaveBeenCalledWith(
expect.objectContaining({
channel: "telegram",
to: "telegram:999",
accountId: "acc-1",
}),
);
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3f07711469
ℹ️ 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".
| suppressDirectUserDelivery: suppressAcpChildUserDelivery, | ||
| isRoutableChannel: routeReplyRuntime.isRoutableChannel, | ||
| }); | ||
| const originatingTo = ctx.OriginatingTo; | ||
| const originatingTo = ctx.OriginatingTo ?? fallbackOriginatingTo; | ||
| const routeAccountId = ctx.AccountId ?? fallbackOriginatingAccountId; |
There was a problem hiding this comment.
Use fallback account for final route-reply sends
For system-event turns, this change correctly derives routeAccountId from persisted delivery context, but the main final-reply path in sendFinalPayload still calls routeReply with ctx.AccountId instead of the fallback. When exec-event/cron-event/heartbeat completions lose live account metadata (the regression this commit targets), final replies can still route with an undefined or wrong account, which breaks multi-account channel delivery or applies incorrect per-account messaging config.
Useful? React with 👍 / 👎.
3f07711 to
863429f
Compare
|
Updated the PR branch against latest main, resolved the merge conflict, and added the requested routeReply accountId assertion for the persisted delivery-context fallback test. Local targeted test passed: pnpm test src/auto-reply/reply/dispatch-from-config.test.ts. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 863429fd24
ℹ️ 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".
| const effectiveOriginatingChannel = ctx.OriginatingChannel ?? fallbackOriginatingChannel; | ||
| const effectiveOriginatingTo = ctx.OriginatingTo ?? fallbackOriginatingTo; |
There was a problem hiding this comment.
Honor sendPolicy for fallback-routed system-event replies
When a heartbeat/cron-event/exec-event turn is missing live origin fields, this code now reconstructs effectiveOriginatingChannel/effectiveOriginatingTo from session state and routes outbound replies there, but sendPolicy is still resolved later from the raw context (ctx.OriginatingChannel ?? ctx.Surface ?? ctx.Provider). In that scenario, channel rules are evaluated against exec-event instead of the actual routed channel (for example, telegram), so channel-based deny rules can be bypassed and a reply is still delivered. Please feed the effective fallback route into policy resolution so suppression is evaluated against the true outbound destination.
Useful? React with 👍 / 👎.
|
Static review. LGTM-shape on the fallback direction. One design question + two gaps:
|
fd6c33c to
e85f6b6
Compare
…xt loss Exec completion events enqueued under cron or channel-specific session keys were invisible to the heartbeat runner, which always checked the agent's main session key. This caused exec results to silently vanish instead of being relayed back to the originating channel. 1. Propagate sessionKey through scopedHeartbeatWakeOptions in bash exec, ACP spawn, gateway/node exec, and CLI watchdog paths 2. Add resolveEventSessionKey to remap cron session keys to the originating agent's main session key for event enqueueing Scope was previously broader. Dropped per maintainer review feedback on openclaw#50818 (issuecomment-4322767117): - The forceLastTargetWhenNone override for resolveHeartbeatDeliveryTarget, which routed exec completions to the session's last target even when heartbeat.target was explicitly "none". Current main and its regression tests intentionally keep target="none" internal-only, so this routing change is left for a separate focused PR if it is ever revisited. - The openclaw#39978 hook-cancel handling has already shipped via upstream PR openclaw#70258, so it is no longer part of this PR's scope. The PR now closes openclaw#52305 only. Closes openclaw#52305. Related: openclaw#18237. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…xt loss Exec completion events enqueued under cron or channel-specific session keys were invisible to the heartbeat runner, which always checked the agent's main session key. This caused exec results to silently vanish instead of being relayed back to the originating channel. 1. Propagate sessionKey through scopedHeartbeatWakeOptions in bash exec, ACP spawn, gateway/node exec, and CLI watchdog paths 2. Add resolveEventSessionKey to remap cron session keys to the originating agent's main session key for event enqueueing Scope was previously broader. Dropped per maintainer review feedback on openclaw#50818 (issuecomment-4322767117): - The forceLastTargetWhenNone override for resolveHeartbeatDeliveryTarget, which routed exec completions to the session's last target even when heartbeat.target was explicitly "none". Current main and its regression tests intentionally keep target="none" internal-only, so this routing change is left for a separate focused PR if it is ever revisited. - The openclaw#39978 hook-cancel handling has already shipped via upstream PR openclaw#70258, so it is no longer part of this PR's scope. The PR now closes openclaw#52305 only. Closes openclaw#52305. Related: openclaw#18237. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…xt loss Exec completion events enqueued under cron or channel-specific session keys were invisible to the heartbeat runner, which always checked the agent's main session key. This caused exec results to silently vanish instead of being relayed back to the originating channel. 1. Propagate sessionKey through scopedHeartbeatWakeOptions in bash exec, ACP spawn, gateway/node exec, and CLI watchdog paths 2. Add resolveEventSessionKey to remap cron session keys to the originating agent's main session key for event enqueueing Scope was previously broader. Dropped per maintainer review feedback on openclaw#50818 (issuecomment-4322767117): - The forceLastTargetWhenNone override for resolveHeartbeatDeliveryTarget, which routed exec completions to the session's last target even when heartbeat.target was explicitly "none". Current main and its regression tests intentionally keep target="none" internal-only, so this routing change is left for a separate focused PR if it is ever revisited. - The openclaw#39978 hook-cancel handling has already shipped via upstream PR openclaw#70258, so it is no longer part of this PR's scope. The PR now closes openclaw#52305 only. Closes openclaw#52305. Related: openclaw#18237. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…xt loss Exec completion events enqueued under cron or channel-specific session keys were invisible to the heartbeat runner, which always checked the agent's main session key. This caused exec results to silently vanish instead of being relayed back to the originating channel. 1. Propagate sessionKey through scopedHeartbeatWakeOptions in bash exec, ACP spawn, gateway/node exec, and CLI watchdog paths 2. Add resolveEventSessionKey to remap cron session keys to the originating agent's main session key for event enqueueing Scope was previously broader. Dropped per maintainer review feedback on openclaw#50818 (issuecomment-4322767117): - The forceLastTargetWhenNone override for resolveHeartbeatDeliveryTarget, which routed exec completions to the session's last target even when heartbeat.target was explicitly "none". Current main and its regression tests intentionally keep target="none" internal-only, so this routing change is left for a separate focused PR if it is ever revisited. - The openclaw#39978 hook-cancel handling has already shipped via upstream PR openclaw#70258, so it is no longer part of this PR's scope. The PR now closes openclaw#52305 only. Closes openclaw#52305. Related: openclaw#18237. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
exec-eventreplies by falling back to persisted session delivery contextOriginatingChannelandOriginatingToare present on the live turnRoot cause
Async completion turns can lose direct channel metadata by the time the reply is generated. In that case the router currently skips channel route-reply delivery entirely, so channel plugins never receive the final completion message.
Testing
pnpm vitest run src/auto-reply/reply/dispatch-from-config.test.ts