Algolia-Central (Second-Brain)

spectrum/wiki/porting-playbook-to-algolia-contentsearch.md

ACS → Algolia-Central2 ("algolia-contentsearch") Porting Playbook

Source project: Algolia-Central-Spectrum (ACS)~/Dropbox/AI-Development/RAG/Algolia-Central-Spectrum. Target project: Algolia-Central2~/Dropbox/AI-Development/RAG/Algolia-Central2, GitHub arijitchowdhury80/algolia-contentsearch. Target already has a live page — this is an update-in-place, not a fresh scaffold.

Distilled from 9 commits, 3 architecture generations, and ~2 days of build+incident-response on ACS (2026-07-08/09). Every claim below is sourced from a direct read of the ACS repo's commits, SESSION.md, docs/spikes/*.md, source files, tests, and 3 memory files — not from summary-of-a-summary. File:line detail lives in the exhaustive research dump this doc was synthesized from (session transcript, 2026-07-09 evening); this doc is the load-bearing distillation, written so a fresh session on Algolia-Central2 can execute without re-discovering anything below.

Companion copy: ~/Dropbox/AI-Development/RAG/Algolia-Central2/docs/porting-playbook-from-acs.md (repo-side, same content).


1. What to reuse VERBATIM — pick up and place, zero logic changes

These files/patterns are content-agnostic. Copy the pattern (adjust import paths/instance names only), do not redesign:

File (ACS path) Role Port note
web/src/lib/agentStudio.ts Agent Studio SSE completions client — frame parsing (0/9/a/2/3, IGNORED_PREFIXES), collectHits, collectSuggestions, callCompletions Verbatim. File header explicitly says "PORTED VERBATIM from AC2" already — ACS forked this FROM Algolia-Central2 originally, so porting it back is a straight diff-and-merge, not a rewrite.
web/src/hooks/useChat.ts Core orchestration hook: extractDeepDiveOffer, deriveOfferState, buildTechnicalHistory, summarizeForHistory, summarizeSegmentsForHistory, turnToHistory, callWithRetry, useChat() Verbatim logic. Only the persona names/agent IDs it's wired to change.
web/src/components/SourcePills.tsx Grouped source pills, count-badge fold/unfold (today's fix), release-note sub-toggle Verbatim.
web/src/components/ChatMessage.tsx Segment cards, DeepDivePrompt offer card, DiscoveryCard follow-up, judge ConfidenceChip wiring Verbatim.
web/src/components/MessageMarkdown.tsx Markdown-lite renderer incl. today's real <ul><li> bullet-list fix Verbatim.
web/src/types.ts ChatTurn/AnswerSegment/AnswerSource state-machine contracts Verbatim shape.
web/src/lib/agents.ts Env-var plumbing (getEnvConfig/getAgentConfig), instance-agnostic by design Verbatim pattern — only the VITE_* var names/values differ per deployment.
scripts/agents/agentConfig.mjs buildAgentName, buildSuggestionsConfig, buildAgentBody, assertSuggestionsEnabled — pure functions Verbatim functions. Only INDEX, CLONE_BASE, PERSONAS, MAIN_MODEL constants at the top change.
scripts/agents/build_acs_agents.mjs PATCH-in-place agent provisioning script, --list/--delete CLI, dry-run via ACS_AGENT_SUFFIX Verbatim pattern. This is the fix for a real incident (see Gotcha #7) — do not regress to delete+recreate.
vercel.json (repo root) Monorepo subdir deploy config (cd web && npm install/build, outputDirectory: web/dist, SPA rewrite) Verbatim IF the target app also lives in a web/ subdirectory. Confirm first — this exact config is what fixed a 404 on ACS.
lab/judge/ package Provider-agnostic 3-lens judge (Skeptic/Referee/Advocate), grounding hard-gate, calibration harness Unchanged — it's already the shared package both projects import. No port needed, just point at it.
lab/eval/ harness shape runner.tsaskAgent → judge panel → grounding gate → synthesis → summary loop Port the shape; question sets and agent names are project-specific. Carries a known unfixed bug — see Gotcha #15 before trusting any score.

Do not reuse: the retired **HANDOFF:technical** text-sentinel mechanism or the retired client_side tool-call handoff mechanism. Both were built, shipped, and deliberately replaced. Section 3 explains why, so the exact same trap isn't rebuilt on the new project.


2. What MUST change (per Arijit's own scoping, 2026-07-09)

  1. Promptsinstructions_generic.md, instructions_technical.md, suggestions_generic.md, suggestions_technical.md, _shared_grounding_*.md: the "DATA REALITY" section (per-source description of what each index/facet actually contains) is 100% content-specific. Algolia's own content is structurally different from Adobe Spectrum docs (different facets, different depth, different code-vs-prose ratio) — expect the DEPTH DOCTRINE and lane boundaries to need real rework, not just find-replace of product names.
  2. Agent config constantsINDEX, CLONE_BASE, PERSONAS (source filters), agent IDs/names in scripts/agents/agentConfig.mjs and the per-instance config (ACS's web/src/config/instances/spectrum.ts equivalent).
  3. Analytics / real-query review — Arijit's explicit instruction: look at what queries are actually coming in on the existing Algolia-Central2 page before finalizing prompts/sample questions. This has no ACS equivalent to copy — it's new work, specific to this target having a live page with real traffic already.
  4. Update-in-place, not scaffold — the target already has a page. This means: diff the existing useChat/agentStudio/SourcePills/ChatMessage implementations (if AC2 already has its own versions, possibly pre-ACS-fork) against the ACS versions above and merge forward, rather than overwriting blind. Check web/src/hooks/useChat.ts etc. in Algolia-Central2 first — since ACS forked from AC2, there may be drift both ways since the fork point (2026-07-01).

3. The exact architecture to replicate — 3 generations, port the CURRENT one only

The A2A (agent-to-agent) handoff mechanism went through three complete redesigns in ACS. Only Generation 3 is live and correct. Knowing all three matters because CLAUDE.md in the ACS repo is currently stale and still describes Generation 2 as current — don't trust a project's own CLAUDE.md blindly; cross-check SESSION.md's top status block.

Generation 1 (retired 2026-07-03/08): text sentinel

Generic's prose contained a literal **HANDOFF:technical** token, scanned out of the streamed text client-side. Simple, but fragile (any prose containing the literal string breaks it) and not "real" agent-to-agent — just string matching.

Generation 2 (shipped + verified live 2026-07-08, retired 2026-07-09): real Agent Studio tool call

A genuine client_side tool (consult_technical_specialist) that Generic could call, causing the completions stream to pause (9: tool-call frame, no final text) until the frontend resumed it with a real result. This was proven live with hard evidence (see the two verdict docs cited below) — it's not a dead end, it's a validated-but-abandoned path. Key facts if this is ever revisited (e.g., a future project wants true synchronous A2A instead of an async offer): - Algolia's own documented tool-call JSON shape ({"type":"function","function":{...}}, OpenAI-style) is rejected by the live admin API with a 422 invalid-discriminator error. The real accepted shape is flat: {name, type:"client_side", description (≤200 chars), inputSchema}. - Publishing an agent needs a separate POST /agents/{id}/publish call. Setting status:"published" in the create/PATCH body is silently ignored — it comes back "draft" regardless. This is undocumented and easy to miss (agent looks created, isn't live). - The live API's actual accepted tool-type discriminator has 6 tags: client_side, algolia_search_index, algolia_recommend, algolia_display_results, mcp_tools, unknown — the public docs only describe 3. Verify against a live 422 error message, not the docs, if building a new tool type. - Resume shape (also undocumented, reverse-engineered from validation errors): an assistant message with parts: [{"type":"tool-invocation","toolInvocation":{"state":"result", toolCallId, toolName, args, result}}] — this is Vercel AI-SDK v4's internal UIPart discriminated union. Three other plausible shapes (MCP-approval shape, OpenAI role:"tool" message, naive tool-result part type) all failed with 422s or, worse, one (candidate "A") caused the agent to silently ignore the result and re-issue a fresh tool call — a dangerous false-positive that only shows up behaviorally, not as an error. - Full spike evidence: ACS repo docs/spikes/2026-07-08-agent-to-agent-tool-VERDICT.md and -findings.md.

Why it was retired the next day, per SESSION.md: the simpler native-suggestions mechanism (Gen 3) achieves the same human-gated "bring in a specialist" UX with less custom plumbing and no pause/resume state machine to maintain. This was a deliberate simplification, not a failure of Gen 2.

Generation 3 (CURRENT — port this one): native config.suggestions

Each agent's Agent Studio config carries a suggestions block: {enabled:true, model:<cheap model>, system_prompt:<separate short prompt>, generation:{max_count:1}, context:{include_tool_outputs:true}}. Agent Studio runs a second, async LLM call after the main answer, using that system prompt, to generate one short follow-up suggestion string. Mechanics:

  1. Server side (provisioning): agentConfig.mjs's buildSuggestionsConfig(systemPrompt) builds the config object; build_acs_agents.mjs PATCHes it onto each agent; assertSuggestionsEnabled() is a hard gate the provisioning script fails on if the config didn't round-trip enabled.
  2. The suggestion prompt (suggestions_generic.md for the front-door agent) decides DEEP-DIVE vs ORDINARY: if the question was implementation-heavy, it writes a suggestion string prefixed SPECIALIST: ... naming something concrete from the actual search hits; otherwise it writes an ordinary "you might also ask" follow-up with no prefix. The specialist/terminal agent's suggestion prompt (suggestions_technical.md) never emits SPECIALIST: — it's the end of the line.
  3. Frame delivery (client): the suggestion payload rides on the SSE stream's frame-prefix 2 — the same prefix Agent Studio already uses for an unrelated message-metadata frame. agentStudio.ts's collectSuggestions() discriminates by payload shape (does the parsed array contain an object with a suggestions: string[] key), never by prefix alone, because prefix 2 is overloaded. Getting this discrimination wrong means either swallowing real suggestions or crashing on metadata frames.
  4. Timing quirk (must handle): the suggestion is a separate async call, so it frequently doesn't arrive before the main stream closes on a cold query (main answer streams, no suggestion frame at all that turn), and on a cache-hit repeat query it can arrive with zero 0: text frames preceding it. The parser and UI must tolerate a stream ending with or without a suggestion frame — never assume one is coming.
  5. Client detection + gating: useChat.ts's extractDeepDiveOffer(suggestions) finds the first SPECIALIST:-prefixed entry (case/whitespace-tolerant); deriveOfferState() derives {deepDiveOffered, followUp, deepDiveQuery} together from one source so they can never disagree. The specialist is never called automatically. The UI renders an explicit offer card ("Yes, go deeper" / "No thanks"); only a real user click calls runDeepDive(), which invokes the second agent.
  6. Handoff context, once triggered: buildTechnicalHistory(priorHistory, query, genericText) gives the specialist the user's original query as its own user turn and Generic's answer as a separate assistant turn — never concatenated. (This was a named "Critical #1" finding in ACS's own architecture review — concatenating them was the original, wrong instinct.)
  7. History truncation for long conversations: summarizeSegmentsForHistory() gives each answer segment its own weighted truncation budget (the most recent segment gets double weight) instead of flat end-truncation — flat truncation was silently deleting the specialist's entire answer from history once the generalist's text filled the character budget. Any port needs this same per-segment weighting, not naive .slice(-N).

This is the mechanism to implement/port for Algolia-Central2. If AC2 already has some multi-agent handoff (its own wiki mentions an earlier "Maverick + specialists" design that was later rationalized to General/Developer/Marketer with "General hands off by intent" — see AC2's own ADR 2026-07-01-rationalize-to-3-agents.md), diff against that first — don't blindly overwrite an existing, possibly-different-but-working handoff with this one without checking what's already there and why.


4. Gotchas checklist — do not re-discover these

Each entry: root cause → fix → the standing heuristic to carry forward.

  1. Tool-schema shape. Docs describe an OpenAI-style nested function object; the live admin API 422s on it and wants a flat {name, type, description, inputSchema}. Heuristic: trust a live 422 error message's own field-name hints over the public docs, for any Agent Studio config shape.
  2. Publish is a separate call. status:"published" in a create/PATCH body is silently ignored — always comes back "draft". Heuristic: after any agent create/update, GET it back and check the actual status field; never assume the write body's intent took effect.
  3. Resume shape for a paused tool call (only relevant if reviving Gen 2) is the AI-SDK v4 tool-invocation/state:"result" UIPart shape — not MCP's toolApprovals, not an OpenAI role:"tool" message.
  4. config.suggestions.generation.max_words poisons every completions call with a 500 — confirmed by field-by-field bisection; max_count alone is fine. The write path (PATCH/GET) accepts and round-trips the broken config cleanly, so nothing looks wrong until a live completions call is made. Heuristic: "write-acceptance ≠ runtime-correctness" — for any Agent Studio config field you haven't personally load-tested end to end, assume it might be a landmine even if it PATCHes clean. Never add max_words back.
  5. Frame prefix 2 is overloaded between message-metadata and suggestions — discriminate by payload shape, not prefix.
  6. Suggestion arrival is non-deterministic (async second call; may not arrive before stream close, especially on cache hits) — the parser/UI must handle both "no suggestion this turn" and "suggestion with zero preceding text frames" as normal, not as errors.
  7. Delete+recreate agent provisioning silently orphans hardcoded production agent IDs. A prior version of build_acs_agents.mjs deleted and recreated agents by name on every run; a routine spike-branch redeploy orphaned the hardcoded prod IDs mid-session and broke production. Fixed permanently: look up the existing agent by name first, PATCH in place if found, only POST+publish if truly new. Agent IDs, once created, never change again. Port this exact provisioning pattern — do not regress to delete+recreate for convenience.
  8. Deleting "dead" instruction text can silently delete an unrelated, accidentally-bundled constraint. Removing the retired HANDOFF section from instructions_generic.md also removed the only sentence constraining the generalist's answer depth ("don't write the full code example yourself") — that sentence happened to live in the same paragraph as the dead mechanism. Heuristic: before deleting a paragraph tied to a retired mechanism, read it fully for any OTHER rule riding along, not just the mechanism-specific line.
  9. Full conversation history + a blanket "never make the user repeat themselves" instruction caused unprompted recaps. On a topic switch, the model interpreted "don't make them repeat" as license to re-summarize the previous turn before answering the new one. Fix: an explicit "answer ONLY the current turn's question — a new unrelated question is fresh, don't recap" rule. Any grounding-instruction set carrying that "don't repeat" language plus full history needs this same explicit counter-rule.
  10. Pinned LLM model IDs are a live external dependency. gemini-2.5-flash-lite was deprecated by Google mid-session with zero warning (404 "no longer available"), breaking every agent + the suggestions config simultaneously. Symptoms were misleading: empty responses, a hung "writing the answer" UI state, and ERR_HTTP2_PROTOCOL_ERROR in the browser network tab — curl alone reported the endpoint as healthy, masking the real failure (a browser-only HTTP/2 streaming symptom). Fix pattern: export the model ID as a single named constant (MAIN_MODEL), never let it default to "whatever the clone-base agent happens to have" — a dead model on the clone-base would silently propagate to every future refresh otherwise. Heuristic: never trust curl alone to declare a browser-facing streaming endpoint healthy; reproduce in an actual browser network tab first.
  11. Fixed sample-question chips get cache-poisoned. Agent Studio caches completions by exact literal query text and replays whatever came back on the first hit — good or bad — forever after. Sample-question UI chips are uniquely exposed because every click sends the identical string. This recurred on ACS twice, months apart, and the second recurrence was self-inflicted by this same session's own heavy repeat-testing during the model-deprecation incident. Fix applied both times was reactive (reword the chip). Do proactively instead: when repeat-testing against a live agent for debugging, vary the exact wording each time rather than hammering the literal string the UI depends on. Whether Agent Studio has an undocumented cache-bypass header/param has never been checked — worth checking once, for both projects.
  12. Flat end-truncation of conversation history drops the second agent's answer entirely once the first agent's text fills the truncation budget. Fixed with per-segment weighted budgets (last segment weighted higher) — see §3 point 7.
  13. Retrying only on an empty completion misses a whole failure class. A genuinely thrown network/HTTP error had zero retry logic; only a success-with-empty-content response did. Fix: one shared callWithRetry wrapper that retries on EITHER a thrown error OR a successful-but-empty result, used by every agent leg.
  14. Local git-push/deploy guardrail (mandate-guard.sh) conflicts with Claude Code's own auto-mode permission classifier. Every push/deploy attempt across this entire session was blocked for the assistant, even with repeated explicit in-chat "yes" — the classifier treats the guard's own sanctioned one-shot-unlock-token mechanism as a bypass attempt. Unresolved as of 2026-07-09. Expect the same friction on Algolia-Central2 pushes/deploys until this is fixed at the settings/hook level (two fixes proposed, neither applied yet: an explicit Bash allow-rule, or redesigning the hook to use a real permission prompt instead of a file-write signal).
  15. The eval/judge scoring pipeline has a bug: scores compress to ~1/10 uniformly, including on answers that are visibly correct. A question the agent correctly refuses (a bait/negative-case question) scores 0.00 — a clean refusal should score high. Feeding the judge real source-body text vs. just titles barely moved the mean (1.15 → 1.04), ruling out source-text quality as the cause. CORRECTION (found 2026-07-09 during the AC2 audit): this is NOT an unsolved mystery — Algolia-Central2's own canonical lab/judge/src/parse.ts already has the fix. buildDimensionResolver() there explicitly normalizes and matches on BOTH the rubric's machine id and the LLM's human-readable label, with an explicit comment: "Strong LLMs frequently echo the human LABEL... rather than the machine id... without this, aggregation can't match the score to the dimension and silently floors it to the rubric minimum." That's exactly ACS's symptom. Git history shows this fix landed on AC2 around 2026-06-27/28 — before ACS's 2026-07-01 fork — so either ACS forked from a slightly earlier commit, or a later ACS-side edit to its own parse.ts clobbered/bypassed the fix. Action: diff ACS's lab/judge/src/parse.ts against AC2's directly and pull AC2's buildDimensionResolver forward into ACS — this is a known, already-solved fix, not new investigation. Separately, judge calibration (Spearman rank correlation ≥ 0.7 against a human-ranked answer set, the "P2b gate") has never actually been run on either project — every score produced so far on both is directional-only per the standing rule (running any loop in trust mode before calibration passes = Goodhart risk). Also note: AC2 has two judge copies (lab/judge/ canonical + eval-loop/packages/judge/ a synced snapshot) and the sync script hasn't been re-run recently — the snapshot copy is stale relative to canonical. Always diff/patch the canonical one.
  16. Vercel builds the repo root by default; if the app lives in a subdirectory (e.g. web/), the root has nothing to build and you get a 404. Fixed with a root vercel.json (installCommand/buildCommand: cd web && npm install/build, outputDirectory: web/dist, SPA rewrite for client routing). Check whether Algolia-Central2's app is at the repo root or a subdirectory before assuming this file is or isn't needed.
  17. A project's own CLAUDE.md can silently drift out of sync with its actual shipped state. ACS's CLAUDE.md, as of this writing, still describes the retired Generation-2 tool-call handoff as current, even though SESSION.md (updated in the same commit) correctly describes the Generation-3 native-suggestions mechanism as current and live. Heuristic: when reading an unfamiliar project's status, cross-check CLAUDE.md against SESSION.md's most recent top block — they can disagree, and SESSION.md (append-only, dated) is more likely to be current than a hand-maintained status paragraph in CLAUDE.md.

5. Test coverage to replicate

Seed equivalent tests on the target project — these are the TDD pairs that caught real regressions on ACS, not busywork:

  • agentConfig.test.mjs — asserts buildAgentName/buildSuggestionsConfig/buildAgentBody produce the exact locked config shape, PLUS a named regression test ("R12") asserting no persona's tool list contains a dead handoff-mechanism string. Port the pattern of "assert the retired mechanism can never silently come back" for whatever mechanism Algolia-Central2 retires in the future.
  • instructions.test.mjs — lints every instruction file for dangling references to a retired mechanism's markers (e.g. old sentinel tokens). Cheap, catches exactly the class of bug in Gotcha #8.
  • agentStudio.test.ts — tests parseCompletionStream's suggestion-frame handling using verbatim captured frames from a real live probe, not synthesized fixtures — explicit comment in the ACS test file: "Do not synthesize these." Capture real frames from Algolia-Central2's own agents before writing the equivalent tests; do not guess the shape from documentation.
  • useChat.test.ts — tests every pure function in isolation: extractDeepDiveOffer (offer detection, whitespace/casing tolerance), deriveOfferState (fields can't disagree), buildTechnicalHistory (the "double-user-turn regression" test — asserts the user entry is the query verbatim, never query+answer concatenated), summarizeForHistory/summarizeSegmentsForHistory (word-boundary truncation, and the specific case of keeping BOTH agents' segments alive within one truncation budget), turnToHistory.

6. Exact environment variables (names, not values)

Browser-shippable (VITE_ prefix, read client-side): - VITE_ALGOLIA_APP_ID - VITE_ALGOLIA_SEARCH_API_KEY — must be a search-ONLY key, never admin (convention-enforced, not code-enforced — audit this by hand on the new project too) - VITE_JUDGE_URL — hosted judge service URL, if wiring the judge into the chat UI - VITE_LAB_API_KEY — sent as x-lab-key header to the judge service

Server/script-only (never shipped to the browser): - ALGOLIA_APP_ID, ALGOLIA_ADMIN_API_KEY — agent provisioning scripts - ALGOLIA_SEARCH_API_KEY — source of the browser-safe key above - GOOGLE_API_KEY — judge LLM provider - LAB_API_KEY — judge HTTP service auth (opt-in; unset = open, so set it in any shared/exposed deployment) - RATE_LIMIT — judge service (≤0 disables) - CALIBRATION_THRESHOLD (default 0.7), CALIBRATION_ROUNDS (default 1) — judge calibration gate - ACS_ENV-equivalent — optional override path to a specific .env.local, checked first in a 3-path fallback by every provisioning script - ACS_AGENT_SUFFIX-equivalent — dry-run mechanism (e.g. -dev suffix) so test builds never touch production agent names/IDs. Use this on Algolia-Central2 from day one — it's what let ACS iterate on live agent config without repeatedly breaking the production agents (and it still got broken once anyway, by an older script that predated this mechanism — see Gotcha #7).


7. Deploy checklist

  1. Confirm whether the app lives in a subdirectory (web/-equivalent) or repo root; add/adjust vercel.json accordingly (Gotcha #16).
  2. Confirm Vercel is wired to auto-deploy from the target branch (main-equivalent) — ACS confirmed this repeatedly; don't assume it's already true for Algolia-Central2 without checking.
  3. Set Production env vars via vercel env add (or check they're already set) — Preview-branch env vars had a separate CLI bug on ACS (Vercel CLI 50.37.0 needed an explicit git-branch arg even with --yes) and were skipped; check current CLI version before hitting the same wall.
  4. After deploy, verify the actual served bundle contains your change (grep the built JS for a literal string unique to the fix) — do not trust "the deploy succeeded" alone as evidence the change is live.
  5. Expect mandate-guard.sh/auto-mode-classifier friction on every push and every vercel --prod (Gotcha #14) until that's fixed — budget for running these manually in the meantime.

8. Open items inherited by the new project (not ACS-specific — fix once, benefits both)

  • mandate-guard.sh vs. Claude Code auto-mode classifier conflict (Gotcha #14) — blocks assistant-run pushes/deploys on ANY project on this machine, not just ACS or Algolia-Central2.
  • Judge/eval scoring bug (Gotcha #15) — shared lab/judge/lab/eval code; a fix should be validated on whichever project surfaces it first, then confirmed on the other.
  • Judge calibration (P2b, Spearman ≥0.7) has never been run on ACS; check whether Algolia-Central2 has already run its own calibration (it may be further along, or facing the same gap — check its own SESSION.md/CLAUDE.md before assuming either way).
  • Whether an Agent Studio cache-bypass header/param exists (Gotcha #11) — never checked; would fix the sample-question cache-poisoning class at the root instead of reactively.

9. What's genuinely new for Algolia-Central2 (no ACS equivalent to copy)

  • Prompt content — full rewrite of DATA REALITY / lane boundaries for Algolia's own content (see §2.1).
  • Real-query analytics review — the target page already has live traffic; look at actual incoming queries before finalizing sample questions or suggestion-prompt tuning. ACS never had this input (Adobe Spectrum demo had no pre-existing query log to learn from).
  • Merge-forward, not overwrite — reconciling ACS's current useChat/agentStudio/component code against whatever Algolia-Central2's own post-fork evolution has been (its wiki shows its own multi-agent rationalization history, 2026-06-182026-07-01, independent of ACS). Read AC2's own wiki/dev-log.md and ADRs before assuming a clean copy-paste is safe.