Skip to content

Implement message reaction write path and cross-site delivery#258

Open
general-lex wants to merge 1 commit into
mainfrom
claude/add-reaction-support-jd0tU
Open

Implement message reaction write path and cross-site delivery#258
general-lex wants to merge 1 commit into
mainfrom
claude/add-reaction-support-jd0tU

Conversation

@general-lex
Copy link
Copy Markdown
Collaborator

@general-lex general-lex commented Jun 2, 2026

Summary

Completes the message reactions feature by implementing the write path (add/remove reactions) and cross-site delivery. Reactions are toggled via NATS request/reply, written atomically per-cell to Cassandra's four mirror tables, and broadcast to subscribers via the existing NATS gateway infrastructure. The read path (schema, UDTs, inline reads) was already shipped in a prior commit.

Key Changes

Core reaction write logic:

  • history-service/internal/cassrepo/reactions.go — Per-cell UPDATE (add) and DELETE (remove) operations on all four message tables (messages_by_id, messages_by_room, thread_messages_by_thread, pinned_messages_by_room). Writes to messages_by_id first as source of truth; remaining mirrors run concurrently via errgroup.
  • history-service/internal/service/reactions.goReactMessage handler validates shortcode (built-in or custom emoji), checks subscription and message existence, decides add-vs-remove from in-row map, publishes canonical event for downstream workers.

Emoji validation:

  • pkg/emoji/emoji.go + pkg/emoji/allowlist.go — Shortcode validator supporting built-in Unicode emojis (thumbsup, tada, +1, -1, etc.) and site-scoped custom emoji lookups. Enforces wire format: [a-z0-9][a-z0-9_+-]{0,31}, NFC-normalizes input.
  • history-service/internal/mongorepo/custom_emoji.go — MongoDB adapter for custom emoji existence checks.

Event model & downstream dispatch:

  • pkg/model/event.go — New EventReacted event type and ReactionDelta struct (shortcode, action, actor).
  • broadcast-worker/handler.gohandleReacted routes reactions to room-scoped or DM channels (no encryption branch, no message content). Publishes ReactRoomEvent with delta.
  • notification-worker/handler.go — Publishes NotificationType:"reaction" to message author only when action is "added" and actor is not the author.
  • search-sync-worker/messages.go — Skips reaction events (reactions don't change indexed content).

Subject & dedup:

  • pkg/subject/subject.go — New MsgReactPattern and MsgCanonicalReacted subject builders.
  • pkg/natsutil/canonical_dedup.go — Dedup ID includes actor account, shortcode, action, and timestamp to ensure idempotency across retries.

Integration & wiring:

  • history-service/cmd/main.go — Injects UserStore and CustomEmojiStore into service; registers ReactMessage handler.
  • history-service/internal/service/service.go — Adds UserStore and CustomEmojiStore interfaces; updates mockgen directive.
  • history-service/internal/models/message.go — Exports ReactionKey and ReactorInfo type aliases from Cassandra models.

Tests:

  • history-service/internal/cassrepo/reactions_integration_test.go — Integration tests for AddReaction and RemoveReaction across all four mirror tables (top-level, thread reply, pinned).
  • history-service/internal/service/reactions_test.go — Unit tests for validation, subscription checks, message existence, soft-delete blocking, toggle logic, and event publishing.
  • pkg/emoji/emoji_test.go — Shortcode validation tests (built-in, custom, format, length boundaries).
  • broadcast-worker/handler_test.go — Tests for channel-scoped and DM fan-out of reaction events.
  • notification-worker/handler_test.go — Tests for author-only notification on add, no notification on remove.
  • pkg/model/model_test.go — JSON round-trip tests for ReactionDelta and NotificationEvent with reaction payload.
  • `search-sync-

https://claude.ai/code/session_01Dq1dEXdNcRNCchcBu5bcWn

Summary by CodeRabbit

  • New Features

    • Add/remove emoji reactions with real-time room broadcasts, per-recipient DM routing, and author notifications (suppressed for self-reactions)
    • Reactions return NFC-normalized shortcode, action (added/removed), and timestamp to clients
    • Support for site-scoped custom emoji with validation and caching to improve lookup performance
  • Behavior

    • Reaction events are ignored by search indexing (no search updates)
  • Documentation

    • Updated client RPC docs, subject taxonomy, and design/specs for reactions and storage
  • Tests

    • Expanded unit and integration tests covering validation, fan-out, persistence, caching, and notifications

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds message reactions end-to-end: event model, shortcode validator + cache, Mongo custom-emoji repo, Cassandra per-cell reaction writes with mirror fan-out, HistoryService ReactMessage handler, canonical reacted publication, broadcast/notification fan-out, search-sync skip, tests, and docs.

Changes

Message Reactions Feature

Layer / File(s) Summary
Event model and reaction contracts
pkg/model/event.go, pkg/model/custom_emoji.go, pkg/model/model_test.go
Introduces EventReacted, ReactionDelta, ReactRoomEvent, and CustomEmoji; updates NotificationEvent/MessageEvent to carry reaction payloads and adds serialization tests.
Emoji shortcode validator and cache
pkg/emoji/emoji.go, pkg/emoji/cache.go, pkg/emoji/emoji_test.go, pkg/emoji/cache_test.go
Adds Validator (NFC normalization + regex + CustomEmojiLookup) and CachedLookup (LRU+TTL + singleflight) with comprehensive unit tests.
Custom emoji Mongo repository
history-service/internal/mongorepo/custom_emoji.go, history-service/internal/mongorepo/custom_emoji_test.go
Implements CustomEmojiRepo with EnsureIndexes and CustomEmojiExists; integration tests verify per-site isolation and index uniqueness/idempotency.
Cassandra reaction storage
history-service/internal/cassrepo/reactions.go, history-service/internal/cassrepo/reactions_integration_test.go
Implements AddReaction/RemoveReaction with source-table writes, mirror fan-out, two-statement remove touch, error wrapping, and integration tests for top-level, thread, pinned, idempotency, removal, and multi-reaction scenarios.
History service ReactMessage handler
history-service/internal/service/reactions.go, history-service/internal/service/reactions_test.go, history-service/internal/models/message.go
Adds ReactMessage handler: validates/normalizes shortcode, enforces access, resolves actor, toggles reactions via MessageWriter, publishes canonical EventReacted, and returns ReactMessageResponse. Extensive unit tests cover success and error paths.
Service wiring & main wiring
history-service/internal/service/service.go, history-service/internal/service/mocks/mock_repository.go, history-service/cmd/main.go, history-service/internal/config/config.go, tests
Extends HistoryService constructor to accept UserStore, CustomEmojiStore, and historyFloor; initializes emojiValidator; regenerates mocks; wires customEmojiRepo and userStore in main; adds config fields for cache.
NATS subject helpers & dedup
pkg/subject/subject.go, pkg/subject/subject_test.go, pkg/natsutil/canonical_dedup.go, pkg/natsutil/canonical_dedup_test.go, docs/nats-subject-naming.md
Adds MsgCanonicalReacted and MsgReactPattern, extends canonical dedup ID logic for reacted events (detailed key and nil-delta fallback), and documents canonical JetStream subjects.
Broadcast worker fan-out
broadcast-worker/handler.go, broadcast-worker/handler_test.go
Routes EventReacted to handleReacted, validates reaction/timestamps, builds ReactRoomEvent, and publishes per-room or per-user fan-out; tests for channel and DM publish and malformed-event drops.
Notification worker author notifications
notification-worker/handler.go, notification-worker/handler_test.go
Routes EventReacted to handleReaction, only notifies on "added" actions (not removed), suppresses self/missing-author notifications, and publishes author notification payloads. Tests for notification rules and dispatch.
Search-sync worker skip reactions
search-sync-worker/messages.go, search-sync-worker/messages_test.go
Skips EventReacted in indexing path so reaction events produce no Elasticsearch actions; unit test added.
Specifications and API docs
docs/specs/message-reactions.md, docs/superpowers/specs/2026-05-28-message-reactions-write-path-design.md, docs/client-api.md
Adds/updates design/spec and client API msg.react request/response, canonical publication, write-path, shortcode validation contract, and downstream handling documentation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • hmchangw/chat#221: Shares the v3 reactions MAP/UDT model used by the storage and integration tests.
  • hmchangw/chat#64: Related changes to search-sync indexing behavior for MessageEvent types.
  • hmchangw/chat#182: Related broadcast-worker event-dispatch updates.

Suggested labels

ready

Suggested reviewers

  • mliu33
  • vjauhari-work

"I hopped through code where reactions bloom,
NFC-trimmed shortcodes chase away the gloom.
Mirrors and caches keep the state in line,
broadcasts and notes ring out just fine.
A tiny rabbit cheers—feature landed, sublime!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.37% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly and clearly summarizes the main change: implementing the message reaction write path and cross-site delivery, which is the primary focus of this substantial feature addition.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/add-reaction-support-jd0tU

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
pkg/natsutil/canonical_dedup.go (1)

32-39: ⚡ Quick win

Consider logging the nil-delta fallback for observability.

The nil-delta fallback is well-documented and intentional (collisions make programmer errors visible), but there's no runtime logging when this fallback occurs. Adding slog.Error here would make it easier to detect and diagnose publisher-side bugs in production without relying solely on downstream validation.

📊 Proposed addition
 	case model.EventReacted:
 		if evt.ReactionDelta == nil {
+			slog.Error("EventReacted missing ReactionDelta, falling back to bare messageID dedup",
+				"messageID", evt.Message.ID)
 			return evt.Message.ID
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/natsutil/canonical_dedup.go` around lines 32 - 39, Add an observability
log when the nil-delta fallback is used in the EventReacted branch: inside the
case for model.EventReacted, when checking evt.ReactionDelta == nil (the current
early-return that yields evt.Message.ID), emit a structured slog.Error or
slog.Warn that includes identifying context (evt.Message.ID, evt.Event,
evt.Timestamp and any other useful fields) so publisher-side bugs are visible;
keep the existing return value unchanged. Target the code around the
EventReacted case in canonical_dedup.go and the ReactionDelta nil-check to
insert the log.
history-service/internal/mongorepo/custom_emoji.go (1)

39-53: Consider caching custom-emoji existence.

Each non-built-in reaction triggers a Mongo FindOne. Custom emoji sets change rarely and are small per site, so a short-TTL or bounded LRU (the repo already depends on hashicorp/golang-lru/v2) keyed by (siteId, shortcode) would remove a per-reaction round-trip on the hot path. Built-ins already short-circuit before the lookup, so this only affects the custom path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@history-service/internal/mongorepo/custom_emoji.go` around lines 39 - 53,
CustomEmojiExists currently does a Mongo FindOne on every call; add an in-memory
cache (use the existing hashicorp/golang-lru/v2 dependency) on CustomEmojiRepo
to avoid round-trips for hot custom emoji lookups. Modify CustomEmojiRepo to
hold an LRU cache (or a small bounded cache with short TTL semantics stored with
the cached value), key entries by a deterministic string like
siteID+"|"+shortcode, and update CustomEmojiExists to first consult the cache
and return cached results when present; on cache miss, perform the FindOne as
today, then insert the boolean result into the cache (and handle errors
unchanged). Ensure cache is initialized when the repo is created and consider
eviction/TTL to reflect rare updates to custom emoji sets.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@history-service/internal/mongorepo/custom_emoji.go`:
- Around line 39-53: CustomEmojiExists currently does a Mongo FindOne on every
call; add an in-memory cache (use the existing hashicorp/golang-lru/v2
dependency) on CustomEmojiRepo to avoid round-trips for hot custom emoji
lookups. Modify CustomEmojiRepo to hold an LRU cache (or a small bounded cache
with short TTL semantics stored with the cached value), key entries by a
deterministic string like siteID+"|"+shortcode, and update CustomEmojiExists to
first consult the cache and return cached results when present; on cache miss,
perform the FindOne as today, then insert the boolean result into the cache (and
handle errors unchanged). Ensure cache is initialized when the repo is created
and consider eviction/TTL to reflect rare updates to custom emoji sets.

In `@pkg/natsutil/canonical_dedup.go`:
- Around line 32-39: Add an observability log when the nil-delta fallback is
used in the EventReacted branch: inside the case for model.EventReacted, when
checking evt.ReactionDelta == nil (the current early-return that yields
evt.Message.ID), emit a structured slog.Error or slog.Warn that includes
identifying context (evt.Message.ID, evt.Event, evt.Timestamp and any other
useful fields) so publisher-side bugs are visible; keep the existing return
value unchanged. Target the code around the EventReacted case in
canonical_dedup.go and the ReactionDelta nil-check to insert the log.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2d1e66dc-7e33-476e-a6c8-091dc3b28d0b

📥 Commits

Reviewing files that changed from the base of the PR and between 11b360f and f556692.

📒 Files selected for processing (33)
  • broadcast-worker/handler.go
  • broadcast-worker/handler_test.go
  • docs/client-api.md
  • docs/nats-subject-naming.md
  • docs/specs/message-reactions.md
  • docs/superpowers/specs/2026-05-28-message-reactions-write-path-design.md
  • go.mod
  • history-service/cmd/main.go
  • history-service/internal/cassrepo/reactions.go
  • history-service/internal/cassrepo/reactions_integration_test.go
  • history-service/internal/models/message.go
  • history-service/internal/mongorepo/custom_emoji.go
  • history-service/internal/service/integration_test.go
  • history-service/internal/service/messages_test.go
  • history-service/internal/service/mocks/mock_repository.go
  • history-service/internal/service/reactions.go
  • history-service/internal/service/reactions_test.go
  • history-service/internal/service/service.go
  • notification-worker/handler.go
  • notification-worker/handler_test.go
  • pkg/emoji/allowlist.go
  • pkg/emoji/emoji.go
  • pkg/emoji/emoji_test.go
  • pkg/model/cassandra/reactions.go
  • pkg/model/custom_emoji.go
  • pkg/model/event.go
  • pkg/model/model_test.go
  • pkg/natsutil/canonical_dedup.go
  • pkg/natsutil/canonical_dedup_test.go
  • pkg/subject/subject.go
  • pkg/subject/subject_test.go
  • search-sync-worker/messages.go
  • search-sync-worker/messages_test.go

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/emoji/emoji_test.go (1)

172-173: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix the NFC equivalence test: both forms are currently identical.

The test claims to verify that precomposed and decomposed Unicode forms collapse under NFC normalization, but both precomposed and decomposed are set to the same string literal "é". This means the test verifies the same input twice instead of proving that NFC collapses equivalent forms.

🧪 Proposed fix using explicit Unicode escape sequences
-	// "é" in two forms: precomposed (U+00E9) vs decomposed (U+0065 U+0301).
-	precomposed := "é"
-	decomposed := "é"
+	// "é" in two forms: precomposed (U+00E9) vs decomposed (U+0065 U+0301).
+	precomposed := "\u00e9"           // é as a single codepoint
+	decomposed := "\u0065\u0301"      // e + combining acute accent

Alternatively, construct the decomposed form programmatically:

+import "golang.org/x/text/unicode/norm"
+
 func TestValidator_Validate_NFC_CollapsesEquivalentForms(t *testing.T) {
 	v := emoji.NewValidator(&stubLookup{})
 
-	// "é" in two forms: precomposed (U+00E9) vs decomposed (U+0065 U+0301).
-	precomposed := "é"
-	decomposed := "é"
+	// "é" in two forms: precomposed (U+00E9) vs decomposed (U+0065 U+0301).
+	precomposed := "é"
+	decomposed := norm.NFD.String("é")  // decompose to e + combining acute

Either approach ensures the test actually verifies that two different byte sequences representing the same visual character are handled consistently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/emoji/emoji_test.go` around lines 172 - 173, The NFC equivalence test is
wrong because precomposed and decomposed are identical; change the test so
precomposed is the single-codepoint form and decomposed is the base letter plus
combining accent (or construct decomposed programmatically using runes) so they
are different byte sequences that normalize to the same NFC; update the
variables precomposed and decomposed in emoji_test.go (used by the test function
that asserts NFC collapse) to use explicit Unicode escape sequences (e.g.,
U+00E9 vs U+0065 U+0301) or build decomposed with runes before running the
normalization/assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@pkg/emoji/emoji_test.go`:
- Around line 172-173: The NFC equivalence test is wrong because precomposed and
decomposed are identical; change the test so precomposed is the single-codepoint
form and decomposed is the base letter plus combining accent (or construct
decomposed programmatically using runes) so they are different byte sequences
that normalize to the same NFC; update the variables precomposed and decomposed
in emoji_test.go (used by the test function that asserts NFC collapse) to use
explicit Unicode escape sequences (e.g., U+00E9 vs U+0065 U+0301) or build
decomposed with runes before running the normalization/assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 51427407-3e7a-4546-b1a9-695ae1d24f04

📥 Commits

Reviewing files that changed from the base of the PR and between 3c72b76 and b18d1b1.

📒 Files selected for processing (5)
  • docs/client-api.md
  • docs/specs/message-reactions.md
  • docs/superpowers/specs/2026-05-28-message-reactions-write-path-design.md
  • pkg/emoji/emoji.go
  • pkg/emoji/emoji_test.go
✅ Files skipped from review due to trivial changes (2)
  • docs/specs/message-reactions.md
  • docs/superpowers/specs/2026-05-28-message-reactions-write-path-design.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/client-api.md
  • pkg/emoji/emoji.go

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
notification-worker/handler_test.go (1)

305-307: ⚡ Quick win

Use getRecords() for consistency.

Accessing pub.records directly is inconsistent with the pattern used elsewhere in this file (e.g., lines 88, 149, 219, 258, 281). While safe in this single-threaded test, using getRecords() maintains consistency and avoids establishing a pattern that could be problematic in concurrent contexts.

♻️ Suggested refactor
-	if len(pub.records) != 0 {
-		t.Errorf("expected no notification publish on malformed event, got %d records", len(pub.records))
+	if got := len(pub.getRecords()); got != 0 {
+		t.Errorf("expected no notification publish on malformed event, got %d records", got)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@notification-worker/handler_test.go` around lines 305 - 307, Replace the
direct access to pub.records with the accessor method getRecords() for
consistency with other tests; locate the assertion that checks the length
(currently using pub.records) and call pub.getRecords() to get the slice, then
assert its length (e.g., check len(pub.getRecords()) == 0) so the test uses the
same getRecords() pattern as in other cases (lines referencing pub.getRecords())
and avoids direct field reads.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@notification-worker/handler_test.go`:
- Around line 305-307: Replace the direct access to pub.records with the
accessor method getRecords() for consistency with other tests; locate the
assertion that checks the length (currently using pub.records) and call
pub.getRecords() to get the slice, then assert its length (e.g., check
len(pub.getRecords()) == 0) so the test uses the same getRecords() pattern as in
other cases (lines referencing pub.getRecords()) and avoids direct field reads.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f8fdf582-2f99-4535-8287-d49ac601aee4

📥 Commits

Reviewing files that changed from the base of the PR and between 169419c and 438142d.

📒 Files selected for processing (4)
  • broadcast-worker/handler.go
  • broadcast-worker/handler_test.go
  • notification-worker/handler.go
  • notification-worker/handler_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • notification-worker/handler.go

Adds message reactions on top of the v3 reactions schema:

- Storage: per-cell map UPDATE/DELETE on messages_by_id (source of
  truth) plus the room-or-thread mirror; pinned table deliberately
  excluded since the FE does not render reactions in the pinned panel.
- Validation: pkg/emoji.Validator (NFC + 256B input cap + regex +
  custom-emoji Mongo lookup) with pkg/emoji.CachedLookup as the
  LRU+TTL+singleflight cache so the hot path doesn't hit Mongo per
  request.
- Handler: history-service.ReactMessage — input checks, validate,
  access check, findMessage, resolve actor, in-row toggle decision,
  Cassandra write, best-effort canonical publish on
  chat.msg.canonical.{siteID}.reacted.
- Fan-out: broadcast-worker.handleReacted (channel + DM routing, no
  encryption branch), notification-worker.handleReaction (author-only,
  added-only, no self-react), search-sync-worker skips reaction
  events. Both reaction-consumer guards on malformed payloads
  log-and-Ack rather than NAK to avoid poison-pill loops.
- Model: EventReacted + ReactionDelta + ReactRoomEvent +
  ReactionAction defined type. ReactionAction propagated through
  ReactMessageResponse and the live event so a typo fails to compile.
- Subjects: MsgReactPattern + MsgCanonicalReacted via pkg/subject.
- Dedup: CanonicalDedupID extended for EventReacted (actor +
  shortcode + action + timestamp); nil-delta is a 'loud collision'
  fallback to the bare messageID by design.
- Custom emoji Mongo adapter with EnsureIndexes for the
  (siteId, shortcode) unique compound index — the only dedup mechanism.

Documented design caveats: live event ordering best-effort under
JetStream redelivery (clients reconcile via REST refetch), reactions-
specific retry caveat on mirror torn state (same root cause as
edit/delete), frozen-UDT extensibility caveat on the schema, custom
emoji collection populated out-of-band today, no handler-level
deadline (per-driver bound convention).

https://claude.ai/code/session_01Dq1dEXdNcRNCchcBu5bcWn
@general-lex general-lex force-pushed the claude/add-reaction-support-jd0tU branch from 79d72dd to 4b4fc8d Compare June 3, 2026 10:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants