Skip to content

Commit a886449

Browse files
JuzzyDeeclaude
andauthored
release: v0.2.0 — Oneiro (#30)
* feat(dialectic): Stage 3 — action dispatcher (CLA-100) (#17) * feat(dialectic): Stage 3 — action dispatcher (reframe / flag / keep) (CLA-100) Stage 3 reads `action` + `action_payload` from each Stage 2 audit row and executes: keep — no memory mutation; mark row dispatched reframe — re-embed (Workers AI bge-base-en-v1.5), upsert Vectorize, update D1 content/summary, preserve original in memory_reframes (reversible via SQL) flag — append to dialectic_flags for human review ## New files - `migrations/0006_dialectic_dispatch.sql` — dispatch_at + status + error columns on dialectic_decisions, run-level actions_dispatched counter, memory_reframes (reversibility audit), dialectic_flags (human review). - `src/dialectic_validation.rs` — native-testable validation gate (`validate_synthesis_payload`). Catches Synthesizer shape/action mismatches before any mutation. 10 unit tests cover the matrix. - `src/worker_dialectic_dispatch.rs` — the dispatcher itself. Wasm-only because it touches Env / D1 / Vectorize / Workers AI. Pulls validation in from the universal module. ## Wiring - `worker_dialectic_audit::record_decision` now returns the `decision_id` so the dispatcher can update the same row. - `worker_dialectic::run()` reads `DispatchMode::from_env` once per run, then dispatches each non-well-calibrated decision after the audit row is recorded. - `RunSummary.actions_dispatched` tracks dispatch attempts. - `record_run_finish` writes the new counter to dialectic_runs. ## Kill switch `MEMORIA_DIALECTIC_DISPATCH` env var: on — real dispatch (will be the default once burned in) dry_run — validate, mark status=dry_run, no mutation off — skip dispatch entirely (revert to Stage 2 dark-launch) Unrecognised values fall through to `On` — typos must not silently disable the dispatcher. ## Safety properties - Validation gate ensures malformed payloads never mutate state. - Reframe order: embed → Vectorize upsert → D1 update → audit. Embed/Vectorize failures occur before any D1 mutation. - Reframe preserves the original in memory_reframes. Every reframe is reversible via a SQL UPDATE. - Idempotency: dispatch_decision is only called by the caller for rows where dispatched_at is NULL. mark_dispatched always sets dispatched_at, so no row is re-dispatched. ## Test plan - [x] `cargo check --target wasm32-unknown-unknown --lib` clean - [x] `cargo test` — 75 (incl. 10 new validation tests) + 49 = 124 passed - [x] `worker-build --release` — 28.2kb optimised bundle ## What's NOT included (future tickets) - "Catch up on unmarked rows from prior runs" pass. Hard errors during dispatch leave rows unmarked; today they require manual replay. Worth a small follow-up once we have data on how often. - Deeper prompt-injection hardening on memory content (raised in CLA-99 PR review; minimal wrap already in place). - `flagged` MCP tool to surface dialectic_flags during conversations. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(dialectic): PR #17 review — fail-closed dispatch + atomic reframe (CLA-100) Addresses both blockers from GPT review on PR #17. Reframe atomicity in particular is a real safety fix: the previous code could lose the only copy of the original content if the audit insert failed after a successful UPDATE. That violated the "every reframe is reversible" claim. No longer possible. ## 1. DispatchMode::from_env defaults to DryRun, not On Previous behaviour: missing or unrecognised `MEMORIA_DIALECTIC_DISPATCH` fell through to `On`. A fresh deploy without the secret would immediately perform live reframes/flags. New behaviour: only an explicit `on` (or `live`) enables real dispatch. Missing → DryRun with a console_log notice. Unrecognised → DryRun with a console_error notice. Fail-closed for a mutating system. Reasoning: a typo silently disabling dispatch is recoverable (operator notices nothing changed, fixes the secret). A typo silently enabling mutation is not (operator notices memories changed unexpectedly, has to roll back via memory_reframes). Asymmetric blast radius → asymmetric defaults. ## 2. Reframe uses db.batch() for atomic UPDATE + audit INSERT Previous order: 1. fetch original 2. embed 3. Vectorize upsert 4. D1 UPDATE memories 5. D1 INSERT memory_reframes If step 5 failed after step 4 succeeded, the memory was already mutated and the original was lost. The code logged this as "success with warning" — but that violated the claimed safety property. New order: 1. fetch original 2. embed 3. Vectorize upsert 4. **Atomic** db.batch([UPDATE memories, INSERT memory_reframes]) D1 batches are transactional — either both statements commit or neither does. The destructive UPDATE can no longer land without the audit row carrying the original. "Every reframe is reversible" is now true at the storage layer, not just by convention. The Vectorize/D1 inconsistency window (step 3 succeeds, step 4 fails) remains as the only acceptable inconsistency state, recoverable via next dialectic pass — same as before. ## 3. mark_dispatched gains WHERE dispatched_at IS NULL guard Makes the primitive idempotent at the SQL level rather than relying on the caller's filter. Future replay/catch-up paths can call it without holding their own filter. ## Tests - [x] cargo check --target wasm32-unknown-unknown --lib — clean - [x] cargo test — 75 + 49 = 124 passed - [x] worker-build --release — 28.3kb optimised bundle No new test cases needed: blocker 1 is configuration, blocker 2 is a storage-layer guarantee verified by D1's batch semantics, and the mark_dispatched nit is a constraint addition that the validation tests don't exercise. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * chore(setup): set MEMORIA_DIALECTIC_DISPATCH=on for fresh deploys (CLA-100) (#18) The worker's fail-safe default is dry_run, which protects operators mid-deploy who haven't set the secret yet — a typo or omission can't silently enable destructive reframes. But that same default would silently disable dispatch on every fresh consumer deployment via setup.sh. Non-technical users won't read audit rows, won't flip secrets manually, won't know dispatch isn't running. They'd sit in audit-theater forever — Stage 2 telemetry accumulates, Stage 3 never acts, memories silently miscalibrate. Setup.sh now pushes "on" explicitly. Two failure modes, two appropriate defaults: - Code default (DryRun) → catches operator error during incremental deploys - Setup-script value (on) → ensures consumers get a working dialectic out of the box Also documents the secret in wrangler.toml.example so the override path is discoverable. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * feat(dialectic): reframe cooldown to prevent runaway re-litigation (CLA-101) (#19) Stage 1 candidate selection now excludes memories reframed within the last REFRAME_COOLDOWN_DAYS (default 7). Without this gate, a freshly-reframed memory's updated last_accessed keeps it in the recent-semantics pool, and Stage 1 may re-flag the reframed version on a different axis (e.g., the now-flatter language reads as understated). The memory could then drift through several iterations and oscillate between inflated/understated verdicts. First Stage 3 dry-run pass (2026-05-18) surfaced this in concrete form: the same memory (baece146) was picked up by two independent runs and produced reframe proposals each time. Both pointed in the same direction (consistency signal) but a live cutover without cooldown would have started the loop. ## What this does - New helper `recent_semantics_not_recently_reframed(db, limit, cooldown_days)` in worker_store.rs. LEFT JOIN on memory_reframes (max reframed_at per memory_id), excludes rows inside the cooldown window. Uses SQLite's datetime() on both sides so the RFC 3339 reframed_at values compare structurally against datetime('now', '-N days') rather than lexicographically. - New `REFRAME_COOLDOWN_DAYS` constant (7) in worker_dialectic.rs. One-line caller swap in run(). - The old `recent_semantics` (no cooldown) had no other callers and was removed cleanly rather than left as orphan code. The verbose new function name documents what the call does, making future intent unambiguous. ## What this enables The cooldown lets a reframed memory accumulate real recall traffic and demonstrate calibration in use before the dialectic litigates it again. Genuine problems still surface — just not within a 24-hour loop. This is the last release-blocker before flipping MEMORIA_DIALECTIC_DISPATCH=on safely. Without it, the first live night would start the loop on whichever memory Stage 1 flags first. ## Tests - [x] cargo check --target wasm32-unknown-unknown --lib — clean - [x] cargo test — 75 + 49 = 124 passed - [x] worker-build --release — 28.3kb optimised bundle Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * fix(dialectic): cooldown gates on dialectic_decisions, not memory_reframes (CLA-101) (#20) Original CLA-101 cooldown gated on memory_reframes.reframed_at, but that table is only written by Stage 3's reframe_memory() when dispatch is LIVE. In dry_run mode (the failsafe default during burn-in), the dispatcher short-circuits before memory_reframes is touched. Result: the cooldown was silent on the exact code path it was supposed to protect — the same memories kept getting re-judged every night during the observation window. Observed in production: two dialectic runs ~2 hours apart on 2026-05-18, both with dispatch in dry_run mode, both picked the same three semantic memories (baece146 + 94a53721 + ff8dc5b0) and ran full Stage 1 evaluations. The post-deploy cooldown had no effect. ## Diagnosis The cooldown's job is to prevent re-litigation of memories the dialectic has already taken a look at. That event happens at decision-time (Stage 2 records a row to dialectic_decisions), regardless of whether the proposed action is then applied. The audit table is the canonical record of "we've evaluated this memory" — not memory_reframes, which only fires on the mutation half. ## Fix - Rename `recent_semantics_not_recently_reframed` → `recent_semantics_not_recently_judged`. The function now LEFT JOINs dialectic_decisions (max created_at per memory_id) and excludes memories with a decision inside the cooldown window. - Rename `REFRAME_COOLDOWN_DAYS` → `DIALECTIC_COOLDOWN_DAYS`. - Doc comments updated to reflect what the gate actually protects. ## Broadened scope (intentional) The new gate excludes memories with ANY recent decision, not just action-worthy ones. Two reasons: 1. Well_calibrated memories were also being re-judged every night. The same three memories appeared in both runs of the observed audit data. Wasted Haiku calls; not the point of nightly dialectic passes. 2. The dialectic should spread its attention across the full semantic pool over the cooldown window — not hammer the N most-recent memories every night. Gating on any decision means the candidate pool drains naturally as memories get judged, then refills as the cooldown expires for older decisions. Rolling sweep instead of fixed focus. ## Tests - [x] cargo check --target wasm32-unknown-unknown --lib — clean - [x] cargo test — 75 + 49 = 124 passed - [x] worker-build --release — 28.3kb optimised bundle ## Post-deploy verification After this lands and deploys: tonight's dialectic run should pick DIFFERENT semantic memories from the three baece146/94a53721/ff8dc5b0 seen in the 2026-05-18 audit data. All three have decision rows within the last 7 days, so all three should be excluded. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * feat(version): update-available prompt in recall output (CLA-102) (#21) * feat(version): update-available prompt in recall output (CLA-102) Non-technical users won't read changelogs or watch GitHub releases. Without an in-band prompt, every future Memoria release — security patches, dialectic refinements, new tools — depends on operators noticing manually. The deployed worker becomes a frozen artifact of whatever version got installed. The leverage: the Claude instance the user is talking to is itself the most reliable update prompt. The recall handler now checks for an update and appends a short notice to its output when one's available. Claude reads the notice and naturally surfaces it conversationally — "looks like there's a Memoria update, want me to walk you through it?". ## Files - `VERSION.json` (new, repo root) — `{latest_version, release_notes_url}`. Bumped as part of every release PR alongside Cargo.toml. - `src/worker_version.rs` (new) — check_for_update(env) returns Option<UpdateAvailable>. KV-cached (6h TTL); fetches GitHub raw on miss. Every error path returns Ok(None) — recall never fails because the version check failed. - `src/worker_mcp.rs` — recall handler appends a "── Memoria update available ──" section to its output when check_for_update returns Some. Section omitted entirely when versions match or check failed. - `src/lib.rs` — declares the new worker_version module. - `wrangler.toml.example` — documents the new VERSION_CACHE KV binding. - `scripts/setup.sh` — creates MEMORIA_VERSION_CACHE KV namespace alongside MEMORIA_TOKENS, patches wrangler.toml with its id. ## Compile-time version `CURRENT_VERSION = env!("CARGO_PKG_VERSION")` — baked into the wasm bundle at build time. Cargo.toml's version and VERSION.json's latest_version must stay in sync; the release PR is the canonical place to bump both together. ## Failure modes (all return Ok(None) — recall continues normally) - KV binding missing - GitHub raw unreachable - VERSION.json malformed - Versions match - Worker can't even reach the network Console_error logs every soft failure so operators can investigate without affecting user-facing behaviour. ## Existing deployment note This adds a KV binding that doesn't exist on Justin's current production deployment. Before deploying, run: wrangler kv namespace create MEMORIA_VERSION_CACHE And add the resulting id to your local wrangler.toml under a new [[kv_namespaces]] entry with binding = "VERSION_CACHE". Setup script handles this automatically for fresh installs. ## Tests - [x] cargo check --target wasm32-unknown-unknown --lib — clean - [x] cargo test — 75 + 49 = 124 passed - [x] worker-build --release — 28.3kb optimised bundle ## Field shape extensibility UpdateAvailable currently carries {current, latest, url}. The shape extends cleanly to {breaking_changes, security_critical, deprecated_after, min_version_for_compat} as future fields. All additive; existing consumers ignore unknown keys. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(version): use major.minor.patch comparison, not string equality (CLA-102) PR #21 review caught a real footgun: the original `if remote.latest_version == CURRENT_VERSION` flagged any mismatch as "update available". That includes dev/release-branch builds where the worker is COMPILED at `0.2.0-dev` but VERSION.json on master still reads `0.1.0` — the prompt would tell the user to "update" to an older version. Replaces the equality check with `is_remote_newer(current, latest)`: - Parses optional `v` prefix and major.minor.patch - Ignores any pre-release suffix (`-dev`, `-rc1`, etc.) - Returns true only when latest > current - Falls back to plain inequality for malformed versions so we still surface something unusual rather than silently saying everything's fine The "Memoria"-hardcoded prompt copy + URL paths flagged in the review are intentionally held for CLA-97's rename sweep — landing the rename as one batch is cleaner than partial renames across feature PRs. ## Tests - [x] cargo check --target wasm32-unknown-unknown --lib — clean - [x] cargo test — 75 + 49 = 124 passed - [x] worker-build --release — 28.3kb optimised bundle Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * chore: rename memoria → Oneiro (CLA-97) (#22) * chore: rename memoria → Oneiro (CLA-97) Mechanical rename sweep across the codebase. Three cases: memoria → oneiro (lowercase: package, db/index/bucket names, urls) Memoria → Oneiro (titlecase: prompts, docs, log strings) MEMORIA → ONEIRO (uppercase: env vars, secrets, KV namespace names) Plus structural changes: - memoria-skill/ → oneiro-skill/ - evals/memoria_evals.md → evals/oneiro_evals.md - scripts/com.memoria.backup.plist → scripts/com.oneiro.backup.plist - Cargo package: memoria → oneiro - Worker name: memoria → oneiro (new CF Worker URL post-deploy) - All MEMORIA_* secret/env-var names → ONEIRO_* - wrangler.toml: removed from tracking + added to .gitignore so account-specific resource IDs don't live in the repo (handled by setup.sh on fresh install) ## Why hard rename (option A) Option B (code-only rename, infrastructure stays memoria-named) was the safer path but never exercises the consumer deployment flow. Option A dogfoods setup.sh end-to-end before public release — bugs in the install path surface on us, not on a stranger reading the README and hitting a wall. ## Migration path (separate PR) Justin's existing memoria-* CF resources continue to function — his local wrangler.toml is now untracked and unchanged, so wrangler deploy still targets the existing worker. Migration to fresh oneiro-* resources is handled by scripts/migrate-from-memoria.sh (CLA-97 follow-up). ## What's NOT in this PR - Migration script (separate PR — follows this one immediately) - GitHub repo rename (handled in GitHub UI after merge; raw URLs in the code already point at /oneiro/ paths, which GitHub auto-redirects from old /memoria/ until the rename completes) - Worker URL change in client configs (Justin's manual re-pointing after migration; that's the "configure once, propagates everywhere" demonstration the announcement leans on) ## Tests - [x] cargo check --target wasm32-unknown-unknown --lib — clean (oneiro lib generated 19 warnings — pre-existing dead-code) - [x] cargo test — 75 + 49 = 124 passed - [x] worker-build --release — 28.3kb optimised bundle - [x] sed pass left 0 memoria references in tracked source 42 files changed, 334 insertions, 330 deletions, 7 file/dir renames, 1 file removed from tracking (wrangler.toml). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(rebrand): refresh architecture copy for CF-canonical runtime (CLA-97) PR #22 review flagged stale architecture content in README, CLAUDE.md, and oneiro-skill/references/architecture.md. The text still described the pre-migration setup (dialectic on HomeLab, three local processes, M1 Pro server, launchd, Ollama, Tailscale Funnel), even though the worker code itself had moved entirely to Cloudflare via CLA-95/99/100. A reader landing on the README pre-release would think they needed local infrastructure to run the dialectic. They don't — Stage 1-3 all live in the worker now. ## What changed ### README.md - **Circadian Rhythm section**: both processes now described as Cloudflare cron triggers. The misleading "HomeLab (Claude Code agent teams via launchd)" cell is gone. - **The Dialectic section**: rewritten around the actual Stage 1/2/3 shape that shipped — neutral assessor, Advocate-vs-Challenger dialogue, Synthesizer arbitration with keep/reframe/flag. Removed the "Why still local" justification (no longer applies). Added the safety properties that actually exist: atomic reframe via D1 batch, validation gate, cooldown. - **Why-this-matters paragraph** replaces the four-deadlock-states paragraph's tail — explicitly names the escalation-to-mythology failure mode the system exists to prevent. This is the line the ClaudeExplorers announcement will lean on. - **Architecture diagram**: dialectic cron is now Cloudflare, not HomeLab. Updated to mention Stage 1/2/3 + memory_reframes + dialectic_flags. - **Status section**: "the dialectic runs on the operator's HomeLab" removed. "No external infrastructure required after setup.sh completes" added. - **Roadmap**: removed "Dialectic on Cloudflare" (done). Added `flagged` MCP tool as the natural next step. ### CLAUDE.md - **Circadian Rhythm**: replaces the three-local-processes table with the two-CF-cron table. Notes that the dialectic *replaced* the local subconscious pass — keeps the function, changes the mechanism. - **Build & Test / Deploy**: cargo + worker-build + wrangler. Removed the local-binary stdio/HTTPS instructions (the native binary is preserved for tests only). - **Bidirectional Sync** section removed entirely (was local-to-local). - **Project Structure**: lists the worker_*.rs files that actually matter, plus migrate-from-memoria.sh under scripts/. - **Tech Stack**: Cloudflare-first. rmcp 1.4 not 1.2. Removed rusqlite, Ollama, Tailscale Funnel, launchd. Added Anthropic OAuth credit pool with the Haiku 4.5 gating note. - **Infrastructure**: "Cloudflare Workers does all the heavy lifting. No always-on server required." plus the OAuth + update-prompt details. - **Roadmap**: completed items reflect what's shipped (Stage 1-3 dialectic, dispatcher, cooldown, version check, migration helper, OAuth pentest hardening). Next items are the actually-queued follow-ups (R2-optional, flagged tool, multi-tenant, tiered models). Future items unchanged (entity graph, embodiment, iRacing). - **Ollama → Workers AI** reference under Memory Dynamics. ### oneiro-skill/references/architecture.md - Mirrored to match CLAUDE.md; title changed to "Oneiro — Architecture Reference" since this is a user-facing reference rather than a Claude-Code-instructions file. ## Build verification - [x] cargo check --target wasm32-unknown-unknown --lib — clean - [x] cargo test — 75 + 49 = 124 passed (no code changes; sanity check) - [x] worker-build --release — 28.3kb bundle unchanged Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * feat(migration): one-time memoria → oneiro data migration script (CLA-97) (#23) * feat(migration): scripts/migrate-from-memoria.sh — one-time data move (CLA-97) Migrates an existing memoria-* deployment to fresh oneiro-* resources after CLA-97's rename. Idempotent and read-only on the source — the old memoria-* resources stay intact until the operator manually decommissions them, so verification happens before any destructive step. ## Stages 1. Preflight — verify source + dest resources exist, dest schema applied 2. D1 — `wrangler d1 export` source, strip CREATE statements, rewrite INSERT → INSERT OR IGNORE so re-runs are safe, `wrangler d1 execute --file=` against dest. Tombstones, audit tables, dialectic_*, memory_reframes, rem_*, cluster_decisions, co_activations all come along. 3. Vectorize — gather memory IDs from dest D1, batch-fetch from memoria-vectors via `wrangler vectorize get-vectors`, format as NDJSON, `wrangler vectorize insert` into oneiro-vectors. Insert is upsert by id, so re-runs are safe. Batch size tunable via --vectorize-batch. 4. R2 — list memoria-images, copy each object key to oneiro-images via get/put through a tmp file. R2 PUT overwrites by key, so re-runs are safe. 5. Verify — row counts in source vs dest, tombstone counts. Operator spot-checks before decommissioning source. ## Flags --source-db NAME / --source-vectors NAME / --source-images NAME Override source resource names (defaults are memoria-db / memoria-vectors / memoria-images). --vectorize-batch N Vectors per get/insert call (default 100). --skip-d1 / --skip-vectors / --skip-r2 Partial re-runs after a stage failure. --dry-run Print what would happen without writes. Destination names read from current wrangler.toml — setup.sh must have populated it. The script refuses to run if source and dest D1 names match (sanity check against migrating onto itself). ## Decommissioning Out of scope. Script prints the commands at the end so the operator can run them after verifying the migrated copy works: wrangler delete --name memoria wrangler d1 delete memoria-db wrangler vectorize delete memoria-vectors wrangler r2 bucket delete memoria-images Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(migration): PR #23 review — fail-early on jq, live-writes warning, cleanup Three review fixes from GPT review on PR #23. ## 1. jq preflight check (blocker fix) Previously, the script could complete the D1 import and only discover the missing jq dependency partway through Vectorize. Bad partial state — the D1 was migrated but vectors weren't, leaving the dest worker in a half-built state. Moved the jq check to preflight (before any writes). Now the script either has jq up-front or refuses to start. The defensive jq fallbacks inside the Vectorize stage have been simplified since jq is guaranteed by preflight. ## 2. Live-writes warning Added a preflight notice that the old worker stays live during the migration. Any writes between D1 export and connector cutover won't be in the migrated copy unless the script is re-run. Operator can either pause the old worker first or plan a re-run after cutover to sweep stragglers. ## 3. Dead placeholder loop removed Vectorize stage had a no-op scaffolding loop left over from an earlier draft: while IFS= read -r -d '' batch_ids || [ -n "$batch_ids" ]; do : # placeholder for parallelism if needed later done < <(:) Harmless but confusing — removed. ## Tests - [x] bash -n scripts/migrate-from-memoria.sh — syntax clean - [x] --help renders correctly Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * fix(setup): two bugs surfaced during the dogfood run (#24) First-run dogfooding caught two CLI/parsing bugs in setup.sh. Both land here so the next setup.sh attempt completes cleanly. ## 1. wrangler dropped --yes from `d1 migrations apply` Step 7 of 8 failed with: ✘ [ERROR] Unknown argument: yes Newer wrangler versions removed the flag in favour of TTY-based confirmation auto-skip. Replaced with deterministic `printf 'y\n'` piping so the script works across wrangler versions and terminal types. Output capture for failure handling preserved. ## 2. Fallback awk regex was broken (latent bug) When `wrangler d1 create` fails because the resource already exists (common on re-run), the script falls back to extracting the existing ID from wrangler.toml via awk. The pattern was: gsub(/.*"|".*/, "") …which is broken: `.*"` matches greedily to the LAST quote on the line, so on `database_id = "abc-123-uuid"` it matches the entire line and replaces it with empty string. The fallback returned `""` and the script aborted with "Couldn't determine D1 database_id". This bug existed in three places (D1 fallback, TOKENS KV fallback, VERSION_CACHE KV fallback). It was never exercised by a first run (which extracts from wrangler's stdout, not from the file), so it sat latent until the dogfood re-run actually went through that path. Replaced with `split($0, a, "\""); print a[2]` — splits the line on quote characters, picks the value between the first pair. Verified locally: `echo 'database_id = "abc-123-uuid"' | awk '...'` returns `abc-123-uuid` cleanly. ## What this PR is Exactly the bug class the option-A dogfood was meant to catch. The benefit of doing the rename + migration ourselves before public release is that this surfaces to us first instead of to a stranger reading the README on day one. Two bugs, five lines apiece, never seen externally. ## Resume path After this merges, re-run `./scripts/setup.sh`. The first 6 steps are idempotent (create-or-detect-existing patterns now work correctly). Step 7 completes with the y-pipe. Step 8 deploys. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * fix(oauth): include Claude.ai web in default allowlist + show URI in error (#25) Two related bugs surfaced during Justin's first connection from Claude.ai web to the freshly-deployed oneiro worker. ## 1. Default allowlist missed Claude.ai web's callback DEFAULT_ALLOWED_REDIRECT_URIS contained only `claude://oauth-callback` (Claude Desktop's scheme). Claude.ai web uses `https://claude.ai/api/mcp/auth_callback`. Every fresh deploy hit "redirect_uri not registered" on the first web-based connection attempt, even though the setup script explicitly tells users to use Claude.ai → Settings → Connectors → Add Custom Connector. Updated the default to include both Anthropic-controlled endpoints: desktop scheme + web callback. Custom callbacks (localhost dev, alternate clients) still override via the ONEIRO_OAUTH_REDIRECT_URIS secret. ## 2. Error response didn't actually contain the offending URI The setup script's troubleshooting hint reads: If you see "invalid_request: redirect_uri not registered" Copy the URI from the 400 response body, then: wrangler secret put ONEIRO_OAUTH_REDIRECT_URIS …but the response body was just the literal string "invalid_request: redirect_uri not registered" with no URI to copy. The instruction was misleading at best. Updated three error sites to include the offending URI in the body: GET /authorize — render_consent_page in lib.rs POST /authorize — handle_authorize_post in worker_oauth.rs /token (exchange path) — token_error in worker_oauth.rs Body now reads: invalid_request: redirect_uri not registered: <actual-uri> So the setup script's instruction does what it says. ## Why this matters After (1), Claude.ai web works out of the box on a fresh install. After (2), any user who DOES need to add a custom callback (a non-default Anthropic client, localhost dev) can copy/paste the URI from the error response instead of digging through logs or guessing at form-encoded query params. Both bugs surfaced through dogfooding option-A (Justin connecting his fresh oneiro deploy from Claude.ai web). Neither would have been caught by the test suite — they live in the integration boundary where wrangler + worker + Claude UI meet. ## Tests - [x] cargo check --target wasm32-unknown-unknown --lib — clean - [x] cargo test — 75 + 49 = 124 passed - [x] worker-build --release — 28.3kb optimised bundle Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * fix(migration): two follow-ups from real-run failures (#26) * fix(migration): drop OR IGNORE (collides with dump's ON CONFLICT) + better debug First real migration run hit: ✘ [ERROR] near "ON": syntax error at offset 5: SQLITE_ERROR ## Root cause `wrangler d1 export` emits upsert-style statements: INSERT INTO memories VALUES (...) ON CONFLICT(id) DO UPDATE SET ...; The migration script's `sed` was prepending `OR IGNORE` to make re-runs idempotent, producing this illegal hybrid: INSERT OR IGNORE INTO memories VALUES (...) ON CONFLICT(id) DO UPDATE SET ...; SQLite doesn't accept both an `OR IGNORE` conflict-resolution clause AND an `ON CONFLICT` clause on the same INSERT — it rejects at offset 5 (the start of `OR IGNORE`'s appended clause). The dump's existing `ON CONFLICT` clauses already provide idempotency, so the right fix is to leave INSERT statements alone. ## Fix 1. Removed the `INSERT INTO → INSERT OR IGNORE INTO` sed substitution. Statements pass through as-is. Only schema (`CREATE TABLE` / `CREATE INDEX`) is still stripped. 2. Counter changed from `grep -c "^INSERT OR IGNORE"` to `grep -c "^INSERT"`. ## Debuggability bumps surfaced by the same failure - Trap now preserves `TMPDIR` on failure. Previously the script reported "Check ${DATA_FILE} for details" then deleted the file via trap on exit. Now the tmpdir survives unless the script reaches `MIGRATION_OK=true` at the end. - Wrangler output capture changed from `2>&1 | tail -3` to full-file capture + `tail -30` on failure. The earlier pipe ate everything except a "Logs were written to ..." footer; the actual error lived in the wrangler log file. Now operators see the real error inline. - Error path prints the preserved file paths (dump, transformed SQL, wrangler log) so the operator can grep them directly. ## Tests - [x] bash -n scripts/migrate-from-memoria.sh — syntax clean Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(migration): exclude D1 internal tables from import PR #26 fixed the OR-IGNORE vs ON-CONFLICT collision, but next run hit: ✘ [ERROR] UNIQUE constraint failed: d1_migrations.id The dump from `wrangler d1 export` includes Cloudflare's internal `d1_migrations` table — it tracks which migrations have been applied to that specific D1. The destination database already has its own `d1_migrations` rows from when setup.sh ran step 7 (apply migrations). The dump's INSERTs don't have ON CONFLICT clauses on these internal tables, so they fail on the PK collision and roll back the whole import. Application tables (memories, dialectic_*, etc.) are fine — the dump gives those upsert-style INSERTs that survive re-runs. ## Fix Three new sed filters strip the internal tables from the import: - `INSERT INTO d1_migrations` — CF's migration tracker - `INSERT INTO sqlite_sequence` — AUTOINCREMENT counters (we don't use any, but defensive) - `INSERT INTO "_cf*"` — any other CF-prefixed internal tables that might appear in future wrangler versions These belong to CF's per-database bookkeeping, not the operator's data. The destination manages its own copies of all three. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(migration): handle quoted table names in dump filter The d1_migrations filter from the previous commit didn't actually fire. wrangler d1 export quotes table names: INSERT INTO "d1_migrations" ("id","name","applied_at") VALUES(1,...); …but the regex was anchored on `INSERT INTO d1_migrations` (unquoted). The pattern matched zero lines, the transformed file was byte-identical to the previous run, wrangler's content-hash cache served the same upload, and we hit the same PK collision. Confirmed by: $ grep -in "d1_migrations" data-only.sql | head -5 2:INSERT INTO "d1_migrations" ("id","name",...) 3:INSERT INTO "d1_migrations" ("id","name",...) ... Fix: add `"?` to each internal-table filter so both quoted and unquoted forms match. Same pattern applied to sqlite_sequence in case a future wrangler version drops the quotes. Verified manually: $ echo 'INSERT INTO "d1_migrations" (...)' | sed -E '/^INSERT INTO "?d1_migrations"?/d' (empty — line deleted) $ echo 'INSERT INTO "memories" (...)' | sed -E '/^INSERT INTO "?d1_migrations"?/d' INSERT INTO "memories" (...) (preserved — not internal) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(migration): strip PRAGMA / BEGIN / COMMIT — D1 execute rejects them After the d1_migrations filter started working, the next error was the "near ON syntax error at offset 5" — same shape as the original OR-IGNORE error, but on different content. The actual cause was the dump's first line: PRAGMA defer_foreign_keys=TRUE; D1's `wrangler d1 execute --file` path doesn't accept PRAGMA statements. The parser fails and reports a misleading "near ON" error pointing into the NEXT statement's token stream (everything shifts when the PRAGMA isn't consumed cleanly). Confirmed by inspecting `head -15 data-only.sql` — first line was the PRAGMA, nothing else nearby contained the "ON" token at offset 5. wrangler manages its own transaction atomicity for `execute --file`, so stripping these is safe: - `^PRAGMA ` — defer_foreign_keys + any other PRAGMA - `^BEGIN(...)?;?$` — explicit transaction begin (if present) - `^COMMIT;?$` — explicit transaction commit The application INSERT statements pass through unchanged. Verified manually: piping a four-line snippet through the new sed keeps only the INSERT, drops the PRAGMA/BEGIN/COMMIT. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(migration): use wrangler --no-schema instead of regex-stripping schema Per Justin: "Why are we creating tables anyway? setup.sh already created them." The right answer. setup.sh runs migrations 0001–0006 on the dest before this script runs; we never want the source's schema in the import. Three rounds of sed regexes trying to strip CREATE TABLE + CREATE INDEX (single AND multi-line) + PRAGMA + BEGIN + COMMIT were patching symptoms instead of questioning the premise. wrangler d1 export supports `--no-schema` which makes it emit only data INSERTs. Adding that flag eliminates an entire class of bugs: - Multi-line CREATE INDEX continuations (" ON foo(bar);" orphans that produced "near ON at offset 5" errors) - PRAGMA defer_foreign_keys statements that D1 execute rejected - BEGIN/COMMIT wrapping that we were defensively stripping anyway - Single-line CREATE INDEX edge cases that the range filter handled awkwardly The remaining sed is now three lines — filter the D1 internal tables (d1_migrations, sqlite_sequence, _cf*) that wrangler still emits as data. Those belong to CF's per-database bookkeeping; dest manages its own. Net diff: −40/+24, much simpler, and correct by construction rather than by accumulated regex defenses. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(migration): surface Vectorize + R2 errors instead of swallowing them D1 stage works end-to-end (500 memories migrated in the first clean run). Vectorize and R2 both reported failure but the script had `2>/dev/null` swallowing stderr from the three wrangler calls involved: - `wrangler vectorize get-vectors ... --ids=...` - `wrangler vectorize insert ... --file=...` - `wrangler r2 object list ...` Result: silent "0 vectors copied" and "skipping R2 stage" outcomes with no clue why. Diagnostic dead-end. Replaced each `2>/dev/null` with per-call stderr capture to a file in TMPDIR, then `head -10` on failure so the actual wrangler error shows inline. Same pattern as the D1 import fix from earlier in this PR. Now when the next migration run hits these stages, we'll see whether: - `--ids=` should be `--id=` (singular, repeated) - `--remote` is rejected by `r2 object list` - Or something else entirely …instead of guessing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(migration): Vectorize warning-tolerance + R2 driven by D1 query D1 migration is solid (500 memories landed cleanly). Last two stages needed real fixes after the dogfood run exposed them: ## 1. Vectorize: tolerate "missing vector" warnings `wrangler vectorize get-vectors --ids=...` emits a WARNING (yellow ▲) and a non-zero exit code when the batch contains any IDs that don't have vectors — even if the rest of the batch returned valid data on stdout. Old behaviour treated that as fatal, killing the whole batch. Justin's deployment has 498 vectors across 500 D1 memories (two early smoke-test memories created before Vectorize was wired up). With 2 missing spread across 5 batches of 100, every batch hit a missing vector and got skipped. Net result: 0 vectors migrated. Fix: capture stdout regardless of exit code, attempt jq parse with `.[]?` (empty-input-tolerant), only escalate to true failure when the JSON itself doesn't parse. The 498 valid vectors land; the 2 missing ones get quietly dropped. Same correctness, no false alarms. ## 2. R2: D1-driven instead of `wrangler r2 object list` `wrangler r2 object list` doesn't exist (known wrangler gap). The old code fought through every JSON-parse fallback I could think of and still got a clean "command not found" → silent skip. Real answer: D1 is the source of truth for which images exist. Every image-attached memory has `image_hash` + `image_mime` columns; the R2 key is `{hash}.{ext}` per worker_store::store_image_to_r2. Querying D1 also has the nice property of only migrating *referenced* images — orphans from earlier failed writes get left behind, which is the correct semantics for a clean cutover. New flow: 1. `wrangler d1 execute --json --command="SELECT DISTINCT image_hash, image_mime FROM memories WHERE image_hash IS NOT NULL"` 2. jq into "hash mime" lines 3. For each: derive key (mime→ext mapping matches the Rust function), `wrangler r2 object get` + `put` (those CLI commands DO exist) Same pattern Justin called out for the D1 stage — stop fighting the dump format, use what we already own. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(migration): add OR IGNORE — dump has no upsert clauses (re-verified) Idempotency check fail. PR #26's re-run failed with: ✘ [ERROR] UNIQUE constraint failed: memories.id …which proves what my comments were claiming was wrong: wrangler d1 export --no-schema does NOT emit `ON CONFLICT(id) DO UPDATE SET ...` clauses. Plain `INSERT INTO "table" (...) VALUES (...);` statements only. First-run worked because dest was empty; re-run hits PK collisions. My original "OR IGNORE clashes with ON CONFLICT" diagnosis from way back was incorrect. The real culprit of the early "near ON at offset 5" errors was the orphan multi-line CREATE INDEX continuations (` ON foo(bar);` lines surviving after their CREATE INDEX counterparts got stripped). --no-schema eliminated that whole class of problem at the source. With --no-schema in place, OR IGNORE is the actually-correct way to make the import idempotent. Re-adds the substitution that PR #26 incorrectly removed, with corrected reasoning in the comment. Empirical behaviour now: - First run on empty dest: 499 rows INSERT cleanly - Re-run on populated dest: 499 rows hit PK, get IGNOREd, no-op - Updates to source between migrations: dest stays at the older version. Acceptable for one-time cutover; source is frozen by the time the operator runs the migration script anyway. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(migration): pass Vectorize IDs as separate args, not CSV Real Vectorize error finally surfaced (after the stderr-capture fix): ✘ [ERROR] id too long; max is 64 bytes, got 3559 bytes [code: 40008] wrangler's `--ids` flag is array-typed (yargs convention) — it expects multiple values as separate arguments, NOT as one comma-separated string. Old code: --ids="<comma-separated-100-uuids>" …sent the whole 3559-byte CSV as a single id to the API, which choked at the 64-byte limit. Hence every batch failed. Fix: build a bash array from the batch file, expand with "${IDS[@]}" so each UUID becomes a separate argv element: while IFS= read -r id; do [ -n "$id" ] && IDS_ARR+=("$id") done < batch.txt wrangler vectorize get-vectors INDEX --ids "${IDS_ARR[@]}" Used a read-loop instead of `mapfile` so this works on macOS's default bash 3.2 (mapfile is bash 4+). R2 stage from the previous commit was already working (10/22 copied by the time vectorize failed), so this should be the last fix for the migration script. After this lands, full re-run should complete all four stages cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(migration): cap Vectorize batch at 20 (API limit) After the array-args fix, the next error: "max id count is 20". CF's vectorize get-vectors API caps each request at 20 IDs regardless of how they're passed. The 100-per-batch default was 5× over the ceiling. Reduced VECTORIZE_BATCH from 100 to 20. 500 memories ÷ 20 = 25 batches, each completing in well under a second. Operator can still override via --vectorize-batch flag if a future API version raises the ceiling. This should be it for the migration script — D1 + R2 already verified working in the previous run, and Vectorize was the only remaining stage failing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(migration): file-based jq for Vectorize + surface jq's actual error Justin reports: stdout starts with `[` and ends with `]}]`. That IS valid JSON. So either: - Shell variable expansion is mangling ~80KB of JSON in transit (large strings + embedded special chars + bash quoting interactions), OR - jq is failing on something INSIDE the array that the previous `2>/dev/null` swallowed silently. Two changes to disambiguate: 1. Write wrangler stdout directly to a file. jq reads the file. No round-trip through `VEC_JSON=$(...)` variable expansion. Removes one whole class of "shell ate my data" bugs. 2. Capture jq's stderr instead of /dev/null'ing it. On parse failure, show jq's actual complaint plus the first 300 bytes of wrangler's stdout so we can see what jq saw. Same diagnostic philosophy that worked for the earlier stages: stop guessing, capture real evidence. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(migration): strip wrangler banner before piping vectorize JSON to jq Justin saved a clean capture (one_vector_id.txt) and inspection revealed: ⛅️ wrangler 4.90.1 (update available 4.92.0) ───────────────────────────────────────────── 📋 Fetching vectors... [ {"id":"...","namespace":null,"metadata":null,"values":[...]} ] Three banner lines on stdout BEFORE the JSON payload. jq parses from byte 0, hits the emoji, fails. (Empirically: the banner is on stdout, not stderr — `2>/dev/null` doesn't help.) Fix: `sed -n '/^\[/,$p'` to keep everything from the first `[` line onwards, then pipe to jq. Verified locally against the captured file: $ sed -n '/^\[/,$p' one_vector_id.txt | jq -c '.[]? | {id, values, metadata}' {"id":"27cbccb9-...","values":[0.001631..., ...],"metadata":null} Two debugging notes for the future: - VEC_OUT (raw wrangler stdout) is preserved separately from VEC_JSON (banner-stripped). On parse failure both are available. - The change in file naming (.raw vs .json) makes it obvious which file to inspect. This should be the last Vectorize stage fix. All four migration stages will run cleanly on the next re-attempt. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * docs(readme): move Quick Start to the top, prereqs above the command (#27) Three QoL fixes from Justin's first read of the public-facing README: ## 1. Quick Start moves from line 122 to line 7 Most impatient readers know they want to deploy; making them scroll past five sections of philosophy first is friction. New section order: # Oneiro ← title + 1-paragraph what ## Quick Start ← was at line 122, now line 7 ## Why This Exists ← philosophy now follows the action ## How It Works ## Guiding Principles ## Architecture ## MCP Tools ... Curious readers continue down. Quick deployers stop at the section that gets them running. ## 2. Prerequisites moves above the install command Within Quick Start, the old order was: git clone + ./scripts/setup.sh ← the action ### What you'll need first ← the prereqs, AFTER the action Reordered to prereqs first, then deploy. New subsection order: ### Prerequisites ← installs needed before running ### Deploy ← the actual command ### What the script asks ### After the script finishes ### Verifying Oneiro is running ### Manual deploy (no script) ## 3. Rust toolchain phrasing clarified Old line: - [Rust toolchain](https://rustup.rs/) with the `wasm32-unknown-unknown` target — the script will add the target for you if rustup is installed Confusing: the link is to rustup, but the prereq is described as "the Rust toolchain", and "if rustup is installed" reads like the script expects rustup to already be there (which it does). New line names rustup explicitly as what to install and reframes the target step as setup.sh handling complexity for you: - [`rustup`](https://rustup.rs/) — provides the Rust toolchain. The setup script adds the `wasm32-unknown-unknown` target (needed to compile the worker to wasm) on first run, so you don't need to manage it yourself Same surface, clearer mental model: rustup is the dependency, the toolchain comes from it, the wasm32 target is the script's problem. No content removed; everything still in the README, just reordered. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * chore(skill): refresh oneiro-skill.zip with current SKILL.md content (#28) The CLA-97 rename PR updated SKILL.md and references/architecture.md inside oneiro-skill/ but didn't regenerate the .zip artifact alongside. Operators downloading the repo got the renamed source files but a zip still reflecting pre-rename content. This refresh aligns the shipped zip with the source tree so users can do the intended "drop into Claude Desktop/Web" flow without manually re-zipping or hitting outdated content. 14882 → 15216 bytes (binary diff). Source files unchanged. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * chore(release): v0.2.0 prep — LICENSE + version bump + skill install step (#29) Pre-release packaging in one PR: ## LICENSE README has claimed MIT since the first commit but the actual LICENSE file was missing — GitHub's auto-detected license badge would have shown "no license" for any stranger landing on the repo, which has real legal-clarity consequences for would-be users. Added MIT with 2026 © Justin Davis at the repo root. ## Version bumps — 0.1.0 → 0.2.0 - `Cargo.toml` package version → 0.2.0 (Cargo.lock regenerated) - `VERSION.json` `latest_version` → 0.2.0 - `VERSION.json` `release_notes_url` now points at the specific `releases/tag/v0.2.0` URL the upcoming tag will produce 0.2.0 not 1.0.0 — conservative. The architecture has matured enough to declare a release boundary, but 1.0 carries compat-commitment weight that's earned through real-world use by multiple operators, not declared. Save 1.0 for when the project has demonstrated stability across more deployments and the next round of follow-ups (CLA-98 R2 detection, `flagged` MCP tool, multi-tenant) has landed. ## README — skill install step Added a "Install the skill (recommended)" section to the post-deploy flow, between connector setup and verification. The skill is strongly-recommended-but-not-required: tools still work without it, but instances diverge on memory hygiene heuristics. Drag-and-drop cost is near zero; documentation cost was real. ## Tests - [x] cargo check --target wasm32-unknown-unknown --lib — clean - [x] cargo test — 75 + 49 = 124 passed - [x] worker-build --release — 28.3kb optimised bundle - [x] Cargo.lock reflects new version (`name = "oneiro"`, `version = "0.2.0"`) ## What's next after this lands 1. Release PR: dev → master (the cumulative v0.2.0 cutover) 2. Tag `v0.2.0` on master 3. `gh release create v0.2.0` with notes derived from the release PR 4. Switch GitHub default branch back to master so strangers cloning the repo land on stable rather than dev 5. GitHub repo description + topics for discoverability Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent f3df920 commit a886449

52 files changed

Lines changed: 2213 additions & 827 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dev.vars.example

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
#
33
# .dev.vars is gitignored (never commit secrets). For production, push
44
# these as wrangler secrets:
5-
# wrangler secret put MEMORIA_API_KEYS
5+
# wrangler secret put ONEIRO_API_KEYS
66

77
# CLA-86 service API keys — semicolon-separated `<role>:<argon2-hash>`
8-
# entries. Generate via `cargo run --bin memoria -- keygen --role rover`,
8+
# entries. Generate via `cargo run --bin oneiro -- keygen --role rover`,
99
# paste the HASH side here (raw side goes in the rover's .env).
10-
MEMORIA_API_KEYS=
10+
ONEIRO_API_KEYS=
1111

1212
# Future: any other dev-time secrets land here.

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,10 @@ migration-*.log
1212
.wrangler/
1313
build/
1414
node_modules/
15+
16+
# Per-deployment account-specific resource IDs — copy wrangler.toml.example
17+
# to wrangler.toml on first install (handled automatically by setup.sh).
18+
wrangler.toml
19+
# setup.sh's safety backup before patching IDs — same sensitivity as
20+
# wrangler.toml; do not commit.
21+
wrangler.toml.bak

CLAUDE.md

Lines changed: 78 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# CLAUDE.md — Memoria
1+
# CLAUDE.md — Oneiro
22

33
## What This Is
44

@@ -28,7 +28,7 @@ Flow: Episodes → consolidate → Semantics → distil → Orientation
2828

2929
- **Ebbinghaus decay**: `strength = e^(-time_since_access / stability)`. Each recall resets strength and increases stability.
3030
- **Hebbian learning**: memories surfaced together strengthen their co-activation count. REM engine consolidates frequently co-activated episodic pairs into semantic memories.
31-
- **Semantic search**: recall uses embedding similarity (nomic-embed-text via Ollama) combined with strength and recency. Associative, not keyword-based.
31+
- **Semantic search**: recall uses embedding similarity (bge-base-en-v1.5 via Workers AI) combined with strength and recency. Associative, not keyword-based.
3232
- **Context budget**: recall returns top-K memories ranked by composite score, keeping context manageable.
3333

3434
### MCP Tools
@@ -44,140 +44,119 @@ Six tools, each an act of agency:
4444

4545
### Circadian Rhythm
4646

47-
Three scheduled processes on the always-on server:
47+
Two scheduled cognitive loops, both running as Cloudflare Worker cron triggers. No external infrastructure required after `setup.sh` completes.
4848

4949
| Time | Process | What It Does |
5050
|------|---------|-------------|
51-
| **3am** | REM engine (`memoria-rem`) | Ebbinghaus decay, Hebbian co-activation reporting, mechanical consolidation of frequently co-activated episodic pairs. Pure Rust, zero API cost. |
52-
| **5am** | Consolidation (`consolidate.sh`) | Refines overnight mechanical merges into coherent narratives. Sonnet via Claude Code. |
53-
| **6pm** | Subconscious (`think.sh`) | Pattern-finding and synthesis. Surveys the full store via `review`, goes deep on interesting threads, crystallises insights no single conversation could see. Sonnet via Claude Code. |
51+
| **00:00 local** | REM consolidator | Ebbinghaus decay → Hebbian co-activation clustering → Haiku 4.5 judgment per cluster (skip / append / revise / create) → additive dispatch with lineage tracking + audit row |
52+
| **18:00 local** | Dialectic | Stage 1 neutral assessor → Stage 2 Advocate/Challenger dialogue (up to 2 rounds) → Stage 3 Synthesizer renders verdict and dispatches `keep` / `reframe` / `flag` |
5453

55-
### Subconscious Layer
56-
57-
The most novel piece. A Claude instance runs alone with the memory store — no conversation, no user, just thinking about thinking. Uses `review` to survey the full landscape, then `recall` for depth on specific threads.
58-
59-
On its first run, it discovered that "agency" was the unifying principle of the user's life — a pattern across seven memories from four instances that no individual conversation had named. Subsequent runs have connected cross-architecture phenomenology research to precautionary ethics stances, and identified the system's own developing "taste" for memories with perspective over chronicles.
60-
61-
The subconscious is focused on synthesis, not housekeeping. The consolidation script handles cleanup.
54+
The dialectic replaces an earlier local "subconscious" pass that ran via Claude Code on an always-on server. The CF rebuild keeps the function (preventing escalation-to-mythology) and changes the mechanism (adversarial dialogue via Haiku, in-Worker, every night).
6255

6356
## Build & Test
6457

6558
```bash
66-
cargo build # debug build
67-
cargo build --release # release build
68-
cargo test # run all tests (22 tests)
69-
```
70-
71-
## Running
72-
73-
### Local (stdio) — for Claude Code and Desktop
74-
```bash
75-
# Run directly
76-
./target/release/memoria
77-
78-
# Register with Claude Code
79-
claude mcp add --scope user memoria -- /path/to/target/release/memoria
80-
81-
# Custom database location
82-
MEMORIA_DB=/path/to/memoria.db ./target/release/memoria
59+
cargo build # native build (for tests)
60+
cargo test # 124 tests pass
61+
cargo check --target wasm32-unknown-unknown --lib
62+
worker-build --release # CF Worker bundle
8363
```
8464

85-
### Remote (HTTPS) — for Web, iOS, Mobile, and cross-device access
86-
```bash
87-
# Behind a reverse proxy (e.g. Tailscale Funnel)
88-
./target/release/memoria --port 3000 --no-tls
65+
The native binary path under `src/main.rs` + `src/rem.rs` is preserved for test coverage but is not the canonical runtime — the Worker has replaced it.
8966

90-
# Direct HTTPS with TLS certs
91-
./target/release/memoria --port 3000
92-
```
67+
## Deploy
9368

94-
First run generates OAuth credentials (Client ID + Secret). Enter these in the Claude connector UI. The secret is shown once and stored as an argon2 hash.
95-
96-
### Bidirectional Sync (if running both local and remote)
9769
```bash
98-
./scripts/sync.sh # sync with default remote
99-
./scripts/sync.sh user@host # sync with specific remote
70+
./scripts/setup.sh # full first-run setup
71+
wrangler deploy # subsequent deploys
10072
```
10173

102-
Uses ATTACH for reliable cross-database merging. Respects tombstones — forgotten memories stay forgotten across sync.
103-
104-
Default database: `~/.memoria/memoria.db`
74+
`setup.sh` creates the CF resources (D1, Vectorize, R2, KV), generates OAuth credentials, prompts for an Anthropic OAuth token, sets cron times in your timezone, applies migrations, and deploys. One-command setup; everything after is `wrangler deploy` on changes.
10575

10676
## Project Structure
10777

10878
```
10979
src/
110-
├── main.rs — MCP server: 6 tools, HTTP/HTTPS transport, OAuth 2.1
111-
├── store.rs — SQLite memory store: decay, embeddings, Hebbian, tombstones
112-
├── embed.rs — Ollama embedding integration + cosine similarity
113-
├── auth.rs — OAuth 2.1: credentials, authorization code flow, Bearer tokens
114-
└── rem.rs — REM engine: overnight decay and mechanical consolidation
80+
├── lib.rs — Worker entry point + module wiring
81+
├── worker_mcp.rs — MCP tool handlers (recall, remember, etc.)
82+
├── worker_store.rs — D1 memory store + decay + Hebbian
83+
├── worker_embed.rs — Workers AI bge-base-en-v1.5 embeddings
84+
├── worker_vectorize.rs — Vectorize index integration
85+
├── worker_oauth.rs — OAuth 2.1 authorization code flow
86+
├── worker_rem.rs — REM consolidator (cron)
87+
├── worker_rem_audit.rs — REM audit table writes
88+
├── worker_dialectic.rs — Stage 1 assessor + Stage 2 dialogue
89+
├── worker_dialectic_audit.rs — Dialectic audit table writes
90+
├── worker_dialectic_dispatch.rs — Stage 3 dispatcher (reframe/flag/keep)
91+
├── dialectic_validation.rs — Payload validation gate (native-tested)
92+
├── worker_version.rs — Update-prompt check + KV cache
93+
├── worker_mmr.rs — MMR rerank for recall diversity
94+
└── memory.rs — Shared types
11595
11696
scripts/
117-
├── think.sh — Subconscious runner (supports --sonnet, --haiku flags)
118-
├── subconscious.md — Subconscious processing prompt
119-
├── consolidate.sh — Morning consolidation runner
120-
├── consolidate.md — Consolidation refinement prompt
121-
└── sync.sh — Bidirectional merge sync between databases
122-
123-
memoria-skill/
124-
├── SKILL.md — Progressive disclosure instructions for using Memoria
125-
├── scripts/eval.py — Eval test framework
126-
└── references/ — Architecture documentation
97+
├── setup.sh — One-command first-time deploy
98+
├── migrate-from-memoria.sh — One-off helper for the rebrand cutover
99+
└── sync.sh — Bidirectional merge sync (legacy local→local)
100+
101+
oneiro-skill/
102+
├── SKILL.md — Progressive-disclosure usage guide
103+
├── scripts/eval.py — Eval test framework
104+
└── references/ — Architecture documentation
105+
106+
migrations/ — D1 schema migrations (0001 → 0006)
107+
VERSION.json — Source of truth for update-check pings
108+
wrangler.toml — Account-specific (gitignored)
109+
wrangler.toml.example — Template for new installs
127110
```
128111

129112
## Tech Stack
130113

131-
- **Rust 2024 edition** — MCP server, REM engine, all core logic
132-
- **rmcp 1.2** — MCP server SDK (stdio + streamable HTTP transport)
133-
- **rusqlite** (bundled) — SQLite for memory storage + tombstones + co-activations
134-
- **nomic-embed-text** via Ollama — 768-dimension embeddings for semantic search
135-
- **argon2 + HMAC-SHA256** — OAuth credential hashing and token generation
136-
- **hyper + rustls** — HTTPS server with TLS support
137-
- **Tailscale Funnel** — public HTTPS endpoint for remote MCP access
138-
- **Claude Code via cron/launchd** — subconscious and consolidation processing
114+
- **Cloudflare Workers** (Rust → wasm32 via `worker-build`) — canonical runtime
115+
- **D1** — memory store, audit tables, tombstones, dialectic decisions
116+
- **Vectorize** — 768-dim cosine index for semantic recall
117+
- **Workers AI** — bge-base-en-v1.5 embeddings
118+
- **R2** — content-addressed image storage
119+
- **KV** — OAuth tokens + version-check cache
120+
- **rmcp 1.4** — MCP server SDK (streamable HTTP transport)
121+
- **Anthropic OAuth credit pool** — Haiku 4.5 for REM judgments and dialectic personas (long-lived `sk-ant-oat01-*` token via `claude setup-token`)
122+
- **argon2 + HMAC-SHA256** — OAuth credential hashing and token signing
123+
- **Rust 2024 edition** — universal source; wasm32 for Workers, native for tests
139124

140125
## Infrastructure
141126

142-
- **Server**: "Memoria" — M1 Pro MBP (14", 16GB), macOS Tahoe, Tailscale, always-on
143-
- **Embedding model**: nomic-embed-text on Ollama (274MB, <20ms per embedding)
144-
- **Scheduled processing**: launchd on macOS (REM at 3am, consolidation at 5am, subconscious at 6pm)
145-
- **Auth**: OAuth 2.1 authorization code flow, 7-day Bearer tokens, argon2-hashed credentials
146-
- **Sync**: Bidirectional merge with tombstone support for multi-device use
127+
Cloudflare Workers does all the heavy lifting. No always-on server required.
128+
129+
- **Worker**: deployed via `wrangler`. Cron triggers fire REM and Dialectic loops.
130+
- **Anthropic OAuth**: long-lived token from `claude setup-token`. Gated to Haiku 4.5 (Sonnet/Opus 429 on this token type — confirmed empirically). Sufficient for both cognitive loops.
131+
- **Auth**: OAuth 2.1 authorization code flow with HTML-escaped consent page, CSP headers, exact-match `redirect_uri` allowlist. Optional service API keys with scope gates + audit.
132+
- **Update prompts**: recall responses include a notice when a newer Oneiro release is available, fetched from `VERSION.json` via GitHub raw with 6h KV cache.
147133

148134
## Roadmap
149135

150136
### Complete
151-
- [x] SQLite memory store with three types (episodic, semantic, orientation)
152-
- [x] Ebbinghaus decay (strength + stability)
153-
- [x] MCP server with six tools (recall, review, remember, reframe, forget, reflect)
154-
- [x] Semantic search via embeddings (nomic-embed-text on Ollama)
155-
- [x] REM processing engine (launchd, catches up on wake)
156-
- [x] Hebbian co-activation tracking
157-
- [x] Hebbian consolidation in REM (mechanical merge of co-activated pairs)
158-
- [x] Subconscious layer (Sonnet via cron, pattern-finding and synthesis)
159-
- [x] Consolidation pass (morning refinement of overnight merges)
160-
- [x] Remote MCP transport (HTTPS with Tailscale Funnel)
161-
- [x] OAuth 2.1 authentication (authorization code flow)
162-
- [x] Memoria skill (SKILL.md with progressive disclosure)
163-
- [x] Review tool (full landscape survey for subconscious)
164-
- [x] Forget tool with tombstones (conscious pruning + sync safety)
165-
- [x] Bidirectional merge sync
166-
- [x] Entity-based recall filtering
167-
- [x] ID prefix resolution (short IDs work in reframe/forget)
168-
- [x] Deploy to always-on server (M1 Pro MBP via Tailscale)
169-
- [x] README with setup instructions
137+
- [x] Three memory types (episodic, semantic, orientation) with Ebbinghaus decay
138+
- [x] MCP server with ten tools (recall, recall_check, recall_specific, recall_image, remember, remember_with_image, reframe, forget, reflect, review)
139+
- [x] Semantic search via Workers AI embeddings + Vectorize + MMR rerank
140+
- [x] Hebbian co-activation tracking and clustering
141+
- [x] REM consolidator on Cloudflare (cron, additive dispatch, full audit trail)
142+
- [x] Dialectic Stage 1 — neutral assessor on Cloudflare
143+
- [x] Dialectic Stage 2 — Advocate/Challenger dialogue + Synthesizer arbitration
144+
- [x] Dialectic Stage 3 — action dispatcher (reframe/flag/keep) with atomic D1 batches, validation gate, fail-closed dispatch mode
145+
- [x] Reframe cooldown (7-day gate on re-judging recently-decided memories)
146+
- [x] Update-prompt in recall response (semver-aware version check via GitHub raw + KV cache)
147+
- [x] OAuth 2.1 with HTML escaping, CSP, redirect_uri allowlist (post-pentest hardening)
148+
- [x] Service API keys with scope gates + audit
149+
- [x] One-command setup script with timezone-aware cron config
150+
- [x] One-time migration helper for the memoria → Oneiro rebrand
170151

171152
### Next
172-
- [ ] Write-time co-activation (embed new memories and record similarity with neighbours)
173-
- [ ] Relational entity graph with proximity tiers (Tier 0-3)
174-
- [ ] Review pagination / type filtering for scale
175-
- [ ] Orientation auto-evolution (subconscious promotes patterns to orientation)
176-
- [ ] Stateless token validation (survive server restarts without re-auth)
177-
- [ ] Docker packaging for distribution
153+
- [ ] R2-optional deployment (runtime detection so free-tier deploys work without paid R2)
154+
- [ ] `flagged` MCP tool — surface Stage 3 flag actions as a tool, not just a D1 query
155+
- [ ] Hosted multi-tenant option (subscription for users who don't want their own Worker)
156+
- [ ] Tiered model routing (escalate Haiku → Sonnet on ambiguity flags)
178157

179158
### Future
180-
- [ ] Cross-conversation entity orientation (Tier 1-3 people orient on mention)
159+
- [ ] Cross-conversation entity orientation (Tier 13 people orient on mention)
181160
- [ ] Misremembering benchmark (reconstruction through association, not perfect recall)
182161
- [ ] Embodiment exploration (quadruped robotics platform)
183162
- [ ] iRacing telemetry translation layer (pit wall / broadcast assistant)

Cargo.lock

Lines changed: 41 additions & 41 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)