v0.36.6.0 feat: cross-modal search wave (text↔image + unified column + LLM intent)#1165
Merged
Conversation
Commit 0 of the cross-modal search wave. Foundation for Phase 1-3:
- embedMultimodal accepts MultimodalInput text variant + EmbedMultimodalOpts
with inputType: 'document' | 'query' (D22-2). Default unchanged so
importImageFile keeps document-side embedding.
- embedQueryMultimodal(text) + embedQueryMultimodalImage(input) wrappers
for hybridSearch + searchByImage query paths.
- embedMultimodalSafe binary-search retry on transient batch failure +
failed_indices surfacing. Phase 3 reindex uses this so a single bad
chunk doesn't discard the 31 in-flight embeddings around it.
- Voyage path: text + image inputs in one batch via content arrays.
- openai-compat path: text + image inputs in one request per input.
- src/core/ssrf-validate.ts (D19): DNS-resolve-and-fetch-by-IP defense
for redirect chains. Closes the DNS-rebinding gap that url-safety.ts'
static check leaves open. Uses node:dns/promises with {all: true,
family: 0} to inspect every A and AAAA record before connecting.
fetchWithSSRFGuard helper validates per-redirect-hop and limits chain
depth (default 3).
- Re-exports from src/core/embedding.ts public seam.
Tests:
- test/embed-multimodal-batching.test.ts (13 cases): text variant, query
inputType discipline, mixed text+image batches, embedQueryMultimodal,
embedQueryMultimodalImage, embedMultimodalSafe happy/empty/all-fail/
mid-batch-recovery/permanent-misconfig.
- test/ssrf-validate.test.ts (20 cases): static rejections via
isInternalUrl, scheme + credentials rejection, DNS rebinding defense
(single-record + multi-record), public happy path, IPv6 literals,
malformed URLs.
No regression in existing voyage-multimodal.test.ts or
openai-compat-multimodal.test.ts (33 cases all pass).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ackfill Phase 1 of the cross-modal search wave. Wires the existing 1024d Voyage multimodal embedding space (already populated for image chunks via importImageFile) into the user-facing query path. Text queries that match cross-modal intent regex route through Voyage multimodal-3 instead of the text embedding model, then search content_chunks.embedding_image. - query-intent.ts: new `suggestedModality: 'text' | 'image' | 'both'` axis on `QuerySuggestions`. Module-scope CROSS_MODAL_PATTERNS regex array (D15 — compiled once at module load). Conservative on purpose; LLM intent escalation (Commit 4) catches genuinely ambiguous phrasings. - query-intent.ts: new `isAmbiguousModalityQuery(query)` pure heuristic for Commit 4's escalation gate. Returns true ONLY when regex misses AND a visual noun + reference marker both fire. - types.ts: `SearchOpts.crossModal: 'text' | 'image' | 'both' | 'auto'` + `SearchResult.modality: 'text' | 'image'` for downstream renderers. - mode.ts: 7 new knobs in ModeBundle (D2): cross_modal_both_text_weight, cross_modal_both_image_weight, image_query_text_refinement_weight, image_query_image_refinement_weight, unified_multimodal, unified_multimodal_only, cross_modal_llm_intent. All three mode bundles default to the same values (cross-modal is opt-in). - mode.ts: D2 cache-key fix — KNOBS_HASH_VERSION bumped 2→3, all 7 new knobs participate in knobsHash so a text-mode cache hit can't be served to an image-mode caller. - mode.ts: D3 registry — all 7 keys land in SEARCH_MODE_CONFIG_KEYS so `gbrain search modes` / `stats` / `tune` see them. - hybrid.ts: routing branch at the embed step. Resolves effective modality from (per-call opts → suggestions → 'text'). Image route: embedQueryMultimodal + searchVector(embedding_image), skip expansion + keyword (D9 mode-bundle override). Both route: parallel text + image vector searches merged via weighted RRF (D6) with cross_modal_both_* weights. Fail-open: multimodal misconfigured → structured warn + text fallback. 'auto' literal normalized to undefined (D22-1). - operations.ts: thread `cross_modal` param through `query` op. - backfill-registry.ts: new `modality` backfill kind. SQL filter requires `chunk_source='image_asset'` (D22-7 defensive guard). Idempotent. - doctor.ts: `cross_modal_modality_backfill` check surfaces unflagged image-asset chunks with paste-ready `gbrain backfill modality` hint. Tests: - cross-modal-phase1.test.ts (45 cases): regex classification (positive + negative + plural-safe), isAmbiguousModalityQuery, D3 registry, D2 knobsHash diffs across all 7 new knobs, MODE_BUNDLES defaults, resolveSearchMode precedence chain. - cross-modal-hybrid-integration.test.ts (7 cases): PGLite + stubbed gateway. Verifies image-modality calls Voyage and not OpenAI, text calls OpenAI and not Voyage, 'auto' literal normalizes, 'both' mode hits both endpoints, fail-open routes to text on multimodal misconfig. - search-mode.test.ts: updated MODE_BUNDLES + KNOBS_HASH_VERSION assertions (148 cross-suite tests still pass; no regression). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pend cap Phase 2 of the cross-modal search wave. Adds the `search_by_image` MCP op, the SSRF-defended image loader, and the daily per-OAuth-client spend cap on paid Voyage multimodal calls. D17 honest framing applied: Phase 2 ships image→similar-images + image-OCR-text retrieval. True image→full-text- knowledge requires Phase 3's unified column. - src/core/search/image-loader.ts: loadImageInput accepts local path, data: URI, or http(s):// URL. Magic-byte sniff for PNG/JPEG/WebP (no other formats). Hard size cap (10MB local default, 2MB remote default). http(s) path uses fetchWithSSRFGuard from Commit 0: every redirect hop re-resolved via DNS lookup + every record checked against the internal IP deny list. Max 3 redirect hops. 5s total fetch timeout. Pre-flight Content-Length check + post-fetch size guard for lying servers. - src/core/search/by-image.ts: searchByImage runs the image branch always; D13 hybrid intersect runs a parallel text branch when `query` is provided, merged via weighted RRF. Phase 3 will widen the column routing to embedding_multimodal once that lands. - src/core/operations.ts: new search_by_image op (scope: read, NOT localOnly). D18 P0 — when ctx.remote === true AND image_path is set, rejects with permission_denied at handler entry (validateParams would catch it again at dispatch). D5 source-id thread via sourceScopeOpts. D12 per-param length cap enforced via remote-vs-local maxBytes config read at handler entry. D23-#6 pre-flight checkBudget + post-call recordSpend (best-effort; failures don't block response). - src/core/spend-log.ts: BudgetExceededError + checkBudget + recordSpend + getTodaySpendCents. UTC day-aligned aggregation so the cap rolls over deterministically. Local CLI callers (no clientId) bypass the gate entirely. Pre-v0.36 brains without the mcp_spend_log table fail open to spend=0; the migration brings the table in on first start. - src/core/migrate.ts: new migration v67 mcp_spend_log table + indexes for the (client_id, day) and (token_name, day) hot reads. PGLite parity via sqlFor.pglite. - src/core/search/hybrid.ts: RRF_K constant exported so by-image.ts can share the same effective-K math as the main hybrid path. Tests: - cross-modal-phase2.test.ts (15 cases): magic-byte sniffing (PNG + JPEG + WebP positive, GIF rejection), oversized rejection (default + custom cap), data: URI happy path + malformed + decoded-non-image + oversized, invalid input shapes (empty + ftp), SSRF defense via DNS rebinding stub. - search-by-image-op.test.ts (7 cases): D18 remote image_path rejection + local CLI accepts; input validation (missing all three / multiple together); D23-#6 budget block-at-cap + allow-under-cap + local-CLI-bypass; migration v67 mcp_spend_log table applied cleanly. All 166 tests across the cross-modal suite pass; no regression in existing voyage-multimodal / openai-compat-multimodal / search-mode suites. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…+ D23-#2 Phase 3 of the cross-modal search wave. Adds the unified multimodal column on content_chunks + the `gbrain reindex --multimodal` sweep + the `search.unified_multimodal` routing flag with D8 source-aware coverage guard + fail-open behavior. D17 honest framing: this is the phase that unlocks true image→full-text-knowledge — Phase 2's searchByImage transparently upgrades to the richer retrieval once the unified column has coverage. D10 reindex-core extraction filed as a follow-up TODO. The existing markdown reindex walks pages and re-imports via importFromFile; this walks content_chunks and re-embeds via the gateway. Patterns rhyme but cores diverge enough that extraction balloons the diff. Both commands stand alone with their own checkpoint + cost-prompt logic. - migrate.ts v68 (embedding_multimodal_column): column-only ALTER on content_chunks. HNSW partial index deferred to post-reindex build (D20: pgvector docs recommend post-load build for HNSW). Both engines. - types.ts SearchOpts.embeddingColumn type widened to include 'embedding_multimodal'. - postgres-engine.ts + pglite-engine.ts searchVector: route to embedding_multimodal column when opts.embeddingColumn set. NO modality filter (unified column carries both text + image content). - hybrid.ts unified routing branch: when search.unified_multimodal=true, bypasses dual-column branching and runs embedQueryMultimodal + searchVector(embedding_multimodal). D8 fail-open: zero rows + not strict-mode → falls through to dual-column text path with structured warning. search.unified_multimodal_only=true bypasses the fallback. - src/commands/reindex-multimodal.ts: `gbrain reindex --multimodal`. D7 lock via tryAcquireDbLock('gbrain-reindex-multimodal'); 6h TTL. Cost prompt + 10s Ctrl-C grace window in TTY; auto-proceeds non-TTY. GBRAIN_NO_REEMBED=1 bypass. Checkpoint at ~/.gbrain/reindex-multimodal-checkpoint.json for resume. D23-#2 auto-flip prompt at coverage=100% completion. - cli.ts: `gbrain reindex --multimodal` dispatch with --limit, --dry-run, --cost-estimate, --no-embed, --yes, --json flags. - doctor.ts: unified_multimodal_coverage check (D21 source-aware) + reports per-source % when search.unified_multimodal is on. Warns at <95% lowest source; fails when unified_multimodal_only=true AND lowest source <99%. Falls open to OK when column not yet present. Tests: - unified-multimodal.test.ts (8 cases): schema migration v68 applies, reindex --dry-run + --cost-estimate + GBRAIN_NO_REEMBED bypass + zero-pending fast-path, hybridSearch unified routing forces voyage endpoint, D8 fail-open routes to text on empty unified, D8 strict blocks text fallback. All 211 tests across the cross-modal + related suite pass; no regression in voyage-multimodal / openai-compat-multimodal / search-mode / intent / search base suites. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Commit 4 of the cross-modal search wave (opt-in default off).
When `search.cross_modal.llm_intent` is true AND the regex classifier
returned 'text' AND `isAmbiguousModalityQuery(query)` fires, hybridSearch
awaits a Haiku tie-break via gateway.chat() before routing. The
ambiguous-modality gate (introduced in Commit 1) ensures the LLM call
only fires on the narrow band where regex misses but a visual noun +
reference marker both fire — roughly <1% of queries with the flag on.
- src/core/search/llm-intent.ts: new module. `classifyModalityWithLLM`
routes through gateway.chat() with a fixed system prompt ("Output
exactly one word: text, image, or both"). 1s timeout via AbortController.
`parseModality` is a pure exported helper that tolerates trailing
punctuation + casing. Fail-open on every error path (gateway
unavailable, timeout, parse failure, unrecognized output).
- src/core/search/hybrid.ts: escalation branch slots BEFORE the unified
routing branch. Gated by: no explicit per-call crossModal opt, regex
result == 'text', config flag on, ambiguity heuristic fires. Fail-open
to regex result on any error from the LLM tie-break.
Tests:
- llm-intent-escalation.test.ts (14 cases): parseModality tolerance
matrix (text / image / both / trailing punct / whitespace /
unrecognized / empty), classifyModalityWithLLM happy paths for all 3
outputs, fail-open on throw / unrecognized output / gateway-not-
configured, explicit-fallback-honored.
- llm-intent-hybrid-integration.test.ts (6 cases): hybridSearch
escalation gate fires ONLY when flag-on + ambiguous; off when flag-off,
unambiguous, regex-confident, or explicit per-call opt set; fail-open
on LLM throw.
All 231 tests across the cross-modal + related suite pass; no
regression in voyage-multimodal / openai-compat-multimodal /
search-mode / intent / search base suites.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three small fixes to pass the full unit + E2E sweep after the cross-modal
wave commits land.
- migrate.ts v67: drop date_trunc('day', created_at) from
mcp_spend_log indexes. TIMESTAMPTZ truncation depends on session
timezone and isn't IMMUTABLE, so Postgres rejects the function in
the index expression with SQLSTATE 42P17. BTREE on
(client_id, created_at) covers the per-day rollup query via range
scan on created_at — same performance, no IMMUTABLE constraint.
- pglite-schema.ts + src/schema.sql: shorten the embedding_multimodal
column comment. The longer version contained a comma inside a SQL
line comment ("...search.unified_multimodal=true, all queries..."),
which broke parseBaseTableColumns in test/schema-bootstrap-coverage
(the parser splits on commas at depth-0 before stripping comments,
so the comma inside the comment shortened the column-definition part
and an "all" token from "all queries" got picked up as the next
column name — silently hiding embedding_multimodal from coverage).
- schema-embedded.ts: regenerated via `bun run build:schema`.
- test/e2e/v030_1-integration-pglite.test.ts: listBackfills assertion
extended to include the new `modality` entry registered in
src/core/backfill-registry.ts as part of Commit 1.
- test/search/knobs-hash-reranker.test.ts: KNOBS_HASH_VERSION assertion
updated from 2→3 to match the cross-modal-wave hash-key extension
(D2 cache contamination fix). Same shape as the prior
v0.32→v0.35 bump.
- test/unified-multimodal.test.ts: migrated process.env mutation to
withEnv() helper to satisfy the scripts/check-test-isolation R1
rule.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…regen Final docs commit for the cross-modal wave (v0.36.0.0). - VERSION + package.json: bump 0.35.5.1 → 0.36.0.0 - CHANGELOG.md: full Garry-voice release entry with five-commit breakdown, the-numbers-that-matter table, what-this-means-for-you, and the required to-take-advantage-of-v0.36.0.0 block - docs/issues/cross-modal-search.md: cherry-picked from PR #1127 head (164 lines, the original spec doc preserved as historical reference for Phase 2 + 3 background) - CLAUDE.md: Key Files entries for src/core/ssrf-validate.ts, src/core/search/image-loader.ts, src/core/search/by-image.ts, src/core/search/llm-intent.ts, src/core/spend-log.ts, src/commands/reindex-multimodal.ts, plus extension annotations on src/core/search/query-intent.ts, src/core/search/mode.ts, src/core/search/hybrid.ts, src/core/backfill-registry.ts, src/core/migrate.ts (v67 + v68) - llms-full.txt + llms.txt: regenerated via `bun run build:llms` `bun run verify` clean (privacy + proposal-pii + test-names + jsonb + source-id-projection + progress + test-isolation + wasm + admin-build + admin-scope-drift + cli-exec + system-of-record + eval-glossary + typecheck). `bun test test/build-llms.test.ts` clean (7/7). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts: # CHANGELOG.md # VERSION # package.json # src/commands/search.ts # src/core/search/mode.ts # src/core/types.ts # test/search-mode.test.ts # test/search/knobs-hash-reranker.test.ts
Master shipped its own v67 (`facts_typed_claim_columns`) during the cross-modal wave's review cycle. The merge picked up both side's v67 entries, breaking the migration-distinct-versions test. Renumbering moves cross-modal's table + column ALTER off the collision: - v67 mcp_spend_log → v69 mcp_spend_log - v68 embedding_multimodal_column → v70 embedding_multimodal_column References updated in CHANGELOG, CLAUDE.md, pglite-schema.ts, schema.sql. schema-embedded.ts regenerated. llms-full.txt regenerated. 7006 unit tests pass, 0 fail. No test code touched — just version renumbering plus comment refs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumping to v0.36.4.0 to land in the queue slot the user requested. No behavior change; pure version bump across VERSION, package.json, CHANGELOG.md header, llms-full.txt regen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts: # CHANGELOG.md # VERSION # package.json
# Conflicts: # CHANGELOG.md # VERSION # package.json
# Conflicts: # CHANGELOG.md # VERSION # package.json # src/core/operations.ts # src/core/pglite-engine.ts # src/core/postgres-engine.ts # src/core/search/hybrid.ts # src/core/search/mode.ts # src/core/types.ts # test/search-mode.test.ts # test/search/knobs-hash-reranker.test.ts
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md # VERSION # package.json
mgunnin
added a commit
to mgunnin/gbrain
that referenced
this pull request
May 28, 2026
* upstream/master: v0.38.2.0 fix(doctor): bounded frontmatter scan + partial-state surfacing (supersedes garrytan#1287) (garrytan#1297) v0.38.1.0 feat(agents): provider-agnostic subagent loop + remote MCP dispatch + budget meter (garrytan#1289) v0.38.0.0 ingestion cathedral — gbrain capture + write-through + IngestionSource contract (garrytan#1275) v0.37.11.0: fresh-install PGLite embedding setup fix wave (garrytan#1286) v0.37.10.0 feat(init): env-detection + interactive picker + preflight invariants (garrytan#1278) v0.37.9.0 fix(frontmatter): canonical-style normalization for tag arrays (garrytan#1252) v0.37.8.0 feat: voyage-code-3 discoverability + reindex-code cost-preview fix (garrytan#1267) v0.37.7.0 fix wave: federated brains + autopilot safety + OAuth confidential clients (garrytan#1253) v0.37.6.0 feat(ai): OpenRouter recipe + generic default_headers seam (cherry-pick garrytan#1210) (garrytan#1246) v0.37.5.0 fix(markdown): YAML-aware NESTED_QUOTES validator (stops flagging valid YAML) (garrytan#1229) feat: pgGraph-inspired CI scaffolding wave (v0.37.4.0) (garrytan#1228) v0.37.3.0 feat: skill_brain_first doctor check + auto-fix + declarative opt-out (supersedes garrytan#1206) (garrytan#1215) v0.37.2.0: takes_resolution_consistency CHECK accepts 'unresolvable' (garrytan#1211) v0.37.1.0 feat: brainstorm + lsd — bisociation idea generator grounded in your own brain (garrytan#1214) v0.37.0.0 feat(skillpack): registry cathedral — third-party publish + install + 10/10 quality bar (garrytan#1208) v0.36.6.0 feat: cross-modal search wave (text↔image + unified column + LLM intent) (garrytan#1165)
brandonlipman
added a commit
to brandonlipman/gbrain
that referenced
this pull request
May 29, 2026
* upstream/master: v0.37.0.0 feat(skillpack): registry cathedral — third-party publish + install + 10/10 quality bar (garrytan#1208) v0.36.6.0 feat: cross-modal search wave (text↔image + unified column + LLM intent) (garrytan#1165) v0.36.5.0 feat: secure DATABASE_URL access for shell jobs (inherit: ["database_url"]) (garrytan#1192) v0.36.4.0 feat: brain-health-100 — autonomous remediation via doctor --remediate + Minions (garrytan#1193) fix(docs): comprehensive drift audit — contradictions, broken links, stale refs (garrytan#1201) v0.36.3.0 feat: dynamic embedding column selection for search (garrytan#1164) v0.36.2.0 feat: ZeroEntropy as default + zero-based README rewrite (garrytan#1136) v0.36.1.1 fix-wave: community PR triage + 28 atomic fixes (garrytan#1182) v0.36.1.0 Hindsight calibration wave: brain learns how you tend to be wrong (garrytan#1139) v0.36.0.0 feat(skillpack): scaffold + reference + harvest (retire managed-block install) (garrytan#1130) v0.35.8.0 feat(cycle): phantom-page redirect inside extract_facts (garrytan#1138) v0.35.7.0 feat: temporal trajectory + founder scorecard (Phases 2-4) (garrytan#1131) v0.35.6.0 feat(search): floor-ratio gate for metadata boost stages (closes garrytan#1091) (garrytan#1129) v0.35.5.1 fix(doctor): stop counting clean supervisor exits as crashes (garrytan#1108) v0.35.5.0 fix wave: bootstrap + orphans + think MCP + worktree + walker (garrytan#1111) v0.35.4.0 fix(doctor,entities): supervisor crash classification + bare-name resolver + 58x perf + stub guard observability (garrytan#1085) v0.35.3.1 feat(eval): temporal-aware contradiction probe + verdict enum (garrytan#1052) v0.35.3.0 fix wave: extract_facts items + git --no-recurse-submodules placement (garrytan#1053) # Conflicts: # src/core/postgres-engine.ts # test/schema-bootstrap-coverage.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The cross-modal search wave. Wires the 1024d Voyage multimodal column (already populated for ~11K image chunks via importImageFile) into the user-facing query path. Five atomic commits, bisect-friendly:
Commit 0 — Foundation:
embedMultimodalaccepts text variant +inputType: 'query'opt;embedMultimodalSafedoes binary-search retry on transient batch failure withfailed_indicessurfacingembedQueryMultimodal(text)+embedQueryMultimodalImage(input)query-side wrapperssrc/core/ssrf-validate.ts: DNS-resolve-and-fetch-by-IP defense for redirect chains. Closes the DNS rebinding gap that url-safety.ts's static check leaves open.Commit 1 — Phase 1 text → image:
suggestedModality: 'text' | 'image' | 'both'axis onQuerySuggestions; module-scopeCROSS_MODAL_PATTERNSregexSearchOpts.crossModal+SearchResult.modality; 7 new ModeBundle knobs;KNOBS_HASH_VERSION2→3 (cross-modal knobs APPEND after floor_ratio per CDX2-F13)gbrain backfill modality(registered asmodalitybackfill kind); doctorcross_modal_modality_backfillcheckCommit 2 — Phase 2 image-as-query:
src/core/search/image-loader.ts: SSRF-defended viafetchWithSSRFGuard; 10MB cap; PNG/JPEG/WebP magic-byte sniffsrc/core/search/by-image.ts: D13 hybrid intersect for optional text refinementsearch_by_imageMCP op (scope: read, NOT localOnly); D18 P0 — remotectx.remote=truewithimage_pathset rejected at handler entrymcp_spend_logtable (migration v69) +BudgetExceededErrorgateCommit 3 — Phase 3 unified column:
ALTER TABLE content_chunks ADD COLUMN embedding_multimodal vector(1024). Column-only; HNSW deferred to post-reindex per pgvector best practiceSearchOpts.embeddingColumnwidened to include'embedding_multimodal'; both engines'searchVectorroute accordinglygbrain reindex --multimodalwith D7 writer lock + cost prompt + 10s Ctrl-C grace + checkpoint resume +GBRAIN_NO_REEMBED=1bypass + D23-feat: GBrain v0.2.0 — incremental sync, file storage, install skill #2 auto-flip at coverage=100%unified_multimodal_only)unified_multimodal_coverage(D21 source-aware)Commit 4 — LLM intent escalation (opt-in):
src/core/search/llm-intent.ts:classifyModalityWithLLMviagateway.chat()with fixed system prompt + 1s timeout + fail-openTest Coverage
109 new test cases across 9 new test files. 7006 unit tests pass; 0 fail; 0 skip. 620+ E2E tests pass against real Postgres.
bun run verifyclean (privacy + jsonb + source-id-projection + progress + test-isolation + wasm + admin-build + scope-drift + cli-exec + system-of-record + eval-glossary + typecheck).embed-multimodal-batchingssrf-validatecross-modal-phase1cross-modal-hybrid-integrationcross-modal-phase2search-by-image-opunified-multimodalllm-intent-escalationllm-intent-hybrid-integrationPre-Landing Review
Plan went through full
/plan-eng-reviewwith 23 decisions captured (D1-D23) + codex outside-voice review surfacing 35 findings (22 incorporated, 4 deferred to TODOs). Plan file at~/.claude/plans/system-instruction-you-are-working-buzzing-metcalfe.mdcarries the full audit trail.Plan Completion
All 18 implementation tasks from the plan are DONE. D10 (reindex-core extraction) deferred — markdown and multimodal reindex cores diverge enough that shared infrastructure extraction is best done in a follow-up.
TODOS
4 follow-up items captured: D6 weighted-RRF eval-driven tuning, multi-image queries in
searchByImage, OCR pre-processing in image-as-query path, cross-modal in thequeryop (Phase 4).Documentation
CLAUDE.mdKey Files entries added forsrc/core/ssrf-validate.ts,src/core/search/{image-loader,by-image,llm-intent}.ts,src/core/spend-log.ts,src/commands/reindex-multimodal.ts. Extension annotations onsrc/core/search/{query-intent,mode,hybrid}.ts,src/core/backfill-registry.ts,src/core/migrate.ts(v69 + v70).docs/issues/cross-modal-search.mdcherry-picked from PR feat: cross-modal search — text↔image retrieval #1127 (the spec doc that started this wave; preserved as historical reference).llms-full.txt/llms.txtregenerated.Test plan
bun run verifyclean (13 pre-checks + typecheck)image_pathrejected at validateParamsCloses #1127 (spec doc preserved at
docs/issues/cross-modal-search.md).🤖 Generated with Claude Code