35 Commits
Author SHA1 Message Date
james.maesandClaude Opus 4.7 5ebe510627 docs: scrub Phase-7 + JarvisBusWorld doc drift (#19, #45) (#195)
* 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>
2026-05-14 09:51:03 -05:00
james.maes 5fc2d3fabf docs: reconcile Ollama model baseline to actual production names (#184)
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
2026-05-13 16:25:00 -05:00
james.maes f59f5d12e3 fix(memory): v0.1 CRIT cluster — facts_vec / replay correlation / FTS5 sanitize / search_memory preamble (#12, #13, #14, #15, #16, #153)
#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.
2026-05-13 06:30:35 -05:00
james.maesandClaude Opus 4.7 a5999e2dde fix(memory): drop broken FK on facts.source_turn_id — closes Toby-fact persistence
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>
2026-05-12 12:07:44 -05:00
james.maesandClaude Opus 4.7 5718bdd820 test(memory): rename qwen25Coder32B test to qwen36
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>
2026-05-11 14:02:57 -05:00
james.maesandClaude Opus 4.7 8d0a959a19 feat(model): qwen3.6 for memory extractor; q8_0 for agent-loop fallback
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>
2026-05-11 10:36:30 -05:00
james.maesandClaude Opus 4.7 cb584a2ac8 feat(d5d6): real memory database — vec0 statically linked, hard-fail boot, stats tool
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>
2026-05-11 10:25:56 -05:00
james.maesandClaude Opus 4.7 48ad8d631c fix(b-02): thread recent turns into AgentOrchestrator messages array
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>
2026-05-11 08:50:40 -05:00
james.maesandClaude Opus 4.7 508cf924d0 docs(packages): per-package README files
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>
2026-05-04 17:13:33 -05:00
james.maesandClaude Opus 4.7 f9cd25d66e chore(test-naming): converge on Mock/Fake/Stub convention; document in CLAUDE.md; rename worst-mismatch stragglers
Per audit-2026-05-04 P2-15: 26 Mock*, 21 Fake*, 12 Stub* across the test
corpus, often used interchangeably for the same role. New CLAUDE.md
section "Test naming conventions" (line 127) sets the rule:

- Mock*: records calls AND returns scripted responses (assertion driver).
- Stub*: returns canned data without recording (deterministic input).
- Fake*: alternative implementation with realistic behavior (in-memory
  state-holder).

Renamed 15 worst-mismatch stragglers in the older corpus (per audit
constraint, did NOT touch types added in today's P1+P2 fix commits):

Stub* → Mock* (records + scripts):
- HybridSearchTests:    StubEmbedder → MockEmbedder
- HybridSearchTests:    StubStore → MockStore
- SessionHistoryTests:  StubStore → MockStore
- InProcessMemoryToolsTests: StubHybrid → MockHybrid
- InProcessMemoryToolsTests: StubHistory → MockHistory
- InProcessMemoryToolsTests: StubForget → MockForget

Mock* → Stub* (canned data, no recording):
- STTBackendSwitchTests: MockSpeechAnalyzerBridge → StubSpeechAnalyzerBridge
- STTBackendSwitchTests: MockWhisperKitBridge → StubWhisperKitBridge
- VoiceWiringTests:      MockVADEngineForW4 → StubVADEngineForW4
- VoiceWiringTests:      MockSTTProviderForW4 → StubSTTProviderForW4
- VoiceWiringTests:      MockTTSForW4 → StubTTSForW4
- VoiceWiringTests:      MockOrchestratorForW4 → StubOrchestratorForW4
- VoiceWiringTests:      MockBannerForW4 → StubBannerForW4
- VoiceWiringTests:      MockBusForW4 → StubBusForW4

Fake* → Mock* (records + scripts):
- WebviewBridgeOutboundTests:        FakeJSEvaluator → MockJSEvaluator
- HudStateCoordinatorBusWiringTests: FakeJSEvaluator → MockJSEvaluator
  (the comment on the type already said "Records each call for assertion")

Skipped per audit constraint:
- StubCapture/StubSink (Vision FrameAttachConfirmSendWiringTests, P1+P2)
- MockVADEngine + MockOrchestratorForController siblings (Voice
  VoiceControllerTests/VADGatedSessionTests, today's track-B churn)

Tests pass: Voice 87+23, MCP 85, Memory 88, Bus 58, AgentCore 19+72,
Vision 19, Shell 7+5. App target builds clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 16:42:16 -05:00
james.maesandClaude Opus 4.7 a4f76661fc fix(memory p2-6): scrub raw search_memory query from HybridSearch debug log (T-06-05-03 analog)
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>
2026-05-04 16:06:26 -05:00
james.maesandClaude Opus 4.7 c3bd0a5728 test(track-d 4): end-to-end remember-Brutus regression scenario with fakes
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>
2026-05-04 14:26:37 -05:00
james.maesandClaude Opus 4.7 51750c161b fix(track-d 3): MemoryExtractionOrchestrator passes real priorFacts to extractor
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>
2026-05-04 14:22:42 -05:00
james.maes dcbd50e4c9 fix(phase-09): align Phase 7 grep-gate test with Plan 09-04 install order (vision → agent → voice) 2026-05-01 22:38:56 -05:00
james.maesandClaude Opus 4.7 87aec282a0 feat(09-02): wire FrameAttachController + Bus frameAttachRequested + broadcaster release subscriber (D-15, D-16, INT-07-04)
- 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>
2026-05-01 19:41:29 -05:00
james.maes fdb56d2785 feat(09-01): installAgent + broadcaster wiring + BLOCKER-1 closure (INT-07-01)
- 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.
2026-05-01 14:56:13 -05:00
james.maes e057039ef0 refactor(09-01): MemoryExtractionCoordinator.start accepts generic AsyncSequence (S-4)
- 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.
2026-05-01 14:42:09 -05:00
james.maes 6f4f2fb6c2 feat(07-06): four cross-plan grep gates + AppDelegate install-order test (Task 3)
- 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.
2026-04-29 18:58:11 -05:00
james.maes 56aeaed171 test(07-06): env-gated 10-scenario memory regression corpus (Task 2)
- 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.
2026-04-29 18:44:56 -05:00
james.maes 65cd7d2e87 feat(07-03): HybridSearch + SessionHistory actors + conformance extensions
- 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).
2026-04-29 17:05:26 -05:00
james.maes 001ed9187b test(07-03): add failing HybridSearch + SessionHistory + forget tests
- HybridSearchTests: stub embedder + stub store; verifies single embed
  per call, k pass-through, recordRetrieval per result, empty-query
  short-circuit
- SessionHistoryTests: pass-through + 500-cap defensive bound
- ForgetFactTests: env-gated stubs (real-DB cases land in 07-06)

RED: tests reference HybridSearch / SessionHistory / EmbeddingProviding /
MemoryReadStore / SessionHistoryReading — none exist yet.
2026-04-29 17:03:15 -05:00
james.maes 5387a311ae feat(07-03): memory read primitives + .memoryRetrieval single emission site
- MemoryQueries: hybrid RRF SQL (RESEARCH §8 verbatim, k=60, active-row
  predicate, k literal interpolation per P9), sessionHistorySQL,
  forgetFactSQL (UPDATE-only), activeFactsBySubjectSQL
- MemoryEvent: FactRef gains score/triggerTurnId/timestamp + Codable;
  TurnRow added (TEXT-03 chat-panel hydration shape)
- MemoryError: forgetTargetNotFound case (reserved)
- MemoryStore: recordRetrieval (sole .memoryRetrieval emission site,
  D-05/MEM-08), runHybridSearchSQL, queryActiveFacts,
  recentTurnsForSession, forgetFact (closes valid_to + forgotten_at,
  never DELETEs)
- ReplayEvent: .memoryRetrieval(Data) + encoded() arm
- Schema: ReplayEventKind.memoryRetrieval rawValue
- SQLiteConnection: columnDouble accessor for RRF score readback

56 Memory tests (5 env-gated skipped), 27 Replay tests, 0 failures.
.memoryRetrieval single-emission-site grep gate green.
2026-04-29 17:00:28 -05:00
james.maes a04a018785 test(07-03): add failing memory-read primitives + grep gate tests
- MemoryQueriesAndModelTests: hybrid SQL shape, FactRef/TurnRow Codable,
  ReplayEvent.memoryRetrieval kind round-trip
- SingleEmissionSiteGrepTests: testMemoryRetrievalHasSingleEmissionSite
  symmetric to the 07-02 memoryMutation gate

RED gate: tests reference MemoryQueries, FactRef.score/triggerTurnId,
TurnRow, ReplayEvent.memoryRetrieval — none of which exist yet.
2026-04-29 16:53:18 -05:00
james.maes 08d95e74b4 feat(07-02): MemoryExtractionOrchestrator + Coordinator (Task 4 GREEN)
- MemoryExtractionOrchestrator: bounded channel (capacity 32, .dropOldest); single serial detached drain Task; injectable applyOp callback as test seam (production wiring closes over MemoryStore in 07-06); FNV-1a stable hash for TurnID -> Int64 source_turn_id correlation
- MemoryExtractionCoordinator: actor subscribes to OrchestratorEvent stream, filters .turnEnd with stopReason == .endTurn, fetches user/assistant via TurnContentLookup, enqueues ExtractionJob (never blocks turnEnd)
- 8 orchestrator tests pass — including MEM-06 100-enqueue/60s-stalled-extractor non-blocking gate, serial drain ordering, error survival, coordinator filtering of refusal/maxTokens/toolUse, stop semantics
- Full Memory suite: 48 executed, 5 env-gated skips, 0 failures
- Replay suite: 27 / 0 failures (no regression)
- App target builds cleanly via scripts/check-app-builds.sh
2026-04-29 16:30:35 -05:00
james.maes 040e578654 test(07-02): add failing orchestrator + coordinator tests; wire AgentOrchestrator dep (Task 4 RED)
- 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
2026-04-29 16:22:44 -05:00
james.maes c4514ed417 feat(07-02): MemoryPrompts + MemoryTools + MemoryExtractor (Task 3 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
2026-04-29 16:20:12 -05:00
james.maes c099ee7efd test(07-02): add failing MemoryExtractor + prompt + tool schema tests (Task 3 RED) 2026-04-29 16:17:29 -05:00
james.maes 4e5be55c04 feat(07-02): OllamaEmbeddingClient with loopback guard + dim validation (Task 2 GREEN)
- 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
2026-04-29 16:15:55 -05:00
james.maes 8c40bbe199 test(07-02): add failing OllamaEmbeddingClient + network-sandbox tests (Task 2 RED)
- 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
2026-04-29 16:13:44 -05:00
james.maes 53206d550b feat(07-02): MemoryStore.applyOp single emission site + MemoryOp parser (Task 1 GREEN)
- MemoryEvent.swift: MemoryOp enum (.add/.update/.noop) + parseApplyMemoryOps schema-validating parser + FactRef
- ExtractionJob.swift: value type carrying turnId/userText/assistantText/subjects/source
- MemoryError: appended embeddingNonLoopbackHost / extractorInvalidArgs / extractorMissingTool
- MemoryStore.applyOp: MEM-05 supersede transaction (UPDATE valid_to + superseded_by, never DELETE), MEM-06 SOLE emission site for ReplayEvent.memoryMutation
- MemoryReplaySink protocol decouples Memory from concrete ReplayLog
- ReplayEvent: append .memoryMutation(Data) case + encoded() arm
- ReplayEventKind: append memory_mutation rawValue
- All Memory tests pass (4 env-gated DB tests skip cleanly); 27 Replay tests still pass
2026-04-29 16:11:45 -05:00
james.maes af4cf898f9 test(07-02): add failing applyOp + single-emission-site tests (Task 1 RED)
- MemoryStoreApplyOpTests covers ADD/UPDATE/NOOP, rollback, parse, encoded() round-trip
- SingleEmissionSiteGrepTests guards MEM-05 (never DELETE) + MEM-06 (single emit site)
- Tests fail to compile until MemoryOp/applyOp/MemoryReplaySink land in Task 1 GREEN
2026-04-29 16:05:38 -05:00
james.maes a6aea0e802 feat(07-01): MemorySchema + Fact + MemoryError + MemoryStore (TDD 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.
2026-04-29 15:57:46 -05:00
james.maes f0057df094 test(07-01): add MemorySchema and MemoryStore tests (TDD RED)
- MemorySchemaTests: 4 cases enforcing
    testSchemaInterpolatesEmbeddingDim — facts_vec DDL renders FLOAT[768]
    testPragmasIncludeWAL              — WAL/foreign_keys/busy_timeout
    testFactsDDLHasForgottenAt         — D-02 forget_fact column + MEM-05 temporal
    testActiveIndexIsPartial           — partial index predicate present
- MemoryStoreTests: 2 cases
    testInitThrowsWhenVecDylibMissing — graceful failure when vec0 absent
    testInitWithStubVecDylib          — env-gated probe (S-8); skips by default
- Drop MemoryTestsScaffold placeholder; real tests cover the surface now.

Real implementation (MemoryStore actor + MemorySchema + Fact + MemoryError)
lands in the next commit (TDD GREEN).
2026-04-29 15:57:25 -05:00
james.maes 4ee018d95c feat(07-01): add SQLiteConnection.withHandle + MemoryConstants + symbol-uniqueness test
- 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.
2026-04-29 15:49:09 -05:00
james.maes 64e38ba15d feat(07-01): create packages/Memory SPM scaffold + wire into project.yml
- 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.
2026-04-29 15:45:35 -05:00