Closes the Memory CRITICAL/HIGH cluster from the 2026-05-12 audit. All five issues are addressed by one commit each.
#13 (CRITICAL) — replay log poisoned by synthetic memory-trigger-<hash> turn_ids. AppDelegateMemoryReplaySink now attributes every memory event to a per-process sentinel TurnID that installAgent writes into replay.sqlite/turns after beginSession. ReplayLog.flushPending gains a batch-split fallback so one bad row can no longer take its 30 batch-siblings down. Real turn events queued behind a poison row now persist.
#15 (HIGH) — MemoryExtractionOrchestrator.process no longer FNV-1a-hashes the TurnID UUID into facts.source_turn_id. The hash joined nothing in the turns table. We pass 0 as a sentinel meaning "no correlated turn rowid"; the schema comment documents the trade-off (audit option 1).
#12 (CRITICAL) — MemoryStore.init accepts an optional embedder: any EmbeddingProviding?. insertNewFact runs the embedder against subject predicate object after the facts row commits and writes the matching facts_vec row. Failures log + leave the FTS row (degraded but searchable). AppDelegate.installMemory shares one OllamaEmbeddingClient between writer + reader sides.
#16 (HIGH) — MemoryQueries.sanitizeFTS5Query strips non-alphanumeric chars, splits on whitespace, double-quotes each token. HybridSearch.searchFacts calls it before binding to MATCH ?. Embedding still runs on the raw query so semantic recall isn't degraded. Short-circuits to empty results when no tokens remain.
#14 (CRITICAL) — ContextBuilder.selfAwarePreamble now names search_memory and forget_fact with the cue from SearchMemoryTool.toolDescription. Length still well under the 4096-char cache-eligibility boundary; the existing test_preambleDoesNotEnableCacheUnintentionally still passes.
swift test --package-path packages/Memory — 98 tests, 10 skipped, 0 failures (skipped suite is env-gated real-models corpus).
swift test --package-path packages/Replay — 34 tests, 0 failures.
swift test --package-path packages/AgentCore — pre-existing tests + new preamble assertion all pass.
bash scripts/check-app-builds.sh — clean.
All 18 boundary gates green (for s in scripts/check-*.sh; do bash "$s"; done).
Notes for review
Empirical end-to-end verification of facts_vec writes and search round-trips needs the user's environment (local Ollama + nomic-embed-text + qwen2.5-coder:32b). The MemoryRegressionCorpusTests env-gated suite (#56/#58) is the right surface for that; default CI runs the stub-embedder tests in MemoryStoreVecWriteTests which prove the wire shape without touching the network.
MemoryStore.applyOp is now async throws (was throws). All call sites already used try await because MemoryStore is an actor, so the change is transparent.
AppDelegateMemoryReplaySink gains a sentinelTurnID: TurnID field; the matching turns row is inserted in installAgent right after beginSession. Until that row exists, sink emissions still flow but get dropped via the new batch-split path — the chain stays uncorrupted.
Test plan
User restarts the app and confirms ~/Library/Logs/Jarvis/replay.<date>.log no longer shows "replay flush failing chronically" or consecutive_failures climbing.
User asks "what is my dog's name?" — agent calls search_memory (visible in ~/Library/Logs/Jarvis/memory.<date>.log[memory.search] channel) BEFORE responding.
User confirms a new fact added via "remember X" results in both a facts row AND a facts_vec row (sqlite3 jarvis.db "SELECT COUNT(*) FROM facts; SELECT COUNT(*) FROM facts_vec_rowids;").
User confirms natural-language search queries with ?, ', etc. don't throw FTS5 syntax errors.
## Summary
Closes the Memory CRITICAL/HIGH cluster from the 2026-05-12 audit. All five issues are addressed by one commit each.
- **#13 (CRITICAL)** — replay log poisoned by synthetic `memory-trigger-<hash>` turn_ids. `AppDelegateMemoryReplaySink` now attributes every memory event to a per-process **sentinel TurnID** that `installAgent` writes into `replay.sqlite/turns` after `beginSession`. `ReplayLog.flushPending` gains a **batch-split** fallback so one bad row can no longer take its 30 batch-siblings down. Real turn events queued behind a poison row now persist.
- **#15 (HIGH)** — `MemoryExtractionOrchestrator.process` no longer FNV-1a-hashes the TurnID UUID into `facts.source_turn_id`. The hash joined nothing in the `turns` table. We pass `0` as a sentinel meaning "no correlated turn rowid"; the schema comment documents the trade-off (audit option 1).
- **#12 (CRITICAL)** — `MemoryStore.init` accepts an optional `embedder: any EmbeddingProviding?`. `insertNewFact` runs the embedder against `subject predicate object` after the facts row commits and writes the matching `facts_vec` row. Failures log + leave the FTS row (degraded but searchable). `AppDelegate.installMemory` shares one `OllamaEmbeddingClient` between writer + reader sides.
- **#16 (HIGH)** — `MemoryQueries.sanitizeFTS5Query` strips non-alphanumeric chars, splits on whitespace, double-quotes each token. `HybridSearch.searchFacts` calls it before binding to `MATCH ?`. Embedding still runs on the raw query so semantic recall isn't degraded. Short-circuits to empty results when no tokens remain.
- **#14 (CRITICAL)** — `ContextBuilder.selfAwarePreamble` now names `search_memory` and `forget_fact` with the cue from `SearchMemoryTool.toolDescription`. Length still well under the 4096-char cache-eligibility boundary; the existing `test_preambleDoesNotEnableCacheUnintentionally` still passes.
Each fix has a TDD-style regression test:
- `test_preambleNamesMemoryTools` (#14)
- `testSearchFactsSanitizesPunctuationBeforeFTS5` + `testSearchFactsSanitizesQuotesAndOperators` + `testSearchFactsEmptyAfterSanitizeReturnsEmpty` (#16)
- `testApplyOpADDWritesFactsVecRowWhenEmbedderInjected` + `testApplyOpADDPersistsFactWhenNoEmbedderInjected` + `testApplyOpADDPersistsFactWhenEmbedderThrows` (#12)
- `testProcessUsesSentinelSourceTurnIdNotFNVHash` (#15)
- `test_L10_batchSplitOnFKFailurePreservesOtherEvents` (#13)
## Verification
- `swift test --package-path packages/Memory` — 98 tests, 10 skipped, 0 failures (skipped suite is env-gated real-models corpus).
- `swift test --package-path packages/Replay` — 34 tests, 0 failures.
- `swift test --package-path packages/AgentCore` — pre-existing tests + new preamble assertion all pass.
- `bash scripts/check-app-builds.sh` — clean.
- All 18 boundary gates green (`for s in scripts/check-*.sh; do bash "$s"; done`).
## Notes for review
- **Empirical end-to-end verification** of `facts_vec` writes and search round-trips needs the user's environment (local Ollama + `nomic-embed-text` + `qwen2.5-coder:32b`). The `MemoryRegressionCorpusTests` env-gated suite (#56/#58) is the right surface for that; default CI runs the stub-embedder tests in `MemoryStoreVecWriteTests` which prove the wire shape without touching the network.
- `MemoryStore.applyOp` is now `async throws` (was `throws`). All call sites already used `try await` because `MemoryStore` is an actor, so the change is transparent.
- `AppDelegateMemoryReplaySink` gains a `sentinelTurnID: TurnID` field; the matching `turns` row is inserted in `installAgent` right after `beginSession`. Until that row exists, sink emissions still flow but get dropped via the new batch-split path — the chain stays uncorrupted.
## Test plan
- [ ] User restarts the app and confirms `~/Library/Logs/Jarvis/replay.<date>.log` no longer shows "replay flush failing chronically" or `consecutive_failures` climbing.
- [ ] User asks "what is my dog's name?" — agent calls `search_memory` (visible in `~/Library/Logs/Jarvis/memory.<date>.log` `[memory.search]` channel) BEFORE responding.
- [ ] User confirms a new fact added via "remember X" results in both a `facts` row AND a `facts_vec` row (`sqlite3 jarvis.db "SELECT COUNT(*) FROM facts; SELECT COUNT(*) FROM facts_vec_rowids;"`).
- [ ] User confirms natural-language search queries with `?`, `'`, etc. don't throw FTS5 syntax errors.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Closes the Memory CRITICAL/HIGH cluster from the 2026-05-12 audit. All five issues are addressed by one commit each.
memory-trigger-<hash>turn_ids.AppDelegateMemoryReplaySinknow attributes every memory event to a per-process sentinel TurnID thatinstallAgentwrites intoreplay.sqlite/turnsafterbeginSession.ReplayLog.flushPendinggains a batch-split fallback so one bad row can no longer take its 30 batch-siblings down. Real turn events queued behind a poison row now persist.MemoryExtractionOrchestrator.processno longer FNV-1a-hashes the TurnID UUID intofacts.source_turn_id. The hash joined nothing in theturnstable. We pass0as a sentinel meaning "no correlated turn rowid"; the schema comment documents the trade-off (audit option 1).MemoryStore.initaccepts an optionalembedder: any EmbeddingProviding?.insertNewFactruns the embedder againstsubject predicate objectafter the facts row commits and writes the matchingfacts_vecrow. Failures log + leave the FTS row (degraded but searchable).AppDelegate.installMemoryshares oneOllamaEmbeddingClientbetween writer + reader sides.MemoryQueries.sanitizeFTS5Querystrips non-alphanumeric chars, splits on whitespace, double-quotes each token.HybridSearch.searchFactscalls it before binding toMATCH ?. Embedding still runs on the raw query so semantic recall isn't degraded. Short-circuits to empty results when no tokens remain.ContextBuilder.selfAwarePreamblenow namessearch_memoryandforget_factwith the cue fromSearchMemoryTool.toolDescription. Length still well under the 4096-char cache-eligibility boundary; the existingtest_preambleDoesNotEnableCacheUnintentionallystill passes.Each fix has a TDD-style regression test:
test_preambleNamesMemoryTools(#14)testSearchFactsSanitizesPunctuationBeforeFTS5+testSearchFactsSanitizesQuotesAndOperators+testSearchFactsEmptyAfterSanitizeReturnsEmpty(#16)testApplyOpADDWritesFactsVecRowWhenEmbedderInjected+testApplyOpADDPersistsFactWhenNoEmbedderInjected+testApplyOpADDPersistsFactWhenEmbedderThrows(#12)testProcessUsesSentinelSourceTurnIdNotFNVHash(#15)test_L10_batchSplitOnFKFailurePreservesOtherEvents(#13)Verification
swift test --package-path packages/Memory— 98 tests, 10 skipped, 0 failures (skipped suite is env-gated real-models corpus).swift test --package-path packages/Replay— 34 tests, 0 failures.swift test --package-path packages/AgentCore— pre-existing tests + new preamble assertion all pass.bash scripts/check-app-builds.sh— clean.for s in scripts/check-*.sh; do bash "$s"; done).Notes for review
facts_vecwrites and search round-trips needs the user's environment (local Ollama +nomic-embed-text+qwen2.5-coder:32b). TheMemoryRegressionCorpusTestsenv-gated suite (#56/#58) is the right surface for that; default CI runs the stub-embedder tests inMemoryStoreVecWriteTestswhich prove the wire shape without touching the network.MemoryStore.applyOpis nowasync throws(wasthrows). All call sites already usedtry awaitbecauseMemoryStoreis an actor, so the change is transparent.AppDelegateMemoryReplaySinkgains asentinelTurnID: TurnIDfield; the matchingturnsrow is inserted ininstallAgentright afterbeginSession. Until that row exists, sink emissions still flow but get dropped via the new batch-split path — the chain stays uncorrupted.Test plan
~/Library/Logs/Jarvis/replay.<date>.logno longer shows "replay flush failing chronically" orconsecutive_failuresclimbing.search_memory(visible in~/Library/Logs/Jarvis/memory.<date>.log[memory.search]channel) BEFORE responding.factsrow AND afacts_vecrow (sqlite3 jarvis.db "SELECT COUNT(*) FROM facts; SELECT COUNT(*) FROM facts_vec_rowids;").?,', etc. don't throw FTS5 syntax errors.🤖 Generated with Claude Code