fix: append to the vector cache on embed instead of clearing it - #94
Conversation
setEmbedding cleared the whole per-workspace vector cache on every background embed batch, so the next recall() re-read and re-decoded every embedding BLOB from SQLite before scoring — O(N) I/O + allocation per query during active work, when batches fire continuously. Both the id and the vector are known at write time, so extend the cached matrix in place instead: appends for new episodes, O(1) replacement via an id → index map for re-embeds. insert-with-embedding gets the same treatment, and uint8ToFloat32 now allocates once per row instead of twice. At 20k episodes × 384 dims, the first recall after an embed batch drops from ~73 ms avg (spikes to 150 ms) to ~0.06 ms. Fixes #86 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis PR normalizes generated embedding vectors before persisting them in the daemon's embedding worker and adds a test suite validating incremental vector-cache behavior in SqliteEpisodeStore, covering insert, setEmbedding, lazy cache initialization, and vector copy immutability. ChangesEmbedding normalization and vector cache tests
Estimated code review effort: 2 (Simple) | ~10 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #94 +/- ##
==========================================
+ Coverage 73.06% 73.12% +0.06%
==========================================
Files 65 65
Lines 11032 11057 +25
==========================================
+ Hits 8060 8085 +25
Misses 2972 2972
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
… 21.6% -> 35.1%) feat/conductor was 22 commits behind main; rebased onto origin/main (conflict-free — P0 adds only new files). Re-ran the baseline on the current base: cross-workspace P@1 rose 21.6% -> 35.1% (MRR 0.45 -> 0.54) because main's #94 (append-to-vector-cache) improves vector coverage. Within-workspace unchanged at 89.2%; R@5 still 81%; same small-workspace-domination failure mode. 35.1% is the refreshed P1 target. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…log (#117) The 0.2.0 entry only covered the protocol/packages train (#100-#116) and missed ten PRs that also ship in this release: the untrusted-content sanitization and cross-tenant memory fixes (#91, #93 — now under a proper Security heading), the performance run (#94-#99), and the model catalog work (#78, #79). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… 21.6% -> 35.1%) feat/conductor was 22 commits behind main; rebased onto origin/main (conflict-free — P0 adds only new files). Re-ran the baseline on the current base: cross-workspace P@1 rose 21.6% -> 35.1% (MRR 0.45 -> 0.54) because main's #94 (append-to-vector-cache) improves vector coverage. Within-workspace unchanged at 89.2%; R@5 still 81%; same small-workspace-domination failure mode. 35.1% is the refreshed P1 target. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ner-delegated identity (P2) (#51) * docs: add conductor design, session-resolution architecture, and build plan Spec set for the codeoid conductor: a single global, identity-native supervisor session that resolves fuzzy natural-language references to the right coding session across all workspaces, coordinates the existing session fleet, and never goes out of context. - conductor-design.md: architecture + locked decisions (owner-delegated privileged identity, durable conductor / disposable children, confirm before send-class acts, approval-gated egress, metrics-only cost guard) - conductor-session-resolution.md: SOTA retrieval architecture (BGE-M3 hybrid + bge-reranker-v2-m3 + LFM2-350M-Extract cards + bi-temporal state), backed by a 9-agent research fan-out - conductor-build-plan.md: phased plan P0-P8 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: P0 conductor foundations — session-card store + eval metrics First P0 slice of the conductor (see docs/conductor-build-plan.md). - SessionCardStore (src/daemon/memory/cards.ts): per-session digest cards with a standalone FTS5 mirror for keyword/identifier recall, plus a bi-temporal fact log (Zep/Graphiti pattern) — state changes are invalidated-not-deleted (valid_at/invalid_at event time + created_at/expired_at system time), giving time-travel + a lossless audit trail. - Eval harness metrics (src/daemon/eval/metrics.ts): precision@1 / MRR / recall@k + latency percentiles for known-item session resolution — the go/no-go gate for the whole feature. 13/13 unit tests pass; tsc + biome clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: firstmate prior-art analysis + reconcile conductor plan with mobile app - conductor-prior-art-firstmate.md: analysis of firstmate (a shipped bash+prompt conductor, codeoid's architectural inverse). Borrows ranked: read-only-by-construction, zero-token event-driven supervision, ship/scout task shapes, per-project autonomy modes, /afk + /stow, harness dispatch profiles, secondmates (nested-conductor scaling). Where codeoid is already better: cryptographic identity, semantic session resolution, determinism (code vs 122KB prompt), daemon-native events. - build-plan: added 'Informed by firstmate' refinements and 'Reconciliation with the mobile app plan'. Key reconciliation: the conductor needs ZERO new client wire types (mobile doc §8), so the P3 protocol-level fleet.find is downgraded to optional; shared @codeoid/core extraction up front; sequencing = conductor backend (P1 session resolution) first as the risk-retiring gate, mobile app in parallel on today's protocol, converging at mobile-P5. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: hermes prior-art + upgrade P4 (durable queue/roles/hardening) + add P4.5 routines - conductor-prior-art-hermes.md: analysis of NousResearch/hermes-agent, the most complete personal-assistant prior art (multi-platform gateway, cron routines, autonomous skills, delegate + Kanban). Borrows: routines (cron+webhooks+script-injection [SILENT]), durable Kanban work-queue, leaf/orchestrator delegate role model, session-lifecycle hardening, multi-platform gateway shape, zero-context-cost tool-RPC scripts, Curator safe-autonomy invariants, ACP + serverless-persistence notes. Where codeoid stays ahead: cryptographic identity, rerank+bi-temporal retrieval, typed modular daemon (hermes is a Python monolith). - build-plan: upgraded P4 dispatch (durable Kanban-style queue + leaf/orchestrator roles enforced via ZeroID scopes + session-lifecycle hardening: resume_pending/stuck-loop/clean-shutdown/burst-collapse queue) and added P4.5 Routines (scheduled + webhook-triggered autonomy, [SILENT] monitors, cron hardening). Updated phase table, dependency graph, and added the 'Informed by hermes' section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: P0 baseline — session-resolution eval runner + fixture + measured number Wires a baseline resolver over the current workspace-scoped searchSessions and measures it against the real Hetzner corpus (16 sessions / 11 workspaces / 11,938 episodes) with 37 hand-labeled fuzzy references. Result: within-workspace P@1 = 89.2% (primitives sound), cross-workspace P@1 = 21.6% with R@5 = 81% — a ranking/fusion problem, not recall: searchSessions normalizes BM25 batch-relative PER workspace, so a small workspace's inflated scores dominate the naive cross-workspace merge (~22 misses return the same wrong small-workspace session at #1). 21.6% is the number P1 must beat; global normalized fusion + cross-encoder rerank should convert the 81% R@5 into P@1. See src/daemon/eval/BASELINE.md. memory.db is NOT committed (real session content); only the derived fuzzy-reference fixture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: refresh P0 baseline after rebase onto main (cross-workspace P@1 21.6% -> 35.1%) feat/conductor was 22 commits behind main; rebased onto origin/main (conflict-free — P0 adds only new files). Re-ran the baseline on the current base: cross-workspace P@1 rose 21.6% -> 35.1% (MRR 0.45 -> 0.54) because main's #94 (append-to-vector-cache) improves vector coverage. Within-workspace unchanged at 89.2%; R@5 still 81%; same small-workspace-domination failure mode. 35.1% is the refreshed P1 target. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: P1 slice 1 — cross-workspace global fusion for session resolution The conductor resolves a fuzzy reference across ALL workspaces, which the current workspace-scoped searchSessions can't do. Adds engine.recallGlobal(): unions FTS + vector candidates across every workspace and ranks in ONE batch, so the ranker's BM25 min-max normalization is GLOBAL (fixes the small-workspace-domination failure a naive per-workspace merge has). searchSessions() goes global when no workspaceId is passed. New store primitives: listWorkspaceIds, ftsSearchGlobal, episodesByIds. Measured on the real Hetzner corpus (37 labeled refs), vs the naive-merge baseline: - latency 4224 -> 24ms p95 (~200x: one search vs 11 per-workspace) - R@5 81% -> 92%, R@3 76% -> 81%, MRR 0.54 -> 0.61 - P@1 35.1% -> 37.8% (modest; remaining misses are rank 2-3 as big verbose sessions fill #1 — slice 2 cross-encoder rerank targets exactly this) 4 new deterministic cross-workspace tests (17 total green); tsc + biome clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: P1 slice 2 — cross-encoder rerank clears the resolution gate Adds a Reranker interface + transformers.js cross-encoder impl (Xenova/ms-marco-MiniLM-L-6-v2, swappable for bge-reranker-v2-m3). MemoryEngine reranks the top-8 candidate sessions by (query, evidence) when a reranker is present; searchSessions({rerank}) gates it (defaults on when ready) and degrades to fusion-only if the model fails to load. Measured on the real Hetzner corpus (37 refs): cross-workspace P@1 37.8% -> 86.5% (MRR 0.61 -> 0.91, R@3 81% -> 95%), latency +~30ms (88ms p95). Converts slice 1's 92% R@5 into precision@1 — essentially the within-workspace ceiling (97.3% with rerank on). P1 go/no-go gate CLEARED: cross-workspace P@1 35.1% (baseline) -> 86.5%, p95 < 100ms vs the 2s budget. 18 tests green (added a deterministic rerank test); tsc + biome clean. Remaining P1 slices (BGE-M3, identifier-aware lexical, session cards) are now optional polish. See src/daemon/eval/BASELINE.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: add session-resolution how-it-works explainer First-class explainer of the shipped cross-workspace session-resolution capability (two-stage: global fusion -> cross-encoder rerank). Distinct from the design/plan doc (conductor-session-resolution.md) and the eval writeup (BASELINE.md): covers the problem, the pipeline, why two stages, measured results (P@1 35.1% -> 86.5%), the local models, a code map, repro, and limitations. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: genericize eval fixture references + refresh numbers codeoid is public; the eval fixture named internal project specifics. Genericized all 37 reference strings to neutral software-work descriptions (gold labels are opaque UUIDs — unchanged) and scrubbed an internal name from BASELINE.md. Re-ran: pure-conceptual references (no exact identifiers) are a harder, conservative eval — cross-workspace P@1 21.6% (naive) -> 35.1% (fusion) -> 73.0% (rerank), R@5 92%, <100ms. Two-stage story unchanged; identifier-bearing references resolve higher still. Refreshed numbers in BASELINE.md, docs/session-resolution.md, and the build plan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: P2 conductor identity foundation — durable owner-delegated identity Conductor scope profile (R1): session:read / session:dispatch join the protocol scopes; CONDUCTOR_SCOPES deliberately omits tools:write and tools:execute, so ZeroID's per-hop scope intersection makes the conductor's whole delegation subtree read-only-by-construction on targets. Durable identity (R2): registerConductor(ownerSub) registers a ZeroID orchestrator identity and persists {identityId, wimseUri, apiKey} to the Store; resumeSessions reloads it on daemon restart — one stable WIMSE URI across process lifetimes, with the actor keypair regenerated (never at rest) and re-registered per boot. mintConductorToken exchanges the owner's subject token for the conductor's working token (RFC 8693), and deactivateConductor cascade-revokes the subtree via ZeroID's parent_jti walk. Integration test (bun run test:integration, live ZeroID required): mints the owner → conductor → child → sub-agent chain at delegation_depth 3 with a verified act chain per hop, proves tools:write can't be minted below the conductor, and asserts deactivation kills conductor/child/sub-agent tokens while the owner's token stays active. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: unit-test the conductor lifecycle and reranker backend Fetch-stubbed ZeroID covers register/resume/mint/deactivate — including the stale-row drop, key rotation on resume, and the actor-assertion wire contract — plus Store persistence per tenant. The cross-encoder reranker is covered with @xenova/transformers mocked (no model download): batching shape, single-logit vs two-class score extraction, and re-init after close. The live-ZeroID integration test remains the depth-3 / cascade-revocation proof; these keep the CI patch-coverage gate honest without a server. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: address CodeRabbit review — write ordering, transactions, bounded init - registerConductor persists the identity BEFORE exposing it in memory, so a Store failure reads as registration failure, not a phantom durable identity. - deactivateConductor keeps the persisted row when the remote deactivation fails — it's the only durable record of a still-live identity, and the next call retries against it (test added). - Card upsert + FTS mirror refresh and assertFact's supersede + insert each commit in one transaction, so a crash can't leave the FTS drifted or a (subject, predicate) with no open fact. - assertFact rejects out-of-order validAt instead of closing the open fact with invalid_at < valid_at, which made the row unsatisfiable for every factsAsOf() read (test added). - reranker.init() is bounded by a 120s timeout — a stalled model download degrades to fusion-only instead of wedging daemon startup; close() disposes the ONNX model to actually free WASM memory. - conductor-design.md: fleet MCP allowlist gotcha noted for P3, §4 updated to the CONDUCTOR_SCOPES profile as implemented, §9 Shield marked later-phase (v1 = owner approval only, per R4). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Problem (#86)
setEmbeddingcleared the entire per-workspace vector cache on every background embed batch. Since batches fire continuously during active work, the nextrecall()re-read every embedding BLOB from SQLite and re-decoded the full matrix before the cosine loop even started — O(N) I/O + allocation per query that should be O(1), all on the shared daemon event loop.Fix
Both the id and the vector are already known at write time, so the cached matrix is extended in place instead of being invalidated:
setEmbeddingnow takes theworkspaceId(the engine has the episode loaded) and upserts the row into the cached matrix.id → indexmap, so the re-embed path replaces in O(1) instead of duplicating.insert-with-embedding gets the same append treatment (it used todeletethe workspace entry).loadVectorMatrix()picks the row up from SQLite with everything else.uint8ToFloat32allocates theFloat32Arrayfirst and blits bytes into its buffer: one allocation per row instead of two (halves the transient garbage of the remaining cold build).The cached vector is copied on upsert so later caller mutation can't desync cache from the persisted BLOB.
Complexity
Micro-bench
20,000 episodes × 384-dim embeddings (the issue's ~50k scenario scaled down; scaling is linear), 5 rounds each:
The old cost recurred on every recall that followed an embed batch — i.e. more or less every recall during active work. It's now paid once per process per workspace (first recall), and never again.
Tests
New
describeblock pins the behavior via object identity (loadVectorMatrixreturns the same object after writes) plus value checks: append onsetEmbedding, append on insert-with-embedding, O(1) replace on re-embed without duplication, no-op before first build, and defensive copy semantics. Verified the cached rows match a cold rebuild from a fresh connection.bun x tsc --noEmit✓ ·bun run lint✓ ·bun run test756 pass / 0 fail ✓Related but separate: #71 tracks replacing the brute-force cosine scan itself with an ANN backend.
Fixes #86
🤖 Generated with Claude Code
Summary by CodeRabbit