Phase 8 deleted the Python models for these tables; this migration
drops the orphan SQL.
Dropped tables (CASCADE-safe):
conversations, messages, generation_tool_log,
moments + moment_embeddings + moment_people/places/tasks/notes,
pending_curator_actions, push_subscriptions, weather_cache,
rss_item_embeddings (legacy pre-pivot experiment)
Dropped per-user settings: every voice_*, journal_*, briefing_*,
curator_* key, plus default_model, background_model, assistant_name,
auto_consolidate_tasks, chat_retention_days, think_enabled,
rag_default_scope.
Hard cutover — no downgrade. Existing data in these tables is lost;
the spec explicitly accepted this in exchange for a clean schema.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tested that services/tools/_registry exposed event tools to the LLM
tool layer. That layer was removed in Phase 8 commit 91bafb6; event
CRUD is now covered via the MCP tools in test_mcp_tool_events.py.
The bench-*.md files got swept into the Phase 8 mega-commit by
`git add -A`. They were local working-tree notes the user never
intended to track.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The earlier sed-delete of the Push/ChatHistory/About sections from
the Notifications tab also clipped the tab's outer </div>. Vue's
type-checker happily accepted the unbalanced structure (templates
type-check on script bindings, not tag pairing) but Vite's Vue
compiler failed at build time:
Element is missing end tag.
file: src/views/SettingsView.vue:1062:5
(The reported line 1062 was the outermost .settings-content div —
Vue's parser blames the outermost open tag when an inner sibling
goes unclosed.)
Confirmed by counting: 115 open <div, 116 </div> before — and now
116/116 after restoring the closing tag between the Email Notifications
section and the Integrations tab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 6 smoke caught:
Error executing tool list_events:
can't compare offset-naive and offset-aware datetimes
Event.start_dt is stored timezone-aware; the wrapper was passing naive
datetimes built from datetime.fromisoformat("YYYY-MM-DD"), so the SQL
comparison crashed. Also: the docstring promises "date_to inclusive at
end-of-day" but the code was using midnight-of-date_to, which would
silently miss same-day events after midnight.
Extracted the range math into _day_range_utc() so create/update_event's
_combine() can stay as-is (it stays naive — the service localizes
create/update inputs against the user's tz, that path didn't crash).
Test updated to match: assert tz-aware UTC datetimes and the +24h
bump for end-of-day-inclusive semantics.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FastMCP defaults to an allow-list of localhost variants for the Host
header (DNS-rebinding protection). Any deployment behind a reverse
proxy hitting a non-localhost hostname (e.g. devassistant.traefik.internal)
gets 421 Misdirected Request with:
WARNING mcp.server.transport_security: Invalid Host header: <name>
The protection exists to stop a malicious browser page from rebinding
DNS to attack a localhost MCP server. Our deployment is HTTP transport
behind a reverse proxy with bearer-token auth, which already gates
every request — so the rebinding threat doesn't apply. Disabling
the check lets any Host through; auth still rejects unauthorized
requests at 401.
This also makes the integration test pass without test-only host
hackery — every realistic Host header (Traefik internal hostname,
CDN domain, custom DNS) now reaches FastMCP cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FastMCP's transport_security module enforces a Host header as DNS-
rebinding protection. Raw-ASGI scope construction doesn't fill it in
automatically (real HTTP clients always send one), so the test
request was getting 421 Misdirected Request with a log warning:
Missing Host header in request
Production is unaffected — real curl, Claude Code, and any real
client send a Host header.
After fixing the /mcp path forwarding in 1fd303a, requests now reach
FastMCP — but its StreamableHTTPSessionManager raises:
RuntimeError: Task group is not initialized. Make sure to use run().
The session manager owns a task group that must be running before it
can handle requests. In a stand-alone Starlette app this happens via
the `lifespan` parameter (lifespan = session_manager.run). Hosted
inside Quart, my dispatch wrapper only forwards HTTP events, not
lifespan, so the manager never got its startup signal.
Fix: hook session_manager.run() (an async context manager) into
Quart's @app.before_serving and @app.after_serving so the task group
is alive across the serving window.
The CI integration test was hitting the same crash because it drives
app.asgi_app raw without going through Quart's serving lifecycle —
@before_serving never fires. Updated the test to manually enter
session_manager.run() around the request.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The dispatch wrapper was rewriting scope['path'] from '/mcp' to '/'
before handing off to FastMCP. But FastMCP's streamable_http_app
mounts the JSON-RPC handler at '/mcp' (its default), so the rewritten
'/' had no matching route and FastMCP returned 404. Auth middleware
was correctly firing first (a no-auth request still gets 401), the
bug was only on the post-auth path.
Symptom: `claude mcp add ...` succeeds, registration shows in
`claude mcp list`, but connection fails because the initialize
handshake returns 404 instead of an MCP capabilities response.
Fix: pass the scope through unmodified. FastMCP's own routing matches
the '/mcp' path.
Also tightened the integration test that should have caught this —
it was asserting `status != 401`, which a 404 trivially passes. Now
asserts `== 200`, the actual expected response for initialize.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The generated Claude Code snippet was outputting:
claude mcp add ... scribe-dev --url <URL> --header ...
But `claude mcp add` errors out with `unknown option '--url'`. The
URL is a positional argument, not a flag:
claude mcp add [--transport ...] [--scope ...] <name> <url> [--header ...]
Dropped --url and put the URL inline as a positional. Claude Desktop
JSON snippet was already correct (uses {url, headers} keys, not a
CLI flag).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MCP clients see tools namespaced by the server's local name already
(mcp__<server>__<tool>), so the fable_ prefix on every tool name was
redundant and ate tokens in the model's tool list.
Tools renamed (34 total):
fable_search → search
fable_list_notes / get_note / create_note / update_note / delete_note → list_notes / ...
fable_list_tasks / get_task / create_task / update_task / add_task_log → list_tasks / ...
fable_list_projects / get_project / create_project / update_project → list_projects / ...
fable_list_milestones / create_milestone / update_milestone → list_milestones / ...
fable_list_events / create_event / get_event / update_event / delete_event → list_events / ...
fable_list_tags → list_tags
fable_get_recent → get_recent
fable_list_persons / create_person / update_person → list_persons / ...
fable_list_places / create_place / update_place → list_places / ...
fable_list_lists / create_list / update_list → list_lists / ...
Also rebranded in MCP scope:
FastMCP("fable", ...) → FastMCP("scribe", ...)
auth realm "fable-mcp" → "scribe-mcp"
ASGI scope key fable_user_id → scribe_user_id
ContextVar label fable_mcp_user_id → scribe_mcp_user_id
Tool docstrings "in Fable" / "Fable task" → "in Scribe" / "Scribe task"
Server _INSTRUCTIONS prose
Deliberately kept:
- The internal Python package name `fabledassistant` (per project naming
convention — internal stays).
- "Fabled Scribe" as the official product/brand name (page footer,
smtp_from_name default).
- References to the legacy `fable-mcp/` standalone package in docstrings
explaining what we ported from — accurate until that directory is
deleted in Phase 10.
Client impact: existing MCP registrations need
claude mcp remove <name> && claude mcp add ...
once with a freshly-copied snippet from Settings → MCP Access. Claude
Code then re-discovers tools on connect — old conversations that
referenced fable_* tool names will see "tool not found" on those calls
until updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Settings → MCP Access now lets you tune the generated snippet:
- Server name input (default: 'scribe', stored per browser in
localStorage). The name appears both in the claude mcp add
command and as the JSON key in claude_desktop_config.json's
mcpServers map.
- Scope dropdown: user / project / local. Drives the --scope
flag in the claude mcp add snippet. Picks 'project' to commit
the server into the current repo's .mcp.json.
User-visible 'Fable' → 'Scribe' in MCP Access tab copy (lead text,
Claude Desktop step). Branding pivot in the rest of the app
(assistant_name placeholder, SMTP defaults, version line, etc.) is
deferred — chat/journal copy is going away in Phase 7 anyway.
Deliberately NOT touched:
- Tool names (fable_*) — protocol-level identifiers; renaming
breaks any Claude session, agent, or automation that referenced
them. Warrants its own phase.
- mcp/server.py: FastMCP('fable', ...) server name — same reason.
- Internal package name fabledassistant — per the project's
naming convention (CLAUDE.md memory), internal stays.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites the apikeys settings tab for the new MCP architecture:
- Tab label: 'API Keys' → 'MCP Access'
- Shows the in-app MCP URL (<origin>/mcp) with a copy button
- Claude Code snippet uses --transport http + --url + --header
- Claude Desktop snippet uses {url, headers: {Authorization}}
- Drops the wheel-download flow, the 'Other' client tab, and the
stdio env file / Claude config download helpers — those were
for the standalone fable-mcp package which goes away in phase 8
The api_keys backend stays unchanged — keys double as bearer tokens
for the /mcp endpoint via the existing auth.py middleware.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the Ollama HTTP get_embedding with a fastembed.TextEmbedding
singleton loaded lazily on first call. Model: BAAI/bge-small-en-v1.5
(384-dim), cached to /data/fastembed-cache.
Public API unchanged:
- get_embedding(text, model=None) — `model` now silently ignored
- upsert_note_embedding
- semantic_search_notes
- backfill_note_embeddings
_cosine_similarity gains a defensive length-mismatch check so any
stale 768-dim row that survived the migration is treated as 0.0
similarity rather than crashing zip().
The Ollama client dep stays in pyproject for now (other services still
use it); Phase 7 removes it once chat/journal/curator are gone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nine tools — list/create/update for each of person, place, list.
Get and delete reuse fable_get_note / fable_delete_note (typed
entities share the Note model).
Lists: the wrappers accept an `items: list[str]` for ergonomics and
translate to the {text, checked} dict shape that
services/knowledge.py and KnowledgeView.vue expect. items=[] clears;
items=None leaves unchanged.
Updates do an explicit get → merge → update round trip so updating
one typed field doesn't clobber the others stored alongside it in
entity_meta (which is a single JSONB column).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>