Skip to content

fix(cron): avoid delivered status for empty outbound receipts#79811

Open
indulgeback wants to merge 1 commit into
openclaw:mainfrom
indulgeback:codex/cron-announce-not-delivered-regression
Open

fix(cron): avoid delivered status for empty outbound receipts#79811
indulgeback wants to merge 1 commit into
openclaw:mainfrom
indulgeback:codex/cron-announce-not-delivered-regression

Conversation

@indulgeback

@indulgeback indulgeback commented May 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #79753.

Cron announce/direct delivery could still treat an outbound adapter result with no platform delivery identity as a successful send on text/media paths. That lets a run show delivered: true even though the channel adapter produced no usable message receipt.

This tightens the shared outbound delivery result handling so only results with a real delivery identity (messageId, chatId, channelId, roomId, conversationId, toJid, or pollId) are counted as sent. Empty/identity-less results now flow through the existing suppressed/no-visible-result path, so cron records delivered: false instead of a false positive.

Real behavior proof

  • Behavior addressed: A cron announce/direct delivery path must not report a channel delivery as successful when the outbound channel adapter returns no platform message identity.
  • Real environment tested: Local OpenClaw checkout on macOS, branch codex/cron-announce-not-delivered-regression, Node v25.7.0, using the patched shared outbound delivery runtime through sendDurableMessageBatch.
  • Exact steps or command run after this patch: Ran a local OpenClaw runtime smoke command that registers a Telegram outbound adapter returning { channel: "telegram", messageId: "" }, then calls sendDurableMessageBatch with skipQueue: true.
  • Evidence after fix: Terminal output from the patched checkout:
$ pnpm exec tsx -e '(async () => { const { sendDurableMessageBatch } = await import("./src/channels/message/send.ts"); const { setActivePluginRegistry } = await import("./src/plugins/runtime.ts"); const { createOutboundTestPlugin, createTestRegistry } = await import("./src/test-utils/channel-plugins.ts"); const sendText = async () => ({ channel: "telegram", messageId: "" }); setActivePluginRegistry(createTestRegistry([{ pluginId: "telegram", source: "local-smoke", plugin: createOutboundTestPlugin({ id: "telegram", outbound: { deliveryMode: "direct", sendText } }) }])); const result = await sendDurableMessageBatch({ cfg: {}, channel: "telegram", to: "123", payloads: [{ text: "cron receipt smoke" }], skipQueue: true }); console.log(JSON.stringify(result, null, 2)); })();'
{
  "status": "suppressed",
  "results": [],
  "receipt": {
    "platformMessageIds": [],
    "parts": [],
    "sentAt": 1778397010778,
    "raw": []
  },
  "reason": "adapter_returned_no_identity",
  "payloadOutcomes": [
    {
      "index": 0,
      "status": "suppressed",
      "reason": "adapter_returned_no_identity"
    }
  ]
}
  • Observed result after fix: The outbound runtime suppressed the empty platform receipt (status: "suppressed", results: [], reason: "adapter_returned_no_identity") instead of returning a sent result. The cron regression test on the same branch verifies this maps to delivered: false with deliveryAttempted: true.
  • What was not tested: I did not run a live WeChat or Feishu cron announce job locally because I do not have those channel credentials in this environment.

Tests

  • pnpm test src/cron/isolated-agent.direct-delivery-core-channels.test.ts
  • pnpm test src/channels/message/send.test.ts src/infra/outbound/deliver.test.ts
  • pnpm test src/cron/isolated-agent.direct-delivery-core-channels.test.ts src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts src/channels/message/send.test.ts src/infra/outbound/deliver.test.ts
  • pnpm test src/cron/isolated-agent.direct-delivery-core-channels.test.ts src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts src/channels/message/send.test.ts src/infra/outbound/deliver.test.ts extensions/telegram/src/outbound-adapter.test.ts extensions/feishu/src/outbound.test.ts extensions/slack/src/outbound-delivery.test.ts
  • pnpm exec oxfmt --check --threads=1 src/infra/outbound/deliver.ts src/cron/isolated-agent.direct-delivery-core-channels.test.ts
  • git diff --check

@openclaw-barnacle openclaw-barnacle Bot added size: S triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 9, 2026

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

Copy link
Copy Markdown

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: 81868a92b1

ℹ️ 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 +1624 to 1630
...filterIdentifiedDeliveryResults(
await handler.sendFormattedText(
payloadSummary.text,
applySendReplyToConsumption(sendOverrides),
),
),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid emitting stale messageId after filtering no-identity sends

After this change filters identity-less results, a payload can now produce zero new results entries even though earlier payloads already populated results. In that case, the later emitMessageSent still takes messageId from results.at(-1), which points to a previous payload’s message ID. This misattributes a failed/suppressed send to the wrong platform message whenever multiple payloads are delivered in one call and a later text send returns only identity-less results.

Useful? React with 👍 / 👎.

@clawsweeper

clawsweeper Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed June 2, 2026, 1:09 AM ET / 05:09 UTC.

Summary
The PR filters identity-less outbound adapter receipts from shared text, formatted-text, and media delivery results and adds cron/direct-delivery regressions.

PR surface: Source +20, Tests +97. Total +117 across 3 files.

Reproducibility: yes. Source inspection shows current main appends empty text/media results and durable send treats non-empty results as sent; the PR body also supplies terminal proof that the patched runtime suppresses the empty-result case.

Review metrics: 1 noteworthy metric.

  • Shared outbound result filters: 3 result collection paths tightened. Text, formatted-text, and media delivery now decide sent status from identified platform results, which is the merge-sensitive behavior change.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • none.

Risk before merge

  • [P1] Merging changes shared outbound accounting so adapters that previously returned an empty or identity-less receipt can now produce suppressed/not-delivered instead of delivered.
  • [P1] The supplied proof is a local runtime smoke plus focused tests, not live WeChat or Feishu delivery; maintainers should treat live-channel validation as optional merge confidence rather than a contributor blocker.

Maintainer options:

  1. Accept Stricter Empty-Receipt Semantics (recommended)
    Land after maintainers accept that empty or identity-less adapter results should count as suppressed delivery across shared outbound paths.
  2. Audit High-Use Adapter Receipts First
    Pause merge to inspect bundled and high-use external adapter result shapes if maintainers are unsure whether any valid delivery path returns only empty IDs.
  3. Defer To Broader Delivery Contract Work
    Keep this PR open but wait for the broader durable fallback contract issue if maintainers want one cross-channel semantics change instead of this narrow fix first.

Next step before merge

  • [P2] The branch has no narrow automation repair left; the remaining action is maintainer acceptance of the stricter shared outbound receipt semantics and normal merge validation.

Security
Cleared: The diff is limited to TypeScript delivery accounting and tests; it does not change dependencies, workflows, secrets handling, install scripts, or package execution surfaces.

Review details

Best possible solution:

Land this patch if maintainers accept the stricter empty-identity receipt contract, and leave the broader durable fallback semantics work tracked in #87561.

Do we have a high-confidence way to reproduce the issue?

Yes. Source inspection shows current main appends empty text/media results and durable send treats non-empty results as sent; the PR body also supplies terminal proof that the patched runtime suppresses the empty-result case.

Is this the best way to solve the issue?

Yes, with maintainer acceptance. The shared outbound delivery layer is the right fix boundary because cron consumes durable send results, and a channel-specific workaround would leave other direct delivery paths inconsistent.

AGENTS.md: found and applied where relevant.

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

Label changes

Label justifications:

  • P1: The PR addresses false delivered status for cron/channel delivery, which can hide real message loss in scheduled workflows.
  • merge-risk: 🚨 compatibility: The diff changes the shared adapter result contract for empty platform identities during upgrade.
  • merge-risk: 🚨 message-delivery: The diff controls whether durable sends, hooks, diagnostics, and cron announce paths report sent, suppressed, or delivered across channels.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body includes after-fix terminal output from a local OpenClaw runtime smoke showing an empty Telegram adapter receipt becomes a suppressed durable send with empty results.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes after-fix terminal output from a local OpenClaw runtime smoke showing an empty Telegram adapter receipt becomes a suppressed durable send with empty results.
Evidence reviewed

PR surface:

Source +20, Tests +97. Total +117 across 3 files.

View PR surface stats
Area Files Added Removed Net
Source 1 33 13 +20
Tests 2 98 1 +97
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 3 131 14 +117

Acceptance criteria:

  • [P1] pnpm test src/cron/isolated-agent.direct-delivery-core-channels.test.ts src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts src/channels/message/send.test.ts src/infra/outbound/deliver.test.ts.
  • [P1] pnpm test extensions/telegram/src/outbound-adapter.test.ts extensions/feishu/src/outbound.test.ts extensions/slack/src/outbound-delivery.test.ts.
  • [P1] pnpm exec oxfmt --check --threads=1 src/infra/outbound/deliver.ts src/cron/isolated-agent.direct-delivery-core-channels.test.ts.
  • [P1] git diff --check.

What I checked:

  • Repository policy applied: Root and scoped outbound/channel guidance were read; shared channel delivery and fallback behavior are treated as compatibility-sensitive review surfaces. (AGENTS.md:1, ebf20241bd17)
  • Current-main source repro: Current main filters identity-less sendPayload results but still appends text/formatted/media results before classifying delivery, so { messageId: "" } can still make the result set non-empty. (src/infra/outbound/deliver.ts:1510, ebf20241bd17)
  • Durable-send status boundary: Durable send returns suppressed only when outbound delivery returns zero results, making shared outbound result filtering the relevant fix boundary for cron delivery accounting. (src/channels/message/send.ts:241, ebf20241bd17)
  • PR implementation: The patch adds identified-result helpers, applies them to shared text/formatted/media result collection, and changes hook emission to read message IDs from the new delivered slice rather than earlier payloads. (src/infra/outbound/deliver.ts:864, e046c2868b6a)
  • Regression coverage: The branch adds a cron announce test for an empty Telegram receipt and an outbound hook test proving a later suppressed payload does not reuse an earlier message ID. (src/cron/isolated-agent.direct-delivery-core-channels.test.ts:454, e046c2868b6a)
  • Linked user report: The linked user report is still open and describes cron announce delivery reporting delivered: true while WeChat and Feishu messages never arrived.

Likely related people:

  • steipete: GitHub path history shows repeated recent work on shared outbound delivery and message delivery APIs, including lint/refactor touches on src/infra/outbound/deliver.ts. (role: feature owner and recent area contributor; confidence: high; commits: deb7bc653935, c2d31a5e59c1; files: src/infra/outbound/deliver.ts, src/channels/message/send.ts)
  • piersonr: Recent plugin SDK reply-payload hook work changed shared outbound delivery, durable-send interaction, and delivered mirror behavior around this path. (role: recent adjacent contributor; confidence: medium; commits: b474f429ee4b; files: src/infra/outbound/deliver.ts, src/channels/message/send.ts)
  • vincentkoc: Local blame in the shallow checkout attributes the current identity helper and remaining text-path behavior to the grafted main boundary, and GitHub path history shows recent outbound diagnostics work in this file. (role: current-main line provenance and adjacent owner; confidence: medium; commits: ebf20241bd17, bd32b1a906f3; files: src/infra/outbound/deliver.ts)
  • osolmaz: The related durable fallback semantics issue defines the broader delivery contract that this PR implements for empty identities. (role: canonical behavior-contract follow-up owner; confidence: medium; files: src/infra/outbound/deliver.ts, src/channels/message/send.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Jun 1, 2026
@clawsweeper clawsweeper Bot added rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P1 High-priority user-facing bug, regression, or broken workflow. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. labels Jun 1, 2026
@indulgeback indulgeback force-pushed the codex/cron-announce-not-delivered-regression branch from 423e5ee to e046c28 Compare June 1, 2026 07:07
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label Jun 1, 2026
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label Jun 1, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the stale Marked as stale due to inactivity label Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. P1 High-priority user-facing bug, regression, or broken workflow. proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: S status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Cron announce fallback delivery reports success but message never arrives (WeChat + Feishu) on 2026.5.7

1 participant