Skip to content

feat(outbox): add write-ahead outbox, recovery worker, and plugin compat layer#29148

Closed
nohat wants to merge 11 commits intoopenclaw:mainfrom
nohat:lifecycle/write-ahead-outbox
Closed

feat(outbox): add write-ahead outbox, recovery worker, and plugin compat layer#29148
nohat wants to merge 11 commits intoopenclaw:mainfrom
nohat:lifecycle/write-ahead-outbox

Conversation

@nohat
Copy link
Contributor

@nohat nohat commented Feb 27, 2026

Summary

  • Problem: Deliveries are fire-and-forget — if the gateway crashes mid-send, the reply is lost; plugin channels lack durable delivery guarantees; one-shot recovery only runs at startup
  • Why it matters: Users lose replies on crashes; plugin channels have weaker delivery semantics than built-in channels; no continuous retry for transient failures
  • What changed: Write-ahead pattern (enqueue outbox entry before sending, ack on success, retry on failure); continuous outbox worker replaces one-shot startup recovery; startup cutoff filter prevents double-delivery; v1/v2 plugin adapter compat layer gives plugin channels the same durability
  • What did NOT change (scope boundary): No inbound turn tracking yet (comes in PR 3); no deduplication of inbound messages; no orphan turn recovery

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

User-visible / Behavior Changes

  • Replies are now persisted to SQLite before delivery — crash recovery is automatic via continuous worker loop
  • Plugin channels with outboundContract: "v2" get write-ahead delivery; v1 plugins continue to work via compat layer
  • outboundContract field appears in channel status/schema

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: Node 22+ / Bun
  • Model/provider: N/A
  • Integration/channel (if any): All outbound channels + plugin channels
  • Relevant config (redacted): N/A

Steps

  1. Start gateway — verify outbox worker starts and logs recovery passes
  2. Send a reply — verify outbox entry is created before delivery, acked after
  3. Kill gateway mid-delivery — verify entry is recovered on restart
  4. Test with a v1 plugin channel — verify compat layer normalizes adapter

Expected

  • Outbox worker runs continuously, recovering pending entries
  • Entries enqueued during current instance's lifetime are skipped (startup cutoff)
  • v1/v2 plugin adapters both work through compat layer

Actual

  • Verified via test suite (948 tests pass)

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

  • Verified scenarios: pnpm build, pnpm test (948 pass), pnpm check
  • Edge cases checked: Startup cutoff filtering, permanent error detection, v1/v2 adapter detection, worker shutdown
  • What you did not verify: Live gateway crash recovery, real plugin channel delivery

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No
  • If yes, exact upgrade steps: N/A

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly: Revert commit; previous one-shot recovery still works from PR 1
  • Files/config to restore: N/A
  • Known bad symptoms reviewers should watch for: Duplicate deliveries (startup cutoff not filtering correctly); worker not stopping cleanly on shutdown

Risks and Mitigations

  • Risk: Startup cutoff filter too aggressive — skipping entries that should be recovered
    • Mitigation: Only entries with no prior attempt AND enqueued after startup are skipped; entries with attempt_count > 0 or last_attempt_at IS NOT NULL are always included
  • Risk: Plugin compat layer misdetects adapter version
    • Mitigation: Explicit outboundContract field on channel config; fallback to v1 wrapping when absent

Part 2 of 3: #29147 (SQLite outbox) → write-ahead outbox + worker → #29149 (turn tracking)
Merge after #29147. Incremental diff: git diff lifecycle/sqlite-outbox...lifecycle/write-ahead-outbox


E2E Test Results

All 4 tests pass ✅

# Test Result
1 Write-Ahead Delivery — send Telegram message → verify outbox entry created with status='delivered' before=0, after=1
2 Continuous Recovery Worker — seed pending outbox entries → verify worker picks them up without restart attempt_count: 1→2 (both entries)
3 Startup Cutoff Filter — Entry A (fresh, no attempts) skipped; Entry B (prior attempt) recovered A=queued, B=failed_retryable (attempts: 1→2)
4 Outbox Pruning — age delivered rows by 49h → verify pruned; recent rows retained ✅ old entries pruned, recent retained

Test script: https://gist.github.com/nohat/d98c213adfeaebf196e4137ed7d693c0

nohat and others added 2 commits February 27, 2026 13:11
Replace unbounded file-based delivery queue with queryable SQLite
message_outbox table. Adds TTL/expiry for stale entries, delivery
outcome retention, and one-time legacy file queue import on startup.

Closes openclaw#23777, openclaw#16555, openclaw#29128
…pat layer

Write-ahead delivery pattern: enqueue outbox entry before sending, ack on
success, retry on failure. Continuous outbox worker replaces one-shot
recovery. Plugin channels get durable delivery guarantees via v1/v2
adapter compat layer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 28, 2026

Greptile Summary

This PR implements a write-ahead outbox pattern for durable message delivery, replacing the previous fire-and-forget approach with persistent, crash-recoverable delivery semantics.

Key Changes

  • Added SQLite-backed outbox (message_outbox table in message-lifecycle.db) with proper indexing for recovery queries and turn tracking
  • Implemented write-ahead delivery pattern: entries are persisted to the outbox before sending, then acked on success or marked for retry on failure
  • Replaced one-shot startup recovery with continuous outbox worker that runs every 1s, recovering pending entries with exponential backoff (5s → 25s → 2m → 10m)
  • Added startup cutoff filter to prevent double-delivery: entries enqueued during the current instance's lifetime with no prior attempts are skipped by the recovery worker (they're actively being delivered on the direct path)
  • Created plugin compatibility layer (compat.ts) that normalizes v1 adapters (sendText/sendMedia) and v2 adapters (sendFinal) to a unified interface, giving plugin channels the same durability as built-in channels
  • Extended channel adapter types with outboundContract field and ChannelOutboundDeliveryMetadata for tracking turn IDs, outbox IDs, and retry attempts
  • Integrated lifecycle worker into gateway startup/shutdown (server-message-lifecycle.ts) with proper cleanup on close
  • Added permanent error detection patterns (chat not found, user blocked, etc.) that immediately mark entries as terminal rather than retrying

Architecture

The outbox worker runs continuously with a time-budgeted recovery pass (75% of interval). Entries go through states: queueddelivered (success) or failed_retryable (transient) or failed_terminal (permanent). The startup cutoff timestamp is captured before the worker starts, ensuring entries enqueued after that point with no prior attempts are recognized as "live deliveries in progress" and skipped to avoid duplication.

Test Coverage

Comprehensive test suite in outbound.test.ts covers:

  • Enqueue/ack lifecycle
  • Retry counting and backoff
  • Startup cutoff filtering (regression test for double-delivery)
  • Max retry enforcement
  • Permanent error detection
  • Partial failure handling in bestEffort mode
  • Recovery time budget enforcement
  • Backoff eligibility checks

All 948 existing tests pass, and E2E tests verify write-ahead delivery, continuous recovery, startup cutoff filtering, and outbox pruning.

Confidence Score: 5/5

  • This PR is safe to merge with high confidence - the implementation is well-architected, thoroughly tested, and handles edge cases correctly
  • The implementation demonstrates excellent engineering practices: proper transaction handling with BEGIN IMMEDIATE, comprehensive error classification (permanent vs transient), clean shutdown handling, startup cutoff logic to prevent double-delivery, extensive test coverage (948 tests pass, includes regression tests), and backward compatibility with legacy file queue import. The write-ahead pattern is correctly implemented with proper cleanup on both success and failure paths. The continuous recovery worker has time-budgeted passes to prevent overlap. No logical errors, race conditions, or security vulnerabilities were identified during review.
  • No files require special attention

Last reviewed commit: 8db9185

Copy link

@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

const wrappedParams = params.onError
? {
...params,
onError: (err: unknown, payload: NormalizedOutboundPayload) => {

P1 Badge Track best-effort send failures without relying on onError

This wrapper only marks hadPartialFailure when a caller provides onError, but deliverOutboundPayloadsCore can still swallow per-payload failures when bestEffort is true and onError is absent. In that case the outbox row is acked as delivered even though one or more sends failed, so recovery will never retry those failed payloads (for example, current bestEffort: true call sites that do not pass onError).

ℹ️ 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".

@nohat
Copy link
Contributor Author

nohat commented Feb 28, 2026

Addressed the P1 Track best-effort send failures without relying on onError from the Codex review.

Change: When bestEffort is true we now always pass a wrapper that sets hadPartialFailure on per-payload failure, whether or not the caller provides onError. So outbox rows are correctly failed (and retried by the recovery worker) when some payloads fail, even for call sites that only pass bestEffort: true (e.g. server-restart-sentinel, server-node-events, delivery-dispatch).

  • deliver.ts: Wrap when params.bestEffort || params.onError; wrapper calls params.onError?.(err, payload) so it’s a no-op when onError is absent.
  • deliver.test.ts: New test that bestEffort partial failure without onError still calls failDelivery and does not ack.

Commit: 2c37968

Copy link

@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: 2c379683c4

ℹ️ 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".

Copy link

@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: 8e5d3dfe1c

ℹ️ 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".

Copy link

@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: 2b34946507

ℹ️ 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 207 to 210
sendPayload: outbound.sendFinal
? async (payload, overrides) =>
outbound.sendPayload!({
outbound.sendFinal({
...resolveCtx(overrides),

Choose a reason for hiding this comment

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

P2 Badge Preserve chunking/media fan-out for inferred sendFinal adapters

createPluginHandler now enables sendPayload whenever outbound.sendFinal exists, but normalizeChannelOutboundAdapter synthesizes sendFinal for plain sendText/sendMedia adapters too. That means any payload with channelData now takes the sendPayload fast path and skips the normal chunking and per-media loop in deliverOutboundPayloadsCore; with the inferred implementation this can drop all but the first mediaUrl (and bypass text chunking), so channelData messages with attachments or long text can be delivered incorrectly.

Useful? React with 👍 / 👎.

Copy link
Contributor Author

@nohat nohat Feb 28, 2026

Choose a reason for hiding this comment

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

Fixed in aabc291: sendPayload is now only set when normalized.contract === "v2" (native sendFinal). Inferred sendFinal from v1 sendText/sendMedia adapters no longer takes the sendPayload fast path, so channelData payloads continue through the normal chunking and per-media fan-out loop.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Already addressed in aabc291, and further refined in 377a635 which also preserves the sendPayload path for v1 adapters with native sendPayload (e.g. LINE).

Copy link

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

ℹ️ 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".

mediaUrl,
}),
sendPayload:
isNativeV2 && outbound.sendFinal

Choose a reason for hiding this comment

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

P1 Badge Preserve sendPayload path for legacy adapters

Restricting sendPayload to normalized.contract === "v2" drops channel-data delivery for existing v1 adapters that still implement sendPayload (for example extensions/line/src/channel.ts handles quick replies/templates via payload.channelData but does not declare outboundContract: "v2"). In that configuration, deliverOutboundPayloadsCore no longer calls the adapter’s sendPayload and falls back to sendText/sendMedia, which silently strips LINE-specific rich message metadata.

Useful? React with 👍 / 👎.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch. Fixed in 377a635: createPluginHandler now also exposes sendPayload when the original v1 adapter implements it natively (e.g. LINE). The isNativeV2 guard still prevents inferred sendFinal from taking the fast path, but v1 adapters with their own sendPayload (like LINE's channelData handler) are passed through directly via hasNativeSendPayload.

Copy link

@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: 377a635868

ℹ️ 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 +180 to +184
queueId = await enqueueDelivery({
channel: deliveryQueueContext.channel,
to: deliveryQueueContext.to,
accountId: deliveryQueueContext.accountId,
payloads: [normalized],

Choose a reason for hiding this comment

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

P1 Badge Preserve dispatch kind when queuing dispatcher payloads

These rows enqueue every normalized payload without recording whether it was a tool, block, or final send, but crash recovery replays all queued rows through deliverOutboundPayloads (see src/gateway/server-message-lifecycle.ts). That means channels that intentionally suppress non-final sends in their live dispatcher (for example src/web/auto-reply/monitor/process-message.ts returns early when info.kind !== "final") can leak those suppressed payloads after a crash if the process dies between enqueueDelivery and ackDelivery; on restart, recovery will treat them as normal outbound messages and send them to users.

Useful? React with 👍 / 👎.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 46581bc. enqueueDelivery now accepts and stores dispatchKind ("tool" / "block" / "final") in the outbox payload JSON. The reply dispatcher passes kind through at the call site.

During recoverPendingDeliveries, after loading pending entries, non-final entries (those with dispatchKind set and not "final") are marked failed_terminal with terminal_reason='non_final_recovery_skip' and excluded from recovery. This is safe because the turn recovery worker replays the entire turn, regenerating tool/block outputs — recovering them from the outbox would bypass channel-specific kind filtering (e.g. web channel suppresses non-final sends in the live path).

Entries without dispatchKind (legacy rows, or entries from deliverOutboundPayloads) are treated as final and recovered normally.

Copy link

@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: 46581bc267

ℹ️ 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".


export const slackOutbound: ChannelOutboundAdapter = {
deliveryMode: "direct",
outboundContract: "v2",

Choose a reason for hiding this comment

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

P1 Badge Keep text/media adapters on v1 until sendFinal is native

Fresh evidence in this commit is that core adapters like Slack are now marked outboundContract: "v2" even though they only implement sendText/sendMedia. That makes createPluginHandler treat them as native v2 and route any channelData payload through sendPayload/sendFinal; for inferred sendFinal, compat.ts only forwards the first media URL, so multi-media and chunked deliveries can be truncated when channelData is present (the fast path is taken whenever effectivePayload.channelData exists). Keep these adapters on v1 until they provide a real sendFinal that preserves fan-out/chunk semantics.

Useful? React with 👍 / 👎.

Comment on lines 161 to 163
} catch (err) {
const code =
err && typeof err === "object" && "code" in err
? String((err as { code?: unknown }).code)
: null;
if (code !== "ENOENT") {
throw err;
}
// Already removed — no-op.
logVerbose(`delivery-queue: ackDelivery failed: ${String(err)}`);
}

Choose a reason for hiding this comment

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

P2 Badge Propagate ackDelivery failures instead of swallowing them

ackDelivery now catches and suppresses every DB error, but recovery still counts the entry as recovered immediately afterward. If the SQLite update fails (for example SQLITE_BUSY during concurrent access), the row remains pending and will be replayed on later worker ticks, producing duplicate outbound sends while logs report success. Letting this error propagate (or returning an explicit failure) would allow recovery to treat it as a failed attempt instead of silently requeueing duplicates.

Useful? React with 👍 / 👎.

@openclaw-barnacle openclaw-barnacle bot added commands Command implementations channel: feishu Channel integration: feishu channel: twitch Channel integration: twitch channel: irc labels Feb 28, 2026
@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

7 similar comments
@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

12 similar comments
@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

Copy link

@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: 03ced37e59

ℹ️ 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".

channel,
to,
accountId: params.ctx.AccountId?.trim() || undefined,
threadId: params.ctx.MessageThreadId,

Choose a reason for hiding this comment

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

P1 Badge Queue the effective thread target for crash recovery

In resolveDeliveryQueueContext, the queued threadId is copied only from ctx.MessageThreadId, but some channels compute the actual reply thread at send time; for example Slack with replyToMode: "first" resolves the first reply thread via replyPlan.nextThreadTs() in src/slack/monitor/message-handler/dispatch.ts while MessageThreadId remains unset for top-level messages. If the process crashes after enqueue and before send, recovery replays through deliverOutboundPayloads without a thread id and the recovered reply is posted to the channel root instead of the intended thread.

Useful? React with 👍 / 👎.

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

Labels

channel: bluebubbles Channel integration: bluebubbles channel: discord Channel integration: discord channel: feishu Channel integration: feishu channel: googlechat Channel integration: googlechat channel: imessage Channel integration: imessage channel: irc channel: line Channel integration: line channel: matrix Channel integration: matrix channel: mattermost Channel integration: mattermost channel: msteams Channel integration: msteams channel: nextcloud-talk Channel integration: nextcloud-talk channel: nostr Channel integration: nostr channel: signal Channel integration: signal channel: slack Channel integration: slack channel: telegram Channel integration: telegram channel: tlon Channel integration: tlon channel: twitch Channel integration: twitch channel: whatsapp-web Channel integration: whatsapp-web channel: zalo Channel integration: zalo channel: zalouser Channel integration: zalouser commands Command implementations gateway Gateway runtime size: XL

Projects

None yet

1 participant