Skip to content

fix(auto-reply): restore route replies for exec-event completions#70258

Merged
steipete merged 3 commits intoopenclaw:mainfrom
wzfukui:fix/openclaw-exec-event-delivery-context
Apr 23, 2026
Merged

fix(auto-reply): restore route replies for exec-event completions#70258
steipete merged 3 commits intoopenclaw:mainfrom
wzfukui:fix/openclaw-exec-event-delivery-context

Conversation

@wzfukui
Copy link
Copy Markdown
Contributor

@wzfukui wzfukui commented Apr 22, 2026

Summary

  • restore route-reply delivery for async exec-event replies by falling back to persisted session delivery context
  • keep the existing direct routing behavior when OriginatingChannel and OriginatingTo are present on the live turn
  • add regression coverage for system-event replies that must return to the originating channel surface

Root 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

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 22, 2026

Greptile Summary

This PR fixes async exec-event completions silently dropping their channel route-replies by falling back to the persisted session delivery context (deliveryContext / lastChannel / lastTo / lastAccountId) when the live turn carries no OriginatingChannel or OriginatingTo. The isSystemEventTurn guard that gates the fallback is consistent with the same three-provider grouping (heartbeat, cron-event, exec-event) already used in session.ts.

Confidence Score: 5/5

Safe 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 sendPayloadAsync prevents unintended routing when no channel metadata is available at all. The single P2 finding (missing accountId assertion in the new test) is a test-completeness suggestion, not a production defect.

No files require special attention.

Prompt To Fix All 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.

Reviews (1): Last reviewed commit: "fix(auto-reply): route exec-event replie..." | Re-trigger Greptile

Comment on lines +466 to +472
expect(dispatcher.sendFinalReply).not.toHaveBeenCalled();
expect(mocks.routeReply).toHaveBeenCalledWith(
expect.objectContaining({
channel: "telegram",
to: "telegram:999",
}),
);
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 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.

Suggested change
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.

Copy link
Copy Markdown

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

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: 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".

});
const originatingTo = ctx.OriginatingTo;
const originatingTo = ctx.OriginatingTo ?? fallbackOriginatingTo;
const routeAccountId = ctx.AccountId ?? fallbackOriginatingAccountId;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

@hxcdaniel-coder
Copy link
Copy Markdown

QC 多模型审核结果

模型 权重 状态 评分 结论
github-copilot/gpt-5.4 2.5 票 ❌超时 - -
kimi/kimi-k2.6 1 票 88/100 CONDITIONAL_PASS
bailian/deepseek-v3.2 1 票 85/100 CONDITIONAL_PASS

有效票数: 2 票
加权评分: 86.5/100
最终结论: CONDITIONAL_PASS ⚠️

主要问题

测试缺少 accountId: "acc-1" 断言。虽然 routeAccountId fallback逻辑是修复的核心部分,但测试没有验证accountId的正确传递。

建议补充accountId断言后再合并。

@wzfukui wzfukui force-pushed the fix/openclaw-exec-event-delivery-context branch from 3f07711 to 863429f Compare April 23, 2026 01:08
@wzfukui
Copy link
Copy Markdown
Contributor Author

wzfukui commented Apr 23, 2026

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.

Copy link
Copy Markdown

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

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: 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".

Comment on lines +334 to +335
const effectiveOriginatingChannel = ctx.OriginatingChannel ?? fallbackOriginatingChannel;
const effectiveOriginatingTo = ctx.OriginatingTo ?? fallbackOriginatingTo;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

@martingarramon
Copy link
Copy Markdown
Contributor

Static review. LGTM-shape on the fallback direction.

One design question + two gaps:

  1. Stale-route risk at channel level. dispatch-from-config.ts:290-293 carries an explicit warning: "Do not read thread ids from the normalised session store here: origin.threadId can be folded back into lastThreadId/deliveryContext during store normalisation and resurrect a stale route after thread delivery was intentionally cleared." The PR now reads deliveryContext.channel / .to / .accountId from that same store at channel granularity. Does the same resurrection risk apply? Failure mode: a user who moved a Slack session to Telegram mid-life gets exec-event replies fired back to Slack.
  2. accountId revalidation. routeAccountId replaces ctx.AccountId in both route runtime and direct-dispatcher paths. If sessionStoreEntry.entry?.lastAccountId is a revoked/rotated profile, exec-event replies go through an authprofile the user meant to drop. Re-validating against current profiles would close it.
  3. Test matrix. Happy path covered. Worth adding: (a) exec-event with no deliveryContext AND no lastChannel (should not fabricate a route), (b) persistedDeliveryContext.channel diverging from both Provider and Surface (does suppressAcpChildUserDelivery still short-circuit correctly?).

steipete added a commit to wzfukui/openclaw that referenced this pull request Apr 23, 2026
@steipete steipete force-pushed the fix/openclaw-exec-event-delivery-context branch from fd6c33c to e85f6b6 Compare April 23, 2026 19:24
@steipete steipete merged commit 2b45bc1 into openclaw:main Apr 23, 2026
67 checks passed
@steipete
Copy link
Copy Markdown
Contributor

Landed via rebase onto main.

  • Gate: pnpm test src/auto-reply/reply/dispatch-from-config.test.ts; pnpm check:changed
  • Source head before merge: e85f6b6
  • Land commit: 2b45bc1

Thanks @wzfukui!

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants