← Corpus / augment-it / spec
Search-Results Queue — a right-rail microfrontend where concurrent agent searches land, signal, and get triaged
Agent searches run minutes; the operator shouldn't. Every search becomes a card in a persistent right rail — fire many, triage on arrival.
- Path
- specs/Search-Results-Queue-Remote.md
- Authors
- Michael Staton
- Augmented with
- Claude Code on Claude Fable 5
- Tags
- Spec · Augment-It · Search-Results · Didi-Crawl · Microfrontends · Concurrency · Workspace
Search-Results Queue — the search-results remote
Summary
Today an agent search is a synchronous invoke: the button disables, one
column is hijacked, and the operator babysits a 60–210s wait (observed live:
links 87s; team 147s and 211s). This spec makes searches asynchronous
jobs: a search.submit capability returns a search_id immediately, the
workspace service owns a durable search registry, completion broadcasts as a
WS event, and a new search-results remote renders the registry as a queue
of cards in a persistent right rail — collapsed while running, signalling on
arrival, expanding into the accept surfaces, dismissed by the operator when
dealt with.
Goals
- Fire-and-forget from every door. The 🤖 buttons (links, streams, team), and later chat, enqueue a search and return the operator’s attention immediately.
- Concurrency as the norm. N searches in flight across N orgs; the queue is the worklist.
- Honest waiting. Every card shows status (
queued / running / done / failed), elapsed time, and typical duration for its target — the antidote to the frozen “crawling…” line. - Arrival is an event, not a discovery. Done cards signal (badge + count in the rail header); the operator notices without polling.
- Results survive everything. Tab refresh, remount, WS reconnect, even closing the browser: the registry lives server-side (persisted like sessions and the active workspace), TTL-bounded. This finishes what the claim protocol (#41) started.
- Act in place, then clear. Expand → per-row ➕ (links/streams) or the staged-people accept gates (team) → mark complete → card leaves the queue.
Non-goals (this spec)
- Progress traces inside a run — the per-card progress line renders
whatever
search.progressevents exist, but emitting rich narration from prompt-runner is [[../issues/Crawl-Progress-Is-A-Black-Box-Needs-Traces-The-Operator-Can-Watch]]‘s build (Phase 4 hooks it in). - Manual 🔍 term searches in the queue — search-and-add keeps the interactive term/palette flow; unification is an open question below.
- JetStream-grade durability — the registry persists to the workspace
volume, not a message log; a crash mid-crawl marks the search
failedwith a retry affordance, which is honest and sufficient. - Cross-operator queues — the registry is per-workspace, not per-user, in v1 (single-operator reality today).
Decisions
- D1 — The registry lives in the workspace service. A
searches.tsmodule: in-memory map + write-through JSON atSEARCH_STORE_PATH(/data/searches.json, the sessions/active-workspace volume precedent). Server-side because: survives every client failure mode, one source of truth for N surfaces, chat can enqueue, and completion events broadcast on the existing WS event machinery. - D2 — Submit/execute split.
search.submitvalidates, writes the registry entry (queued), returns{search_id}and kicks execution without awaiting it: the workspace dispatches the existingorganization.crawl.requestedNATS request itself (600s timeout, as today) and on settle updates the entry (done+ results |failed+ error) and broadcasts. prompt-runner is UNCHANGED in Phase 1 — the async boundary moves into workspace, not the runner. - D3 — Events over polling.
search.updated {search_id, status}joinsBROADCAST_SUBJECTS-style WS event frames. The remote refetches the registry on events (+ once on mount). No polling loops. - D4 — A persistent right rail, mirroring the chat rail. The shell
mounts
search-resultsas a right-side companion with its own toggle (🔎 queuebeside💬 chat) and localStorage visibility — NOT a numbered rotation step, because searches are fired from every flow and must be reachable from every flow. The rail header shows a done-count badge even while collapsed… the rail, not the cards, is the “furthest right column”. - D5 — Cards carry their provenance.
{search_id, entity {org_slug, display_name}, target, status, submitted_at, started_at?, finished_at?, error?, result_summary {count}, typical_ms}— enough to render collapsed without fetching results. Results fetch on expand (search.results). - D6 — Typical durations are hardcoded v1 from live observations (links/streams ~90_000ms, team ~200_000ms), displayed as “elapsed 1:20 · typically ~1:30”. A rolling per-target average in the registry is the cheap v2.
- D7 — Accept actions reuse today’s verbs, per-remote copies of the UI.
Links/streams rows: ResultRow-shaped list with ➕ →
organization.links.add/streams.add(kind/name carried through, as search-and-add does since v1.2). Team: a StagedPeople-shaped section with the same candidates-gate accept →person.apply+person.affiliate+ link adds. Copied-and-adapted per the knots-style no-shared-runtime rule; this is deliberately more fuel for the component library ([[../issues/No-Component-Library-UI-Improvised-Not-Component-Based]]). - D8 — Mark complete = dismiss.
search.dismiss {search_id}deletes the entry (results included). No archive in v1; the canonical layer already holds everything the operator accepted. Failed cards offer Retry (resubmits same entity+target) alongside dismiss.
Capability contract
All served by the workspace service itself (no NATS round-trip for registry ops; the crawl dispatch inside execution reuses the existing subject and timeout):
| Capability | Args | Returns |
|---|---|---|
search.submit | {entity: {org_slug, display_name?}, target: 'links'|'streams'|'team', client} | {ok, search_id} — immediate |
search.list | {client} | {ok, searches: SearchCard[]} (D5 shape, no results) |
search.results | {search_id} | {ok, status, results?|people?, filtered_note?, source_urls?, error?} |
search.dismiss | {search_id} | {ok} |
| WS event | — | search.updated {search_id, status, org_slug, target} broadcast to all sessions |
Execution note: workspace’s executor calls organization.crawl.requested
over NATS exactly as dispatch('organization.crawl', …) does today (same
600s budget, same reply shapes per target); the crawl capability stays
chat-legal and untouched for direct use.
Architecture — the new remote
apps/search-results/ (scaffold template: apps/search-and-add; port
3018 — the spec first said 3017, but corpora-curator had already
claimed it by build time; federation name searchResults):
src/
App.svelte — rail shell: header (count badge, clear-done), card list
SearchCard.svelte — collapsed row: target chip · org · status · elapsed/typical · signal dot
expanded: results region per target + Mark complete / Retry
ResultsAccept.svelte — links/streams: rows with ➕ (copy-adapt ResultRow/ResultsList)
TeamAccept.svelte — team: staged-people accept gates (copy-adapt StagedPeople)
lib/search-client.ts — submit/list/results/dismiss wrappers + typical-duration map
lib/types.ts — SearchCard, per-target result shapes (mirrors)
mount.ts / app.css — per the remote scaffold conventions
Shell (shell/src/): SEARCH_RESULTS_REMOTE beside CHAT_REMOTE
(remotes.ts); right-rail slot + 🔎 queue toggle beside the chat toggle
(App.svelte); rail layout mirrors the chat rail’s (chat left · stage center
· queue right).
Door rewiring (org-workbench): makeCrawl(...) and PeopleReveal’s crawl()
stop invoking organization.crawl synchronously and instead
search.submit + (if rail hidden) flip it visible. The search-and-add crawl
mode and its envelope crawl flag are removed once the queue lands
(the manual term/scan modes remain).
Implementation phases
Phase 1 — Registry + async execution (service only)
searches.ts (registry, persistence, TTL sweep ~24h), the four
capabilities, the executor, the search.updated broadcast; capability map +
timeouts. Prove over NATS/WS with a scripted submit → event → results →
dismiss round-trip against the Aspen safe target.
Phase 2 — The rail
Scaffold apps/search-results; shell registration + toggle; cards render
the registry live (submit via a temporary script or Phase 3 doors); status,
elapsed vs typical, arrival badge; expand fetches results read-only; dismiss
works. Browser-drive: submit two searches, watch both cards progress,
expand the finished one.
Phase 3 — Accept surfaces + door switchover
ResultsAccept (links/streams ➕ with kind/name carry-through) and TeamAccept
(candidate gates, consume-on-accept per gh #37); org-workbench doors switch
to search.submit; retire search-and-add’s crawl mode; entity-updated
events keep firing on accepts so cards and counts refresh.
Phase 4 — Progress line (joint with #35)
prompt-runner emits search.progress beats; executor relays them onto the
card’s progress line. Separately planned; the card reserves the slot.
Constraints & assumptions
- Wire discipline unchanged: slugs/uuids only, client tagging on accepts, candidates-never-auto-write.
- The claim protocol (#41) stays — it protects every other long invoke; the queue simply stops using long invokes for searches.
- Federation rules per [[../blueprints/Module-Federation-Rsbuild-Dev-Loop-Gotchas]]: no shared runtime, remote owns its Svelte, theme.css before app.css, new remote = shell rebuild + fresh browser load.
docker composefrontends untouched (rsbuild dev servers); dev.sh gains the :3018 line.
Open questions (deliberately few)
- Does dismissing a
donecard that still has unaccepted rows warn (“3 candidates not reviewed”)? Lean yes, one inline confirm. - Chat door:
organization.crawlstays for didi v1, or didi learnssearch.submitso chat-fired crawls also land in the rail? Lean the latter, one slab edit, Phase 3. - Rolling typical-duration averages (D6 v2) — worth it when? After the rail proves itself.
Related
- [[../issues/Concurrent-Agent-Searches-Queue-Into-A-Search-Results-Column]] — issue of record (operator’s design, absorbed here)
- [[Augment-From-DB-Flow]] §v1.2 — the crawl capability this queues
- [[../issues/Invokes-Survive-Reconnects-The-Claim-Protocol]] · [[../issues/Crawl-Replies-Can-Be-Lost-Eternal-Spinner-No-Client-Timeout]] — the reliability substrate and its lesson (results belong server-side)
- [[../issues/Crawl-Progress-Is-A-Black-Box-Needs-Traces-The-Operator-Can-Watch]] — Phase 4’s partner
- [[../issues/No-Component-Library-UI-Improvised-Not-Component-Based]] — the copies D7 knowingly adds