Phases 2 to 8 of the AI assistant overhaul plus fixes from real-model chats: one tool registry with native tool calls, server-kept chat logs with prompt caching, an FTS5 library index with opt-in meaning search, keys bound to their address and a guard on every AI route, page tools and opt-in web search, saved chats with streaming, Try again, a context meter and compaction, chat images, delete/category/tag permissions with Undo, constraint and test cards, card fixes and previews, questions with answers to click, the derivation NOT toggle (schema v10, community catalog 0021), request fixes for the newest Claude models, and OpenAI's Responses API. Review fixes: Try again clears the old turn's fixes and answers, category Undo removes only what it made, a failed delete rolls back in order, not_matches patterns are checked, busy chats refuse a second request, and chat search ignores stored JSON.
Migration 019 adds an optional `negative_template` column to `templates`
(row-level, no schema bump). NULL marks a template saved before
negatives existed. The repository, importer (add, replace with
COALESCE, undo), templates API and TS types carry it, and the manager's
template editor gets a Negative template field.
Migration 018 adds a test_scenarios table, and /wp/api/test/scenarios
gives it list/get/create/update/delete. A scenario stores its stack
(library modules and bundles referenced live by id), pinned $var
values, seed spec and output variable, plus two JSON blobs the Test
Runner writes: a baseline to compare against and the latest run
summary. Listing can filter to the scenarios that use a given module
or bundle, for the "used in N scenarios" hint.
Scenarios are local only. They are not part of Import/Export or the
community, and this is a row-level table, so no schema_version bump.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xwq95EvrKu6WgE9D5rahKy
`migrate()` skips everything `<= MAX(version)`, which made an integer
the entire identity of a schema. It is not, and two failures followed
from that, both silent:
1. A database migrated by a NEWER build. Every migration is already
"applied", so the runner does nothing and the caller reads a schema
it has never seen. Additive columns survive; a shape change does not.
2. A database at the SAME version but a different shape, because a
migration file changed after it had already run somewhere — a
revised migration, a hand-edited file, an abandoned branch. Version
numbers match, so nothing compared anything.
Both corrupt cumulatively: every subsequent write goes through code
whose assumptions about the row shape are wrong, and the user has no
signal until something much later fails for an unrelated-looking reason.
Migration 017 adds a nullable `checksum` column; the runner records the
sha256 of the file it actually executed. `assert_schema_compatible`
checks both failure modes and runs at the top of `migrate()`, so no
caller can forget it.
NULL is treated as "unknown", never "mismatched". Rows applied before
017 existed cannot retroactively grow a checksum, and backfilling them
from the files on disk would assert exactly the compatibility the check
exists to verify. So the version-ahead half protects every database
immediately; the divergence half protects migrations from 018 onward.
Adds `content_rating TEXT NOT NULL DEFAULT 'safe'` to modules + bundles.
Defaults all pre-015 rows to 'safe'. Repository create/update accept a
new content_rating kwarg with validation against {'safe', 'nsfw'}.
Importer _insert_module / _insert_bundle pass entity.content_rating
through; _update_module / _update_bundle use COALESCE(?, content_rating)
so an install path that omits the key preserves the existing flag.
Why: user wants two NSFW behaviors:
1. Edit page toggle to mark a module/bundle as NSFW locally.
2. Community posts with content_rating='nsfw' auto-stamp NSFW on
the local row at install time.
This is the engine half of that work -- the schema, the data
plumbing, and validation. The SPA half (IdentityCard toggle + pill
render on ModuleListView + install.ts plumbing) lands next.
Tests cover: migration head bump to 15, column existence on both
tables, safe default backfill, accepts nsfw, rejects invalid, update
toggle round-trip, update without content_rating preserves prior.
Implements spec §4 Flow 4: bulk-at-boot is NOT a parallel forward-chain
implementation; it's eager iteration of lazy_migrate_row over every
stale row. Same original-first logic, same re-projection, zero
divergence. Eliminates the transient-wrong window where bulk could
write fabricated defaults over recoverable data in original_payload_json.
run_bulk_lazy returns the failure count; caller surfaces a dashboard
banner when > 0. STALE_PREDICATE catches both 'sv < current' AND
'tolerantly-installed row whose original now outranks the projection'.
Migration 014 invokes run_bulk_lazy at the end of up() — no-op pass
at v1, but validates the wiring is correct. Future bump migrations
(015 = v2, etc) will reuse the same helper.
Adds the four payload-versioning columns to modules + bundles per
the community-web spec sections 4-5:
- schema_version (NOT NULL DEFAULT 1)
- original_payload_json (NULL - only set during tolerant install)
- tolerant_drift_status ('none' | 'pending' | 'broken')
- schema_migrated_at (separate from updated_at - structural migration
is not a user edit)
Backfill is implicit via the DEFAULT on the NOT NULL columns.
Library rows installed from the community embed now carry the source
post slug + installed version_number in the engine sqlite. The SPA
uses both surfaces: a "community" pill next to the row name (so users
can tell at a glance which entries were authored locally vs pulled
from the embed), and a warn-coloured "v{N}" pill when the post has
shipped a newer version since install.
Engine:
- Migration 013 adds community_post_slug + community_version_number
to modules + bundles. Both nullable; locally-authored rows stay
NULL forever. Implemented as a .py migration with PRAGMA
table_info guards so rerunning against an already-migrated DB
(test fixtures rewind migrations to exercise older paths) is a
clean no-op rather than "duplicate column name".
- _insert_module / _insert_bundle (importer.py) persist the fields
when the entity dict carries them.
- _row_to_module / _row_to_bundle (repositories.py) surface them on
every read path. Try/except guard for pre-013 DB shape so a
connection opened mid-rollout does not crash.
Manager:
- installEnvelope grows optional `origin: { post_slug, version_number }`
in InstallOptions. When set, stamps every module + bundle in the
selection before the commit roundtrip.
- Host bridge in main.ts threads origin from `install(envelope, opts)`
through to installEnvelope. ABI stays at 1 (additive).
- New communityUpdateStore walks the library snapshot, batch-fetches
each unique community post (deduped, 8-wide concurrency), surfaces
rows whose installed version is behind the post's
latest_version_number. Network failures clear the store silently
so an offline session does not render misleading "v2 available"
pills.
- AppLayout kicks one check after the initial library fetch
resolves. No background polling.
- ModuleListView renders two distinct pills per row: a static
"community" indicator on origin-stamped rows, and a separate
warn-coloured "v{N}" pill when the update store has an entry.
Migration tests updated for the new schema head + bundle/module
column sets.
Introduces a persisted prompt-template library: migration 012 creates the
templates table (name/description/category_id FK/tags/is_favorite/
template_string), TemplateRepository provides CRUD with partial-update
sentinel semantics, and wp_api/templates.py exposes list/get/create/update/
delete/favorite with FK violations mapped to 400. Bumps the migration-head
version assertions to 12.
Migration 006 added the snapshot_fingerprint column but only populated
it on subsequent writes — every pre-006 row stayed NULL. The collision
detector correctly handles NULL as `exists-unknown` (amber EXISTING
badge), but a freshly exported-then-reimported untouched row still
showed EXISTING when content was byte-identical. Backfilling on first
open turns those into silent-skip (no badge), removing the visual noise.
Pure read+UPDATE — no schema change, no version bump on rows.
Adds engine/importer.py exposing commit_import / get_undo_entry /
undo_import. The committer applies a classified adds/replaces/renames
payload inside a single sqlite transaction, dispatching by `kind` to
the modules table (5 module types), bundles, or module_categories.
Module writes recompute snapshot_fingerprint; bundle writes recompute
payload_hash; categories use case-insensitive name-based merge
(matching the legacy import_bundle handler).
Every successful commit persists an import_undo row with enough
metadata (imported_records, replaced_snapshots, rename_map) to
reverse the commit. undo_import restores replaced rows bit-for-bit
(preserving original version + payload_hash + timestamps) and
deletes inserted rows, then evicts the undo entry.
Migration 007 creates the import_undo metadata table. Adds
17 importer tests covering adds across all 7 buckets, replace
snapshotting, rename with old→new mapping, collision rollback,
and undo restore/delete paths.