Skip to content

fix(threads): typed ThreadNotFound error + skip Sentry for stale-thread RPCs (OPENHUMAN-TAURI-4H, -60)#1570

Open
oxoxDev wants to merge 9 commits into
tinyhumansai:mainfrom
oxoxDev:fix/thread-not-found-noise
Open

fix(threads): typed ThreadNotFound error + skip Sentry for stale-thread RPCs (OPENHUMAN-TAURI-4H, -60)#1570
oxoxDev wants to merge 9 commits into
tinyhumansai:mainfrom
oxoxDev:fix/thread-not-found-noise

Conversation

@oxoxDev
Copy link
Copy Markdown
Contributor

@oxoxDev oxoxDev commented May 12, 2026

Summary

  • Adds a typed ThreadsError::NotFound path for stale conversation thread references.
  • Normalizes missing-thread display text to thread <id> not found.
  • Returns structured JSON-RPC error data with kind: "ThreadNotFound" and skips Sentry reporting for this expected user state.
  • Teaches the app RPC client and thread store to clear stale thread state, refresh the thread list, and suppress user-facing error toast noise.
  • Adds backend, JSON-RPC, Sentry transport, and frontend tests for stale-thread handling.

Problem

  • Stale tabs or deleted/expired thread IDs can cause openhuman.threads_message_append and openhuman.threads_generate_title to run against threads that no longer exist.
  • These are expected user-state races, but they were reported through report_error, creating Sentry noise under domain=rpc operation=invoke_method.
  • The backend emitted two string shapes (does not exist and not found), and the frontend had no structured discriminator to branch on safely.

Solution

  • Introduce ThreadsError::NotFound { thread_id } and canonical parsing for both current and legacy missing-thread text.
  • Map stale thread failures from threads_message_append and threads_generate_title to the typed error.
  • At the JSON-RPC boundary, classify ThreadNotFound only for openhuman.threads_* methods, return structured error data, and skip report_error.
  • Add a Sentry TestTransport smoke test proving ThreadNotFound does not emit an event while unrelated RPC errors still do.
  • Extend frontend RPC error taxonomy with thread_not_found; clear stale thread refs, reload threads, avoid retrying the failed RPC, and suppress the send-error toast for the benign stale-thread case.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • Diff coverage ≥ 80% — not run locally (pnpm test:coverage / pnpm test:rust not run); CI diff-cover is the authoritative check.
  • Coverage matrix updated — N/A: no feature rows added, removed, or renamed.
  • All affected feature IDs from the matrix are listed in the PR description under ## Related — N/A: Sentry-noise cleanup is not represented by a matrix feature ID.
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • Manual smoke checklist updated if this touches release-cut surfaces — N/A: no release smoke checklist surface changed.
  • Linked issue closed via Closes #NNN in the ## Related section — N/A: source task tracks Sentry issue IDs, not a GitHub issue number.

Impact

  • Desktop/app thread UX: stale thread references now disappear quietly instead of causing repeated failed sends/title-generation attempts or visible error toasts.
  • Core observability: expected stale-thread RPC failures no longer report to Sentry; unrelated RPC failures still report normally.
  • Compatibility: JSON-RPC error message remains human-readable while adding structured data for newer clients.
  • No migration or external network dependency changes.

Related

  • Closes OPENHUMAN-TAURI-4H
  • Closes OPENHUMAN-TAURI-60
  • Follow-up PR(s)/TODOs: none

AI Authored PR Metadata (required for Codex/Linear PRs)

Keep this section for AI-authored PRs. For human-only PRs, mark each field N/A.

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: fix/thread-not-found-noise
  • Commit SHA: be9647f596d185a416edf69749232f7a5aa82f63

Validation Run

  • pnpm --filter openhuman-app format:check (also passed in pre-push hook)
  • pnpm typecheck
  • Focused tests:
    • cargo test --lib thread_not_found_rpc_error -- --nocapture
    • cargo test --lib openhuman::threads
    • cargo test --test json_rpc_e2e
    • pnpm debug unit src/services/__tests__/coreRpcClient.test.ts src/store/__tests__/threadSlice.test.ts
    • pnpm --filter openhuman-app test:unit
  • Rust fmt/check (if changed):
    • cargo fmt --check --all
    • cargo check --manifest-path Cargo.toml
    • cargo test --lib openhuman::threads
    • cargo test --test json_rpc_e2e
  • Tauri fmt/check (if changed):
    • cargo fmt --manifest-path app/src-tauri/Cargo.toml --all --check
    • pnpm --filter openhuman-app rust:check

Validation Blocked

  • command: cargo clippy --workspace -- -D warnings
  • error: fails on pre-existing workspace warnings/errors outside this patch, including unused imports in src/openhuman/agent/harness/builtin_definitions.rs, src/openhuman/channels/providers/telegram/channel.rs, src/openhuman/composio/providers/profile.rs, clippy::empty_line_after_doc_comments in src/openhuman/tools/impl/agent/onboarding_status.rs, and many existing derivable_impls / manual_pattern_char_comparison / too_many_arguments findings.
  • impact: workspace clippy remains blocked independently of this branch; changed behavior is covered by the focused Rust/JSON-RPC/frontend tests above.

Behavior Changes

  • Intended behavior change: missing thread RPCs are treated as expected stale-client state instead of reportable backend failures.
  • User-visible effect: stale selected/active threads are cleared and the list is refreshed without a toast or retry loop.

Parity Contract

  • Legacy behavior preserved: unrelated RPC errors still call report_error; session-expired suppression remains unchanged.
  • Guard/fallback/dispatch parity checks: JSON-RPC classification is scoped to openhuman.threads_* and rejects unrelated not found strings such as message-level misses.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): N/A
  • Canonical PR: this PR
  • Resolution (closed/superseded/updated): N/A

Summary by CodeRabbit

  • New Features

    • Backend now returns structured error payloads so the app can reliably detect "thread not found" conditions.
  • Bug Fixes

    • App detects missing/deleted threads, clears stale selection and cached messages, and auto-refreshes the thread list instead of showing generic errors.
    • Standardized user-facing "thread ... not found" messages for missing-thread cases.
  • Tests

    • Expanded unit and end-to-end tests covering thread-not-found flows and observability behavior.

Review Change Stack

@oxoxDev oxoxDev requested a review from a team May 12, 2026 15:50
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

📝 Walkthrough

Walkthrough

Structured "thread not found" errors are added end-to-end: Rust introduces ThreadsError and structured envelopes, rpc_handler propagates error.data, TypeScript CoreRpcError/classifyRpcError recognize the payload, Redux clears stale threads and refreshes lists, UI suppresses send error, and tests cover the flow.

Changes

Thread-not-found structured error handling

Layer / File(s) Summary
Rust error type system
src/openhuman/threads/error.rs, src/openhuman/threads/mod.rs
Adds ThreadsError (NotFound/Message), THREAD_NOT_FOUND_KIND, parsing of legacy/canonical messages, and conversions that emit a structured sentinel envelope for NotFound.
Thread operations and store normalization
src/openhuman/threads/ops.rs, src/openhuman/memory/conversations/store.rs
message_append and thread_generate_title now return ThreadsError and map store errors; conversation store error text standardized to "thread {id} not found".
JSON-RPC structured error payload
src/rpc/structured_error.rs, src/core/jsonrpc.rs, src/rpc/mod.rs
Adds StructuredRpcError sentinel encoding/decoding, rpc_handler decodes structured envelopes, includes error.data in responses, and suppresses Sentry reporting for expected-user-state errors.
TypeScript CoreRpcError classification
app/src/services/coreRpcClient.ts, app/src/services/__tests__/coreRpcClient.test.ts
Extends CoreRpcError with data, adds thread_not_found kind, classifyRpcError accepts data and detects thread-not-found first, and exports isThreadNotFoundCoreRpcError. Tests added for structured error.data classification.
Redux thread slice state management
app/src/store/threadSlice.ts, app/src/store/__tests__/threadSlice.test.ts
Adds THREAD_NOT_FOUND_MESSAGE, clearStaleThread reducer, thunks detect thread-not-found to clear stale thread state and refresh threads; loadThreads validates selected/active IDs. Tests cover reducer and thunk behaviors.
UI error handling in Conversations
app/src/pages/Conversations.tsx
Conversations imports THREAD_NOT_FOUND_MESSAGE and clears send error / returns early when addMessageLocal rejects with that message.
Rust tests and end-to-end
src/openhuman/threads/ops_tests.rs, tests/json_rpc_e2e.rs, src/core/jsonrpc_tests.rs
Adds ops tests asserting ThreadsError::NotFound, RPC handler tests verifying structured data passes through and Sentry suppression, and an end-to-end test asserting JSON-RPC error data.kind = "ThreadNotFound" and thread_id.
Cargo: sentry test feature
Cargo.toml
Enables test feature for sentry to support TestTransport in tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • senamakel
  • graycyrus

Poem

🐰 I found a thread that hopped away,
I wrapped its absence up in JSON play.
From Rust to JS the sentinel sings,
No noisy Sentry, just tidy cleanings.
Hooray for rabbits fixing broken threads!

🚥 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 PR title accurately describes the main change: introducing a typed ThreadNotFound error and implementing Sentry skip behavior for stale-thread RPCs, with specific issue references.
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.

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


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

Copy link
Copy Markdown
Contributor

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/src/store/threadSlice.ts (1)

142-155: ⚡ Quick win

Deduplicate stale-thread recovery flow across thunks.

The stale-thread branch is repeated in three places (clear local state → refresh threads → dev log → reject standardized message). Extracting this into one helper will reduce drift risk when recovery behavior changes.

Also applies to: 192-205, 221-234

🤖 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 `@app/src/store/threadSlice.ts` around lines 142 - 155, Extract the repeated
stale-thread recovery sequence into a single helper (e.g.,
handleStaleThreadRecovery) and call it from each thunk instead of duplicating
logic: the helper should accept (dispatch, payload.threadId, rejectWithValue)
and perform the same steps currently repeated — call
isThreadNotFoundCoreRpcError flow’s clearStaleThread via
dispatch(clearStaleThread(threadId)), await dispatch(loadThreads()).unwrap()
inside a try/catch that logs the refreshError when IS_DEV (same debug message
text), and finally return rejectWithValue(THREAD_NOT_FOUND_MESSAGE); replace the
three duplicated blocks (the one around addMessageLocal and the blocks at the
other locations) with calls to this helper to centralize behavior and avoid
drift.
🤖 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/core/jsonrpc.rs`:
- Around line 63-73: The jsonrpc handler currently contains a domain-specific
branch using thread_not_found_rpc_error_data(&method, &message) and logging a
thread-specific "thread-not-found" message; remove this domain-specific
classification from rpc_handler/jsonrpc.rs and instead surface a structured
ThreadNotFound error through the controller/error boundary (e.g., return a
domain error variant or attach a ThreadNotFound payload from the
controller/handler code so jsonrpc.rs only sees a generic error enum). Update
the controller/registered handler that currently recognizes openhuman.threads_*
to produce the ThreadNotFound structured error (so
thread_not_found_rpc_error_data is no longer used in jsonrpc.rs), keep the
generic is_session_expired_error(&message) check in jsonrpc.rs, and ensure
jsonrpc.rs treats the propagated ThreadNotFound as a general error
(logging/metrics) without domain-specific fields so transport layer stays
generic.

---

Nitpick comments:
In `@app/src/store/threadSlice.ts`:
- Around line 142-155: Extract the repeated stale-thread recovery sequence into
a single helper (e.g., handleStaleThreadRecovery) and call it from each thunk
instead of duplicating logic: the helper should accept (dispatch,
payload.threadId, rejectWithValue) and perform the same steps currently repeated
— call isThreadNotFoundCoreRpcError flow’s clearStaleThread via
dispatch(clearStaleThread(threadId)), await dispatch(loadThreads()).unwrap()
inside a try/catch that logs the refreshError when IS_DEV (same debug message
text), and finally return rejectWithValue(THREAD_NOT_FOUND_MESSAGE); replace the
three duplicated blocks (the one around addMessageLocal and the blocks at the
other locations) with calls to this helper to centralize behavior and avoid
drift.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 56935af7-5556-4fb1-b09e-74c0735985d0

📥 Commits

Reviewing files that changed from the base of the PR and between 15c7442 and be9647f.

📒 Files selected for processing (14)
  • Cargo.toml
  • app/src/pages/Conversations.tsx
  • app/src/services/__tests__/coreRpcClient.test.ts
  • app/src/services/coreRpcClient.ts
  • app/src/store/__tests__/threadSlice.test.ts
  • app/src/store/threadSlice.ts
  • src/core/jsonrpc.rs
  • src/core/jsonrpc_tests.rs
  • src/openhuman/memory/conversations/store.rs
  • src/openhuman/threads/error.rs
  • src/openhuman/threads/mod.rs
  • src/openhuman/threads/ops.rs
  • src/openhuman/threads/ops_tests.rs
  • tests/json_rpc_e2e.rs

Comment thread src/core/jsonrpc.rs Outdated
oxoxDev and others added 4 commits May 12, 2026 23:24
…oundary

Introduces `StructuredRpcError` + sentinel-encoded transport so domain
controllers can surface typed RPC errors (with `data`, `expected_user_state`)
without forcing every handler to change signature and without the JSON-RPC
transport branching on RPC method names.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves the `kind=ThreadNotFound` / `thread_id` payload and the "skip Sentry"
flag out of the JSON-RPC transport layer and into the threads error
boundary. `From<ThreadsError> for String` now encodes the structured
envelope at the controller handler boundary; jsonrpc.rs decodes it
generically without knowing about the threads domain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`rpc_handler` no longer inspects the RPC method name to recover
ThreadNotFound semantics or to decide whether to suppress Sentry. It now
decodes any `StructuredRpcError` envelope emitted by a domain controller
and honors the generic `expected_user_state` flag uniformly across
domains, matching the workspace rule that transport layers must stay
domain-agnostic (CLAUDE.md / AGENTS.md).

Replaces the threads-scoped helper test with a generic dispatch test
that asserts the structured envelope surfaces on the wire without leaking
the encoded sentinel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The transport layer no longer stamps the originating RPC method into
`error.data` — that field leaked the transport concern into the domain
envelope. Frontend keys on `data.kind` + `data.thread_id` (see
coreRpcClient.isThreadNotFoundRpcData), so the assertion was checking a
field nothing actually consumes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

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

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/openhuman/threads/error.rs`:
- Around line 26-31: from_thread_scoped_store_error currently treats any parsed
"not found" id as the requested thread and returns Self::not_found(thread_id);
change it to first call parse_thread_not_found_message(&err), and only return
Self::not_found(thread_id) when the parser yields Some(parsed_id) and parsed_id
equals the provided thread_id (compare as strings/str slices). If the parsed id
is None or does not match thread_id, return Self::Message(err) to avoid clearing
the wrong thread. Ensure you reference parse_thread_not_found_message,
from_thread_scoped_store_error, Self::not_found and Self::Message when making
the change.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4a7e48c8-316b-4089-9a0c-23cea9200730

📥 Commits

Reviewing files that changed from the base of the PR and between be9647f and 454a58f.

📒 Files selected for processing (7)
  • src/core/jsonrpc.rs
  • src/core/jsonrpc_tests.rs
  • src/openhuman/threads/error.rs
  • src/openhuman/threads/mod.rs
  • src/rpc/mod.rs
  • src/rpc/structured_error.rs
  • tests/json_rpc_e2e.rs
✅ Files skipped from review due to trivial changes (1)
  • src/rpc/mod.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/openhuman/threads/mod.rs

Comment on lines +26 to +31
pub fn from_thread_scoped_store_error(thread_id: &str, err: String) -> Self {
if parse_thread_not_found_message(&err).is_some() {
Self::not_found(thread_id)
} else {
Self::Message(err)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Only classify NotFound when the ids agree.

from_thread_scoped_store_error ignores the id parsed from the store error and always re-emits the requested thread_id. If the store ever reports a different missing id, the frontend will clear the wrong stale thread and the underlying mismatch disappears. Fall back to Message(err) unless the parsed id matches.

Suggested guard
 pub fn from_thread_scoped_store_error(thread_id: &str, err: String) -> Self {
-    if parse_thread_not_found_message(&err).is_some() {
-        Self::not_found(thread_id)
+    if let Some(found_thread_id) = parse_thread_not_found_message(&err) {
+        if found_thread_id == thread_id {
+            Self::not_found(thread_id)
+        } else {
+            Self::Message(err)
+        }
     } else {
         Self::Message(err)
     }
 }
📝 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
pub fn from_thread_scoped_store_error(thread_id: &str, err: String) -> Self {
if parse_thread_not_found_message(&err).is_some() {
Self::not_found(thread_id)
} else {
Self::Message(err)
}
pub fn from_thread_scoped_store_error(thread_id: &str, err: String) -> Self {
if let Some(found_thread_id) = parse_thread_not_found_message(&err) {
if found_thread_id == thread_id {
Self::not_found(thread_id)
} else {
Self::Message(err)
}
} else {
Self::Message(err)
}
}
🤖 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/openhuman/threads/error.rs` around lines 26 - 31,
from_thread_scoped_store_error currently treats any parsed "not found" id as the
requested thread and returns Self::not_found(thread_id); change it to first call
parse_thread_not_found_message(&err), and only return Self::not_found(thread_id)
when the parser yields Some(parsed_id) and parsed_id equals the provided
thread_id (compare as strings/str slices). If the parsed id is None or does not
match thread_id, return Self::Message(err) to avoid clearing the wrong thread.
Ensure you reference parse_thread_not_found_message,
from_thread_scoped_store_error, Self::not_found and Self::Message when making
the change.

Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

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

Review — fix(threads): typed ThreadNotFound error + skip Sentry for stale-thread RPCs

Walkthrough

Clean end-to-end implementation of typed stale-thread error handling. A generic StructuredRpcError envelope lets domain errors carry machine-readable data through the existing Result<_, String> channel without changing every controller signature. The transport layer decodes it domain-agnostically — no method-name branching. Frontend gets a thread_not_found error kind, clears stale state, and suppresses the toast. Test coverage is solid across all layers including a Sentry TestTransport smoke test.

Changes

File Summary
Cargo.toml Adds "test" feature to existing sentry 0.47.0
src/rpc/structured_error.rs New — generic structured-error envelope with encode/decode
src/openhuman/threads/error.rs NewThreadsError enum with NotFound variant + parser
src/openhuman/threads/ops.rs message_append and thread_generate_title return ThreadsError
src/openhuman/memory/conversations/store.rs Normalises "does not exist" → "not found"
src/core/jsonrpc.rs Transport decodes StructuredRpcError, skips Sentry for expected_user_state
app/src/services/coreRpcClient.ts thread_not_found kind + isThreadNotFoundCoreRpcError helper
app/src/store/threadSlice.ts clearStaleThread reducer + stale-thread recovery in 3 thunks
app/src/pages/Conversations.tsx Suppresses send-error toast for stale threads
Tests (6 files) Unit, JSON-RPC E2E, Sentry transport smoke, frontend slice + RPC client

Verified / looks good

  • StructuredRpcError::decode is pure and panic-free — graceful None on corrupt envelopes
  • sentry "test" feature is correctly scoped — not a new runtime dependency
  • clearStaleThread reducer correctly handles welcomeThreadId
  • isThreadNotFoundCoreRpcError with missing errorThreadId returns true — correct conservative behavior
  • Generic test IDs ("t-1", "thread-123") — no real names/emails
  • No import.meta.env, no window.__TAURI__, no dynamic imports in production code
  • New Rust files placed in proper subdirectories, not at src/openhuman/ root

See inline comments for actionable findings.

Comment thread src/core/jsonrpc.rs
display_message
);
} else if !is_session_expired_error(&display_message) {
crate::core::observability::report_error(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[major] Potential merge-conflict regression: report_error vs report_error_or_expected

The current main branch uses report_error_or_expected at this call site (introduced to suppress Sentry noise for LocalAiDisabled and ApiKeyMissing conditions). This PR replaces it with bare report_error, which would re-introduce Sentry noise for those expected errors once merged.

This likely happened because the PR branched before report_error_or_expected landed on main. When rebasing, make sure this line becomes:

crate::core::observability::report_error_or_expected(
    display_message.as_str(),
    "rpc",
    "invoke_method",
    &[("method", method.as_str()), ("elapsed_ms", &ms.to_string())],
);

Both suppression mechanisms (structured expected_user_state flag for new domains + expected_error_kind() pattern matching for existing plain-string errors) need to coexist.

}
return { threadId: payload.threadId, message: persisted };
} catch (error) {
if (isThreadNotFoundCoreRpcError(error, payload.threadId)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[minor] Stale-thread recovery block is copy-pasted three times (here, addInferenceResponse, and generateThreadTitleIfNeeded).

Consider extracting to a shared helper:

async function handleStaleThread(
  threadId: string,
  dispatch: AppDispatch,
  thunkName: string
): Promise<void> {
  dispatch(clearStaleThread(threadId));
  try {
    await dispatch(loadThreads()).unwrap();
  } catch (refreshError) {
    if (IS_DEV) {
      console.debug(`[threadSlice] ${thunkName} stale-thread refresh failed`, {
        threadId,
        error: refreshError,
      });
    }
  }
}

Functionally correct as-is — all three copies are tested — but DRY would reduce the chance of future divergence when the recovery logic evolves.

await dispatch(addMessageLocal({ threadId: sendingThreadId, message: userMessage })).unwrap();
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
if (msg === THREAD_NOT_FOUND_MESSAGE) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[minor] String equality check on THREAD_NOT_FOUND_MESSAGE works today but is fragile.

dispatch(thunk).unwrap() on a rejectWithValue throws the payload directly (a plain string), so String(error) matches. But if addMessageLocal ever wraps the rejection in an Error object (e.g. in a future middleware), this equality check silently stops matching.

A comment here would clarify the contract:

// error is the rejectWithValue payload (plain string), not an Error object —
// THREAD_NOT_FOUND_MESSAGE is a string sentinel from threadSlice.
if (msg === THREAD_NOT_FOUND_MESSAGE) {

Alternatively, checking isThreadNotFoundCoreRpcError before unwrap() would be more robust, but that changes the control flow.

/// `Result<_, String>` error channel.
pub fn encode(&self) -> String {
// serde_json::to_string on a struct of String/Option<Value>/bool
// cannot fail in practice, so unwrap is acceptable.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nitpick] .unwrap_or_else(|_| String::from("{}")) silently produces an invalid envelope if serialization fails. Since serde_json::to_string on String + Option<Value> + bool cannot fail in practice, consider expect("StructuredRpcError serialization cannot fail") — any future struct change that breaks serialization would then fail visibly in CI rather than producing a silent sentinel-prefixed {}.

Comment thread tests/json_rpc_e2e.rs
"createdAt": "2026-01-01T00:00:00Z"
}
}),
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[nitpick] The comment below says the transport no longer stamps method into the structured error data — but the test only asserts kind and thread_id are present without asserting method is absent. Adding:

assert!(
    append_err["data"]["method"].is_null(),
    "method must not appear in structured error data"
);

would make the contract explicit and catch a future accidental re-introduction.

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