* docs(memory): scrub Phase-7 hedge language from MemoryStore (#19)
Audit 2026-05-12 flagged five doc-comment references that mis-described
the implementation state. The cited plans (07-02, 07-03, 07-06) shipped
months ago — the bounded extraction channel, the hybrid FTS5 + vec0 read
path, the ReplayEvent emission, the regression corpus test seams, and
`installMemory`'s replay-sink wiring are all live, not "wires" or
"will replace" promises.
Five touchpoints rewritten to describe the code as it is:
- `MemoryReplaySink` doc: drop the "Plan 07-06" plan reference; the seam
is just used at startup.
- `MemoryStore` actor doc: replace the "Plan 07-02 wires / Plan 07-03
wires" sentence with the actual contract (single writer; hybrid search
through `MemoryReadStore`; `recordRetrieval` emits `memoryRetrieval`).
- Read-path MARK: drop the "Plan 07-03" plan label; keep the
requirement-ID list (MEM-07 / TEXT-03 / D-02 / D-05) since those are
still useful for traceability to the audit corpus.
- `recordRetrieval` doc: replace "Mirrors recordMemoryMutation from 07-02"
with "Mirrors recordMemoryMutation on the write path."
- Test-seams MARK: drop "Plan 07-06" prefix; describe by the test target
(JARVIS_REAL_MODELS=1 corpus suite) instead.
The 2026-05-11 D-5/D-6 markers and the "Boot policy (changed 2026-05-11)"
doc are preserved unchanged — they document current behaviour, not
historical hedges.
`swift build --package-path packages/Memory` clean.
Closes#19
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(bus): scrub JarvisBusWorld drift + add grep gate (#45)
`fb41c5f` (2026-05-02) unified `Injection.js` and the HUD bundle into the
default `WKContentWorld.page` and retired the prior isolated
`JarvisBusWorld` named world. The doc-comments and test fixtures across
20+ lines kept describing the bug-era topology — pre-existing-bus attach
"because cross-world isolation", message-handler-only-visible-in-named-
world, etc. — for months afterward. Result: a new reader following the
docs would conclude the bus uses two worlds when it actually uses one.
Five rationale rewrites:
- `WebviewBridge.swift` class doc — handler is on the page world, not an
isolated named world. Page scripts in the loaded HUD bundle address it
via `window.webkit.messageHandlers.jarvisBus`.
- `Injection.js` header — names the page world explicitly; one-line nod to
`fb41c5f` for the historical context.
- `bridge.ts` JSDoc + `isJarvisBusShape` rationale — recast the H-02
attach-to-existing path as a "defensive double-install guard" inside
ONE page world. Injection.js fires first at document-start; the bundle
installer fires later as a top-level `main.tsx` side-effect; the guard
prevents the bundle from clobbering Injection.js's queued early
messages and captured handshake state. The duck-type `isJarvisBusShape`
CODE is unchanged — only the comment narrative was misleading.
- `webview/packages/hud/src/main.tsx` — drop the cross-world-isolation
framing; describe the single-world reality.
- `App/AppDelegate.swift` install comment — page world, not isolated.
Test-side updates (same scrub, plus one latent test bug surfaced):
- `WebviewBridgeTests.test_scriptMessageHandlerIsRegisteredUnderConfigured-
Name`: was removing from `WKContentWorld.world(name: "JarvisBusWorld")`
while the bridge registered against `.page` — silent no-op since
`removeScriptMessageHandler` doesn't throw on absent. Now removes from
`.page` (the world the bridge actually uses), making the
"registration-is-evidence-of-success" assertion meaningful again.
- `WebviewBridgeOutboundTests`: comment-only.
- `RealWKWebViewIntegrationTests`: comment-only (forensic narrative kept,
literal world name replaced with "prior isolated named content world").
- `webview/packages/bus/tests/bridge.test.ts`: H-02 describe-block + one
it-name rewritten to match the new defensive-double-install rationale.
Infrastructure:
- `scripts/check-no-jarvis-bus-world-references.sh`: new boundary gate.
Greps for the literal `JarvisBusWorld` in tracked files; excludes
`.planning/`, `docs/`, `webview/packages/*/dist/`, and this script
itself. Wired into `CLAUDE.md`'s gate listing; the CI
`boundary-gates` job auto-discovers all `scripts/check-*.sh`, so no
workflow edit is needed.
- `webview/packages/bus/package.json`: 0.0.0 → 0.0.1 (per spec —
comment-only changes still bump patch so consumers can pin).
- `webview/packages/bus/dist/bridge.{js,d.ts}`: rebuilt via `pnpm --filter
@jarvis/bus build`; NOT committed because `dist/` is not tracked (the
build phase regenerates on each xcodebuild invocation).
Verified locally:
- `swift build --package-path packages/Bus` clean
- `pnpm --filter @jarvis/bus test` — 37/37 pass
- `bash scripts/check-no-jarvis-bus-world-references.sh` — PASS
- `bash scripts/check-bus-protocol-version.sh` — PASS (v2.3.0)
- `bash scripts/check-bus-harness-parity.sh` — PASS
Closes#45
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
The codebase moved to qwen3.6:latest (memory extractor) and
qwen2.5-coder:32b-instruct-q8_0 (agent fallback) in commit 8d0a959, but
CLAUDE.md and the MemoryRegressionCorpusTests doc-comments still cited
the vanilla qwen2.5-coder:32b string. Both updated to match what
ModelID.swift, MemoryExtractor.swift, and BootHealth.swift actually
use today.
No behavior change — documentation alignment only.
Closes#58
#14 (CRIT): ContextBuilder.selfAwarePreamble names search_memory + forget_fact with explicit cue 'call search_memory FIRST. Never claim to remember without searching.'
#16 (HIGH): MemoryQueries.sanitizeFTS5Query strips non-alphanumeric, quotes tokens; HybridSearch.searchFacts calls it before binding to MATCH ?. Embedding still runs on raw query.
#12 (CRIT): MemoryStore.init accepts optional embedder; insertNewFact writes facts_vec after the facts commit. Vec failures log + leave FTS row (no rollback). installMemory shares one embedder between writer + reader.
#15 (HIGH): drop stableHash; pass sentinel 0 as sourceTurnId (option 1 per audit — smaller change surface than threading real rowid through 5 files). Schema comment updated.
#13 (CRIT): two-layer fix. Layer 1: AppDelegate owns a per-process sentinel TurnID; installAgent writes the matching turns row after beginSession; AppDelegateMemoryReplaySink attributes events to it. Layer 2: ReplayLog.flushPending retries failed batches event-by-event so one poisoned row only drops itself.
Memory 98 + Replay 34 + AgentCore 243 tests pass; all 18 boundary gates green.
Root cause of "facts never persist" surfaced during 2026-05-12 dogfood:
ERROR [memory.orchestrator] memory applyOp failed: applyOpFailed(
underlying: Replay.SQLiteError.stepFailed(
code: 19, message: "FOREIGN KEY constraint failed"))
The schema declared:
source_turn_id INTEGER REFERENCES turns(id)
But the writer at MemoryExtractionOrchestrator.swift:125 inserts:
let turnIdInt: Int64 = stableHash(job.turnId.rawValue)
`TurnID` is a UUID-string wrapper; the writer FNV-1a-hashes it to an
Int64 for the integer column. The hash space and `turns.id`
(AUTOINCREMENT) are disjoint number systems by design, so every
INSERT into `facts` violated the FK and SQLite rolled back the row.
The BootHealth probe reports the symptom honestly ("26 turns, 0/0
facts") but couldn't flag this as a failure — 0 facts is the
legitimate empty-history case. The bug stayed invisible until the
user asked "is Toby a fact in the database?" and search returned
nothing despite asynchronous extraction running.
This bug was latent for the entire project history — the original
RESEARCH §1 schema specified the FK, but the writer was never wired
to use real `turns.id` rowids. Reintroducing the FK without first
fixing the writer (forward through MemoryStore.appendTurn returning
the rowid, then plumbed back to MemoryExtractionCoordinator) would
just re-break persistence.
Fix: drop the REFERENCES on `source_turn_id`. The column is now
documented as a "correlation tag", not a hard foreign key. The
`superseded_by REFERENCES facts(id)` constraint stays — that one
IS valid because supersede chains use `priorFact.id` (a real
AUTOINCREMENT rowid).
Existing DBs: `CREATE TABLE IF NOT EXISTS` won't ALTER an existing
table; the user must delete `~/Library/Application Support/Jarvis/
jarvis.db*` and relaunch. Their facts table has 0 rows so nothing
of value is lost. A proper migration framework (PRAGMA user_version)
is deferred to M-1+.
Follow-up (M-1+): plumb the real `turns.id` rowid through
`MemoryStore.appendTurn` → orchestrator state → coordinator →
applyOp so `source_turn_id` becomes a real FK again. Requires
changes across 4 files; not in scope for this hotfix.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Matches commit 8d0a959 model flip — extractor default is now
ModelID.qwen36 ("qwen3.6:latest"). The previous test name + assertion
were stale and failed on `swift test --package-path packages/Memory`
with the "Optional(qwen3.6:latest)) is not equal to (qwen2.5-coder…)"
diff. No production code change.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Round 1 of the post-D-5/D-6 model strategy split. The user has both
`qwen3.6:latest` (~22GB MoE) and `qwen2.5-coder:32b-instruct-q8_0` (~32GB)
pulled. CLAUDE.md previously pinned the entire local-model strategy to
the base `qwen2.5-coder:32b` because of an April 2026 Ollama tool-call
bug affecting Qwen 3 / 3.5 / Gemma 4. That ban was correct then; today
(2026-05-11) it isn't.
**Empirical retest 2026-05-11:** POST /api/chat (non-streaming) against
qwen3.6:latest with a representative `apply_memory_ops` tool definition
returned a clean tool_calls array, well-formed JSON args, no leaked
<think> tags. Upstream Ollama appears to have closed the relevant
issues (#14493 / #14601 / #14745 / #15315) in the interim. Documented
in ModelID.qwen36 doc comment.
Two-model strategy:
- **MemoryExtractor default → qwen3.6:latest** (`ModelID.qwen36`)
Single-turn, single-tool, no cap recovery — the lowest-risk place to
validate the newer model. If extraction stays stable for a few days
of dogfooding, we revisit the agent-loop fallback.
- **Agent-loop Ollama fallback → qwen2.5-coder:32b-instruct-q8_0**
(`ModelID.qwen25coder32b`'s rawValue is now the Q8_0 variant the user
already has — higher fidelity than the default Q4_K_M, same model
family, same tool-call wire format). The agent loop has multi-turn
tool chains, cap recovery, tool_choice=none paths, and streaming all
in scope — kept conservative until Qwen3.6 is verified there too.
Changes:
- packages/AgentCore/Sources/AgentCore/ModelID.swift
+ `qwen36` constant for qwen3.6:latest with empirical retest doc.
! `qwen25coder32b` rawValue is now `qwen2.5-coder:32b-instruct-q8_0`
(same constant name, the Q8_0 raw value). All ModelID.qwen25coder32b
consumers pick up the change automatically.
- packages/Memory/Sources/Memory/MemoryExtractor.swift
- `model: ModelID = .qwen25coder32b` → `.qwen36`.
Docstring updated to explain the empirical retest + why the
extractor is the safest place for the newer model.
- packages/Harness/Sources/Harness/Runners/LiveOllamaRunner.swift
`requiredModelID` constant now sources from
`ModelID.qwen25coder32b.rawValue` so a future single-edit on the
canonical model name propagates here automatically. Doc updated to
point at the q8_0 variant + ~32GB pull size.
User-facing note: no model pulls required for this commit; both
models are already on the user's machine.
Round 2 (boot-phase health check) and Round 3 (Status menu dialog)
deferred to a fresh session for context budget — handoff doc to follow.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes D-5 / D-6 (sqlite-vec dylib bundling) carry-forwards from the
2026-05-04 audit by adopting jkrukowski/SQLiteVec as a SPM dependency.
The package's CSQLiteVec target compiles sqlite3.c + sqlite-vec.c into
one statically-linked C target with `core_vec_init()` registering vec0
as an auto-extension. All sqlite3_* calls in Replay's SQLiteConnection
and Memory's MemoryStore resolve to this bundled SQLite (vec0 always
available) instead of Apple's stripped /usr/lib/libsqlite3.dylib.
The retired hand-rolled approach (dlsym pathway + JARVIS_VEC0_STUB_PATH
env-flag + Bundle.module placeholder text file) never worked on macOS
because Apple stripped sqlite3_load_extension from the system libsqlite3.
Memory init has been silently dormant since Phase 7 (2026-04 onward),
which is what made B-02 (history threading) appear permanently broken
to the user during dogfooding even after the 48ad8d6 tactical patch
landed.
Live verification 2026-05-11: fresh launch with empty jarvis.db creates
schema, persists turn rows across submits, B-02 multi-turn context
resolves correctly ("what breed is Toby?" → "Golden Retriever" after
prior turn established Toby is a Golden Retriever), and the new
get_memory_stats tool reports `turnsCount=6, vecVersion=v0.1.6,
sqliteVersion=3.51.0`.
Changes:
- packages/Replay/Package.swift
+ SQLiteVec SPM dependency (from: "0.0.14")
+ CSQLiteVec product wired into Replay target
- packages/Replay/Sources/Replay/SQLiteConnection.swift
- import SQLite3
+ @_exported import CSQLiteVec
All sqlite3_* call sites unchanged — linker now resolves to our
bundled sqlite3 with vec0 statically linked in.
- packages/Memory/Package.swift
+ SQLiteVec SPM dependency (named product so MemoryStore can import
CSQLiteVec directly for the core_vec_init() one-shot call)
- resources: PLACEHOLDER.txt copy (defunct vec0.dylib stub slot)
- packages/Memory/Sources/Memory/MemoryStore.swift
- import SQLite3 → import CSQLiteVec
+ _vecAutoExtensionRegistered static-let one-shot: calls
CSQLiteVec.core_vec_init() which registers vec0 via
sqlite3_auto_extension on the first MemoryStore construction.
Idempotent (static-let initializer fires exactly once).
- loadVecExtension(on:) dlsym/JARVIS_VEC0_STUB_PATH/Bundle.module
pathway (retired; ~70 lines removed)
Updated docstring to reflect the new hard-fail boot policy.
- packages/Memory/Sources/Memory/Resources/PLACEHOLDER.txt — deleted
- App/AppDelegate.swift installMemory()
Reverses Track-D D-1 (75a10be, 2026-05-04): MemoryStore.init failures
are now FATAL via Logger.critical + TCCAlertService.presentHardBlock
+ NSApp.terminate. The graceful-degradation path that kept the app
booting with memoryStore=nil hid the entire B-02/B-04/B-05 silent-
failure bug class. Aligns with the 2026-05-07 API-first pivot
thesis: "no silent failures".
`store` is now non-optional throughout installMemory; downstream
callers no longer need `if let store = store`.
- packages/MCP/Sources/MCP/InProcess/GetMemoryStatsTool.swift (new)
+ MemoryStatsDispatching protocol
+ MemoryStats Codable struct (turnsCount, factsTotal, factsActive,
sessionsCount, dbFileBytes, vecVersion, sqliteVersion,
lastTurnAtUnixMs, lastFactAtUnixMs)
+ GetMemoryStatsTool conforming to InProcessTool
- App/MCP/InProcessMemoryAdapters.swift
+ MemoryStatsStoreAdapter — bridges the protocol to MemoryStore's
public queryRowCount / querySingleString seams and FileManager
for the on-disk file size. Read-only, no confirmation.
- App/AppDelegate.swift installSelfKnowledgeTools()
+ Registers GetMemoryStatsTool with MemoryStatsStoreAdapter.
- packages/Memory/Tests/MemoryTests/*
Retired the JARVIS_VEC0_STUB_PATH XCTSkipUnless guards from 9 tests
(vec0 is always available now — no need to env-gate). Rewrote
MemoryStoreTests to drop the now-impossible "init throws when vec
missing" case and assert facts_vec virtual table creation +
vec_version() readback. Added seedTurnRow helper to
MemoryStoreApplyOpTests so the now-enforced
`facts.source_turn_id REFERENCES turns(id)` foreign key is honored
in tests that previously passed when FK enforcement was dormant.
Test summary: Memory full suite 91 tests, 10 skipped (env-gated real-
Ollama tests), 0 failures. App target builds clean. Substrate verified
end-to-end via live launch (vec_version: v0.1.6 logged at boot;
get_memory_stats returns actual schema row counts).
Pending D-7 carry-forward: ollama pull nomic-embed-text (~270MB) for
embedder; user already has qwen2.5-coder:32b-instruct-q8_0 which is
a higher-quality variant of the project's hardcoded
qwen2.5-coder:32b — small ModelID config delta to follow.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Carry-forward bug B-02 (live evidence 2026-05-07): conversation
continuity broken — "yes" / "why 4.5?" lose prior turn context because
AgentOrchestrator.process(_:) built [system, user(input.userText)]
with no history fetch.
Tactical patch outside the M-0..M-7 migration sequence, per
JARVIS-API-MIGRATION-PLAN.md §3 and UQ-2 (option B). Lands on develop
directly. M-4 (Memory surface) will reframe this path against the new
API contract but the user-visible bug closes today.
Substrate gap discovered + addressed in-scope. The `turns` table was
unwritten (schema present at MemorySchema.swift, FTS5 trigger ready,
read API at MemoryStore.recentTurnsForSession, but no INSERT site
anywhere in the codebase). The migration plan assumed turn writes
existed; they did not. Patch adds the missing writer plus the read
side wiring.
Changes:
- packages/Memory/Sources/Memory/MemoryQueries.swift
+ turnInsertSQL — single-row INSERT into turns
- packages/Memory/Sources/Memory/MemoryStore.swift
+ appendTurn(sessionId:role:content:source:createdAt:)
Single-row INSERT; FTS5 trigger mirrors content into turns_fts
automatically. T-06-05-03: function MUST NOT log content.
- packages/AgentCore/Sources/AgentOrchestrator/AgentOrchestrator.swift
+ PriorTurn struct + SessionHistoryLookup typealias
+ emptySessionHistoryLookup default (preserves pre-B-02 test setups)
+ sessionHistoryLookup ctor parameter
Replaces the [system, user] messages-array build with prior-history
hydration: system + chronological prior user/assistant rows + current
user. Lookup throws -> warning log + empty history (matches D-3
PriorFactsLookup resilience contract).
- App/AppDelegate.swift
+ sessionHistoryLookup closure: recentTurnsForSession(limit: 10),
reverse DESC -> chronological, filter to user/assistant only, return
[PriorTurn]
+ extends existing turnContent flush closure with appendTurn calls
(rides on memory-extraction drain path; assistant row offset +1 ms
so DESC + id-tiebreaker preserves user->assistant order)
Tests:
- packages/AgentCore/Tests/AgentOrchestratorTests/HistoryThreadingRegressionTests.swift
HT-1: third turn includes prior user/assistant pairs in chronological
order. 3/3 PASS.
HT-2: empty-history default produces [system, user] (pre-B-02
baseline preserved).
HT-3: three-turn happy path emits zero LLMProviderError
.streamTruncatedFinal events. Pins R-006 / Phase E gate behavior:
patch grows messages array, not finalSystem, so
CacheHints.eligibleForSystemPrompt(finalSystem) is unchanged.
- packages/Memory/Tests/MemoryTests/MemoryStoreAppendTurnTests.swift
AT-1: appendTurn round-trips via recentTurnsForSession.
AT-2: user/assistant pair reverses to chronological.
AT-3: session_id scoping isolates rows.
AT-4: turns_fts trigger fires on append.
All env-gated on JARVIS_VEC0_STUB_PATH per existing MemoryStore-test
convention; skip cleanly without vec0.dylib.
Verification:
- check-app-builds.sh PASS.
- Boundary gates sampled (check-no-leftover-stubs, applescript-confirm,
corpus-secrets, bus-protocol-version, single-memory-mutated-emit,
single-writer-hudstate) PASS.
- AgentCore affected-subset (Submit/Retry/TextEnd2End/CapRecovery/
HistoryThreading) 26/26 PASS, 0 failures.
- Memory full suite 92 run, 23 skipped (env-gated), 0 failures.
Degradation contract: when MemoryStore is nil (dormant memory — vec0
or Ollama unavailable), turn persistence is also dormant; the
appendTurn call is guarded by memoryStore != nil. When D-5/D-6 close
and memory comes up live, persistence activates without further code
changes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds README.md to each of the 13 SPM packages (AgentCore, Bus, Config,
DevOverlay, Harness, Keychain, Logging, MCP, Memory, Replay, Shell, Vision,
Voice). Each covers purpose, key public types, deps, consumers, invariants
(pulled from CLAUDE.md + grep-gates + audit findings), test stack, and
notable files. Memory README explicitly marks the package functionally OFF
until vec0.dylib + Ollama models land.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per audit 2026-05-04 security MEDIUM-1: HybridSearch.searchFacts logged
the raw user query string at debug level. Voice transcripts are guarded
by T-06-05-03; memory queries are equivalent user-private content.
Replace `query='\(query)'` with `queryLen=\(query.count)` — log
metadata only, never content. Add regression test that grep-asserts the
source file at runtime so a future log-content regression is caught at
test time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds `RememberBrutusEndToEndTests` proving the extraction -> store ->
search loop holds end-to-end. Without this test, Tracks D-1..D-3 each
move a single component but no test covers their composition; a future
regression that severs the loop at the extraction/search boundary
would slip past per-component tests.
Scenario:
1. User: "My dog Brutus is a Bernese Mountain Dog."
2. Stub LLM provider returns `apply_memory_ops(ADD subject=Brutus
predicate=breed object="Bernese Mountain Dog")`.
3. Real `MemoryExtractionOrchestrator` + `MemoryExtractor` drive the
ApplyOp callback into a `FakeStore` actor.
4. User: "What breed is Brutus?"
5. Real `HybridSearch` actor + stub embedder + `FakeStore`'s
`MemoryReadStore` conformance return one hit; assert
`summary == "Brutus breed Bernese Mountain Dog"` and the trigger
turnId propagated to retrieval recording.
Fakes used:
- `FakeStore` actor — captures applyOp via custom `applyAdd`, conforms
to `MemoryReadStore` so `HybridSearch.searchFacts` runs unmodified.
No real SQLite, no vec0.dylib needed.
- `StubEmbedder` — constant 768-dim vector (real path is
`OllamaEmbeddingClient` -> `nomic-embed-text`).
- `BrutusProvider` — single-shot stub `LLMProvider` emitting one
`toolUseRequested` then `messageStop`.
Boundary: stops at `HybridSearch.searchFacts`. The MCP-side dispatch
through `SearchMemoryTool` is covered by
`InProcessMemoryToolsTests.testSearchMemoryToolDispatch` in the MCP
package. Together those two tests cover the path the agent takes:
prompt -> extractor -> store -> search tool -> agent context.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pre-D-3, `MemoryExtractionOrchestrator.process(_:)` hardcoded
`priorFacts: []` on every job. The mem0 extractor was therefore blind
to existing facts and could only emit ADD ops — UPDATE/supersede
never fired, so superseded values silently duplicated on every turn.
This commit:
- Adds `PriorFactsLookup` typealias and `priorFactsLookup` parameter on
`MemoryExtractionOrchestrator.init`. Default is `emptyPriorFactsLookup`
to preserve pre-D-3 behavior for tests that don't care about priors.
- Wires `process(_:)` to call `priorFactsLookup(job)`. Lookup throws
degrade to `priorFacts = []` with a warning log — the drain task
must never die because a single SQL read failed.
- Adds `MemoryStore.recentActiveFacts(limit:)` + matching SQL in
`MemoryQueries.recentActiveFactsSQL`. Active-only (`valid_to IS NULL
AND forgotten_at IS NULL`), most-recently-created first, server-side
cap. D-02 forgotten facts excluded.
- AppDelegate's `installMemory` passes a closure that returns up to 50
recent active facts. 50 is a deliberate cap — RESEARCH §5 caps the
rendered priorFacts block at ~4 KB (~50 facts at typical S/P/O
length). Closure returns [] when `memoryStore` is nil, preserving
the pre-D-3 no-store path.
Tests:
- `testD3_priorFactsLookupDrivesUpdateOp` — stub provider that emits
UPDATE when it sees a `[99]` prior fact id in the system prompt;
asserts `applyOp` receives `.update(supersedes: 99, …)`. Pre-D-3
this would have been `.add` because the orchestrator never told
the model about the prior fact.
- `testD3_priorFactsLookupErrorDegradesToEmpty` — lookup throws,
job still applies its ADD op (drain survives the failure).
T-06-05-03 invariant respected: warnings log turnId only, never
fact content.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- App/Vision/AppDelegateFrameAttachAdapters.swift (NEW): FrameAttachCaptureSourceAdapter wraps CameraCapture, FrameAttachReplaySinkAdapter wraps Replay.ReplayLog (placeholders log via JarvisLogChannel.replay — see file docstring for FK rationale), FrameAttachControllerReplaySinkBridge bridges Vision's FrameAttachReplaySink actor to FrameAttachController.ReplaySink protocol
- AppDelegate: new frameAttachController + frameAttachReleaseTask properties; installVision now constructs FrameAttachController (D-15); installAgent waits for visionInstallTask before constructing the orchestrator and passes visionRouter:; new step 5d frame-attach release subscriber subscribes broadcaster's .frameAttach priority and fires onAssistantTurnComplete() on .turnEnd for image-bearing turns (D-16)
- AppDelegate: tryPhraseAttachIfMatch(_:) helper exposed for Plan 4's chatSubmit + chatCancelAndSubmit handlers (WARNING-4: must call from BOTH paths)
- AppDelegate.installBus: bridge.onInbound now switches on BusInbound; .frameAttachRequested → frameAttachController.requestAttach(reason: .hudButton)
- BusInbound: additive .frameAttachRequested case + Discriminator + Codable arms (NO default — exhaustive-switch drift preventer); BUS_PROTOCOL_VERSION 2.1.0 → 2.2.0 in Swift + TS mirror
- TS protocol: matching frameAttachRequested case + decoder arm; round-trip.test.ts asserts version 2.2.0 + decodes new fixture
- New fixtures (parity): packages/Bus/Tests/BusTests/Fixtures/frameAttachRequested.json + webview/packages/bus/fixtures/frameAttachRequested.json (byte-identical, no trailing newline per check-bus-protocol-version.sh)
- Memory package bumped from .v13 to .v14 (cascade: Memory depends on AgentCore which now requires v14 due to JarvisVision dependency added in Task 1)
- xcodegen regen for the new App/Vision/AppDelegateFrameAttachAdapters.swift source file
- 2 new FrameAttachReleaseSubscriberTests prove broadcaster→onAssistantTurnComplete glue (image-bearing turns release once; text-only turns NEVER release); subscription uses devOverlay priority for the sync wait to avoid competing with broadcaster's drain on orch.events
- 1 new CodableRoundTripTests entry (test_roundTrip_frameAttachRequested)
- Phase 7 SOLE-emission-site invariant intact: FrameAttachDiscardSiteGrepTests passes; check-presence-vision-isolation.sh exits 0; orchestrator-events single-consumer grep gate exits 0; 65 AgentOrchestratorTests + 51 BusTests + 34 webview tests all green
- App target compiles cleanly (check-app-builds.sh exits 0)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- AppDelegate.installAgent() constructs AgentOrchestrator with the
existing 7-arg ctor (visionRouter / presenceSnapshot stay nil for
Plans 2 + 3), starts the OrchestratorEventBroadcaster, and creates
the TurnTranscriptStore. Memory + transcript + devOverlay subscribers
drain into MemoryExtractionCoordinator, the assistant-side
accumulator, and a no-op consumer respectively.
- ConfigStore is now built in applicationWillFinishLaunching from the
loaded launch + perTurn snapshots (was previously discarded).
- installMemory no longer calls coord.start; the wiring moved to
installAgent step 5a where the broadcaster's memory child stream is
available. Old agentOrchestratorEvents() nil-stub is gone.
- MemoryExtractionCoordinator gains a MemoryEnqueueing protocol seam
so the BLOCKER-1 end-to-end test can spy on enqueue without driving
the full extractor stack.
- New scripts/check-orchestrator-events-single-consumer.sh enforces
D-05/D-08 (exactly one production for-await over orchestrator.events;
WARNING-2 whitelist scoped to OrchestratorEventBroadcaster.swift).
Wired as a project.yml preBuildScript and exercised in-process by
PhaseNineGrepGateTests.
- MemoryWiringEndToEndTests proves enqueue receives non-nil userText +
assistantText through real broadcaster + transcript store + memory
coordinator wiring (BLOCKER-1 closure for INT-07-01).
- Test home note: MemoryWiringEndToEndTests lives in MemoryTests
(not AgentOrchestratorTests as the plan suggested) because Memory
depends on AgentOrchestrator via SPM and the cycle would block
test compilation. Recorded in the plan SUMMARY.
- start<S: AsyncSequence & Sendable> where S.Element == OrchestratorEvent
replaces the BoundedAsyncChannel<OrchestratorEvent>-only signature.
- AsyncSequence's throwing iterator is wrapped in do/catch with a new
warnDrainError helper; the start function stays non-throwing.
- BoundedAsyncChannel already conforms to AsyncSequence so existing
call sites compile unchanged (verified by 8 prior coordinator tests).
- New test testStartAcceptsAsyncStream drives the coordinator through
an AsyncStream — the type the OrchestratorEventBroadcaster's
subscribers emit — proving the broadcaster will plug in directly.
- scripts/check-single-memory-mutated-emit.sh — MEM-06 single-emission-site
for ReplayEvent.memoryMutation (allowlist: MemoryStore.swift,
Replay/ReplayEvent.swift, Replay/Schema.swift; tests + comments excluded).
- scripts/check-single-memory-used-emit.sh — D-05 single-emission-site for
ReplayEvent.memoryRetrieval (same allowlist shape, MemoryStore.recordRetrieval
is the sole emit site).
- scripts/check-presence-vision-isolation.sh — VISION-03 three-layer guard:
Layer 1 (Vision/Package.swift declares no Voice or AgentOrchestrator dep),
Layer 2 (no `import Voice` / `import AgentOrchestrator` and no TTSEngine /
AgentOrchestrator.runTurn / cancelAndSubmit token references in
packages/Vision/Sources/), Layer 3 (no App/ file co-locates a
PresenceSignalBus reference with a forbidden TTS / runTurn token).
- scripts/check-embedding-dim-literal.sh — MEM-02 invariant: no literal 768
in packages/Memory/Sources/Memory/ outside Constants.swift; comment-only
lines tolerated.
- All four scripts wired as Jarvis pre-build phases in project.yml; the
generated project.pbxproj reflects the updates.
- packages/Memory/Tests/MemoryTests/PhaseSevenGrepGateTests.swift mirrors
the four shell gates as XCTest assertions plus a fifth assertion
(testAppDelegateInstallOrder) verifying the call order in
applicationWillFinishLaunching: installMemory before installVoice
before installVision.
- App/Voice/NullVoiceAdapters.swift extracted from AppDelegate.swift to
honor the Layer 3 boundary cleanly. NullOrchestratorAdapter and
NullTTSAdapter are placeholder no-ops mirroring the prior structure;
Phase 7 future wiring replaces them with real bridges.
- Negative-case demonstration completed: introducing a stray
`.memoryMutation(...)` literal in App/AppDelegate.swift made
check-single-memory-mutated-emit.sh exit non-zero; the violation was
reverted before commit. Documented as evidence of gate sensitivity.
- MemoryRegressionCorpusTests with 10 scenarios mirroring the
OrpheusTTFATests env-gate pattern (JARVIS_REAL_MODELS=1):
1-3: ADD / NOOP / UPDATE extraction (D-03 / MEM-04)
4-5: supersede invariants — never DELETE (MEM-05)
6-8: FTS5+vec hybrid retrieval ranking (MEM-07)
9-10: forget_fact valid_to closure (D-02)
- All 10 tests skip cleanly when JARVIS_REAL_MODELS != 1; runs
interactively against real local Ollama (nomic-embed-text +
qwen2.5-coder:32b). Researcher Open Q #3 satisfied.
- MemoryStore gains four small read-only test seams used by the corpus:
searchFacts(query:k:embedder:) — facade over runHybridSearchSQL that
embeds via the injected EmbeddingProviding;
factById(_:) — fetch any-state row;
rawCountFacts(predicate:) — count rows regardless of valid_to /
forgotten_at (proves never-DELETE on supersede);
activeFacts(subject:predicate:) — uses idx_facts_active partial index.
- Memory test target gains OllamaProvider product dependency so the
corpus can construct a live LLMProvider for the env-gated runs.
- HybridSearch actor: embeds query once via EmbeddingProviding, runs RRF
SQL via MemoryReadStore, emits one recordRetrieval per result; empty
query short-circuits without an embed call
- SessionHistory actor: 500-cap defensive bound; pass-through to
recentTurnsForSession
- MemoryStore conforms to MemoryReadStore + SessionHistoryReading;
OllamaEmbeddingClient conforms to EmbeddingProviding (production wiring)
67 Memory tests green (9 env-gated skipped); .memoryRetrieval grep gate
remains at 1 production hit (MemoryStore.recordRetrieval).
- Adds MemoryExtractionOrchestratorTests with 8 cases covering MEM-06 non-blocking, serial drain, applyOp reach-through, error survival, coordinator turnEnd filtering, callback wiring, stop semantics
- Memory Package.swift now depends on AgentOrchestrator product (needed for OrchestratorEvent type used by coordinator)
- Tests fail to compile until orchestrator + coordinator land in Task 4 GREEN
- MemoryPrompts: RESEARCH §5 verbatim mem0 system prompt + per-turn user formatter; sole producer of extractor's system prompt; 4 KB priors cap
- MemoryTools: apply_memory_ops ToolSchema with RESEARCH §5 JSON schema (ADD/UPDATE/NOOP enum, top-level required ops, per-item required op)
- MemoryExtractor: actor wrapping LLMProvider.stream as a single-turn driver against qwen2.5-coder:32b with toolChoice .auto; collects exactly one apply_memory_ops ToolUseRequest, decodes via MemoryOp.parseApplyMemoryOps; empty list when no tool call
- 11 MemoryExtractorTests pass
- Actor POSTs to 127.0.0.1:11434/api/embed for nomic-embed-text
- assertLoopback static guard rejects any baseURL whose host is not 127.0.0.1, localhost, or ::1 — throws at init before any byte hits the wire
- Validates embeddings[0].count == MemoryConstants.embeddingDim on every response (MEM-02)
- Maps URLSession transport errors / non-2xx HTTP / shape errors to typed MemoryError cases
- 8 OllamaEmbeddingClientTests + 3 EmbeddingNetworkSandboxTests pass
- OllamaEmbeddingClientTests covers loopback guard, /api/embed routing, body shape, dim validation, HTTP/shape errors via URLProtocolStub
- EmbeddingNetworkSandboxTests is the dedicated MEM-04 file mandated by CONTEXT.md
- Tests fail to compile until OllamaEmbeddingClient lands in Task 2 GREEN
- MemorySchema: pragmas + DDL for turns, facts (with valid_from/valid_to/
superseded_by/forgotten_at), facts_fts, turns_fts, FTS5 sync triggers,
facts_vec virtual table interpolating MemoryConstants.embeddingDim.
- Fact: row model (Sendable, Equatable) — D-02 + MEM-05 invariants in fields.
- MemoryError: vecLoadFailed / vecVersionMissing / dimensionMismatch /
embeddingShapeError / embeddingHTTP / embeddingTransportFailed /
applyOpFailed / cancelled.
- MemoryStore actor: init opens jarvis.db, applies WAL pragmas via
SQLiteConnection.execute, loads vec0 via direct C API, runs schema DDL,
probes vec_version(). querySingleString and queryRowCount expose
read-only seams for tests.
- Drop placeholder Memory.swift; the real surfaces fulfil the SwiftPM
source-presence requirement.
Deviation Rule 4 -> Rule 1 (architectural finding adapted in scope):
Apple's system libsqlite3.dylib does NOT export sqlite3_load_extension or
sqlite3_enable_load_extension — these are stripped from the public ABI in
MacOSX.sdk/usr/lib/libsqlite3.tbd. The plan's direct C-API call therefore
cannot link against the system overlay (Swift's import SQLite3 sees only
the public symbols).
Resolved within Plan 07-01's contract: resolve the symbols at runtime via
dlsym(RTLD_DEFAULT). On unmodified macOS dlsym returns NULL and we throw
MemoryError.vecLoadFailed('symbol unavailable') — exactly the graceful-
degradation contract testInitThrowsWhenVecDylibMissing asserts and
AppDelegate.installMemory (Plan 07-06) expects.
A future ops/build plan (also responsible for the vec0.dylib bundle copy)
must replace this with a custom-built libsqlite3.dylib carrying
SQLITE_ENABLE_LOAD_EXTENSION=1 under Contents/Frameworks/. Documented in
SUMMARY for downstream propagation.
All 8 Memory tests pass; testInitWithStubVecDylib skips (S-8 env-gated).
27 Replay tests still pass (no regressions). App target compiles cleanly
via scripts/check-app-builds.sh.
- SQLiteConnection.withHandle(_:): closure-based accessor so MemoryStore
can call sqlite3_load_extension without leaking the OpaquePointer.
- MemoryConstants.embeddingDim = 768 (MEM-02): single canonical symbol
for vector dimension. Hard-pinned to nomic-embed-text 768d output.
- EmbeddingDimSymbolTests:
testEmbeddingDimIsSeventySixtyEight — value sanity.
testEmbeddingDimIsSingleSymbol — workspace grep guard against any
parallel literal definition.
Deviation Rule 1: the plan-supplied repoRoot heuristic walked up looking
for 'a Package.swift WITHOUT a parent Package.swift', but this workspace
has no top-level Package.swift (it uses XcodeGen project.yml). Walk up
looking for project.yml or Jarvis.xcodeproj instead.
Deviation Rule 1: the plan-supplied symbol-grep flagged the test file's
own docstrings and the Constants.swift docstring as 'rogue literals'
(self-reference bug). Filtered #file and Constants.swift from the hit
list — the test still flags any other shadow definition anywhere in the
workspace, which is the intended invariant.
- New packages/Memory SPM target with Logging/AgentCore/Replay deps
- Resources/PLACEHOLDER.txt reserves bundle slot for future vec0.dylib
- Memory.swift + MemoryTestsScaffold.swift placeholders so the empty
target compiles; real symbols land in Tasks 2 and 3
- project.yml: add Memory to packages and Jarvis target deps (Voice analog)
- Jarvis.xcodeproj regenerated via xcodegen
Deviation Rule 3: plan referenced 'root Package.swift' but this workspace
uses XcodeGen project.yml (no root Package.swift exists). Wired Memory the
same way Voice was wired in Plan 06-05.