Skip to content

fix(CS-11508): code submode recovers from initial 404 after indexing (v2)#5278

Open
FadhlanR wants to merge 5 commits into
mainfrom
cs-11508-create-file-not-found-v2
Open

fix(CS-11508): code submode recovers from initial 404 after indexing (v2)#5278
FadhlanR wants to merge 5 commits into
mainfrom
cs-11508-create-file-not-found-v2

Conversation

@FadhlanR

@FadhlanR FadhlanR commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Re-opens the work from #5202, which was merged then reverted in #5277 because the new acceptance test hung the host CI shard. The hang is fixed here by replacing the gate-based race test with a simpler acceptance test, and by switching read to keepLatestTask so the race the original test was pinning can no longer occur.

Summary

  • Hoist the realm-event subscription into FileResource.modify() via realm.realmOf() so a 404-then-recover flow can react to the later index/incremental invalidation. The SSE callback is extracted to a class field so it can safely run while innerState is still loading or not-found.
  • Treat create-file: clientRequestIds (the path WriteTextFileCommand / the AI assistant uses) as reload-worthy in the invalidation handler, alongside instance:, editor:*, and bot-patch:.
  • Match invalidations against both _url and innerState.url so events for a freshly-requested URL aren't dropped during the window where modify() has updated _url but the new fetch hasn't settled yet.
  • Switch FileResource.read to keepLatestTask. The in-flight read completes normally and the event-driven reload runs after it, so state writes are sequential and a cancelled task's not-found can no longer land on top of a fresh task's ready.
  • Acceptance test drives the create-file path via two visitOperatorMode hops with a waitFor on the URL-bar error in between, so each navigation phase settles before the create-file write triggers the realm broadcast.

Test plan

  • New acceptance test: code submode recovers when a newly-created file arrives via a realm index/incremental event (covers Buck's repro).
  • Full host acceptance suite green on CI (no hang).
  • Manual: in the preview deployment, repeat Buck's New Card Definition flow and confirm the URL bar lands on ready, not not-found.

FadhlanR and others added 4 commits June 18, 2026 20:37
The FileResource only wires its realm-event subscription on the read
success branch. When code submode navigates to a file that has not yet
been indexed (AI assistant creates a .gts then immediately updates
codePath), the first authedFetch 404s and `read` early-returns before
the subscription is set up, so the subsequent `index/incremental`
invalidation has no listener and the URL bar stays on
"This resource does not exist".

Hoist the subscription into `modify()` via realm.realmOf() so it is
wired before the first fetch. The success-branch subscription is kept
(setSubscription is idempotent on the same realmURL). The SSE callback
is extracted to a class field so it can safely run while innerState is
still `loading` or `not-found`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t the requested URL

Two fixes layered on the previous commit:

1. Reload on `create-file:` clientRequestId.

   `cardService.saveSource(..., 'create-file')` — the path
   `WriteTextFileCommand` uses for the AI assistant — tags the request
   with `X-Boxel-Client-Request-Id: create-file:<uuid>`, which the realm
   echoes on the matching `index/incremental` event. The previous handler
   recognized `instance:`, `editor:*`, and `bot-patch:` but ignored
   `create-file:`, so the AI assistant's actual flow stayed stuck in
   `not-found` even with the subscription wired correctly. Treat
   `create-file:` like `bot-patch:` (always reload) — the id being in
   `cardService.clientRequestIds` does NOT imply we already have the
   content the way it does for `editor:` writes.

2. Match invalidations against `_url` AND `innerState.url`.

   When the resource is reused for a new URL, `_url` updates immediately
   in `modify()` but `innerState.url` is the prior file's URL until the
   new fetch settles. An `index/incremental` event for the new URL
   arriving in that window was matched against the stale
   `innerState.url` and dropped — leaving the new file stranded if its
   own fetch 404'd. Build a candidate set { normalize(_url),
   normalize(innerState.url) } and match if any invalidation is in it.
   This keeps the redirect case working (innerState.url is the canonical
   form) without dropping events for the newly requested URL during a
   transition.

The acceptance test is also updated to drive the create-file path via
`cardService.saveSource(..., 'create-file')` so the test would fail
without (1) as well as without the subscription-hoisting fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When an invalidation event arrives while FileResource.read is in
flight, the handler restarts the read; ember-concurrency raises
TaskCancelation at the cancelled task's awaited fetch. The catch
block treated that the same as a real network error and called
updateState({ state: 'not-found' }) AFTER the restarted task had
already landed state: 'ready', leaving the URL bar stuck.

Filter cancellation via didCancel(err) at the top of the catch so
only genuine fetch failures fall through to the not-found state
update. Adds an acceptance test that gates the two fetches
independently and releases the cancelled read after the fresh
read has already set state to ready, pinning the bad ordering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The earlier restartableTask + didCancel approach had to compensate for a
cancel-after-restart race: ember-concurrency raises TaskCancelation when
the cancelled task's awaited fetch finally resolves, and the catch block
treated that the same as a real fetch failure. The fresh task's
state: 'ready' could be overwritten by the cancelled task's state:
'not-found' whenever the cancelled response landed last.

Switch read to keepLatestTask. The in-flight read now completes normally
and the event-driven reload runs after, so state writes are sequential
and no TaskCancelation ever flows through the catch block. Drop the
didCancel guard along with it.

Replace the gate-based race test (it hung the CI shard) with a simpler
acceptance test for the navigate-from-ready-to-newly-created path, which
covers Buck's actual repro without trying to pin a race that can no
longer occur.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@FadhlanR FadhlanR force-pushed the cs-11508-create-file-not-found-v2 branch from 6fb9a05 to f406050 Compare June 18, 2026 13:41
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files      1 suites   1h 53m 45s ⏱️
3 114 tests 3 099 ✅ 15 💤 0 ❌
3 133 runs  3 118 ✅ 15 💤 0 ❌

Results for commit 8ef38cb.

Realm Server Test Results

    1 files  ±0      1 suites  ±0   12m 29s ⏱️ +7s
1 735 tests ±0  1 735 ✅ ±0  0 💤 ±0  0 ❌ ±0 
1 828 runs  ±0  1 828 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 8ef38cb. ± Comparison against earlier commit f406050.

The first attempt used updateCodePath + a concurrent saveSource so the
modify-driven read and the POST raced. The modify-driven read returned
404, then the test asserted before the event-driven reload settled,
leaving the URL bar stuck on not-found.

Switch to two visitOperatorMode hops with a waitFor on the URL-bar
error between them. Each hop drains the runloop, so the modify-driven
read for the new URL is guaranteed to complete (and surface the
expected 404 error) before the create-file POST runs and the realm
broadcasts the incremental invalidation that should clear the error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@FadhlanR FadhlanR marked this pull request as ready for review June 19, 2026 02:14

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8ef38cb0d9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// task's `state: 'ready'` with `state: 'not-found'` whenever the
// cancelled response happened to land after the restart's response.
// Queuing the latest extra perform keeps state writes sequential.
private read = keepLatestTask(async (opts?: { force?: boolean }) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep navigation reads from applying stale responses

When a second navigation calls modify() while a previous file fetch is still in flight, keepLatestTask lets the old fetch finish instead of canceling it. That stale response still calls updateState(), and because updateState() compares the old response URL to the now-current this._url, an old extensionless/canonicalizing request can invoke onRedirect() and replace the user's newer code path back to the stale file before the queued latest read runs. This regresses quick code-mode navigation that restartableTask previously protected; either capture/validate the requested URL before applying results or keep navigation reads cancelable while only queuing invalidation reloads.

Useful? React with 👍 / 👎.

@FadhlanR FadhlanR requested a review from a team June 19, 2026 02:18
@jurgenwerk jurgenwerk requested a review from Copilot June 19, 2026 07:19

Copilot AI 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.

Pull request overview

This PR re-introduces (v2) the fix for code submode getting stuck in a “This resource does not exist” state when navigating to a newly-created file that initially 404s but becomes available after realm indexing emits an index/incremental invalidation event.

Changes:

  • Update FileResource to subscribe to realm invalidation events earlier (including on the initial 404 path) and treat create-file: clientRequestIds as reload-worthy.
  • Switch FileResource.read from restartableTask to keepLatestTask to avoid cancellation-driven state races.
  • Add acceptance coverage for recovering from initial 404s when a new file arrives via an index/incremental event (both cold-start and navigate-from-ready flows).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
packages/host/app/resources/file.ts Hoists realm subscription into modify(), refactors invalidation handler, treats create-file: as reload-worthy, and changes read task semantics to keepLatestTask.
packages/host/tests/acceptance/code-submode/create-file-test.gts Adds acceptance tests that reproduce the initial-404-then-recover behavior via realm index/incremental events.
Comments suppressed due to low confidence (1)

packages/host/app/resources/file.ts:238

  • With keepLatestTask, an in-flight read is no longer cancelled when modify() is called again. Because this task reads this._url inside the try/catch, a URL change during the await can cause the previous request’s 404/error handling to update state for the new URL (or log the wrong URL). Capture the URL at the start of the task and ignore stale results when this._url has changed.
  private read = keepLatestTask(async (opts?: { force?: boolean }) => {
    let response;
    try {
      response = await this.network.authedFetch(this._url, {
        headers: { Accept: SupportedMimeType.CardSource },

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 380 to 383
realmEventsLogger.debug(
`reloading file resource ${normalizedURL} because request id is ${clientRequestId}`,
`reloading file resource ${normalizedURL} because request id is ${clientRequestId}, not contained within known clientRequestIds`,
Object.keys(this.cardService.clientRequestIds),
);
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.

3 participants