Skip to content

feat(api): agent-inbox write paths — reply + status (HT-18) - #15

Merged
zaridan merged 3 commits into
mainfrom
feat/ht-18-agent-inbox-writes
Jul 11, 2026
Merged

feat(api): agent-inbox write paths — reply + status (HT-18)#15
zaridan merged 3 commits into
mainfrom
feat/ht-18-agent-inbox-writes

Conversation

@zaridan

@zaridan zaridan commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Closes HT-18 — and closes the dogfoodable loop: mail in → thread → store → GET (read) → POST reply (mints the token, actually sends) → sent → the customer's reply threads back. Framework-agnostic, on top of HT-17's read layer.

What's here

  • POST /api/v1/conversations/{id}/replies — the Agent sends { text, html? }; the server derives to/from/Re:-subject and the In-Reply-To/References chain from the conversation, then sendReply mints the reply token, persists the outbound thread, and sends via the injected EmailSender. 201 with the created ThreadView; reopens a closed conversation; 404 missing/deleted; 400 bad body; 502 if the provider rejects.
  • PATCH /api/v1/conversations/{id} — close/reopen via a new setConversationStatus (excludes deleted).
  • Router + InboxApiDeps extended (sender/keyring/mailDomain/supportAddress).

Codex adversarial review (reply/token path — standing rule)

Returned DON'T-SHIP with 3 findings; all fixed with tests:

  • HIGH — double-send hole: a send that succeeded but failed to record sent was reported as 502 "not delivered", which would make a client resend. sendReply now returns typed outcomes instead of an ambiguous throw — a send-success-then-mark-failure resolves to ok:true (the message was delivered), never a failure. Regression tests at unit and API level.
  • send-failed is now a distinct result carrying persistedStatus (failed|pending), so the 502 message claims only what's always true ("could not be delivered"), never a false "saved as failed" when the row is actually stuck pending.
  • the reply mints the token from the canonical conversation.id (the fetched row), not the raw path segment — an upper-cased path id can't smuggle a non-canonical id into the outbound Message-ID.

Codex confirmed the header derivation, validation, routing, auth-before-routing, and no-store guarantees are correct.

Testing

Real PGlite + fake/throwing EmailSender: reply happy-path (derived headers + verbatim Message-ID), no-double-Re:, reopen-on-reply, missing/deleted/non-UUID → 404, bad body → 400, provider-reject → 502 (+persisted failed), sent-but-mark-fails → 201 (the double-send regression), PATCH open/closed/404/400, method-routing 405s, auth, no-store. 227 tests pass; typecheck + Biome clean.

A Codex confirm pass on the redesigned sendReply contract is running; I'll note the verdict.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added endpoints to post replies to conversations and to open/close them.
    • Reply creation derives email threading headers and can reopen closed conversations.
  • Bug Fixes
    • Improved delivery failure handling for replies: send failures return spec-aligned errors and persist failed/pending state without throwing.
    • Prevents deleted conversations from being reopened/modified; invalid requests return clearer 400/404 responses.
  • Documentation
    • Updated Agent Inbox API error envelope and request/response contracts for reply and status operations.
  • Tests
    • Expanded routing and inbox API test coverage (including 405 and 502 cases).

Closes the dogfoodable loop: mail in → thread → store → GET → POST reply
(mints token, sends) → sent. Framework-agnostic, on top of HT-17.

- POST /api/v1/conversations/{id}/replies — Agent sends { text, html? };
  server derives to/from/Re:-subject and the In-Reply-To/References chain
  from the conversation, then sendReply mints + persists + sends. 201 with
  the created ThreadView; reopens a closed conversation; 404 missing/deleted;
  400 bad body.
- PATCH /api/v1/conversations/{id} — close/reopen via a new store
  setConversationStatus (excludes deleted).
- Router + InboxApiDeps extended (sender/keyring/mailDomain/supportAddress).

Codex adversarial review (reply/token path, standing rule) → all fixed:
- HIGH double-send hole: a send that SUCCEEDED but failed to record 'sent'
  was reported as 502 "not delivered", which would make a client resend.
  sendReply now returns typed outcomes instead of an ambiguous throw — a
  send-success-then-mark-failure resolves to ok:true (the message WAS
  delivered), never a failure. Regression tests at unit + API level.
- send-failed is now a distinct result carrying persistedStatus
  (failed|pending), so the 502 message claims only what's always true ("could
  not be delivered"), never a false "saved as failed".
- reply mints the token from the CANONICAL conversation.id (the fetched row),
  not the raw path segment, so an upper-cased path id can't embed a
  non-canonical id in the outbound Message-ID.

227 tests pass; typecheck + Biome clean.

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

coderabbitai Bot commented Jul 11, 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: fc2b8c19-82ca-4268-87d2-41f4e685f09a

📥 Commits

Reviewing files that changed from the base of the PR and between 86b1628 and 28ebfa4.

📒 Files selected for processing (2)
  • specs/api/agent-inbox-v1.md
  • src/api/conversations.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • specs/api/agent-inbox-v1.md
  • src/api/conversations.ts

📝 Walkthrough

Walkthrough

The PR adds reply creation and conversation status update endpoints, extends routing and dependency injection, adds outbound delivery result handling, persists status changes, and expands API, store, router, mail, and specification coverage.

Changes

Agent Inbox write paths

Layer / File(s) Summary
Write contracts and route matching
specs/api/agent-inbox-v1.md, src/api/router.ts, src/api/router.test.ts
Defines reply and status-update contracts, adds route variants, validates supported methods, and rejects unmatched trailing paths.
Conversation status persistence
src/store/conversations.ts, src/store/conversations.test.ts
Adds guarded open/closed status updates that exclude deleted conversations and return refreshed summaries.
Reply delivery outcomes
src/mail/send.ts, src/mail/send.test.ts
Returns structured send failures, records failed or pending delivery states, and tolerates failures persisting sent status after provider acceptance.
API handlers and dependency wiring
src/api/conversations.ts, src/api/index.ts
Adds reply and patch handlers, validates bodies and UUIDs, derives mail headers, wires injected dependencies, and maps errors.
End-to-end write-path coverage
src/api/index.test.ts
Adds sender fixtures, request helpers, endpoint coverage, authentication and method-routing checks, delivery failure tests, and construction hardening tests.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant InboxApi
  participant ConversationStore
  participant sendReply
  participant EmailSender
  Client->>InboxApi: POST conversation reply
  InboxApi->>ConversationStore: Read conversation and threads
  InboxApi->>sendReply: Send derived reply
  sendReply->>EmailSender: Deliver outbound email
  EmailSender-->>sendReply: Delivery result
  sendReply-->>InboxApi: Return delivery outcome
  InboxApi->>ConversationStore: Read created thread
  InboxApi-->>Client: ThreadView or mapped error
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: new agent-inbox write paths for replies and conversation status updates.
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-18-agent-inbox-writes

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

… a guaranteed 'failed' persist (HT-18)

Codex confirm residual: the handleReply JSDoc still said the reply is durably
persisted as delivery_status='failed' on a 502. That over-claims — sendReply
returns a send-failed result (no throw) and the row may be 'failed' OR stuck
'pending'. Doc now matches the runtime + the user-safe message.

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

zaridan commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

🧹 Nitpick comments (1)
src/api/index.test.ts (1)

596-612: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Cover the pending persistence outcome too.

sendReply can return 502 while leaving the outbound thread pending if marking it failed throws. Add an end-to-end test for that branch, not only the successful failed transition.

🤖 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/api/index.test.ts` around lines 596 - 612, Extend the reply-sending tests
around sendReply and the existing throwing EmailSender case to cover when
persisting the failed delivery status also throws. Configure the store or
persistence layer so the outbound thread remains pending, assert the API still
returns 502 with the send_failed error shape, then reload the conversation and
verify the outbound thread’s deliveryStatus is pending.
🤖 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 `@specs/api/agent-inbox-v1.md`:
- Around line 139-140: Update the 404 behavior description in the sendReply
specification to remove the claim that nothing is minted; state that no message
is sent and that any token minted before append resolution is discarded when the
conversation is missing or deleted, matching sendReply.
- Around line 123-124: Fix the Markdown code span in the subject description:
change the formatting so `Re:` excludes the trailing space, while explicitly
stating that the prefix is followed by a space and retaining the
case-insensitive, no-double-prefix behavior.
- Around line 142-146: Clarify the `send_failed` contract in the agent-inbox
response documentation: state that the persisted reply may have `delivery_status
= 'failed'`, or remain `pending` if updating it to failed also errors. If the
error-envelope examples are exhaustive, include a `send_failed` example with the
502 response.

---

Nitpick comments:
In `@src/api/index.test.ts`:
- Around line 596-612: Extend the reply-sending tests around sendReply and the
existing throwing EmailSender case to cover when persisting the failed delivery
status also throws. Configure the store or persistence layer so the outbound
thread remains pending, assert the API still returns 502 with the send_failed
error shape, then reload the conversation and verify the outbound thread’s
deliveryStatus is pending.
🪄 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: b98421d9-325f-4c1a-966a-a780a0407a7a

📥 Commits

Reviewing files that changed from the base of the PR and between 151384c and 6dfeaca.

📒 Files selected for processing (10)
  • specs/api/agent-inbox-v1.md
  • src/api/conversations.ts
  • src/api/index.test.ts
  • src/api/index.ts
  • src/api/router.test.ts
  • src/api/router.ts
  • src/mail/send.test.ts
  • src/mail/send.ts
  • src/store/conversations.test.ts
  • src/store/conversations.ts

Comment thread specs/api/agent-inbox-v1.md Outdated
Comment thread specs/api/agent-inbox-v1.md Outdated
Comment thread specs/api/agent-inbox-v1.md Outdated

@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

🧹 Nitpick comments (1)
specs/api/agent-inbox-v1.md (1)

123-124: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Trailing space inside code span (MD038).

`Re: ` has a trailing space inside the backticks, flagged by markdownlint (MD038).

📝 Proposed fix
-- **`subject`** = the conversation's `subject`, prefixed `Re: ` if it isn't already
-  (case-insensitive check — never double-prefix `Re: Re:`).
+- **`subject`** = the conversation's `subject`, prefixed with `"Re: "` if it isn't already
+  (case-insensitive check — never double-prefix `Re: Re:`).
🤖 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 `@specs/api/agent-inbox-v1.md` around lines 123 - 124, Remove the trailing
space from the inline code span in the `subject` description and rephrase the
surrounding text as needed to preserve the intended `Re:` prefix behavior
without placing whitespace inside backticks.

Source: Linters/SAST tools

🤖 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 `@specs/api/agent-inbox-v1.md`:
- Around line 142-147: Update the `502 send_failed` section to avoid
guaranteeing `delivery_status = 'failed'` after an `EmailSender` failure. State
that the reply may be persisted with status `'failed'` or remain `'pending'` if
the status update fails, and describe only that delivery failed without
promising it is saved for retry.

---

Nitpick comments:
In `@specs/api/agent-inbox-v1.md`:
- Around line 123-124: Remove the trailing space from the inline code span in
the `subject` description and rephrase the surrounding text as needed to
preserve the intended `Re:` prefix behavior without placing whitespace inside
backticks.
🪄 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: 2097aefa-9720-4168-a5a5-0dd4ab33eeb8

📥 Commits

Reviewing files that changed from the base of the PR and between 151384c and 86b1628.

📒 Files selected for processing (10)
  • specs/api/agent-inbox-v1.md
  • src/api/conversations.ts
  • src/api/index.test.ts
  • src/api/index.ts
  • src/api/router.test.ts
  • src/api/router.ts
  • src/mail/send.test.ts
  • src/mail/send.ts
  • src/store/conversations.test.ts
  • src/store/conversations.ts

Comment thread specs/api/agent-inbox-v1.md Outdated
CodeRabbit on #15 (3 Minor): the spec still described the OLD reply-failure
behavior after the code was fixed to typed results.
- 502 send_failed: provider-reject → thread 'failed' OR stuck 'pending';
  message says only 'could not be delivered'; note the sent-but-mark-fails →
  201 asymmetry. (was: over-claimed 'saved as failed, will be retried')
- 404: drop the 'nothing is minted' self-contradiction (a token IS minted
  pre-append, then discarded).
- error-envelope code enum made exhaustive (add method_not_allowed,
  send_failed); fix the 'Re: ' code-span MD038 whitespace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqG66PPZreBrj17VbAqe3b
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