Reproduce
On your deployed app, submit four or five tickers. You may see:
- One card error (fake “Exa rate limit”).
- Other cards aborted even if they were running.
- No synthesis phase.
That matches the baseline setup from page 1: parallel work in one process, and one rejection fails the entire batch.
maybeFail
Open tasks/src/search.ts:
function maybeFail(query: string) { if (Math.random() < 0.3) { throw new Error(`Exa rate limit hit on query: "${query}"`) }}searchOne calls maybeFail(spec.query) before the real Exa client (first argument is _topic, unused). The message looks like infrastructure. It is not.
Why one failure stops everything
In tasks/src/research.ts, each search runs inside Promise.all:
try { const result = await searchOne(query, spec, index) onEvent({ type: 'search:done', index, articleCount: result.articles.length }) return result} catch (err) { onEvent({ type: 'search:failed', index, error: String(err) }) throw err}The UI can show search:failed on one card, but the callback rethrows. Promise.all rejects on the first rejection. runner.ts then emits { type: 'failed', error } for the whole run. Synthesis never runs.
Each search has ~70% chance to pass maybeFail. All four must pass: 0.7^4 ≈ 24% completion under this toy model.
What this means in production
If the same pattern reaches production with real transient dependency errors:
- User requests fail in batches even when most sub-steps would have succeeded.
- You waste cost and latency by restarting full runs instead of retrying only failed units.
- Incident debugging gets noisy because one request mixes all concurrent failures in one process.
What you learned
- maybeFail throws on ~30% of searches to simulate transient infrastructure errors
- Per-card search:failed does not save the run: rethrow makes Promise.all fail the whole pipeline
- All four searches must succeed for synthesis to run (~24% under this toy model)