Skip to content

fix(webui): recover from stale thread 404s - #5928

Closed
serrrfirat wants to merge 2 commits into
mainfrom
codex/webui-stale-thread-404
Closed

serrrfirat wants to merge 2 commits into
mainfrom
codex/webui-stale-thread-404

Conversation

@serrrfirat

@serrrfirat serrrfirat commented Jul 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • evict missing threads from sidebar and session transcript caches on thread-scoped 404s, including stale background timeline loads
  • navigate an active stale conversation back to /chat and show This thread no longer exists instead of an inline generic Not found error
  • preserve the submitted composer text and staged attachments when a send discovers the active thread no longer exists
  • keep stale background send cleanup from mutating the newly viewed thread processing state

Change Type

  • Bug fix
  • Feature
  • Refactor
  • Documentation-only
  • Test-only

Linked Issue

None.

Validation

  • pnpm test
  • pnpm typecheck
  • pnpm lint
  • pnpm build
  • cargo fmt --all -- --check
  • cargo test -p ironclaw_webui_v2 --features webui-v2-beta post-review rerun deferred to remote CI because local Rust compile was blocked under toolchain load after push
  • Browser smoke test for stale timeline 404 and send-time 404 recovery, including draft restoration

Security Impact

No auth, permission, token, secret, CORS, listener, or trust-boundary behavior changes. The change only handles existing 404 responses in the WebUI v2 client.

Trust-Boundary Impact

No new trust boundary. The browser still consumes same-origin WebUI v2 APIs and treats backend 404s as missing-thread state.

DB Impact

None. No schema, migration, persistence, or backend write behavior changes.

Blast Radius

Scoped to WebUI v2 chat thread history/send error handling and client-side caches/drafts.

Rollback

Revert this PR. The fallback behavior returns to the prior generic Not found/chat error path for stale thread 404s.

Review Follow-Through

Addressed Gemini and CodeRabbit feedback by evicting sidebar cache from useHistory, preserving send drafts across missing-thread redirects, resetting missing-thread dedupe on thread changes, and keeping stale background send cleanup isolated from the active thread.

@ironloopai

ironloopai Bot commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

🔎 IronLoop Review Status

Head: 65a424da0ba86d8092b57eaf1e7793e44d3bd47a
Result: 1/1 reviewers completed without blocking findings.
Next: Ready for normal human review and CI checks.
Updated: 2026-07-10T13:47:04.715Z

Current reviewers:

Reviewer State Verdict Findings Last update
ironloop/common-reviewer (reviewer) Completed Approved 0 blocking findings / 0 notes 2026-07-10T13:47:04.576Z
Reviewer summaries
Reviewer Detail
ironloop/common-reviewer (reviewer) Approved; 0 blocking findings; No concrete blocking issues found in the stale-thread 404 recovery changes. The PR adds focused handling and tests for active/background missing-thread cases and keeps the cache/h…
Recent activity
Time Reviewer State Detail
2026-07-10T13:41:30.401Z ironloop/common-reviewer (reviewer) Queued Accepted review request for head 65a424d.
2026-07-10T13:41:30.401Z ironloop/common-reviewer (reviewer) Queued Waiting for this reviewer lane to become available.
2026-07-10T13:41:30.513Z ironloop/common-reviewer (reviewer) Queued Added to the local review work handoff.
2026-07-10T13:41:31.641Z ironloop/common-reviewer (reviewer) Started Reviewer worker started attempt 1.
2026-07-10T13:41:34.787Z ironloop/common-reviewer (reviewer) Workspace ready Prepared isolated checkout (merge_ref) at 7d855f0.
2026-07-10T13:46:54.993Z ironloop/common-reviewer (reviewer) Running Codex is reviewing; process live; elapsed 5m 22s; timeout in 14m 38s; last heartbeat 2026-07-10T13:46:54.993Z. Codex emitted stderr output at 2026-07-10T13:45:48.727Z.
2026-07-10T13:47:04.576Z ironloop/common-reviewer (reviewer) Result captured Approved; 0 blocking findings.
2026-07-10T13:47:04.576Z ironloop/common-reviewer (reviewer) Completed Review completed and terminal status was persisted.
Available commands
  • @ironloopai help
  • @ironloopai agents
  • @ironloopai review
  • @ironloopai review --agent <agent>
  • @ironloopai status
Run metadata

Admission: webhook accepted the request and IronLoop persisted reviewer state before this projection.

@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5928 July 10, 2026 10:47 Destroyed
@github-actions github-actions Bot added size: M 50-199 changed lines risk: low Changes to docs, tests, or low-risk modules contributor: core 20+ merged PRs labels Jul 10, 2026
@coderabbitai

coderabbitai Bot commented Jul 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Thread Not-Found Recovery

Layer / File(s) Summary
Error detection and cache eviction
crates/ironclaw_webui_v2/frontend/src/lib/thread-errors.*, crates/ironclaw_webui_v2/frontend/src/pages/chat/lib/thread-cache.*, crates/ironclaw_webui_v2/frontend/src/pages/chat/hooks/useHistory.ts
Adds 404 detection and removes missing threads from cached lists and history while preserving pagination metadata.
History and send error handling
crates/ironclaw_webui_v2/frontend/src/pages/chat/hooks/useChat.ts, crates/ironclaw_webui_v2/frontend/src/pages/chat/hooks/useHistory.ts, crates/ironclaw_webui_v2/frontend/src/pages/chat/lib/useChat-send.test.ts, crates/ironclaw_webui_v2/frontend/src/pages/chat/lib/useHistory.test.ts
Handles missing-thread failures during history loading and sending, deduplicates cleanup, forwards callbacks, preserves background-thread state, and rethrows send errors.
Chat notification and validation
crates/ironclaw_webui_v2/frontend/src/pages/chat/chat.tsx, crates/ironclaw_webui_v2/frontend/src/i18n/en.ts, crates/ironclaw_webui_v2/frontend/src/pages/chat/lib/chat.test.ts, crates/ironclaw_webui_v2/src/static_assets/assets.rs
Clears thread state, restores recovered composer data, shows the translated error toast, deselects the missing thread, and updates coverage and import checks.

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

Possibly related issues

  • Issue 5077: Both changes handle missing chat threads by clearing stale conversation state and returning to a new-chat state.

Suggested reviewers: think-in-universe

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title uses Conventional Commits style and accurately summarizes the stale-thread 404 recovery work.
Description check ✅ Passed The description covers summary, change type, validation, security, DB, blast radius, rollback, and follow-up; only the Reborn checklist/track label is missing.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request implements robust handling for missing chat threads (404 errors) in the frontend, ensuring that stale threads are evicted from the cache and history, a toast notification is shown, and the user is redirected. The review feedback suggests two improvements: first, ensuring that removeThreadFromCache is imported and called in useHistory.ts to evict stale threads from the sidebar cache even if the user has navigated away before the 404 resolves; second, removing a redundant submitBusyRef.current = false assignment in useChat.ts as it is already handled in the finally block.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

import React from "react";
import { fetchTimeline } from "../../../lib/api";
import { authScope } from "../../../lib/auth-scope";
import { isThreadNotFoundError } from "../../../lib/thread-errors";

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.

medium

Import removeThreadFromCache to allow evicting stale threads from the sidebar cache when a background thread load returns a 404.

Suggested change
import { isThreadNotFoundError } from "../../../lib/thread-errors";
import { isThreadNotFoundError } from "../../../lib/thread-errors";
import { removeThreadFromCache } from "../lib/thread-cache";

Comment on lines +189 to +190
if (isThreadNotFoundError(err)) {
evictThreadHistory(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.

medium

When a background thread load returns a 404, the thread should be evicted from the sidebar cache (removeThreadFromCache) even if it is not the currently active thread. Currently, removeThreadFromCache is only called inside handleThreadNotFound when threadIdRef.current === threadId, leaving stale threads in the sidebar if the user navigated away before the 404 resolved.

        if (isThreadNotFoundError(err)) {
          removeThreadFromCache(threadId);
          evictThreadHistory(threadId);

Comment on lines +804 to +806
updateCurrentRunState(() => setIsProcessing(false));
submitBusyRef.current = false;
throw 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.

medium

The assignment submitBusyRef.current = false is redundant here because the finally block of the send function (on line 844) unconditionally resets submitBusyRef.current = false on any exit path.

Suggested change
updateCurrentRunState(() => setIsProcessing(false));
submitBusyRef.current = false;
throw err;
updateCurrentRunState(() => setIsProcessing(false));
throw err;

@ironloopai ironloopai Bot left a comment

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.

❌ IronLoop Review: reviewer

Review at a glance

Verdict Blocking Notes Inline Head
❌ Changes requested 1 0 1 983bffa7040a

Head: 983bffa7040a3f247a19224c6223d13bc22d550e
Next: Fix the blocking findings, push the PR branch, then re-run this reviewer.

Run details

Status: Current
Needs human: no
Needs validation: no

Summary

Found one blocking WebUI regression in the stale-thread 404 recovery path: sending a message to a deleted thread can discard the user's submitted draft.

Findings

Blocking: 1 / Notes: 0

Blocking findings

1. ❌ [MEDIUM] Preserve submitted draft when redirecting after send 404

Location: crates/ironclaw_webui_v2/frontend/src/pages/chat/chat.tsx:75
The new missing-thread handler is also invoked from useChat.send after the composer has already cleared the submitted text/attachments. ChatInput restores failed sends to the old/current draftKey, but this redirect switches to /chat with the new-chat draft key and the 404 path skips the generic retryable error bubble, so the user's just-submitted message can disappear. Preserve the submitted payload into the new-chat draft/location state before navigating, or keep a retryable failure visible until the user can recover it.

Developer follow-up

After fixing this feedback:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloopai review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloopai review when the fix may affect multiple areas.
  4. Use @ironloopai status to check queued/running/completed/failed/superseded state while reviewers run.

(missingThreadId) => {
clearThreadState(missingThreadId);
toast(t("chat.threadNoLongerExists"), { tone: "error", duration: 5000 });
onSelectThread?.(null, { replace: true });

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.

This redirect is also reached from useChat.send after ChatInput has cleared the submitted draft. The failure restore writes back under the old thread draft key, but navigating to /chat switches to the new-chat key and this 404 path skips the retryable error bubble, so the user's just-submitted text/attachments can disappear. Please preserve the payload into the new-chat draft/location state before navigating, or keep a retryable failure visible.

@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

🤖 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 `@crates/ironclaw_webui_v2/frontend/src/pages/chat/chat.tsx`:
- Around line 71-78: Preserve the composer draft in handleThreadNotFound when
redirecting from a missing thread: capture the active draft before
clearThreadState and pass it through the replace navigation to the new chat
route, or retain the prior composerDraftKey during the handoff so unsent text is
not stranded under the missing thread ID.

In `@crates/ironclaw_webui_v2/frontend/src/pages/chat/hooks/useChat.ts`:
- Around line 122-125: Reset missingThreadIdsRef whenever threadId changes so
stale 404 deduplication does not survive leaving and reopening a thread; update
the relevant useChat effect/logic near handleThreadNotFound and add a regression
test covering a thread becoming not found, switching away, then reopening it and
verifying recovery runs again.
- Around line 797-807: Only reset processing state and clear submitBusyRef
inside the active-target branch where threadIdRef.current is absent or matches
sendThreadId; leave the stale-send branch limited to removing the cached thread
and evicting its history. Update the thread-not-found handling around
isThreadNotFoundError and handleThreadNotFound accordingly.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 0ff46036-a436-4c23-b317-c890629bb093

📥 Commits

Reviewing files that changed from the base of the PR and between 5ae88dd and 983bffa.

📒 Files selected for processing (12)
  • crates/ironclaw_webui_v2/frontend/src/i18n/en.ts
  • crates/ironclaw_webui_v2/frontend/src/lib/thread-errors.test.ts
  • crates/ironclaw_webui_v2/frontend/src/lib/thread-errors.ts
  • crates/ironclaw_webui_v2/frontend/src/pages/chat/chat.tsx
  • crates/ironclaw_webui_v2/frontend/src/pages/chat/hooks/useChat.ts
  • crates/ironclaw_webui_v2/frontend/src/pages/chat/hooks/useHistory.ts
  • crates/ironclaw_webui_v2/frontend/src/pages/chat/lib/chat.test.ts
  • crates/ironclaw_webui_v2/frontend/src/pages/chat/lib/thread-cache.test.ts
  • crates/ironclaw_webui_v2/frontend/src/pages/chat/lib/thread-cache.ts
  • crates/ironclaw_webui_v2/frontend/src/pages/chat/lib/useChat-send.test.ts
  • crates/ironclaw_webui_v2/frontend/src/pages/chat/lib/useHistory.test.ts
  • crates/ironclaw_webui_v2/src/static_assets/assets.rs

Comment thread crates/ironclaw_webui_v2/frontend/src/pages/chat/chat.tsx
Comment thread crates/ironclaw_webui_v2/frontend/src/pages/chat/hooks/useChat.ts
Comment thread crates/ironclaw_webui_v2/frontend/src/pages/chat/hooks/useChat.ts
@github-actions

github-actions Bot commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

Coverage ratchet

Ratchet mode: ENFORCING

RATCHET PASS: global
  observed: 85.07% (285397 / 335501 lines)
  floor:    85.3% (tolerance 0.5pp -> effective floor 84.8%)
  denominator: 335501 lines now vs 320188 at floor capture (+15313 lines, +4.78%) — not a material change

⚠️ 2 Reborn crate(s) have 0 int-tier coverage (target: 0) — ironclaw_prompt_envelope, ironclaw_scripts

Reborn integration-tier coverage

Line coverage (Reborn crates): 85.07% — 285397 / 335501 lines

Per-crate breakdown (63 crates, lowest-covered first)
Crate Line % Covered / Total
ironclaw_prompt_envelope 0% 0 / 88
ironclaw_scripts 0% 0 / 345
ironclaw_runtime_policy 31.75% 80 / 252
ironclaw_event_projections 43.31% 673 / 1554
ironclaw_run_state 52.36% 222 / 424
ironclaw_authorization 53.66% 462 / 861
ironclaw_triggers 60.05% 1759 / 2929
ironclaw_observability 61.54% 16 / 26
ironclaw_reborn_cli 62.5% 3766 / 6026
ironclaw_webui_v2 62.65% 2635 / 4206
ironclaw_mcp 63.03% 578 / 917
ironclaw_reborn_migration 66.93% 1168 / 1745
ironclaw_dispatcher 67.15% 92 / 137
ironclaw_filesystem 67.25% 3833 / 5700
ironclaw_memory 69.2% 773 / 1117
ironclaw_trust 72.88% 661 / 907
ironclaw_capabilities 74.08% 1658 / 2238
ironclaw_wasm_limiter 74.6% 47 / 63
ironclaw_reborn_event_store 74.67% 958 / 1283
ironclaw_extractors 74.72% 538 / 720
ironclaw_first_party_extensions 77.66% 5400 / 6953
ironclaw_llm 78.31% 20258 / 25870
ironclaw_product_context 78.57% 11 / 14
ironclaw_process_sandbox 80.65% 671 / 832
ironclaw_wasm_product_adapters 80.71% 1448 / 1794
ironclaw_memory_native 81.22% 3205 / 3946
ironclaw_reborn_openai_compat 81.23% 978 / 1204
ironclaw_secrets 82.7% 2791 / 3375
ironclaw_wasm 82.72% 996 / 1204
ironclaw_events 82.84% 1762 / 2127
ironclaw_turns 84.04% 13325 / 15856
ironclaw_processes 84.06% 965 / 1148
ironclaw_auth 84.08% 3068 / 3649
ironclaw_reborn_config 84.33% 1814 / 2151
ironclaw_host_api 84.8% 2589 / 3053
ironclaw_product_workflow 85.26% 10839 / 12713
ironclaw_threads 85.88% 4226 / 4921
ironclaw_projects 85.92% 659 / 767
ironclaw_network 86.12% 670 / 778
ironclaw_common 86.46% 1514 / 1751
ironclaw_slack_v2_adapter 86.79% 1806 / 2081
ironclaw_product_adapters 86.98% 3207 / 3687
ironclaw_reborn_identity 87.03% 557 / 640
ironclaw_skills 87.58% 4470 / 5104
ironclaw_hooks 87.77% 9914 / 11296
ironclaw_product_adapter_registry 88.06% 531 / 603
ironclaw_reborn_traces 88.19% 11946 / 13546
ironclaw_extensions 88.33% 2633 / 2981
ironclaw_host_runtime 88.49% 16923 / 19125
ironclaw_reborn_composition 88.72% 70018 / 78922
ironclaw_approvals 89.24% 1584 / 1775
ironclaw_runner 89.28% 16690 / 18693
ironclaw_conversations 90% 2925 / 3250
ironclaw_event_streams 90.82% 1009 / 1111
ironclaw_loop_support 92.47% 14702 / 15899
ironclaw_resources 92.81% 4722 / 5088
ironclaw_attachments 93.06% 630 / 677
ironclaw_reborn_webui_ingress 93.19% 2217 / 2379
ironclaw_telegram_v2_adapter 93.87% 2452 / 2612
ironclaw_agent_loop 94.58% 8786 / 9289
ironclaw_safety 94.8% 3668 / 3869
ironclaw_first_party_extension_ports 95.24% 3343 / 3510
ironclaw_outbound 95.59% 3556 / 3720

This table itself is informational and never gates the PR on its own — not the percentage, not the per-crate holes, not the 0-coverage callout. A separate coverage ratchet (dry-run until enforce=true; see tests/integration/coverage-floor.toml) can fail the build on specific configured floors.

Exemptions (3 entry/entries excluded from the accounting above)
Module / Crate Reason Issue
crate: ironclaw_embeddings v1-only: consumed only by root ironclaw (src/app.rs, src/tools/builtin/memory.rs, src/workspace/mod.rs, src/config/{mod,embeddings}.rs); no crates/* dependents. Covered by "Tests (Legacy)". #5657
crate: ironclaw_gateway v1-only: consumed only by root ironclaw (src/channels/web/platform/static_files.rs, src/channels/web/handlers/frontend.rs); no crates/* dependents. Covered by "Tests (Legacy)". #5657
crate: ironclaw_tui v1-only: consumed only by root ironclaw (src/main.rs, src/channels/tui.rs); no crates/* dependents. Crate's own doc comment confirms it bridges INTO v1, not Reborn. Covered by "Tests (Legacy)". #5657

@railway-app

railway-app Bot commented Jul 10, 2026 •

Copy link
Copy Markdown

🚅 Deployed to the ironclaw-pr-5928 environment in ironclaw-ci-preview

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Jul 10, 2026 at 1:49 pm

@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5928 July 10, 2026 13:39 Destroyed
@serrrfirat

Copy link
Copy Markdown
Collaborator Author

Addressed review feedback in 65a424d:

  • Gemini: useHistory now evicts the sidebar cache on any timeline 404, including stale background loads.
  • Gemini/CodeRabbit: removed the redundant send-404 submitBusyRef reset and scoped processing cleanup to active-target sends only.
  • CodeRabbit/IronLoop: send-time missing-thread recovery now carries the submitted text and staged attachments to the new-chat composer before redirecting.
  • CodeRabbit: missing-thread dedupe resets when the viewed thread changes, so reopening the same stale URL recovers again.

Local validation after the review fixes:

  • pnpm test (80 files, 662 tests)
  • pnpm typecheck
  • pnpm lint
  • pnpm build
  • browser smoke for timeline 404 and send-time 404, including draft restoration

Post-review cargo test -p ironclaw_webui_v2 --features webui-v2-beta was deferred to remote CI because local Rust compile was blocked under toolchain load.

@ironloopai review --agent reviewer

@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

🤖 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 `@crates/ironclaw_webui_v2/frontend/src/pages/chat/chat.tsx`:
- Around line 76-82: In the new-chat recovery callback, clear stale recovery
state by detecting when a recovery payload is supplied and unconditionally
writing both the draft and staged attachments stores for NEW_DRAFT_KEY,
including empty values. Update the logic around recovery.composerDraft and
recovery.stagedAttachments so a text-only retry removes prior attachments and an
attachment-only or empty recovery removes any prior draft.

In `@crates/ironclaw_webui_v2/frontend/src/pages/chat/hooks/useChat.ts`:
- Around line 798-804: Prevent stale thread recovery in the thread-not-found
handling around handleThreadNotFound: only treat a null threadIdRef.current as
valid when the send originated from the new-chat flow, otherwise require it to
match sendThreadId before restoring payload or clearing processing. Add a
regression test covering a send from thread A that returns 404 after navigation
to /chat.
- Line 279: Move the missingThreadIdsRef reset out of the render path in useChat
and into the committed effect that responds to threadId changes. Remove the
render-time clear near the thread-switch logic, and call
missingThreadIdsRef.current.clear() inside the existing threadId effect after
the change commits, preserving deduplication when renders are discarded.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 5d167265-64f4-4cd4-85d2-e6a7a7638c05

📥 Commits

Reviewing files that changed from the base of the PR and between 983bffa and 65a424d.

📒 Files selected for processing (6)
  • crates/ironclaw_webui_v2/frontend/src/pages/chat/chat.tsx
  • crates/ironclaw_webui_v2/frontend/src/pages/chat/hooks/useChat.ts
  • crates/ironclaw_webui_v2/frontend/src/pages/chat/hooks/useHistory.ts
  • crates/ironclaw_webui_v2/frontend/src/pages/chat/lib/chat.test.ts
  • crates/ironclaw_webui_v2/frontend/src/pages/chat/lib/useChat-send.test.ts
  • crates/ironclaw_webui_v2/frontend/src/pages/chat/lib/useHistory.test.ts

Comment on lines +76 to +82
(missingThreadId, recovery = {}) => {
const draft = recovery.composerDraft || "";
const stagedAttachments = recovery.stagedAttachments || [];
if (draft) setDraft(NEW_DRAFT_KEY, draft);
if (stagedAttachments.length > 0) {
setStagedAttachments(NEW_DRAFT_KEY, stagedAttachments);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clear prior new-chat recovery state when the recovered value is empty.

The conditional writes retain a previous NEW_DRAFT_KEY draft or attachments. Recovering a text-only send can therefore retain unrelated staged attachments and submit them on the retry. Detect a supplied recovery payload, then overwrite both stores unconditionally.

Proposed fix
   const handleThreadNotFound = React.useCallback(
     (missingThreadId, recovery = {}) => {
       const draft = recovery.composerDraft || "";
       const stagedAttachments = recovery.stagedAttachments || [];
-      if (draft) setDraft(NEW_DRAFT_KEY, draft);
-      if (stagedAttachments.length > 0) {
+      const hasRecovery =
+        Object.prototype.hasOwnProperty.call(recovery, "composerDraft") ||
+        Object.prototype.hasOwnProperty.call(recovery, "stagedAttachments");
+      if (hasRecovery) {
+        setDraft(NEW_DRAFT_KEY, draft);
         setStagedAttachments(NEW_DRAFT_KEY, stagedAttachments);
       }
📝 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
(missingThreadId, recovery = {}) => {
const draft = recovery.composerDraft || "";
const stagedAttachments = recovery.stagedAttachments || [];
if (draft) setDraft(NEW_DRAFT_KEY, draft);
if (stagedAttachments.length > 0) {
setStagedAttachments(NEW_DRAFT_KEY, stagedAttachments);
}
(missingThreadId, recovery = {}) => {
const draft = recovery.composerDraft || "";
const stagedAttachments = recovery.stagedAttachments || [];
const hasRecovery =
Object.prototype.hasOwnProperty.call(recovery, "composerDraft") ||
Object.prototype.hasOwnProperty.call(recovery, "stagedAttachments");
if (hasRecovery) {
setDraft(NEW_DRAFT_KEY, draft);
setStagedAttachments(NEW_DRAFT_KEY, stagedAttachments);
}
🤖 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 `@crates/ironclaw_webui_v2/frontend/src/pages/chat/chat.tsx` around lines 76 -
82, In the new-chat recovery callback, clear stale recovery state by detecting
when a recovery payload is supplied and unconditionally writing both the draft
and staged attachments stores for NEW_DRAFT_KEY, including empty values. Update
the logic around recovery.composerDraft and recovery.stagedAttachments so a
text-only retry removes prior attachments and an attachment-only or empty
recovery removes any prior draft.

// raw setActiveRunState rather than the activeRunRef-mutating wrapper.
if (stateThreadId !== threadId) {
setStateThreadId(threadId);
missingThreadIdsRef.current.clear();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file='crates/ironclaw_webui_v2/frontend/src/pages/chat/hooks/useChat.ts'

wc -l "$file"
printf '\n--- lines 240-320 ---\n'
sed -n '240,320p' "$file"
printf '\n--- grep missingThreadIdsRef ---\n'
rg -n "missingThreadIdsRef|threadIdRef|stateThreadId" "$file"

Repository: nearai/ironclaw

Length of output: 4334


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file='crates/ironclaw_webui_v2/frontend/src/pages/chat/hooks/useChat.ts'

sed -n '250,310p' "$file"
printf '\n---\n'
rg -n "missingThreadIdsRef|threadIdRef|stateThreadId" "$file"

Repository: nearai/ironclaw

Length of output: 3500


🏁 Script executed:

#!/bin/bash
set -euo pipefail
file='crates/ironclaw_webui_v2/frontend/src/pages/chat/hooks/useChat.ts'

printf '\n--- lines 150-175 ---\n'
sed -n '150,175p' "$file"
printf '\n--- lines 748-810 ---\n'
sed -n '748,810p' "$file"

Repository: nearai/ironclaw

Length of output: 3464


Move missingThreadIdsRef reset out of render. useChat.ts:279 still writes a ref in the render branch, which this hook’s own invariant forbids. A discarded thread-switch render can clear the dedup set without committing the matching stateThreadId change, so stale 404 recovery for the old thread can fire again. Move the reset into the committed threadId effect.

🤖 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 `@crates/ironclaw_webui_v2/frontend/src/pages/chat/hooks/useChat.ts` at line
279, Move the missingThreadIdsRef reset out of the render path in useChat and
into the committed effect that responds to threadId changes. Remove the
render-time clear near the thread-switch logic, and call
missingThreadIdsRef.current.clear() inside the existing threadId effect after
the change commits, preserving deduplication when renders are discarded.

Comment on lines +798 to +804
if (isThreadNotFoundError(err) && sendThreadId) {
if (!threadIdRef.current || threadIdRef.current === sendThreadId) {
handleThreadNotFound(sendThreadId, {
composerDraft: renderContent,
stagedAttachments,
});
updateCurrentRunState(() => setIsProcessing(false));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not recover a stale send after navigation to new chat.

When a send from thread A fails after navigation to /chat, threadIdRef.current is null, so this branch restores A’s payload and clears processing in the new chat. Only treat null as active when this send originated from the new-chat flow.

Proposed fix
         if (isThreadNotFoundError(err) && sendThreadId) {
-          if (!threadIdRef.current || threadIdRef.current === sendThreadId) {
+          const shouldRecoverVisibleThread =
+            threadIdRef.current === sendThreadId ||
+            (!threadId && !targetThreadId);
+          if (shouldRecoverVisibleThread) {
             handleThreadNotFound(sendThreadId, {
               composerDraft: renderContent,
               stagedAttachments,
             });

Add a regression test for a send from thread A that returns 404 after navigation to /chat.

📝 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
if (isThreadNotFoundError(err) && sendThreadId) {
if (!threadIdRef.current || threadIdRef.current === sendThreadId) {
handleThreadNotFound(sendThreadId, {
composerDraft: renderContent,
stagedAttachments,
});
updateCurrentRunState(() => setIsProcessing(false));
if (isThreadNotFoundError(err) && sendThreadId) {
const shouldRecoverVisibleThread =
threadIdRef.current === sendThreadId ||
(!threadId && !targetThreadId);
if (shouldRecoverVisibleThread) {
handleThreadNotFound(sendThreadId, {
composerDraft: renderContent,
stagedAttachments,
});
updateCurrentRunState(() => setIsProcessing(false));
🤖 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 `@crates/ironclaw_webui_v2/frontend/src/pages/chat/hooks/useChat.ts` around
lines 798 - 804, Prevent stale thread recovery in the thread-not-found handling
around handleThreadNotFound: only treat a null threadIdRef.current as valid when
the send originated from the new-chat flow, otherwise require it to match
sendThreadId before restoring payload or clearing processing. Add a regression
test covering a send from thread A that returns 404 after navigation to /chat.

@ironloopai ironloopai Bot left a comment

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.

✅ IronLoop Review: reviewer

Review at a glance

Verdict Blocking Notes Inline Head
✅ Approved 0 0 0 65a424da0ba8

Head: 65a424da0ba86d8092b57eaf1e7793e44d3bd47a
Next: No reviewer action needed.

Run details

Status: Current
Needs human: no
Needs validation: no

Summary

No concrete blocking issues found in the stale-thread 404 recovery changes. The PR adds focused handling and tests for active/background missing-thread cases and keeps the cache/history cleanup scoped.

Findings

None.

Developer follow-up

After fixing this feedback:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloopai review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloopai review when the fix may affect multiple areas.
  4. Use @ironloopai status to check queued/running/completed/failed/superseded state while reviewers run.

@serrrfirat serrrfirat closed this Jul 23, 2026

This branch was successfully deployed

No deployments
ironclaw-ci-preview / ironclaw-pr-5928 — 65a424da Deployed Jul 10, 2026 by railway-app[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: low Changes to docs, tests, or low-risk modules size: M 50-199 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant