Skip to content

Answer top-level channel mentions; re-introduce hardened dedup (v0.1.19)#31

Merged
t2tx merged 4 commits into
mainfrom
fix/top-level-mention-dedup
Jun 28, 2026
Merged

Answer top-level channel mentions; re-introduce hardened dedup (v0.1.19)#31
t2tx merged 4 commits into
mainfrom
fix/top-level-mention-dedup

Conversation

@t2tx

@t2tx t2tx commented Jun 28, 2026

Copy link
Copy Markdown
Owner

概要

トップレベルのチャンネル mention が無応答になるバグを修正し、ロールバックしていた dedup + 権限世代検証のハードニングを 修正版で再導入 します。今後の開発は本 OSS リポジトリをベースに一本化します。

バグ(トップレベル mention 無応答)

症状: チャンネルでスレッド外に @iris すると無応答。

根本原因: Slack はトップレベル mention に対し messageapp_mention の 2 イベントを同一 client_msg_id で配信する。message ハンドラが先に走り acceptMessage で共有 seen-id を消費するが、トップレベルの channel post は handleChannelMessagethread_ts 無しで drop する(本来 app_mention 担当)。その後 app_mention ハンドラが自分のイベントを duplicate と誤判定 → 返信されない。

実機で再現・確定(dev ログでイベント順序とパスを観測)。

修正

  • acceptMessageトップレベル channel post を dedup の前に return(seen-id を消費しない)。これで app_mention が自分のイベントを fresh として処理できる。
  • acceptMessage と inbound 型を src/slack/messages.ts に純粋関数として切り出し、SeenSet/now を注入可能化。
  • 回帰テスト src/slack/messages.test.ts(9 ケース、トップレベルが id を消費しないことを含む)。

スレッド内 mention・DM は影響なし(message ハンドラが処理する経路)。

検証

  • 実機(dev): 修正前=無応答 / 修正後=正常応答 をログで対比確認。
  • pnpm verify 全通過、117 tests pass

再導入した内容(v0.1.18 → v0.1.19)

at-least-once dedup(src/dedup.ts)、権限ボタンの世代検証(instanceId)、プロセス exit 時の pending 権限 drain。いずれも以前 CodeRabbit 承認済みの正当なハードニング。今回そのバグを直した上で再導入。

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added Slack inbound message filtering with thread-aware, TTL-based de-duplication to avoid repeated prompts.
    • Introduced per-run process instance scoping for permission prompts to better isolate concurrent/respawned activity.
  • Bug Fixes
    • Prevents duplicate Slack event handling (including retries) while allowing processing again after TTL expiry.
    • Hardened stale permission-button clicks so only the active generation can resolve them.
  • Chores
    • Bumped package version to 0.1.19.
  • Tests
    • Expanded coverage for de-duplication, message acceptance, and instance-scoped permission/session behavior.

…v0.1.19)

Re-introduce the at-least-once dedup + permission-generation hardening
(previously rolled back), and fix a real bug in it: a top-level channel
mention went unanswered.

Root cause: Slack delivers both a `message` and an `app_mention` event for the
same top-level mention, sharing one client_msg_id. The `message` handler ran
first and consumed the shared seen-id in acceptMessage, even though a top-level
channel post is handled by app_mention (handleChannelMessage drops it). The
app_mention handler then rejected its own event as a duplicate → no reply.

Fix: acceptMessage now returns BEFORE dedup for top-level channel posts, so the
id is never consumed and app_mention can claim it as fresh. Extracted
acceptMessage + inbound types into src/slack/messages.ts as a pure, injectable
function and added regression tests (verified in dev: pre-fix unanswered,
post-fix answers correctly).

Threaded mentions and DMs are unaffected (message handler processes those).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 057012ed-0e71-45d7-b32b-8ef2417aa82f

📥 Commits

Reviewing files that changed from the base of the PR and between 41ef633 and dca1aac.

📒 Files selected for processing (1)
  • src/claude.ts

Walkthrough

Adds Slack event deduplication and filtering, per-process Claude instance tracking, instanceId-scoped permission handling, and entrypoint wiring for duplicate suppression and stale-permission cleanup. The package version is also bumped.

Changes

Dedup and generation-aware permissions

Layer / File(s) Summary
SeenSet TTL dedup module
src/dedup.ts, src/dedup.test.ts
New SeenSet class with configurable TTL, timestamp refresh, max-size eviction via Map insertion order, and tests covering TTL, retries, undefined ids, and capacity eviction.
Slack message filtering and acceptMessage
src/slack/messages.ts, src/slack/messages.test.ts
New SlackFile/InboundMessage interfaces, isActionableMessage filtering, and acceptMessage deduping via SeenSet; tests cover bot, subtype, thread, text, and retry cases.
ClaudeProcess.instanceId
src/claude.ts
Adds a module-scoped counter and a readonly instanceId on each ClaudeProcess.
PermissionRegistry instanceId and has()
src/permission.ts, src/permission.test.ts
PendingPermission stores instanceId; register accepts it; drainSession can filter by instanceId; has(actionKey) is added and covered by updated tests.
SessionManager stale-click protection and onExit
src/session.ts
onPermission and onExit carry instanceId, and respondPermission rejects clicks that do not match the active process instance.
index.ts wiring
src/index.ts
Wires SeenSet and acceptMessage into Slack event handling, adds postPermission, drains permissions on exit by instance, and passes instanceId into permission responses.
Version bump
package.json
Version updated from 0.1.18 to 0.1.19.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • t2tx/iris#29: Changes the same deduplication and instanceId-aware permission flow across src/dedup.ts, src/slack/messages.ts, src/permission.ts, src/session.ts, and src/index.ts.
  • t2tx/iris#30: Reverts the dedup and instanceId-related changes introduced here.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% 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 title accurately summarizes the main fixes: top-level channel mentions and dedup hardening, and it is concise and specific.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/top-level-mention-dedup

Comment @coderabbitai help to get the list of available commands.

@t2tx t2tx marked this pull request as draft June 28, 2026 02:53
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@t2tx t2tx marked this pull request as ready for review June 28, 2026 02:56

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
src/permission.ts (1)

40-68: 🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Bind pending permissions to an opaque action key, not just requestId.

instanceId is stored, but the registry still overwrites/resolves by req.requestId. Since the Slack button value is also only requestId, a stale old button can resolve a newer pending entry with the same reused request ID and then pass the newer instanceId check. Use a unique per-button key, e.g. instanceId + requestId or a random token, as the registry key and Slack action value while keeping requestId only for the Claude control response.

As per path instructions, src/**/*.ts must keep an eye on “permission bridging (Block Kit allow/deny)” and “session mapping (Slack thread_ts ⇄ Claude session_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 `@src/permission.ts` around lines 40 - 68, The pending-permission registry is
keyed only by req.requestId, which allows stale Slack actions to collide with
newer entries and bypass the instanceId guard. Update Permission registry
methods like register, resolve, and has to use an opaque per-button action key
(for example a composite of instanceId plus requestId or a generated token) as
the stored lookup key, and pass that same key through the Slack Block Kit button
value for allow/deny actions. Keep req.requestId only for the Claude control
response, and ensure the permission bridging and session mapping code still
resolve the correct pending entry per Slack thread/session.

Source: Path instructions

🤖 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.

Inline comments:
In `@src/index.ts`:
- Around line 143-177: The postPermission flow registers the request before the
Slack calls, but if flushStream or post fails the permission stays registered
and can dead-end. Update postPermission to wrap the awaited Slack section in
error handling and, on rejection, clean up the pending entry via the permissions
API (or mark it failed) before rethrowing or returning. Use the existing
postPermission and permissions.register/permissions.has flow to locate the
cleanup point.

---

Outside diff comments:
In `@src/permission.ts`:
- Around line 40-68: The pending-permission registry is keyed only by
req.requestId, which allows stale Slack actions to collide with newer entries
and bypass the instanceId guard. Update Permission registry methods like
register, resolve, and has to use an opaque per-button action key (for example a
composite of instanceId plus requestId or a generated token) as the stored
lookup key, and pass that same key through the Slack Block Kit button value for
allow/deny actions. Keep req.requestId only for the Claude control response, and
ensure the permission bridging and session mapping code still resolve the
correct pending entry per Slack thread/session.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e2de83ef-cc54-41fc-9e11-1ad9103b66b6

📥 Commits

Reviewing files that changed from the base of the PR and between a238f45 and fad90f5.

📒 Files selected for processing (10)
  • package.json
  • src/claude.ts
  • src/dedup.test.ts
  • src/dedup.ts
  • src/index.ts
  • src/permission.test.ts
  • src/permission.ts
  • src/session.ts
  • src/slack/messages.test.ts
  • src/slack/messages.ts

Comment thread src/index.ts
…ailure

Address two CodeRabbit findings on the permission bridge:

1) (Critical) The registry was keyed by req.requestId alone. Claude can reuse a
   request_id across process generations, so a stale button could resolve a
   newer pending entry and then pass the instanceId guard. Key the registry and
   the Slack button value by an opaque `instanceId:requestId` action key
   instead; requestId is kept only for the Claude control response. A stale
   button now finds no matching entry. Added a regression test.

2) (Major) If posting the buttons to Slack failed, the registration was left
   dangling. postPermission now drops it (resolve) and logs the error.

Verified in dev (manual mode): allow and deny both work end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@src/permission.ts`:
- Around line 10-15: The action key generation in `permission.ts` is still
repeatable across app restarts because `instanceId` is only a module-level
counter combined with `requestId`, so stale Slack buttons can collide with new
pending entries after a restart. Update the `instanceId`/action-key creation
path to include a boot-scoped non-repeating token (for example a random nonce or
UUID initialized once per process) and carry it through the permission flow used
by `requestId`, `registerPendingAction`, and the Slack button value. Ensure the
resulting key remains unique for the lifetime of the app process and cannot
match a previous restart’s pending action.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 27bf9f94-9a2c-4be2-8e95-806683558725

📥 Commits

Reviewing files that changed from the base of the PR and between b179494 and 41ef633.

📒 Files selected for processing (3)
  • src/index.ts
  • src/permission.test.ts
  • src/permission.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/index.ts

Comment thread src/permission.ts
… collisions

The permission action key embeds instanceId, but nextInstanceId reset to 1 on
every Iris restart. A stale Slack button from before a restart could then
collide with a new pending entry if Claude reused the same request_id. Seed the
counter from a random boot offset so ids do not repeat across restarts, while
staying monotonic within a run. (CodeRabbit follow-up.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@t2tx t2tx merged commit 320e642 into main Jun 28, 2026
5 checks passed
@t2tx t2tx deleted the fix/top-level-mention-dedup branch June 28, 2026 03:33
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.

1 participant