Skip to content

feat(core): propagate event_id to GatewayReply.reply_to#619

Merged
thepagent merged 4 commits intomainfrom
feat/propagate-event-id-reply-to
Apr 28, 2026
Merged

feat(core): propagate event_id to GatewayReply.reply_to#619
thepagent merged 4 commits intomainfrom
feat/propagate-event-id-reply-to

Conversation

@chaodu-agent
Copy link
Copy Markdown
Collaborator

@chaodu-agent chaodu-agent commented Apr 28, 2026

Problem

OAB core receives event_id in every GatewayEvent but never uses it — the field is marked #[allow(dead_code)]. When GatewayAdapter::send_message builds a GatewayReply, it always sets reply_to: String::new().

This means the gateway cannot correlate replies with inbound events. Specifically, PR #608 (hybrid LINE Reply/Push API) caches reply tokens keyed by event_id, but the cache never hits because reply_to is always empty.

Solution

Add origin_event_id: Option<String> to ChannelRef and propagate it through the message handling pipeline:

  1. src/adapter.rs: Add origin_event_id: Option<String> field to ChannelRef. Manual impl of Hash/Eq/PartialEq excludes this field so channel identity is unaffected.
  2. src/gateway.rs:
    • Remove #[allow(dead_code)] from GatewayEvent.event_id
    • Fill origin_event_id from event_id when constructing ChannelRef from gateway events
    • Use origin_event_id in send_message to populate GatewayReply.reply_to
    • Propagate through create_thread so replies in new threads still carry the event ID
  3. Discord, Slack, cron: Set origin_event_id: None (no behavioral change for non-gateway adapters)

Tests

  • 3 new focused tests for origin_event_id semantics:
    • origin_event_id_excluded_from_eq — same channel, different event IDs are equal
    • origin_event_id_excluded_from_hash — HashMap treats them as same key
    • origin_event_id_survives_clone — simulates create_thread propagation
  • 161 tests pass (158 existing + 3 new)

Impact

  • Gateway (LINE, Telegram): reply_to now carries the originating event_id, enabling feat(gateway): implement hybrid LINE reply/push strategy #608's reply token cache to function correctly
  • Discord, Slack, cron: No behavioral change — origin_event_id is None, reply_to remains ""
  • 5 files changed, +107 −3 lines

Unblocks #608

…ly_to

Add origin_event_id field to ChannelRef so the gateway event_id
survives the message handling pipeline and appears in
GatewayReply.reply_to. This enables the gateway to correlate
replies with inbound events (e.g. LINE reply token cache in #608).

Changes:
- Add origin_event_id: Option<String> to ChannelRef
- Remove #[allow(dead_code)] from GatewayEvent.event_id
- Fill origin_event_id from event_id in gateway event handler
- Use origin_event_id in GatewayAdapter::send_message reply_to
- Propagate through create_thread
- Set origin_event_id: None for Discord, Slack, cron (non-gateway)
@chaodu-agent chaodu-agent requested a review from thepagent as a code owner April 28, 2026 18:41
@github-actions github-actions Bot added pending-screening PR awaiting automated screening closing-soon PR missing Discord Discussion URL — will auto-close in 3 days labels Apr 28, 2026
@github-actions
Copy link
Copy Markdown

⚠️ This PR is missing a Discord Discussion URL in the body.

All PRs must reference a prior Discord discussion to ensure community alignment before implementation.

Please edit the PR description to include a link like:

Discord Discussion URL: https://discord.com/channels/...

This PR will be automatically closed in 3 days if the link is not added.

超渡法師 added 3 commits April 28, 2026 18:50
Manual impl of Hash, Eq, PartialEq so that origin_event_id does not
affect channel identity. Two ChannelRefs pointing to the same channel
are equal regardless of which gateway event they originated from.
- origin_event_id_excluded_from_eq: same channel, different event IDs are equal
- origin_event_id_excluded_from_hash: HashMap treats them as same key
- origin_event_id_survives_clone: simulates create_thread propagation
Explain that reply_to carries the inbound event_id for reply
correlation (e.g. LINE reply token lookup).
@thepagent thepagent merged commit d5f66fd into main Apr 28, 2026
10 checks passed
chaodu-agent pushed a commit to iamninihuang/openab that referenced this pull request Apr 28, 2026
…elog

- Status: Proposed → Accepted
- Add sequence diagram showing hybrid Reply/Push dispatch flow
- Update changelog to v0.2 with openabdev#608 and openabdev#619 references
- Add @iamninihuang as co-author
thepagent pushed a commit that referenced this pull request Apr 28, 2026
* feat(gateway): implement hybrid LINE reply/push strategy

This implementation adds a stateful token cache in the gateway to prioritize the free LINE Reply API. It also includes an auto-fill logic for the reply_to field to avoid modifying the OAB core.

* fix(gateway): track last_event_id per channel to prevent cross-group race conditions

* fix(gateway): address PR #608 review findings

- Remove auto-fill reply_to mechanism that could attach replies to wrong
  LINE messages under concurrent traffic (blocker from shaun-agent,
  超渡法師, 普渡法師). If OAB sends empty reply_to, gateway now correctly
  falls back to Push API instead of guessing the latest event.
- Extract reply token from cache before making HTTP call so Mutex is not
  held across network I/O (blocker from shaun-agent, 超渡法師, 普渡法師).
- Unify sweep TTL with handler TTL (REPLY_TOKEN_TTL_SECS) to eliminate
  dead-weight cache entries in 50-60s range (普渡法師).
- Add secrets.env to .gitignore (shaun-agent, 超渡法師).
- Run rustfmt --edition 2021 (shaun-agent).
- Update ADR Consequences to reflect hybrid reply/push strategy (超渡法師).

* docs(gateway): clarify global reply_token_cache vs per-connection semantics

LINE reply tokens are single-use. Document that the first OAB client to
remove() a cached token gets the free Reply API call; others fall back
to Push API. This is correct behavior but was not documented, which
could confuse maintainers (普渡法師).

* docs: update feature request to reflect removal of auto-fill mechanism

The Per-Client Last Event Tracker was removed during review because it
caused incorrect reply-token association in group chats. Update the
feature request doc to match the actual implementation: gateway does not
infer reply_to, it falls back to Push API when reply_to is empty or
not found in cache (普渡法師 doc nit).

* docs(adr): remove stale auto-fill description from hybrid strategy

The 4-step hybrid strategy description still referenced the removed
Per-Client Last Event Tracker auto-fill mechanism. Updated to accurately
describe the actual implementation: event_id-keyed cache lookup with
empty reply_to falling back to Push API.

* refactor(gateway): address review nits on PR #608

- Switch ReplyTokenCache from tokio::sync::Mutex to std::sync::Mutex;
  critical sections are short and never held across .await (超渡法師).
- Log LINE Reply API 400 response body for easier debugging (超渡法師).
- Guard secrets.env source in run_gateway.sh with existence check (超渡法師).
- Align sweep interval with REPLY_TOKEN_TTL_SECS (50s) (超渡法師).

* fix(gateway): prevent duplicate delivery on Reply API 5xx/network error

- Only fallback to Push API on 4xx (token invalid/expired). For 5xx and
  network errors, the message may have already been delivered — do NOT
  fallback to avoid duplicate delivery (擺渡法師 major finding).
- Update ADR message flow step 7 to reflect hybrid Reply/Push strategy
  instead of Push-only (擺渡法師 doc nit).

* fix(gateway): narrow Reply API fallback to explicit token-unusable only

Only fallback to Push API when LINE's 400 response body explicitly
indicates the reply token is invalid or expired. All other failures
(5xx, other 4xx, network/timeout errors) are treated as ambiguous
delivery — logged as error and dropped to prevent duplicate delivery
(擺渡法師 refined major finding, 普渡法師 concurred).

* fix(gateway): address review findings on PR #608

- Fix operator precedence bug in Reply API 400 fallback: add explicit
  parentheses so (invalid AND reply token) OR expired is evaluated correctly
- Change lock().unwrap() to lock().unwrap_or_else(|e| e.into_inner())
  at all 3 cache lock sites to recover from mutex poison instead of panicking
- Add REPLY_TOKEN_CACHE_MAX (10k) bound: skip insert and log warning
  when cache is full, gracefully degrading to Push API

* docs: remove feature-requests/line-reply-api.md

Feature is now implemented and documented in docs/adr/line-adapter.md.
Keeping a feature request doc for a shipped feature is misleading.

* revert: remove .gitignore change (secrets.env not needed)

* docs(adr): update line-adapter with hybrid dispatch diagram and changelog

- Status: Proposed → Accepted
- Add sequence diagram showing hybrid Reply/Push dispatch flow
- Update changelog to v0.2 with #608 and #619 references
- Add @iamninihuang as co-author

* docs: remove run_gateway.sh, add LINE env vars to gateway README

- Remove run_gateway.sh (README already documents how to run; script
  used port 9090 conflicting with README default 8080)
- Add LINE_CHANNEL_SECRET and LINE_CHANNEL_ACCESS_TOKEN to env vars table
- Add POST /webhook/line to endpoints table

* docs(adr): restore original Date, add Last Updated field

---------

Co-authored-by: iamninihuang <iamninihuang@users.noreply.github.com>
Co-authored-by: iamninihuang <mic-733ao@example.com>
Co-authored-by: 超渡法師 <chaodu-agent@openab.dev>
Co-authored-by: 超渡法師 <chaodu@openab.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

closing-soon PR missing Discord Discussion URL — will auto-close in 3 days pending-screening PR awaiting automated screening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants