Skip to content

feat(examples): handle attachment expiry with cache-bust and retry#438

Merged
bensonwong merged 8 commits intomainfrom
feat/attachment-expiry-handling
Apr 19, 2026
Merged

feat(examples): handle attachment expiry with cache-bust and retry#438
bensonwong merged 8 commits intomainfrom
feat/attachment-expiry-handling

Conversation

@bensonwong
Copy link
Copy Markdown
Collaborator

@bensonwong bensonwong commented Apr 19, 2026

Summary

  • All example apps now catch ValidationError 404 (attachment IDs expired on the DC server) and clear their in-process attachment cache so the next request re-uploads fresh IDs
  • langchain-rag-chat and mastra-rag-chat extract runAnswerQuestion from answerQuestion and auto-retry the full LLM + verify pipeline transparently after a cache bust
  • agui-chat clears the cache and surfaces a user-friendly error (mid-flight SSE retry is not practical)
  • nextjs-ai-sdk adds clearCorpusCache() to corpusAttachment.ts, returns a 410 ATTACHMENT_EXPIRED response from the verify route, and the client silently reloads the corpus so the next question succeeds without user intervention

Test plan

  • Manually expire an attachment ID (use a stale env var) and confirm langchain-rag-chat retries and returns a valid answer
  • Same test for mastra-rag-chat
  • In nextjs-ai-sdk, use a stale attachment ID — verify route returns 410, client calls /api/corpus/init, subsequent question succeeds
  • In agui-chat with a stale ID, confirm a friendly "Documents expired. Please try again." error appears in the SSE stream and the cache is cleared for the next request

When DeepCitation returns a 404 ValidationError (attachment IDs expired),
all example apps now clear their in-process attachment cache. The
langchain-rag and mastra-rag examples auto-retry the full LLM+verify
pipeline; agui-chat clears and surfaces a user-friendly error (streaming
makes mid-flight retry complex). The nextjs-ai-sdk example returns a 410
ATTACHMENT_EXPIRED response from the verify route so the client can
silently reload the corpus before the next question.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agui-chat-deepcitation Ready Ready Preview, Comment Apr 19, 2026 5:46pm
nextjs-ai-sdk-deepcitation Ready Ready Preview, Comment Apr 19, 2026 5:46pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
deepcitation-langchain-rag-chat Ignored Ignored Preview Apr 19, 2026 5:46pm
mastra-rag-deepcitation Ignored Ignored Preview Apr 19, 2026 5:46pm

@claude
Copy link
Copy Markdown

claude Bot commented Apr 19, 2026

PR Review: feat(examples): handle attachment expiry with cache-bust and retry

Overall this is a clean, well-scoped change. The strategy is sound — detect ValidationError 404, clear the module-level cache, and either retry transparently (langchain/mastra) or return an actionable HTTP error with a client-side refresh (nextjs-ai-sdk). A few things to look at:


Issues

1. loadCorpus() fire-and-forget in page.tsx — unhandled state window

// page.tsx ~line 76
loadCorpus();                                          // starts async refresh
throw new Error(body.error ?? "Documents expired. …"); // error shown immediately

loadCorpus() isn't awaited, so between the throw and the time loadCorpus resolves, corpusLoading hasn't been set to true yet. In practice there's a microtask gap, but on a slow connection the input won't be disabled while the user sees the error — they could submit another question before the new corpus loads. Since loadCorpus sets corpusLoading = true synchronously at the top of the function, the input will be disabled once the next React render fires, so this is unlikely to matter in practice. Still worth noting. void loadCorpus() would communicate the intent clearly.

2. Double-upload race on concurrent expiry

In langchain-rag-chat and mastra-rag-chat, two concurrent requests that both hit the 404 will each call preparedAttachmentCache.clear() and then independently call runAnswerQuestion (and internally getAttachmentPromise). Because the cache was cleared by both, neither will find a cached promise and both will call cacheAttachment for the same source, kicking off two parallel uploads of the same files. The second cacheAttachment call overwrites the first's promise in the map, so the first upload result may be orphaned.

This is an edge case for dev example apps, but if concurrent traffic is expected, consider only clearing and retrying if a flag/lock isn't already set.

3. Error message inconsistency across examples

  • agui-chat: "Documents expired. Please try again."
  • nextjs-ai-sdk server: "Documents expired. Please ask your question again."
  • nextjs-ai-sdk client fallback: "Documents expired. Please ask your question again."

These should match, especially since they're user-facing strings.

4. No guard if the retry itself expires

In langchain/mastra:

} catch (err) {
  if (err instanceof ValidationError && err.statusCode === 404) {
    preparedAttachmentCache.clear();
    return await runAnswerQuestion(dc, openAiClient, question); // could throw ValidationError 404 again
  }
  throw err;
}

If the re-upload completes but the fresh attachment also returns 404 on verify (e.g., network issue, clock skew), the second ValidationError will propagate as an unhandled rejection with no user-friendly message. Wrapping the retry in its own try/catch with a clear error message would make the failure mode explicit:

try {
  return await runAnswerQuestion(dc, openAiClient, question);
} catch (retryErr) {
  throw new Error("Documents could not be refreshed. Please try again later.");
}

Looks Good

  • The runAnswerQuestion extraction is clean and the one-retry-only design avoids infinite loops.
  • Catching at the answerQuestion boundary (not inside runAnswerQuestion) is the right call — it keeps the retry logic co-located with the cache management.
  • clearCorpusCache() is a minimal, focused export — no unnecessary surface area.
  • The nextjs-ai-sdk 410 response is semantically correct (expired resource), and the client-side silent reload pattern is solid.
  • agui-chat correctly handles the non-retryable SSE case by clearing the cache for the next request rather than attempting a mid-stream retry.
  • The err.statusCode === 404 check is explicit and easy to update if the upstream status code ever changes.

Minor Nits

  • corpusAttachment.tsclearCorpusCache could be placed near the other exports (getCorpusAttachments) rather than between internal helpers for readability.
  • Consider adding a console.warn in agui-chat consistent with the one in langchain/mastra: "[DeepCitation] Attachment expired — clearing cache." It would make production log correlation easier.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 19, 2026

✅ Playwright Test Report

Status: Tests passed

📊 Download Report & Snapshots (see Artifacts section)

What's in the Visual Snapshots

The gallery includes visual snapshots for:

  • 🖥️ Desktop showcase (all variants × all states)
  • 📱 Mobile showcase (iPhone SE viewport)
  • 📟 Tablet showcase (iPad viewport)
  • 🔍 Popover states (verified, partial, not found)
  • 🔗 URL citation variants

Run ID: 24635186022

Benson and others added 4 commits April 19, 2026 10:57
Switch all four example apps from the npm-published deepcitation
("^0.3.10") to a local file: reference ("file:../../") so Vercel always
deploys against the current source rather than a stale release.

Add a buildCommand to each vercel.json that compiles the deepcitation
package before the Next.js build, using a subshell to keep the working
directory clean:

  (cd ../../ && npm install && npm run build) && npm run build

Remove transpilePackages: ["deepcitation"] from all next.config.js files
— it was only needed when the package shipped raw TypeScript. With the
pre-built lib/ in place, Next.js resolves through the package's main/
exports fields and no source-level transpilation is required.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Vercel sets NODE_ENV=production, which causes npm install to skip
devDependencies. Build tools like rimraf and tsup live in devDeps, so
the pre-build step failed with "command not found". Switch to
npm install --include=dev to force installation regardless of NODE_ENV.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tsconfig.json in each example maps "deepcitation" → ../../src/index.ts
so Next.js webpack compiles the library from source (enabling hot-reload
and accurate types during local dev). This requires two webpack settings:

  transpilePackages: ["deepcitation"]
    — tells Next.js to run SWC/Babel on the package's TypeScript files

  resolve.extensionAlias: { ".js": [".ts", ".tsx", ".js"] }
    — allows webpack to resolve the ESM-style .js import extensions
      used throughout the deepcitation TypeScript source (e.g.
      import from "./client/DeepCitation.js" resolves to .ts)

Remove the buildCommand added in the previous commit — pre-building the
library is unnecessary now that webpack compiles from source directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The main "deepcitation" entry does not export wrapCitationPrompt —
it's intentionally scoped to the deepcitation/prompts sub-path (matching
the integration snippets in the docs). Move the import in all four
example apps so TypeScript type-checking against the source resolves
correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@@ -1,5 +1,6 @@
import { openai } from "@ai-sdk/openai";
import { sanitizeForLog, wrapCitationPrompt } from "deepcitation";
import { sanitizeForLog } from "deepcitation";
Benson and others added 3 commits April 19, 2026 11:26
The tsconfig.json deepcitation path overrides (→ src/index.ts) were
causing tsc to resolve against the unbuilt source, which is missing
exports compared to the npm-published package. This made every Vercel
type-check fail.

Final approach:
- Remove the deepcitation/deepcitation/* path overrides from all four
  tsconfig.json files. TypeScript now resolves through the file:../../
  symlink → lib/index.d.ts (the proper generated types).
- Restore the buildCommand in vercel.json:
    (cd ../../ && npm install --include=dev && npm run build) && npm run build
  This pre-builds the deepcitation library (creating lib/) before
  next build runs its TypeScript type-check, so the types are available.
- Remove transpilePackages and extensionAlias from next.config.js — no
  longer needed since webpack resolves to the compiled lib/ output.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The DeepCitation class is intentionally not exported from the main
"deepcitation" entry — the package scopes it to deepcitation/client.
The npm-published 0.3.10 had it in the main entry but the source
never did, and the tsconfig paths override was masking this mismatch.

Update all seven call-sites across the four example apps.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace deprecated field aliases with canonical names now that type
checking resolves against the built lib/index.d.ts:

  anchorText → sourceMatch   (Citation)
  fullPhrase → sourceContext  (Citation)
  verifiedMatchSnippet → verifiedSourceMatch  (Verification)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bensonwong bensonwong merged commit ca874ce into main Apr 19, 2026
14 checks passed
@bensonwong bensonwong deleted the feat/attachment-expiry-handling branch April 19, 2026 18:01
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