Skip to content

HT-13: inbound threading decision (5-rule algorithm) - #9

Merged
zaridan merged 3 commits into
mainfrom
feat/ht-13-threading-decision
Jul 10, 2026
Merged

HT-13: inbound threading decision (5-rule algorithm)#9
zaridan merged 3 commits into
mainfrom
feat/ht-13-threading-decision

Conversation

@zaridan

@zaridan zaridan commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

The piece that unites the engine: an inbound email is parsed (HT-11), its threading headers checked for a valid reply token (HT-12), and decideThreading returns append to a conversation or start a new one — per specs/mail/threading.md §3.

Pure, no I/O. The five rules, each tested against the HT-7 fixture that motivated it:

  • valid token (In-Reply-To or References) → append
  • forged/tampered token → new conversation (counted as a security signal)
  • no valid token → new conversation, never by subject
  • valid token beats an unrelated subject
  • ordering: In-Reply-To first, then References most-recent-first

Codex (independent GPT lineage) caught a HIGH bug the Claude reviewers missed: In-Reply-To was matched whole, but RFC 5322 allows comments/multiple ids in it — so a decorated header would orphan a real reply into a new conversation. Fixed by tokenizing both headers; +5 CFWS/multi-id tests. thread.ts at 100% coverage, 100 tests total.

Third core threading piece — parse + tokens + decision now route an inbound reply to its conversation end-to-end.

Jira: https://resonantiq.atlassian.net/browse/HT-13

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Improved inbound email threading decisions using verified reply tokens and a dedicated threading decision flow.
    • Added RFC 5322–aware extraction of message-ids from In-Reply-To and References, including handling comments/quoted strings and multiple ids.
    • Added structural-only detection for whether a Message-ID matches the expected reply-token shape.
  • Bug Fixes
    • Prevents subject-only or unrelated message-ids from incorrectly linking to an existing conversation/thread.
    • Ensures forged/tampered tokens don’t attach to real threads, while tracking forged attempts.
  • Tests
    • Expanded coverage for message-id extraction, token-shape detection, and threading edge cases.

Wires the parser (HT-11) + reply tokens (HT-12) into the inbound
threading decision, specs/mail/threading.md §3. Pure decideThreading(
ParsedEmail, Keyring) -> append {conversationId, threadId} | new. Scans
In-Reply-To then References most-recent-first; first valid token wins;
shaped-but-invalid tokens are forged (counted, ignored); no valid token
-> new conversation, never by subject. Adds isReplyTokenShaped() to
reply-token.ts to tell forged-ours from not-ours.

Codex (independent GPT-lineage) adversarial review caught a HIGH bug the
Claude reviewers missed: In-Reply-To was compared whole, but RFC 5322
allows CFWS/comments and multiple ids in it — so 'In-Reply-To: (note)
<valid-token@d>' would mis-thread a real reply to a NEW conversation.
Fixed: both In-Reply-To and References are tokenized (angle-bracketed
ids extracted) before scanning. +5 tests for CFWS/multi-id/whitespace.

thread.ts 100% coverage; typecheck/lint/tests green. Second time Codex's
independent lineage caught a real threading bug — the standing rule earns it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqG66PPZreBrj17VbAqe3b
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f6ea85d0-7a68-4b1e-b918-f0c817920940

📥 Commits

Reviewing files that changed from the base of the PR and between ac8934a and 7d1df73.

📒 Files selected for processing (2)
  • src/mail/message-id.test.ts
  • src/mail/message-id.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/mail/message-id.test.ts
  • src/mail/message-id.ts

📝 Walkthrough

Walkthrough

Changes

Reply-token threading

Layer / File(s) Summary
RFC 5322 message-ID extraction and parsing integration
src/mail/message-id.ts, src/mail/message-id.test.ts, src/mail/parse.ts
Adds comment- and quoted-string-aware message-ID extraction and uses it for ordered References parsing.
Structural reply-token detection
src/mail/reply-token.ts, src/mail/reply-token.test.ts
Adds isReplyTokenShaped for non-throwing structural token checks and tests genuine, forged-shaped, Gmail-style, and malformed identifiers.
Threading decision flow
src/mail/thread.ts
Adds ThreadingDecision and decideThreading, which extracts ordered header candidates, verifies tokens, appends on the first valid token, and counts forged-shaped candidates.
Threading behavior coverage
src/mail/thread.test.ts
Tests valid and forged tokens, header precedence, reference ordering, subject handling, non-token IDs, and embedded message-ID extraction.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ParsedEmail
  participant decideThreading
  participant extractMessageIds
  participant verifyReplyMessageId
  participant Keyring
  ParsedEmail->>decideThreading: In-Reply-To and References
  decideThreading->>extractMessageIds: Extract ordered message IDs
  extractMessageIds-->>decideThreading: Candidate IDs
  decideThreading->>verifyReplyMessageId: Verify candidates
  verifyReplyMessageId->>Keyring: Resolve signing key
  Keyring-->>verifyReplyMessageId: Key material
  verifyReplyMessageId-->>decideThreading: Valid token or failure
  decideThreading-->>ParsedEmail: Append or new decision
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: implementing the inbound threading decision algorithm with five rules.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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 feat/ht-13-threading-decision

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

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

🤖 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/mail/thread.test.ts`:
- Around line 54-56: Update the comment associated with the reply-with-reference
fixture to refer to the human support staff as an “Agent” rather than an
“agent,” using the canonical role terminology consistently.

In `@src/mail/thread.ts`:
- Around line 111-114: The extractMessageIds function incorrectly treats
angle-bracketed text in comments or quoted content as message IDs. Replace its
regex scan with RFC 5322-aware tokenization that skips comments, quoted strings,
and quoted pairs, returning only actual msg-id productions; add a regression
fixture for an In-Reply-To value such as (<token>) and preserve existing
valid-ID behavior.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9f3faeef-cfa7-4cb0-9959-5e9e7a9ce85b

📥 Commits

Reviewing files that changed from the base of the PR and between 6ceba2e and 30cb358.

📒 Files selected for processing (4)
  • src/mail/reply-token.test.ts
  • src/mail/reply-token.ts
  • src/mail/thread.test.ts
  • src/mail/thread.ts

Comment thread src/mail/thread.test.ts
Comment on lines +54 to +56
// Evidence: fixtures/mail/observed/reply-with-reference.json — a customer
// reply whose In-Reply-To points at the agent reply's Message-ID appended
// to the same conversation (threadsCount 3→4, appendedToSameConversation).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the canonical Agent role name.

“agent reply” refers to human support staff; use Agent consistently.

Proposed wording
-  // reply whose In-Reply-To points at the agent reply's Message-ID appended
+  // reply whose In-Reply-To points at an Agent reply's Message-ID appended

As per coding guidelines, human support staff are Agents, and AI actors are Assistants; never conflate them in schema, code, documentation, or prose.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Evidence: fixtures/mail/observed/reply-with-reference.json — a customer
// reply whose In-Reply-To points at the agent reply's Message-ID appended
// to the same conversation (threadsCount 3→4, appendedToSameConversation).
// Evidence: fixtures/mail/observed/reply-with-reference.json — a customer
// reply whose In-Reply-To points at the Agent reply's Message-ID appended
// to the same conversation (threadsCount 3→4, appendedToSameConversation).
🤖 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/mail/thread.test.ts` around lines 54 - 56, Update the comment associated
with the reply-with-reference fixture to refer to the human support staff as an
“Agent” rather than an “agent,” using the canonical role terminology
consistently.

Source: Coding guidelines

Comment thread src/mail/thread.ts Outdated
zaridan and others added 2 commits July 10, 2026 11:50
CodeRabbit flagged that the /<[^>]+>/g bracket-scan (added for the Codex
In-Reply-To fix, and pre-existing in parse.ts's References handling) also
grabs a <...> sitting INSIDE a comment or quoted string — which RFC 5322
says to ignore. Worst case: 'In-Reply-To: (<token-for-B>) <token-for-A>'
would thread to the WRONG conversation.

Fix: a shared src/mail/message-id.ts extractMessageIds() that walks the
header skipping comments (nested, with \ quoted-pairs) and quoted strings,
returning only top-level msg-ids. Wired into BOTH thread.ts (In-Reply-To +
References) and parse.ts (References — same latent bug, now fixed). 12
extractor tests (comments, nested, quoted, escapes, unterminated) + a
thread.ts regression for the token-in-comment wrong-conversation case.

This honors the charter's 'boringly faithful on mail semantics, RFCs as
primary source'. 113 tests total; typecheck/lint green; message-id.ts ~100%.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqG66PPZreBrj17VbAqe3b
Codex re-review of message-id.ts: a > inside the msg-id's own quoted
id-left ("...") or domain-literal ([...]) was terminating the id early
(indexOf('>')), so <"a>b"@x> and <l@[a>b]> mis-parsed. Doesn't affect
OUR tokens (no quoted parts) but contradicted the module's RFC-aware
contract. The <...> scan now tracks quote/bracket substructure + \
quoted-pairs and only accepts a > outside them. +3 tests.

typecheck/lint green; full suite passes; message-id.ts ~100%.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqG66PPZreBrj17VbAqe3b
@zaridan
zaridan merged commit 619b868 into main Jul 10, 2026
5 checks passed
@zaridan
zaridan deleted the feat/ht-13-threading-decision branch July 10, 2026 19:11
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