Skip to content

Tell a card awaiting its first index pass apart from one that is missing - #5929

Open
habdelra wants to merge 9 commits into
mainfrom
cs-11074-after-uploading-file-it-often-shows-a-broken-link
Open

Tell a card awaiting its first index pass apart from one that is missing#5929
habdelra wants to merge 9 commits into
mainfrom
cs-11074-after-uploading-file-it-often-shows-a-broken-link

Conversation

@habdelra

Copy link
Copy Markdown
Contributor

A card's source file lands on the realm's file system before the indexing pass that makes it servable. card+json — the read every top-level card load goes through — is served from the index, so during that window the read comes back 404. That 404 is shaped exactly like the one for a card that was never there, and the host treats it the same way: the card you just created or uploaded shows up as a missing card, complete with a runtime-error banner, until something forces a reload.

Two things narrow the window but neither closes it. The replica that handled the write drains its own in-flight indexing before serving a card+json read, so a single-replica setup usually gets a slow 200 instead of a fast 404 — but a read served by any other replica has no handle on that write. And linksTo targets resolve through card+source, which reads the file rather than the index, so linked cards were never affected. What's left is the top-level read, which is precisely the one that runs right after you create or upload something.

The realm now looks at the source file before answering. When the index has no row but the .json is on disk and holds a card document, the 404 carries an awaitingIndex marker: the reference is sound, indexing just hasn't caught up. A path with no source file behind it keeps the plain 404 it always had, so "not there" and "not there yet" stay distinct.

The host reads that marker off the card error and renders a placeholder instead of the error chrome. Nothing else changes: the store already keeps the error keyed by the card's id and already reloads it when the realm broadcasts the index event for that id, so the placeholder resolves into the real card on its own, with no reload and no user action. Because the branch sits in the CardError component, every surface that renders a card error — stack items, code-mode preview, the playground, host mode — picks it up.

Before / after

The same card, mid-index, before and after:

Card Error: Not Found, with a runtime-error banner

Preparing this card, with a spinner

Testing

  • packages/realm-server/tests/card-endpoints-test.ts — dropping a card's index row while leaving its .json on disk produces a 404 marked awaitingIndex, on both the plain and the conditional-GET path; a path with no source file behind it does not.
  • packages/host/tests/integration/components/card-error-awaiting-index-test.gts — a marked error renders the placeholder and offers no error detail; an unmarked 404 still renders the card error.
  • packages/host/tests/integration/store-test.gts — end to end: a card written to the realm without an indexing pass renders as being prepared, and swaps itself for the real card once the realm indexes it.

🤖 Generated with Claude Code

habdelra and others added 3 commits August 28, 2026 12:01
A write lands on the realm's file system before it is indexed, and card+json
is served from the index. In that window a read for the new card gets a 404
that is indistinguishable from "this card does not exist", so the host commits
to the missing-card treatment for a card that is on its way.

The realm now checks the source file before answering: when the `.json` is on
disk and holds a card document, the 404 carries an `awaitingIndex` marker. The
host renders a "Preparing this card" placeholder for such an error, and the
store's existing invalidation-driven reload swaps in the real card when the
realm broadcasts the index event.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@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: 63b1722e3b

ℹ️ 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".

Comment thread packages/runtime-common/realm.ts Outdated
Comment thread packages/runtime-common/realm.ts Outdated
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files      1 suites   2h 28m 40s ⏱️
4 603 tests 4 589 ✅ 14 💤 0 ❌
4 618 runs  4 604 ✅ 14 💤 0 ❌

Results for commit 42dece3.

Realm Server Test Results

    1 files      1 suites   18m 28s ⏱️
2 355 tests 2 355 ✅ 0 💤 0 ❌
2 438 runs  2 438 ✅ 0 💤 0 ❌

Results for commit 42dece3.

habdelra and others added 4 commits August 28, 2026 12:30
The awaiting-index marker told a caller to wait, so it has to be reserved for
sources the indexer will actually give an instance row: a `.json` whose `data`
is a single card resource, at a path the indexer does not ignore. A collection
document — which `isCardDocumentString` also accepts — and an ignored path
never acquire a row, so those stay a plain 404.

An invalidation naming a card whose first read is still in flight also no
longer goes to waste. The store had nothing to reload at that point and
dropped the event, and the awaiting-index placeholder that read went on to
install would then be stale with no further event coming for it. The store now
waits out the in-flight read and reloads if it left a placeholder behind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The realm broadcasts its own index event when it indexes the file, and matrix
hands that over after the read has settled — where the ordinary error-reload
path picks it up. The test now delivers exactly one event, inside the window,
so it fails when the in-flight check is removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… index

A from-scratch pass at realm startup announces itself with a bare `full` index
event and no per-card invalidations, so it can be the only word a card being
held as awaiting-index gets that its row now exists. The store re-reads its
awaiting-index placeholders on that event.

The header keeps the `error-header` class consumers style and only adds a
`pending` modifier, and names its own text colour: the colour it was
inheriting comes from the realm's own colour, which says nothing about the
grey the pending state paints — white on near-white for any dark realm. Its
icon is the loading-indicator component rather than the raw glyph, so it
actually turns, and the placeholder is a polite live region so the card
arriving is announced and not just drawn.

The deferred in-flight reload no longer counts itself in the realm-event
telemetry, since whether it reloads depends on what the read settles into.

Adds an acceptance test covering both sides of the wait through the real
routing and stack-item path, and an integration test for the full-index sweep.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A 404 on reload means the index no longer has a row, normally because the
file was deleted — so the store evicts the instance and rewrites every
consumer's link to a not-found sentinel. When the realm reports the source is
still there and only awaiting indexing, none of that is true, and the card
goes to the awaiting-index placeholder like any other failed reload.

Also trims the placeholder's second sentence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@habdelra
habdelra requested a review from a team August 28, 2026 23:13
@habdelra

Copy link
Copy Markdown
Contributor Author

There is a major regression here. in code mode when I use the New+ button to create a card instance (just a base card), I have to wait for indexing to complete before I can start editing the card. THIS IS WRONG. the store should provide a running card with a local ID that is 100% renderable in the card playground. we should not have to round trip thru the server before we can start working with this card. your changes made it such that indexing is now GATING my ability to interact with a newly created card. the whole idea where was that the creating sentinel was a sidecar in the store such that if we went to FETCH the card we would get a creating message instead of a 404. but in the case we just are interacting with the card directly that is already running in the store we should not be prevented from doing so. is that clear? never step in front of an actual running card in the store--only wait when there is a fetch for a newly created card. and you should be able to use a local ID to correlate just like the store does for newly created cards.

A card created in this tab is live under its local id and editable there long
before the realm has indexed it. The realm reporting that it has not caught up
is a statement about the index, not about that instance — but recording it as
an error made `peekError` report it, and every render site reads that to
decide whether to show a placeholder, so a card the user was working in was
replaced by one and its autosave was detached.

The store no longer records an awaiting-index error over a running card, hands
the running card back when a cache-bypassing read meets one, and leaves the
instance untouched when a reload does. `getCard` correlates a remote URL back
to a locally-created instance, so this holds from the moment the server
assigns an id.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@habdelra

Copy link
Copy Markdown
Contributor Author

[Claude Code 🤖] Confirmed and fixed in e46aa51. You were right about where the sentinel belongs — it was leaking out of the fetch path and into the store's identity map.

The mechanism: an awaiting-index 404 was recorded through setIdentityContext like any other card error, so store.peekError(id) reported it. Every render site (stack item, playground, code-mode preview) reads peekError to decide whether to stand something in front of the card, so a running instance got covered even though peek still returned it. Worse, in reloadTask the error replaced the instance as the reload result, which tripped stopAutoSaving on it — that is the part that actually gated editing rather than just hiding the card.

Three changes, all keyed on "is there a running instance for this id":

  • setIdentityContext does not record an awaiting-index error when store.getCard(id) finds a live instance. That is the chokepoint every path stores errors through, so nothing downstream can reintroduce it. getCard already correlates a remote URL back to a locally-created instance — including the last-segment-to-local-id correlation — so it holds from the moment the server assigns the id, not just after the resolver learns it.
  • A cache-bypassing read that meets an awaiting-index 404 returns the running card instead of the error.
  • A reload that meets one leaves the instance exactly as it is, autosave included, and waits for the index event.

The placeholder now only appears where it was meant to: a fetch for a card this tab has never had.

packages/host/tests/integration/store-test.gts gains a test that creates a card, holds a running instance, and asserts that an awaiting-index 404 arriving for it leaves peek on the instance, peekError empty, and autosave attached. It fails on all three without the fix.

@habdelra

Copy link
Copy Markdown
Contributor Author

[Claude Code 🤖] Confirmed manually in code mode now that I could drive a signed-in browser: New+ → Card Instance → General Card creates the instance, the preview opens straight into edit format with its fields live, typing into Name lands immediately and the editor reports Saved, and the value is on the realm's file system. No placeholder at any point, before or after indexing.

The card a client waits on is one another client created: the bytes land on
the realm, this tab hears about it through the realm's file event, and nothing
of the card has passed through its store — which is what makes the placeholder
the right thing to show rather than a stand-in for a card it already holds.
The test now takes that route and asserts the store is empty of it before the
visit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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