Skip to main content

agentmux_srv/backend/storage/
migrations.rs

1// Copyright 2025-2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! SQL schema setup for Store, FileStore, and the saga log.
5//!
6//! `objects.db` uses a **flat schema**: `run_object_schema` defines the
7//! final table set directly in one idempotent `CREATE TABLE IF NOT EXISTS`
8//! batch. It replaced an 11-step incremental migration chain
9//! (`run_forge_v1` … `run_forge_v11`) — see
10//! `docs/specs/SPEC_SCHEMA_FLATTENING_2026_05_19.md`. The chain was pure
11//! historical accretion: per-version data dirs mean every new version is
12//! born with a fresh `objects.db` and ran the whole chain top-to-bottom
13//! anyway, so the intermediate states were never reachable in production.
14//!
15//! `filestore.db` and `sagas.db` were already single-DDL stores; they keep
16//! their existing schema functions and gain only the `user_version`
17//! tripwire (`check_schema_compat` + `stamp_version`).
18
19use rusqlite::Connection;
20use tracing::warn;
21
22use super::error::StoreError;
23
24/// `user_version` stamped into `~/.agentmux/shared/store.db` after
25/// `run_shared_store_schema`. Versioned independently from `objects.db`.
26///   v1 — initial: identity accounts/bundles/bindings/links, memory
27///         bundles, drone definitions, muxbus credentials
28///   v2 — db_cron_jobs: persistent scheduled injection jobs
29///   v3 — Phase 4a of SPEC_PRESET_TO_BUNDLE_REFACTOR_2026_07_02.md: rename
30///        db_identity_accounts -> db_accounts, db_memory_bundles -> db_bundles
31///        (SPEC_ARMORY_PHASE4_STORAGE_RENAME_COMPLETION_2026_07_12.md).
32///   v4 — db_agent_credentials: per-agent M2M Cognito client_id/secret +
33///        cached access token, one row per locally-registered agent_id.
34///        Lets cloud_subscriber use a credential bound to a specific agent
35///        instead of the single shared per-account MUXBUS_TOKEN for every
36///        /reactive/* call.
37///   v5 — db_cron_jobs.max_age_secs: optional hard expiry bound (seconds since
38///        created_at) alongside the existing max_fires bound. NULL = no expiry
39///        (existing rows unaffected). See
40///        docs/specs/SPEC_AGENT_POLLING_AND_WAKEUP_HARDENING_2026_08_04.md Phase 0.
41///   v6 — db_agent_native_memory: durable, location-consistent mirror of
42///        each agent's native memory files, keyed by (agent_id, filename).
43///        See docs/specs/SPEC_NATIVE_MEMORY_DURABLE_SYNC_2026_08_07.md.
44///   v7 — db_bundles.instructions_by_provider: JSON object of
45///        {provider_id: content} instruction variants, additive to the
46///        existing flat `instructions` column. ABF v0.2 §2.2, see
47///        docs/specs/SPEC_ABF_V0_2_PROVIDER_AWARE_COMPONENTS_AND_NATIVE_MEMORY_2026_08_10.md.
48///   v8 — db_agent_native_memory_versions: append-only version history for
49///        native memory content, one row per `agent:memory:write_file`
50///        call (plus reverts and, later, out-of-band-write detection) —
51///        the mirror in v6 only ever holds the current value, with no way
52///        to see what a file contained before its last write. See
53///        docs/specs/SPEC_MEMORY_VERSION_CONTROL_AND_ARMORY_AUDIT_2026_08_19.md.
54///   v9 — db_bundles.is_system: AgentMux-controlled, highest-priority
55///        Global Bundle tier — see OBJECT_SCHEMA_VERSION's v27 doc
56///        comment (objects.db, above run_object_schema) for the full
57///        design; this store just needs schema parity.
58///   v10 — db_bundle_versions: append-only version history for Global
59///        Memory entry content — see OBJECT_SCHEMA_VERSION's v35 doc
60///        comment for the full rationale
61///        (SPEC_AGENT_FACING_GLOBAL_MEMORY_API_2026_09_15.md); this store
62///        just needs schema parity, same as v9 above.
63///   v11 — db_cron_jobs.target_uid: the target agent's UID (`db_agents.id`)
64///        beside the slug column, resolved once at create time and
65///        dual-written; nothing reads it yet, a job still fires by
66///        `target`. Identity M1b of
67///        SPEC_AGENT_IDENTITY_CARRIED_NOT_DERIVED_2026_09_23.md §9.1 —
68///        spec §5.4: non-interactive ingress never resolves a name at fire
69///        time, so the UID has to be captured while a human or model is
70///        present. Empty when the name did not resolve; never guessed.
71///   v12 — db_cron_jobs.created_by_uid and db_bundle_versions.written_by_uid:
72///        the actor's UID beside each actor-name column, taken from the
73///        request's `Caller` (its `X-Agent-Token`), never from a body field.
74///        Empty for an Unattributed request; never guessed. Dual-written,
75///        not yet read. Identity M4c-1 of
76///        SPEC_AGENT_IDENTITY_CARRIED_NOT_DERIVED_2026_09_23.md §6.5.9.
77pub const SHARED_STORE_SCHEMA_VERSION: i64 = 12;
78
79/// `user_version` value stamped into `objects.db` after `run_object_schema`.
80/// The flat schema reset the counter to 1 (the pre-flatten chain never set
81/// `user_version`, so legacy files read 0). Bumped per additive migration:
82///   v1 — flat schema baseline
83///   v2 — db_agent_definitions.updated_at
84///   v3 — db_agent_definitions.user_hidden (Phase 2 hide templates,
85///        SPEC_AGENT_PICKER_TWO_TIER_2026_05_24.md Q2 Decision Y)
86///   v4 — db_agents consolidation table (Phase 3a: every definition/
87///        instance write dual-writes here). Reads moved over
88///        piecemeal afterwards — the instance side in Phase 3b (#3080),
89///        every `agent_def_*` method in Phase 3d (#3092, v31) — and both
90///        legacy tables were dropped in v32.
91///   v5 — db_agents.last_block_id (Phase 3c; latest launch's block, so the
92///        consolidated read can find the session snapshot without joining
93///        db_agent_instances)
94///   v6 — container_image / container_volumes / container_name on both
95///        db_agent_definitions and db_agents (Phase 0 of
96///        SPEC_CONTAINER_PANE_SUPPORT_2026_06_11.md; host agents default
97///        to '' / '[]' / '')
98///   v7 — db_muxbus_credentials: global singleton for MuxBus cloud
99///        Cognito PKCE tokens (access, refresh, id) + expiry + user email
100///   v8 — db_memory_bundles.is_global: global-tier flag for Armory
101///        bundles injected into every agent's CLAUDE.md at launch
102///   v9 — db_memory_bundles.sort_order: explicit ordering for the Armory
103///        global bundles (controls CLAUDE.md injection order). Existing
104///        rows default to 0; the Armory Global section assigns positions via reorder.
105///   v10 — db_skills, db_mcp_servers, db_agent_skills_ref, db_agent_mcp_ref:
106///        standalone MCP Server and Skill primitives with per-agent ref tables
107///        (v1 composable model, SPEC_V1_MCP_SKILLS_PRIMITIVES_2026_06_30.md).
108///   v11 — Phase 4a of SPEC_PRESET_TO_BUNDLE_REFACTOR_2026_07_02.md: rename
109///        db_identity_accounts -> db_accounts, db_memory_bundles -> db_bundles
110///        (SPEC_ARMORY_PHASE4_STORAGE_RENAME_COMPLETION_2026_07_12.md).
111///   v12 — use_ambient_login on both db_agent_definitions and db_agents:
112///        explicit per-agent opt-in to the CLI's global (ambient) login when
113///        no oauth-class account resolves at spawn (layer 3 of
114///        SPEC_ACCOUNT_DELETE_DEAUTH_LAYERS_2_4_2026_07_14.md §2.3). Defaults
115///        to 0 (fail-by-default); the m0017 data migration grandfathers
116///        pre-existing linkless agents to 1.
117///   v13 — db_agent_credentials: per-agent M2M muxbus credential, mirroring
118///        db_muxbus_credentials' presence in both schemas — id_store falls
119///        back to this (per-channel) schema on installs that haven't yet
120///        applied 0011_shared_store_backfill, so cloud_subscriber's calls
121///        must not assume the shared-schema copy (v4 there) is the one in use.
122///   v14 — db_agent_native_memory: durable mirror of each agent's native
123///        memory files, keyed by (agent_id, filename). Same both-schemas
124///        duplication as db_agent_credentials, for the same id_store
125///        fallback reason. See
126///        docs/specs/SPEC_NATIVE_MEMORY_DURABLE_SYNC_2026_08_07.md.
127///   v15 — model_vendor_base_url on both db_agent_definitions and db_agents:
128///        redirects a harness (CLI) at a non-default model vendor backend
129///        (e.g. ANTHROPIC_BASE_URL for a claude-provider agent) — the data-
130///        model side of formalizing harness vs. model-vendor as distinct
131///        concepts. Defaults to '' (use the harness's default vendor).
132///        Only settable via `agent.define`, validated against the resolved
133///        provider's `ProviderConfig::base_url_env_var`.
134///   v16 — db_bundles.instructions_by_provider: JSON object of
135///        {provider_id: content} instruction variants, additive to the
136///        existing flat `instructions` column (which keeps meaning
137///        "default"). ABF v0.2 storage half of provider-scoped
138///        instructions — see
139///        docs/specs/SPEC_ABF_V0_2_PROVIDER_AWARE_COMPONENTS_AND_NATIVE_MEMORY_2026_08_10.md
140///        §2.2. Defaults to '{}' for existing rows; delivering a selected
141///        variant into a running agent is explicitly out of scope for this
142///        version (per that spec's non-goals) — this column only makes the
143///        data storable/exportable/importable.
144///   v17 — auto_continue_enabled on both db_agent_definitions and db_agents:
145///        per-agent opt-in letting a Warden Supervisor watcher agent
146///        auto-continue this agent's session on turn-end (subject to a
147///        server-side consecutive-nudge ceiling). Defaults to 0 (opt-in
148///        required — fail-by-default, same posture as use_ambient_login).
149///        See docs/analysis/ANALYSIS_WARDEN_AUTO_CONTROLLER_CONTINUATION_WATCHER_2026_08_12.md.
150///   v18 — db_agent_jekt_keys: per-agent HMAC-SHA256 signing key for
151///        host-tier jekt sender verification. One row per agent_id, minted
152///        on first use and injected into that agent's own MCP server
153///        process env (AGENTMUX_JEKT_KEY) at spawn — never into any other
154///        agent's env, so a signature can only be produced by the agent it
155///        claims to be from. See
156///        docs/specs/SPEC_JEKT_TRUST_LAYER_COMPLETION_2026_08_13.md §2.2.
157///   v19 — db_agent_definitions.memory_id: the agent's own dedicated ABF
158///        bundle, set once at creation and never updated thereafter
159///        (agent_def_update's SET clause deliberately omits it, same
160///        immutable-after-creation treatment as is_seeded/slug/parent_id).
161///        Distinct from db_agent_instances.memory_id (a specific launch's
162///        bundle, can still diverge on purpose). Mirrored into db_agents as
163///        a SEPARATE default_memory_id column, not db_agents.memory_id —
164///        that column is instance-only by existing convention (see
165///        db_agents' CREATE TABLE comment). Defaults to '' for existing
166///        rows; the m0021 migration
167///        backfills every unbound definition to a freshly-provisioned
168///        bundle. See ARCHITECTURE_MANDATORY_ABF_RETHINK_2026_08_14.md §3.1.
169///   v20 — db_agent_lan_keys: per-agent Ed25519 keypair for LAN-tier jekt
170///        sender verification — mirrors v18, asymmetric instead of HMAC
171///        (LAN is multi-party: a receiving peer must verify without being
172///        able to forge, which a shared secret can't provide). public_key
173///        is distributed to LAN peers on demand (not secret); private_key
174///        is injected into that one agent's own MCP process env
175///        (AGENTMUX_LAN_KEY) at spawn, same never-over-RPC guarantee as
176///        v18. See docs/specs/SPEC_JEKT_LAN_TIER_SIGNING_2026_08_15.md §2.1.
177///   v21 — db_lan_peer_pubkey_pins: trust-on-first-use pin of a REMOTE
178///        agent_id's LAN public key, as first observed from mDNS-discovered
179///        peers. reagentx P0 on the LAN signing PR: peer discovery itself
180///        is unauthenticated (any device can broadcast an mDNS instance and
181///        register an agent under an existing agent's name), so trusting
182///        "whichever peer answers first" for a pubkey lookup lets an
183///        attacker's self-minted key be accepted as authoritative for a
184///        victim's agent_id. Pinning the first-seen key and rejecting any
185///        later mismatch (rather than silently accepting the newest
186///        answer) is the standard SSH-host-key mitigation for exactly this
187///        "no PKI available" situation — closes the always-spoofable case,
188///        narrows it to a race on the very first lookup ever performed for
189///        that agent_id. See
190///        docs/specs/SPEC_JEKT_LAN_TIER_SIGNING_2026_08_15.md §2.2.
191///   v22 — db_background_tasks: durable record of a declared long-running
192///        (`run_in_background: true`) task attached to a block, so its
193///        liveness survives past `DockSnapshotCache`'s 1-hour eviction and
194///        a session reconnect/reload — the ephemeral client-transcript-
195///        derived signal chain had no durable source of truth at all. Per-
196///        channel (block ids are only meaningful within their own
197///        channel), not the shared store. Renumbered from an earlier v20
198///        to v22 when merged alongside the LAN-tier jekt signing PR, which
199///        independently claimed v20/v21 for an unrelated table pair — see
200///        docs/status/STATUS_ATTACHED_TASK_AXIS_AND_DEV_LOOP_2026_08_15.md,
201///        issue #2492.
202///   v23 — db_bundle_skills_ref, db_bundle_mcp_ref: bundle-level references
203///        to the standalone skill/MCP-server primitives, mirroring the v10
204///        agent-level ref tables but keyed by bundle_id — composable model
205///        v2, step 1 of docs/specs/SPEC_BUNDLE_AS_CONTAINER_V2_2026_08_17.md
206///        (GH issue #2024 item 3). A bundle's existing mcp_servers/skills
207///        JSON columns are untouched by this — still the .abf export/import
208///        format — these ref tables are the new live-resolution path.
209///        Deliberately NO FK to db_bundles (unlike the agent-level ref
210///        tables' FK to db_agent_definitions) — see this table's own doc
211///        comment at its CREATE TABLE for why.
212///   v24 — db_agent_native_memory_versions: append-only version history for
213///        native memory content, mirroring the v14 table's own dual-
214///        schema duplication (id_store per-channel fallback). See
215///        docs/specs/SPEC_MEMORY_VERSION_CONTROL_AND_ARMORY_AUDIT_2026_08_19.md.
216///   v25 — db_agent_groups: saved, named target sets for the fleet-control
217///        feature (broadcast/bulk-stop across many agent panes at once —
218///        docs/specs/SPEC_MULTI_AGENT_FLEET_CONTROL_2026_08_20.md). A
219///        group is just a durable list of block ids under a name — the
220///        Ansible-inventory-group lesson from that spec's research: a
221///        saved group, not raw ephemeral selection, is the abstraction
222///        that scales as a fleet grows. Per-channel (block ids are only
223///        meaningful within their own channel), not the shared store.
224///        Renumbered from an earlier v24 — merged alongside the native-
225///        memory-version-history PR, which independently claimed v24 for
226///        an unrelated table (see that entry above for the same class of
227///        collision this codebase has hit before, e.g. v20→v22).
228///   v26 — db_agent_definitions.conversation_visibility (TEXT, default
229///        'private') + db_conversation_trust_grants: `muxspect` Phase B/C's
230///        per-agent disclosure policy for an incoming cross-tier
231///        `transcript_request` jekt (LAN/WAN conversation visibility —
232///        docs/specs/SPEC_MUXSPECT_CROSS_TIER_CONVERSATION_VISIBILITY_2026_08_21.md,
233///        jekt tier rules confirmed in
234///        docs/specs/SPEC_JEKT_TRANSCRIPT_REQUEST_TIER_RULES_2026_08_22.md).
235///        One of "private" (default, auto-deny)/"trusted_peers" (auto-
236///        approve an allow-listed requester)/"ask" (force human
237///        escalation). Also dual-written to `db_agents` (mirrors
238///        `auto_continue_enabled`'s treatment exactly — a simple per-agent
239///        opt-in-style setting, unlike `memory_id`/`model_vendor_base_url`,
240///        which use a differently-named column there). Deliberately NOT
241///        added to `DefinitionRecordV1`'s cross-channel wire format —
242///        channel-local only, same precedent as those two fields; a
243///        cross-channel-reopened agent starts back at the safe "private"
244///        default. `db_conversation_trust_grants` mirrors
245///        `db_lan_peer_pubkey_pins`'s exact shape (own module,
246///        get/set-style methods, case-insensitive agent_id lookups).
247///   v27 — db_bundles.is_system: an AgentMux-controlled, highest-priority
248///        Global Bundle tier (INTEGER, default 0). A system row is always
249///        also is_global=1 (enforced in code, not a CHECK constraint —
250///        see `bundle_upsert_system`). Writable only through the
251///        dedicated `upsertsystemmemory`/`deletesystemmemory` RPCs and
252///        `Store::bundle_upsert_system`/`_delete_system` — the
253///        generic `bundle_upsert`/`_delete`/`_reorder` all refuse
254///        to touch an is_system=1 row. Injected first in
255///        `format_global_bundle_block`'s output, wrapped in explicit
256///        override wording, ahead of every ordinary Global Bundle
257///        section. See docs/specs/SPEC_GLOBAL_MEMORY_SYSTEM_TIER_2026_08_24.md.
258///   v28 — db_agent_activity_summaries: one-shot, on-demand Haiku-generated
259///        activity summary per `definition_id`, used as the AgentPicker's
260///        "My Agents" conversation-preview fallback when a row has no
261///        structured `output.state.json` snapshot to preview (legacy rows
262///        predating snapshot persistence — see `has_snapshot` in
263///        `listrecentsessions`, `agent_handlers/session.rs`). Generated
264///        from the instance's raw `"output"` filestore file (present even
265///        when the newer structured snapshot isn't) via the same shared
266///        Ambient Model Call gateway `dispatch_name`/`term:ambient_summary`
267///        already use (`invoke_haiku`,
268///        `docs/specs/SPEC_AMBIENT_MODEL_CALLS_FRAMEWORK_2026_07_03.md`).
269///        Generated lazily (first picker load that needs it, not on every
270///        turn) and cached forever once non-empty — mirrors
271///        `SubAgent.display_name`'s cache-once posture, not
272///        `term:ambient_summary`'s per-turn refresh. Renumbered from an
273///        earlier v27 — merged alongside the Global Bundle system-tier PR
274///        (#2782), which independently claimed v27 for an unrelated
275///        column (same collision class as this file's own v20→v22 and
276///        v24→v25 history, see those entries above). See
277///        docs/reports/REPORT_AGENT_PICKER_FIELD_ORDER_SORT_AND_DATA_GAPS_AUDIT_2026_08_24.md §5a.
278///   v29 — db_agents.session_id / status / started_at / ended_at: the
279///        agent's LATEST launch state, the four `db_agent_instances`
280///        columns the consolidated row never got (Phase 3a deliberately
281///        left them "on the block", but the recent-sessions picker, the
282///        pane close/reopen continuity write-back and the status-filtered
283///        instance list all kept reading them from the legacy table — the
284///        reason it could not be dropped). One row per agent ⇒ one launch
285///        state per agent; a continuation moves it to the new block/session.
286///        Backfilled from the legacy rows by
287///        `m0025_agents_launch_state_backfill` (latest instance per
288///        projection key), kept current by the instance dual-write, and
289///        the read flip in `SPEC_AGENT_ARCHITECTURE_2026_05_27.md` Phase
290///        3b retires the legacy reads on top of it. No unique index on
291///        `db_agents(slug)` is added: template-launch projections copy
292///        their template's slug, so uniqueness stays a definition-level
293///        rule enforced by the slug collision scan in `agent_def_insert`.
294///   v30 — `db_agent_content` / `db_agent_skills` / `db_agent_history` /
295///        `db_agent_identity_links` / `db_agent_skills_ref` /
296///        `db_agent_mcp_ref`: `agent_id`'s foreign key now targets
297///        `db_agents(id)` instead of `db_agent_definitions(id)`. Every
298///        existing row already satisfies the new target (its `agent_id` is
299///        a definition id, and every definition has a `db_agents` mirror
300///        at the same id) — what changes is that a `db_agents` row with NO
301///        definition counterpart (a template launch) can now hold these
302///        directly too, closing the gap `listrecentsessions`' template
303///        fallback (Phase 3b, #3080) exists for. SQLite has no in-place FK
304///        redefinition, so an existing store needs its six tables rebuilt;
305///        `m0028_agent_child_tables_repoint_fk` does that (rename,
306///        recreate, copy, drop, `PRAGMA foreign_key_check` before commit).
307///        A fresh install gets the new FK directly from the CREATE TABLE
308///        statements below — nothing to rebuild there. Phase 3c
309///        (`SPEC_AGENT_ARCHITECTURE_2026_05_27.md`).
310///   v31 — No DDL change. Every `agent_def_*` write now lands on
311///        `db_agents` only — `db_agent_definitions` is no longer kept in
312///        sync (Phase 3d, #3092). Bumped anyway (codex P1 on #3092):
313///        `check_schema_compat` is a downgrade guard as much as a schema
314///        guard, and this release changes what a NEWER binary's writes
315///        mean to an OLDER one just as much as a column would. Without the
316///        bump, rolling back to the immediately preceding binary after
317///        this version has run would silently accept a store where new
318///        agents exist only in `db_agents` — that binary's `agent_def_get`/
319///        `update`/`delete` still target `db_agent_definitions` and would
320///        404 on them, or replay stale legacy values back into `db_agents`
321///        on edit. Stamping 31 makes that downgrade refuse to open instead.
322///   v32 — `db_agent_definitions` and `db_agent_instances` are dropped.
323///        Nothing has read or written either table since v31 (agent-concept
324///        consolidation's definition and instance flips, #3080/#3092); this
325///        is the final phase (Phase 3e,
326///        `SPEC_AGENT_ARCHITECTURE_2026_05_27.md`) closing out the tracking
327///        spec. `db_agent_instances`' `CREATE TABLE`/indexes and
328///        `db_agent_definitions`' `CREATE TABLE` are gone from this function
329///        — leaving either would silently recreate the tables (empty) on
330///        the very next `Store::open()` after the drop migration runs,
331///        since migrations execute before this function does on every
332///        boot. `db_agent_definitions`' own `ALTER TABLE ADD COLUMN`
333///        entries, by contrast, are KEPT — gated on the table still
334///        existing (see that block's own comment below) — because several
335///        pending migrations (m0007, m0025, m0026, m0028) call
336///        `Store::open()` internally, running this function, before m0029
337///        (numbered last) ever drops the table in the SAME upgrade pass; a
338///        store old enough to be missing one of those columns would fail
339///        those migrations' own reads with "no such column" otherwise
340///        (Codex P1 on #3096). An existing store's tables are actually
341///        removed by `m0029_drop_legacy_agent_tables`; a fresh install
342///        never has them to begin with, so the gate is a no-op for it.
343///        `adopt_legacy_table_names`'s
344///        `db_forge_agents` → `db_agent_definitions` rename entry is left
345///        in place deliberately: an install old enough to still carry
346///        `db_forge_agents` reaches it during an EARLIER migration's own
347///        `Store::open()` call (every migration in this chain lays down
348///        schema that way), so the rename still fires before
349///        `m0029` runs later in the same upgrade pass and correctly drops
350///        what the rename just produced — no special-casing needed, and no
351///        data is stranded under the old name.
352///   v33 — `db_agent_project_instructions`: what each agent was last observed
353///        to read as project instructions from its working directory, keyed
354///        `(agent_id, path)`. Phase 3 of
355///        `SPEC_INSTRUCTION_AND_MEMORY_PORTABILITY_2026_09_09.md`.
356///
357///        Records a content hash and an observation time, never the content:
358///        this is a tracking table, not a mirror. The distinction from
359///        `db_agent_native_memory` above is deliberate — that one stores
360///        content because it is a durable copy of files AgentMux may have to
361///        serve when the originals are unreachable, whereas these files
362///        belong to the repository, are always readable from it, and are
363///        never written by AgentMux. Storing their content would create a
364///        second copy of somebody else's file with no one to keep it honest.
365///
366///        Purely additive: nothing reads it at launch, and an agent with no
367///        row simply has no prior observation to compare against.
368///   v34 — `db_agent_skills_ref` / `db_agent_mcp_ref` / `db_bundle_skills_ref`
369///        / `db_bundle_mcp_ref` drop their `skill_id`/`mcp_id` FK to this
370///        store's LOCAL `db_skills`/`db_mcp_servers`. Phase 2 of
371///        `SPEC_DURABLE_BINDINGS_2026_09_10.md` §5.4 (Codex P1, PR #3182):
372///        once catalog writes redirect to `identity_store`, this channel's
373///        local catalog copies stop gaining new rows, so binding a
374///        post-redirect skill/server would otherwise hit a hard `FOREIGN KEY
375///        constraint failed` (`PRAGMA foreign_keys=ON` in production, see
376///        `Store::configure_and_migrate`). The `agent_id → db_agents` FK on
377///        the agent-level pair is kept verbatim — only the catalog-pointing
378///        FK is dropped. Same shape `db_bundle_skills_ref`/`db_bundle_mcp_ref`
379///        already used for `db_bundles` (see those tables' own doc comment
380///        below): existence moves to the application layer
381///        (`managed_bind_agent`/`managed_bind_bundle`'s new `catalog: &Store`
382///        check). SQLite has no `ALTER TABLE ... DROP CONSTRAINT`, so an
383///        existing store's four tables are rebuilt by
384///        `m0032_drop_catalog_fk_from_ref_tables`; a fresh install gets the
385///        trimmed FK directly from the CREATE TABLE statements below.
386///   v35 — db_bundle_versions: append-only version history for Global
387///        Memory entry content (`name` + `instructions`), one row per
388///        write through the new agent-facing Global Memory API
389///        (`docs/specs/SPEC_AGENT_FACING_GLOBAL_MEMORY_API_2026_09_15.md`
390///        Phase 0) — mirrors `db_agent_native_memory_versions`'s shape
391///        exactly, keyed by a single `bundle_id` instead of that table's
392///        composite `(agent_id, filename)`, since a Bundle's identity is
393///        already a single id. Global Memory (`db_bundles`) previously had
394///        no audit trail at all — only `updated_at`, no way to see what
395///        changed, when, or by which agent, unlike native memory which has
396///        had this since v24. Opened specifically because Global Memory is
397///        about to gain a write path an agent (not just a human at the
398///        Armory UI) can reach. `written_by` is a TRUSTED identity column
399///        (an agent's real `AGENTMUX_AGENT_ID`, or `"armory-ui"`),
400///        deliberately separate from the caller-describable `source`/
401///        `source_detail` pair — an agent could otherwise claim
402///        `source: "human"` with nothing to contradict it (codex P2, PR
403///        #3237, caught before this table ever shipped).
404///   v36 — db_agent_wan_keys: per-agent Ed25519 keypair for general
405///        agent-to-agent WAN-tier jekt signing (issue #2586's second half).
406///        Mirrors v20's shape; a SEPARATE keypair from db_agent_lan_keys by
407///        design, not a reuse — LAN public keys are permanently TOFU-pinned
408///        by peers (v21), which is why they cannot rotate
409///        (SPEC_JEKT_HOST_KEY_TTL_ROTATION_2026_09_14.md §4), whereas WAN
410///        keys are served from muxbus's authoritative ownership registry and
411///        are meant to. One key cannot have both lifecycles. `key_version`
412///        is reserved for the publish-sync and rotation phases (that spec's
413///        §3.2/§3.6) and is not yet read by anything.
414///   v37 — db_agent_tokens: per-agent local identity token, keyed by the
415///        agent's UID (`db_agents.id`) — identity M1 of
416///        `SPEC_AGENT_IDENTITY_CARRIED_NOT_DERIVED_2026_09_23.md` (§6, §9.1).
417///        Minted on first spawn (`agent_token_ensure`), injected into that
418///        one agent's process env as `AGENTMUX_AGENT_TOKEN`, revoked on
419///        delete via `purge_agent_dependents`. Unlike v18's jekt key and the
420///        registry entry files, which are keyed by display name and so are
421///        shared by two same-named agents (spec §6.3), this is keyed by a
422///        `PRIMARY KEY` that is a UUID for every non-template row. Not read
423///        by anything until M4 — mint-and-carry only, revertible by ignoring
424///        the field. See `storage/agent_tokens.rs`.
425///   v38 — db_agent_key_tombstones: the lowercased names (slug, display
426///        name, instance name) of every deleted agent — identity M4a of
427///        `SPEC_AGENT_IDENTITY_CARRIED_NOT_DERIVED_2026_09_23.md` §6.5.4.
428///        Signing keys are keyed by name, so a later agent that takes a
429///        deleted agent's name would inherit its keys; M4d copies name-keyed
430///        keys to UIDs and must never copy a tombstoned name's. Written from
431///        M4a on, before any copy exists, so the record is complete when M4d
432///        reads it. Written only — nothing reads it until M4d.
433///   v39 — db_bundle_versions.written_by_uid: the writer's UID beside the
434///        `written_by` name, taken from the request's `Caller` (its
435///        `X-Agent-Token`), never from a body field; empty for an
436///        Unattributed write, the Armory UI and the seeders. Dual-written,
437///        not yet read. Not only parity with the shared store: when that
438///        store is unavailable `id_store` falls back to this one and writes
439///        here. Identity M4c-1 of
440///        `SPEC_AGENT_IDENTITY_CARRIED_NOT_DERIVED_2026_09_23.md` §6.5.9.
441///   v40 — db_jekt_held: a jekt to a known agent of this channel that no
442///        delivery tier could take (the agent was not running anywhere), held
443///        for up to 24 h and replayed when it registers, with the trust
444///        verdicts it was accepted with as explicit columns.
445///        `SPEC_DURABLE_JEKT_DELIVERY_2026_09_24.md` Phase 1.
446///   v41 — db_background_tasks.owner_tool_use_id: the `tool_use_id` of the
447///        Agent call whose subagent launched this background task (the
448///        `parent_tool_use_id` on the stream line that issued the Bash call);
449///        NULL for the agent's own tasks. Joins a task to its subagent, whose
450///        `meta.json` records the same id. Phase 2 of
451///        `SPEC_BACKGROUND_TASK_STRUCTURED_FEED_AND_SWARM_OWNERSHIP_2026_09_27.md`.
452///   v42 — db_agents.last_runtime: the Runtime menu choices (permission mode,
453///        model, effort) the user last made for this agent, as a JSON object,
454///        '' when none. Lets Continue / Reattach / a fresh open start where
455///        the user left off instead of on the definition's defaults. Written
456///        only when the user picked something. Item 2 of
457///        `SPEC_RUNTIME_MENU_REMAINING_GAPS_2026_10_01.md`.
458///   v43 — db_viewer_devices: the devices paired with this channel's viewer
459///        listener, each with the SHA-256 of its viewer token (never the
460///        token), its name, its optional public key and when it was paired
461///        and last seen; plus db_agents.hide_from_devices, an agent's opt-out
462///        from the viewer routes. agentmux-mobile's
463///        SPEC_AGENT_STATUS_AND_LIVE_PANE_FEED_2026_10_07 §13.2, §4.3. See
464///        `storage/viewer_devices.rs`.
465///   v44 — db_mcp_server_reach: per catalog MCP server, whether it reaches
466///        every agent (`all_agents`) and whether container agents get it
467///        too (`in_containers`). A table of its own beside `db_mcp_servers`,
468///        so nothing that copies catalog rows changes. Both default 0: a
469///        catalog server reaches only the agents it is bound to.
470///        `SPEC_CONNECTORS_MCP_WHAT_AGENTS_RUN_2026_10_10.md` §2.1.
471pub const OBJECT_SCHEMA_VERSION: i64 = 44;
472/// `user_version` value stamped into `filestore.db`.
473pub const FILESTORE_SCHEMA_VERSION: i64 = 1;
474/// `user_version` value stamped into `sagas.db`.
475pub const SAGA_LOG_SCHEMA_VERSION: i64 = 1;
476
477/// Object type table names matching the `db_<otype>` convention.
478const WSTORE_OTYPES: &[&str] = &[
479    "client",
480    "window",
481    "workspace",
482    "tab",
483    "layout",
484    "block",
485    "temp",
486];
487
488/// Legacy `objects.db` table names retired by the de-forge rename, paired
489/// with their replacement. `adopt_legacy_table_names` renames any of these
490/// it finds — the single surviving piece of the old migration chain (it
491/// also subsumes the v11 `db_memories` rename).
492const LEGACY_TABLE_RENAMES: &[(&str, &str)] = &[
493    ("db_forge_agents", "db_agent_definitions"),
494    ("db_forge_content", "db_agent_content"),
495    ("db_forge_skills", "db_agent_skills"),
496    ("db_forge_history", "db_agent_history"),
497    ("db_forge_agent_identities", "db_agent_identity_links"),
498    ("db_memories", "db_memory_bundles"),
499    // Phase 4 of SPEC_PRESET_TO_BUNDLE_REFACTOR_2026_07_02.md — the
500    // "bundle" naming collision fix (SPEC_ARMORY_PHASE4_STORAGE_RENAME_COMPLETION_2026_07_12.md).
501    // Order matters: this entry runs AFTER the `db_memories` rename above
502    // in the same pass, so a database old enough to still have `db_memories`
503    // chains straight through to `db_bundles` in one migration run instead
504    // of stopping at the intermediate name.
505    ("db_identity_accounts", "db_accounts"),
506    ("db_memory_bundles", "db_bundles"),
507];
508
509/// Legacy index names that must be dropped after their table is renamed —
510/// `ALTER TABLE … RENAME` keeps indexes attached but under their old names,
511/// which would collide with the flat DDL's `CREATE INDEX`. The flat DDL
512/// recreates each under the new name.
513const LEGACY_INDEX_DROPS: &[&str] = &[
514    "idx_forge_agents_slug",
515    "idx_forge_history_agent_date",
516    "idx_forge_agent_identities_account",
517    "idx_memories_is_blank",
518    // Phase 4 rename (see LEGACY_TABLE_RENAMES above) — objects.db names,
519    // then the shared store.db names (`idx_ss_*` prefix, see
520    // run_shared_store_schema).
521    "idx_identity_accounts_provider",
522    "idx_memory_bundles_is_blank",
523    "idx_ss_identity_accounts_provider",
524    "idx_ss_memory_bundles_is_blank",
525];
526
527/// Tables retained by the old chain only for a downgrade path the flatten
528/// abandons. Dropped from any legacy DB by the adopt step; never created
529/// by the flat schema. `db_workflow_*` data was already copied into
530/// `db_drone_*` by the old v10 migration, so dropping loses nothing.
531///
532/// `db_identities` (the pre-v11 name), `db_identity_bundles`, and
533/// `db_identity_bindings` were dropped outright rather than renamed
534/// forward in Phase 4c of SPEC_PRESET_TO_BUNDLE_REFACTOR_2026_07_02.md —
535/// `db_agent_identity_links`/`db_accounts` is the sole credential-
536/// resolution path (`identity/resolver/inject.rs::resolve_bindings_for_instance`),
537/// confirmed via the already-applied `m0013`/`m0014` backfill migrations
538/// (see SPEC_ARMORY_PHASE4_STORAGE_RENAME_COMPLETION_2026_07_12.md §6/§8a).
539const DEAD_TABLE_DROPS: &[&str] = &[
540    "db_workflow_definitions",
541    "db_workflow_runs",
542    "db_v10_migrated_legacy_defs",
543    "db_v10_migrated_legacy_runs",
544    "db_identities",
545    "db_identity_bundles",
546    "db_identity_bindings",
547];
548
549/// Initialize (or re-validate) the full `objects.db` schema.
550///
551/// Idempotent — safe on every srv startup. Steps:
552///
553/// 1. `adopt_legacy_table_names` — renames any pre-flatten forge/bundle
554///    tables found (protects dev databases created before the flatten;
555///    see the spec §3/§7) and drops the dead workflow/sentinel tables.
556/// 2. The flat `CREATE TABLE IF NOT EXISTS` batch — the canonical schema.
557/// 3. Seeds the blank Bundle singleton row.
558///
559/// A database stuck at a pre-v11 intermediate schema cannot be fully
560/// adopted (its tables predate later columns). The adopt step still
561/// renames what it finds; the first query referencing a missing column
562/// then fails loudly with `no such column` — a hard error, not silent
563/// empty state, with the data preserved on disk. Per-version data dirs
564/// make that case unreachable for released builds.
565pub fn run_object_schema(conn: &Connection) -> Result<(), StoreError> {
566    adopt_legacy_table_names(conn)?;
567
568    // ---- Generic StoreObj object tables ----
569    for otype in WSTORE_OTYPES {
570        conn.execute_batch(&format!(
571            "CREATE TABLE IF NOT EXISTS db_{otype} (
572                oid     TEXT PRIMARY KEY,
573                version INTEGER NOT NULL DEFAULT 1,
574                data    TEXT NOT NULL
575            );"
576        ))?;
577    }
578
579    // ---- Agent + identity + memory + drone schema ----
580    conn.execute_batch(
581        "CREATE TABLE IF NOT EXISTS db_agent_content (
582            agent_id     TEXT NOT NULL,
583            content_type TEXT NOT NULL,
584            content      TEXT NOT NULL DEFAULT '',
585            updated_at   INTEGER NOT NULL DEFAULT 0,
586            PRIMARY KEY (agent_id, content_type),
587            -- v30: targets db_agents(id), not db_agent_definitions(id) — see
588            -- OBJECT_SCHEMA_VERSION's v30 doc comment above.
589            FOREIGN KEY (agent_id) REFERENCES db_agents(id) ON DELETE CASCADE
590        );
591
592        CREATE TABLE IF NOT EXISTS db_agent_skills (
593            id          TEXT PRIMARY KEY,
594            agent_id    TEXT NOT NULL,
595            name        TEXT NOT NULL,
596            trigger     TEXT NOT NULL DEFAULT '',
597            skill_type  TEXT NOT NULL DEFAULT 'prompt',
598            description TEXT NOT NULL DEFAULT '',
599            content     TEXT NOT NULL DEFAULT '',
600            created_at  INTEGER NOT NULL DEFAULT 0,
601            -- v30: see OBJECT_SCHEMA_VERSION's v30 doc comment above.
602            FOREIGN KEY (agent_id) REFERENCES db_agents(id) ON DELETE CASCADE
603        );
604
605        CREATE TABLE IF NOT EXISTS db_agent_history (
606            id           INTEGER PRIMARY KEY AUTOINCREMENT,
607            agent_id     TEXT NOT NULL,
608            session_date TEXT NOT NULL,
609            entry        TEXT NOT NULL,
610            timestamp    INTEGER NOT NULL DEFAULT 0,
611            -- v30: see OBJECT_SCHEMA_VERSION's v30 doc comment above.
612            FOREIGN KEY (agent_id) REFERENCES db_agents(id) ON DELETE CASCADE
613        );
614        CREATE INDEX IF NOT EXISTS idx_agent_history_agent_date
615            ON db_agent_history(agent_id, session_date);
616
617        CREATE TABLE IF NOT EXISTS db_accounts (
618            id           TEXT PRIMARY KEY,
619            name         TEXT NOT NULL,
620            provider     TEXT NOT NULL,
621            kind         TEXT NOT NULL,
622            display_name TEXT NOT NULL DEFAULT '',
623            secret_ref   TEXT NOT NULL,
624            context      TEXT NOT NULL DEFAULT '{}',
625            status       TEXT NOT NULL DEFAULT 'unknown',
626            created_at   INTEGER NOT NULL DEFAULT 0,
627            updated_at   INTEGER NOT NULL DEFAULT 0
628        );
629        CREATE INDEX IF NOT EXISTS idx_accounts_provider
630            ON db_accounts(provider);
631
632        CREATE TABLE IF NOT EXISTS db_agent_identity_links (
633            agent_id   TEXT NOT NULL,
634            account_id TEXT NOT NULL,
635            provider   TEXT NOT NULL,
636            PRIMARY KEY (agent_id, provider),
637            -- v30: see OBJECT_SCHEMA_VERSION's v30 doc comment above.
638            FOREIGN KEY (agent_id)   REFERENCES db_agents(id) ON DELETE CASCADE,
639            FOREIGN KEY (account_id) REFERENCES db_accounts(id) ON DELETE CASCADE
640        );
641        CREATE INDEX IF NOT EXISTS idx_agent_identity_links_account
642            ON db_agent_identity_links(account_id);
643
644        CREATE TABLE IF NOT EXISTS db_bundles (
645            id            TEXT PRIMARY KEY,
646            name          TEXT NOT NULL UNIQUE,
647            description   TEXT NOT NULL DEFAULT '',
648            is_blank      INTEGER NOT NULL DEFAULT 0,
649            is_global     INTEGER NOT NULL DEFAULT 0,
650            provider      TEXT NOT NULL DEFAULT '',
651            model         TEXT NOT NULL DEFAULT '',
652            instructions  TEXT NOT NULL DEFAULT '',
653            instructions_by_provider TEXT NOT NULL DEFAULT '{}',
654            context_files TEXT NOT NULL DEFAULT '[]',
655            mcp_servers   TEXT NOT NULL DEFAULT '[]',
656            skills        TEXT NOT NULL DEFAULT '[]',
657            sort_order    INTEGER NOT NULL DEFAULT 0,
658            created_at    INTEGER NOT NULL DEFAULT 0,
659            updated_at    INTEGER NOT NULL DEFAULT 0,
660            is_system     INTEGER NOT NULL DEFAULT 0
661        );
662        CREATE INDEX IF NOT EXISTS idx_bundles_is_blank
663            ON db_bundles(is_blank);
664
665        -- Phase 3a consolidation: `db_agents` collapses the legacy
666        -- definitions + instances tables into one table per
667        -- `docs/specs/SPEC_AGENT_CONCEPT_CONSOLIDATION_2026_05_24.md`.
668        -- WRITE-ONLY in 3a: every old-table mutation dual-writes here, but
669        -- every read still hits the old tables. Phase 3b migrates readers,
670        -- Phase 3c drops the old tables. Column names align with the live
671        -- `db_agent_definitions` shape (provider, working_directory, …) —
672        -- NOT the inline draft in the spec (provider_id, cmd, cmd_args, …) —
673        -- because the existing storage layer never grew the cmd-template
674        -- columns the spec sketched; carrying the names we actually have
675        -- avoids inventing data we don't store. See the PR body for the
676        -- field-by-field mapping.
677        CREATE TABLE IF NOT EXISTS db_agents (
678            id                   TEXT PRIMARY KEY,
679            name                 TEXT NOT NULL,
680            icon                 TEXT NOT NULL DEFAULT '',
681            description          TEXT NOT NULL DEFAULT '',
682
683            -- Template vs user agent
684            is_template          INTEGER NOT NULL DEFAULT 0,
685            parent_template_id   TEXT NOT NULL DEFAULT '',
686
687            -- Provider/cmd config (was on definition; named to match the
688            -- live `db_agent_definitions` columns).
689            provider             TEXT NOT NULL,
690            provider_flags       TEXT NOT NULL DEFAULT '',
691            shell                TEXT NOT NULL DEFAULT '',
692            environment          TEXT NOT NULL DEFAULT '',
693            agent_type           TEXT NOT NULL DEFAULT 'standalone',
694            agent_bus_id         TEXT NOT NULL DEFAULT '',
695            accounts             TEXT NOT NULL DEFAULT '',
696            auto_start           INTEGER NOT NULL DEFAULT 0,
697            restart_on_crash     INTEGER NOT NULL DEFAULT 0,
698            idle_timeout_minutes INTEGER NOT NULL DEFAULT 0,
699            slug                 TEXT NOT NULL DEFAULT '',
700            branch_label         TEXT NOT NULL DEFAULT '',
701
702            -- Bindings (was on instance — only meaningful when is_template=0).
703            -- For template rows these stay empty.
704            identity_id          TEXT NOT NULL DEFAULT '',
705            memory_id            TEXT NOT NULL DEFAULT '',
706            working_directory    TEXT NOT NULL DEFAULT '',
707            github_context       TEXT NOT NULL DEFAULT '',
708            instance_name        TEXT NOT NULL DEFAULT '',
709
710            -- Latest launch's block: pointer to the most-recent session's
711            -- block so the consolidated read can locate the conversation
712            -- snapshot without joining db_agent_instances. Was the only
713            -- per-launch field db_agents retained until v29 added the
714            -- four launch-state columns right below it.
715            last_block_id        TEXT NOT NULL DEFAULT '',
716
717            -- Latest launch's state (v29, consolidation Phase 3b/3c). The
718            -- original 3a design left status/session_id/started_at/ended_at
719            -- to the block; in practice the recent-sessions picker, the
720            -- pane close/reopen continuity guarantee
721            -- (SPEC_PANE_CLOSE_REOPEN_CONTINUITY_GUARANTEE_2026_07_27.md §4.1)
722            -- and the status-filtered instance list all still read them from
723            -- db_agent_instances, which is what kept that table alive. One
724            -- row per agent means one launch state per agent: a continuation
725            -- moves these to the new block/session rather than adding a row.
726            session_id           TEXT NOT NULL DEFAULT '',
727            status               TEXT NOT NULL DEFAULT '',
728            started_at           INTEGER NOT NULL DEFAULT 0,
729            ended_at             INTEGER NOT NULL DEFAULT 0,
730
731            -- Provenance
732            created_at           INTEGER NOT NULL DEFAULT 0,
733            updated_at           INTEGER NOT NULL DEFAULT 0,
734            is_seeded            INTEGER NOT NULL DEFAULT 0,
735            user_hidden          INTEGER NOT NULL DEFAULT 0,
736
737            -- Container support (Schema v6 / Phase 0).
738            -- Empty for host agents; populated by ContainerManager.
739            container_image      TEXT NOT NULL DEFAULT '',
740            container_volumes    TEXT NOT NULL DEFAULT '[]',
741            container_name       TEXT NOT NULL DEFAULT '',
742
743            -- Explicit per-agent opt-in to the CLI's global (ambient) login
744            -- when no oauth-class account resolves at spawn (schema v12,
745            -- SPEC_ACCOUNT_DELETE_DEAUTH_LAYERS_2_4_2026_07_14.md §2.3).
746            use_ambient_login    INTEGER NOT NULL DEFAULT 0,
747
748            -- Redirects this agent's harness at a non-default model vendor
749            -- backend (schema v15) — see db_agent_definitions' column doc.
750            model_vendor_base_url TEXT NOT NULL DEFAULT '',
751
752            -- Warden Supervisor auto-continue opt-in (schema v17) — see
753            -- db_agent_definitions' column doc.
754            auto_continue_enabled INTEGER NOT NULL DEFAULT 0,
755
756            -- The DEFINITION's own dedicated ABF bundle (schema v19) —
757            -- mirrors db_agent_definitions.memory_id. Deliberately a
758            -- DIFFERENT column from memory_id above: that one is
759            -- instance-scoped (only meaningful when is_template=0,
760            -- written exclusively by the instance dual-write helpers) and
761            -- this migration doesn't touch its semantics. default_memory_id
762            -- is what agent_def_list's db_agents-backed read actually
763            -- surfaces as AgentDefinition.memory_id (aliased in the SELECT
764            -- below) -- readers that already consult db_agents for listing
765            -- need this to see the definition-level binding at all.
766            default_memory_id    TEXT NOT NULL DEFAULT ''
767        );
768        CREATE INDEX IF NOT EXISTS idx_agents_is_template
769            ON db_agents(is_template);
770        CREATE INDEX IF NOT EXISTS idx_agents_parent_template_id
771            ON db_agents(parent_template_id);
772        CREATE INDEX IF NOT EXISTS idx_agents_is_seeded
773            ON db_agents(is_seeded);
774
775        CREATE TABLE IF NOT EXISTS db_drone_definitions (
776            id          TEXT PRIMARY KEY,
777            name        TEXT NOT NULL,
778            description TEXT NOT NULL DEFAULT '',
779            graph       TEXT NOT NULL DEFAULT '{\"nodes\":[],\"edges\":[]}',
780            viewport    TEXT NOT NULL DEFAULT '{\"x\":0,\"y\":0,\"zoom\":1}',
781            created_at  INTEGER NOT NULL DEFAULT 0,
782            updated_at  INTEGER NOT NULL DEFAULT 0
783        );
784        CREATE INDEX IF NOT EXISTS idx_drone_definitions_updated
785            ON db_drone_definitions(updated_at DESC);
786
787        CREATE TABLE IF NOT EXISTS db_drone_runs (
788            id           TEXT PRIMARY KEY,
789            drone_id     TEXT NOT NULL,
790            status       TEXT NOT NULL DEFAULT 'running',
791            started_at   INTEGER NOT NULL DEFAULT 0,
792            ended_at     INTEGER NOT NULL DEFAULT 0,
793            block_states TEXT NOT NULL DEFAULT '{}',
794            output       TEXT NOT NULL DEFAULT '',
795            error        TEXT NOT NULL DEFAULT '',
796            FOREIGN KEY (drone_id) REFERENCES db_drone_definitions(id) ON DELETE CASCADE
797        );
798        CREATE INDEX IF NOT EXISTS idx_drone_runs_drone_started
799            ON db_drone_runs(drone_id, started_at DESC);
800        CREATE INDEX IF NOT EXISTS idx_drone_runs_status
801            ON db_drone_runs(status);
802
803        -- v10: Standalone skill and MCP server primitives (composable model).
804        CREATE TABLE IF NOT EXISTS db_skills (
805            id          TEXT PRIMARY KEY,
806            name        TEXT NOT NULL,
807            trigger     TEXT NOT NULL DEFAULT '',
808            skill_type  TEXT NOT NULL DEFAULT 'prompt',
809            description TEXT NOT NULL DEFAULT '',
810            content     TEXT NOT NULL DEFAULT '',
811            is_global   INTEGER NOT NULL DEFAULT 0,
812            created_at  INTEGER NOT NULL DEFAULT 0,
813            updated_at  INTEGER NOT NULL DEFAULT 0
814        );
815        CREATE INDEX IF NOT EXISTS idx_skills_is_global ON db_skills(is_global);
816
817        CREATE TABLE IF NOT EXISTS db_mcp_servers (
818            id          TEXT PRIMARY KEY,
819            name        TEXT NOT NULL,
820            transport   TEXT NOT NULL DEFAULT 'stdio',
821            config      TEXT NOT NULL DEFAULT '{}',
822            is_global   INTEGER NOT NULL DEFAULT 0,
823            created_at  INTEGER NOT NULL DEFAULT 0,
824            updated_at  INTEGER NOT NULL DEFAULT 0
825        );
826        CREATE INDEX IF NOT EXISTS idx_mcp_servers_is_global ON db_mcp_servers(is_global);
827        -- v44: who a catalog server reaches beyond its
828        -- binds — see OBJECT_SCHEMA_VERSION's v44 doc comment.
829        CREATE TABLE IF NOT EXISTS db_mcp_server_reach (
830            mcp_id        TEXT PRIMARY KEY,
831            all_agents    INTEGER NOT NULL DEFAULT 0,
832            in_containers INTEGER NOT NULL DEFAULT 0,
833            updated_at    INTEGER NOT NULL DEFAULT 0
834        );
835
836        -- v34: skill_id's FK to this store's LOCAL db_skills is gone — see
837        -- OBJECT_SCHEMA_VERSION's v34 doc comment above. agent_id's FK to
838        -- db_agents (v30) is unchanged.
839        CREATE TABLE IF NOT EXISTS db_agent_skills_ref (
840            agent_id TEXT NOT NULL,
841            skill_id TEXT NOT NULL,
842            PRIMARY KEY (agent_id, skill_id),
843            FOREIGN KEY (agent_id) REFERENCES db_agents(id) ON DELETE CASCADE
844        );
845
846        -- v34: mcp_id's FK to this store's LOCAL db_mcp_servers is gone —
847        -- see OBJECT_SCHEMA_VERSION's v34 doc comment above. agent_id's FK
848        -- to db_agents (v30) is unchanged.
849        CREATE TABLE IF NOT EXISTS db_agent_mcp_ref (
850            agent_id TEXT NOT NULL,
851            mcp_id   TEXT NOT NULL,
852            PRIMARY KEY (agent_id, mcp_id),
853            FOREIGN KEY (agent_id) REFERENCES db_agents(id) ON DELETE CASCADE
854        );
855
856        -- v23: Bundle-level MCP/skill references (composable model v2,
857        -- docs/specs/SPEC_BUNDLE_AS_CONTAINER_V2_2026_08_17.md) — same shape
858        -- as the agent-level ref tables above, keyed by bundle_id instead of
859        -- agent_id. A bundle's existing `mcp_servers`/`skills` JSON columns
860        -- stay untouched (still consumed by .abf export/import); these ref
861        -- tables are the new live-resolution path, wired into launch-time
862        -- config generation alongside the agent-level refs.
863        --
864        -- reagentx P0 review on PR #2639: NO foreign key to db_bundles here,
865        -- unlike the agent-level ref tables' FK to db_agents (v30: was
866        -- db_agent_definitions — see OBJECT_SCHEMA_VERSION's v30 doc comment).
867        -- Bundles are authoritatively written through `id_store` (the
868        -- shared store in a normal production install — see
869        -- `bundle.rs::register_bundle_upsert`), not `mstore`/objects.db,
870        -- where this table lives (it must live here to FK to
871        -- db_mcp_servers/db_skills, which used to be mstore-local too (now
872        -- authoritatively identity_store as of Phase 2 — see
873        -- OBJECT_SCHEMA_VERSION's v34 doc comment). mstore's own copy of
874        -- db_bundles is a schema-compatible but essentially always-
875        -- empty local mirror in that case — an FK against it would make
876        -- every real bind fail. Same no-FK-to-a-table-living-in-the-wrong-
877        -- store reasoning as `db_agent_identity_links.account_id`'s
878        -- deliberate lack of an account FK (see that table's own doc
879        -- comment). Existence is checked at the application layer instead
880        -- — see `Store::bundle_skill_bind`'s `id_store` parameter.
881        --
882        -- v34: skill_id's FK to db_skills is ALSO gone now (it was
883        -- mstore-local before v34; see OBJECT_SCHEMA_VERSION's v34 doc
884        -- comment) — this table has never had a bundle_id FK, so it is now
885        -- FK-free entirely, existence checked purely at the application
886        -- layer for both columns.
887        CREATE TABLE IF NOT EXISTS db_bundle_skills_ref (
888            bundle_id TEXT NOT NULL,
889            skill_id  TEXT NOT NULL,
890            PRIMARY KEY (bundle_id, skill_id)
891        );
892
893        -- v34: mcp_id's FK to db_mcp_servers is gone — see the
894        -- db_bundle_skills_ref comment above; same reasoning, MCP side.
895        -- No longer written or read: a bundle carries no MCP servers, and
896        -- m0036_drop_bundle_mcp emptied it. Still created, because an older
897        -- build sharing the channel expects it.
898        CREATE TABLE IF NOT EXISTS db_bundle_mcp_ref (
899            bundle_id TEXT NOT NULL,
900            mcp_id    TEXT NOT NULL,
901            PRIMARY KEY (bundle_id, mcp_id)
902        );
903
904        -- v7: MuxBus cloud connectivity — global singleton PKCE token store.
905        CREATE TABLE IF NOT EXISTS db_muxbus_credentials (
906            id             TEXT PRIMARY KEY DEFAULT 'global',
907            cognito_domain TEXT NOT NULL DEFAULT '',
908            client_id      TEXT NOT NULL DEFAULT '',
909            access_token   TEXT NOT NULL DEFAULT '',
910            refresh_token  TEXT NOT NULL DEFAULT '',
911            id_token       TEXT NOT NULL DEFAULT '',
912            expires_at     INTEGER NOT NULL DEFAULT 0,
913            user_email     TEXT NOT NULL DEFAULT '',
914            user_sub       TEXT NOT NULL DEFAULT ''
915        );
916
917        -- v13: per-agent M2M muxbus credential — see db_agent_credentials in
918        -- run_shared_store_schema's doc comment for the full rationale.
919        CREATE TABLE IF NOT EXISTS db_agent_credentials (
920            agent_id       TEXT PRIMARY KEY,
921            client_id      TEXT NOT NULL DEFAULT '',
922            client_secret  TEXT NOT NULL DEFAULT '',
923            token_endpoint TEXT NOT NULL DEFAULT '',
924            access_token   TEXT NOT NULL DEFAULT '',
925            expires_at     INTEGER NOT NULL DEFAULT 0,
926            created_at     INTEGER NOT NULL DEFAULT 0
927        );
928
929        -- v14: per-agent native-memory durable mirror — see
930        -- db_agent_native_memory in run_shared_store_schema's doc comment for
931        -- the full rationale (SPEC_NATIVE_MEMORY_DURABLE_SYNC_2026_08_07.md).
932        -- No FK to db_agent_definitions: this table is duplicated into the
933        -- shared store below (same pattern as db_agent_credentials, for
934        -- id_store's per-channel fallback before 0011_shared_store_backfill),
935        -- and SQLite can't enforce a FK across separate database files.
936        -- Orphan rows after an agent's deleted are inert: every RPC handler
937        -- that would read this table first 404s on agent_def_get, so a
938        -- dangling mirror row is never reached, let alone surfaced.
939        CREATE TABLE IF NOT EXISTS db_agent_native_memory (
940            agent_id           TEXT NOT NULL,
941            filename           TEXT NOT NULL,
942            content            TEXT NOT NULL,
943            metadata_type      TEXT NOT NULL DEFAULT '',
944            size_bytes         INTEGER NOT NULL DEFAULT 0,
945            updated_at         INTEGER NOT NULL DEFAULT 0,
946            last_seen_path     TEXT NOT NULL DEFAULT '',
947            last_seen_mtime_ms INTEGER NOT NULL DEFAULT 0,
948            PRIMARY KEY (agent_id, filename)
949        );
950
951        -- v33: what each agent was last observed to READ as project
952        -- instructions. Hash and timestamp only, never content — these files
953        -- belong to the repository and are always readable from it, unlike
954        -- db_agent_native_memory above, which mirrors content precisely
955        -- because the originals can become unreachable. See
956        -- SPEC_INSTRUCTION_AND_MEMORY_PORTABILITY_2026_09_09.md Phase 3.
957        CREATE TABLE IF NOT EXISTS db_agent_project_instructions (
958            agent_id      TEXT NOT NULL,
959            path          TEXT NOT NULL,
960            content_hash  TEXT NOT NULL DEFAULT '',
961            size_bytes    INTEGER NOT NULL DEFAULT 0,
962            owner         TEXT NOT NULL DEFAULT 'foreign',
963            existed       INTEGER NOT NULL DEFAULT 0,
964            observed_at   INTEGER NOT NULL DEFAULT 0,
965            PRIMARY KEY (agent_id, path)
966        );
967
968        -- v24: append-only version history for native memory content — see
969        -- db_agent_native_memory_versions in run_shared_store_schema's doc
970        -- comment for the full rationale
971        -- (SPEC_MEMORY_VERSION_CONTROL_AND_ARMORY_AUDIT_2026_08_19.md).
972        -- Duplicated here for the same id_store-per-channel-fallback reason
973        -- as db_agent_native_memory itself, immediately above.
974        CREATE TABLE IF NOT EXISTS db_agent_native_memory_versions (
975            id                 TEXT PRIMARY KEY,
976            agent_id           TEXT NOT NULL,
977            filename           TEXT NOT NULL,
978            content            TEXT NOT NULL,
979            content_hash       TEXT NOT NULL,
980            parent_version_id  TEXT,
981            source             TEXT NOT NULL DEFAULT 'agent_inferred',
982            source_detail      TEXT NOT NULL DEFAULT '{}',
983            session_id         TEXT NOT NULL DEFAULT '',
984            created_at         INTEGER NOT NULL DEFAULT 0
985        );
986        CREATE INDEX IF NOT EXISTS idx_native_memory_versions_lookup
987            ON db_agent_native_memory_versions(agent_id, filename, created_at);
988
989        -- v35: append-only version history for Global Memory entry content
990        -- — see OBJECT_SCHEMA_VERSION's own doc comment above for the full
991        -- rationale (SPEC_AGENT_FACING_GLOBAL_MEMORY_API_2026_09_15.md).
992        -- Single `bundle_id` key, not a composite one — a Bundle's identity
993        -- is already a single id, unlike native memory's (agent_id, filename).
994        CREATE TABLE IF NOT EXISTS db_bundle_versions (
995            id                 TEXT PRIMARY KEY,
996            bundle_id          TEXT NOT NULL,
997            name               TEXT NOT NULL,
998            instructions       TEXT NOT NULL,
999            content_hash       TEXT NOT NULL,
1000            parent_version_id  TEXT,
1001            source             TEXT NOT NULL DEFAULT 'agent_inferred',
1002            source_detail      TEXT NOT NULL DEFAULT '{}',
1003            written_by         TEXT NOT NULL DEFAULT '',
1004            created_at         INTEGER NOT NULL DEFAULT 0,
1005            -- v39 (identity M4c-1): the writer's UID from the request's
1006            -- Caller, beside the written_by name. Empty when unattributed.
1007            written_by_uid     TEXT NOT NULL DEFAULT ''
1008        );
1009        CREATE INDEX IF NOT EXISTS idx_bundle_versions_lookup
1010            ON db_bundle_versions(bundle_id, created_at);
1011
1012        -- v18: per-agent HMAC-SHA256 signing key for host-tier jekt sender
1013        -- verification (SPEC_JEKT_TRUST_LAYER_COMPLETION_2026_08_13.md §2.2).
1014        -- hmac_key is base64-encoded, 32 random bytes, minted on first use
1015        -- (agent_jekt_key_ensure). As of
1016        -- SPEC_JEKT_HOST_KEY_TTL_ROTATION_2026_09_14.md, a key past
1017        -- JEKT_KEY_TTL_SECS (24h) old is rotated the next time
1018        -- agent_jekt_key_ensure runs for it (that agent's next spawn) — not
1019        -- invalidated in place, so no expires_at column is needed; TTL is
1020        -- computed from created_at at read time. Local to this instance's
1021        -- data dir only — not synced anywhere, not the same secret as any
1022        -- Armory/GitHub credential.
1023        CREATE TABLE IF NOT EXISTS db_agent_jekt_keys (
1024            agent_id   TEXT PRIMARY KEY,
1025            hmac_key   TEXT NOT NULL,
1026            created_at INTEGER NOT NULL DEFAULT 0
1027        );
1028
1029        -- v20: per-agent Ed25519 keypair for LAN-tier jekt sender
1030        -- verification (SPEC_JEKT_LAN_TIER_SIGNING_2026_08_15.md §2.1).
1031        -- public_key is not secret (distributed to LAN peers on demand);
1032        -- private_key is base64, 32-byte seed, minted on first use
1033        -- (agent_lan_key_ensure) and never rotated automatically — and,
1034        -- unlike db_agent_jekt_keys, deliberately NOT given a TTL by
1035        -- SPEC_JEKT_HOST_KEY_TTL_ROTATION_2026_09_14.md §4: rotating a LAN
1036        -- key would desync it from any peer's permanent
1037        -- db_lan_peer_pubkey_pins trust-on-first-use pin of the OLD key,
1038        -- turning every future signature into an unconditional forced-
1039        -- sensitive 'mismatch' for that peer with no re-pin mechanism to
1040        -- recover. Same local-to-this-instance-only guarantee as
1041        -- db_agent_jekt_keys otherwise.
1042        CREATE TABLE IF NOT EXISTS db_agent_lan_keys (
1043            agent_id    TEXT PRIMARY KEY,
1044            public_key  TEXT NOT NULL,
1045            private_key TEXT NOT NULL,
1046            created_at  INTEGER NOT NULL DEFAULT 0
1047        );
1048
1049        -- v36: per-agent Ed25519 keypair for general agent-to-agent WAN-tier
1050        -- jekt signing.
1051        -- Same local-to-this-instance guarantee as db_agent_jekt_keys and
1052        -- db_agent_lan_keys: the private half leaves only by being injected
1053        -- into that ONE agent's own MCP process env (AGENTMUX_WAN_KEY) at
1054        -- spawn, never over any RPC. The public half is not secret — it is
1055        -- what gets published to muxbus's agent-ownership registry so a
1056        -- WAN counterpart can verify this agent's signatures.
1057        --
1058        -- Deliberately NOT a reuse of db_agent_lan_keys: see the v36 doc
1059        -- comment above for why the two tiers' keys must have independent
1060        -- lifecycles. `key_version` starts at 1 and is reserved for the
1061        -- version-synchronised publish (§3.2) and rotation (§3.6) phases;
1062        -- nothing reads it yet.
1063        CREATE TABLE IF NOT EXISTS db_agent_wan_keys (
1064            agent_id    TEXT PRIMARY KEY,
1065            public_key  TEXT NOT NULL,
1066            private_key TEXT NOT NULL,
1067            key_version INTEGER NOT NULL DEFAULT 1,
1068            created_at  INTEGER NOT NULL DEFAULT 0
1069        );
1070
1071        -- v37: per-agent local identity token, keyed by the agent's UID
1072        -- (db_agents.id) rather than by display name -- identity M1,
1073        -- SPEC_AGENT_IDENTITY_CARRIED_NOT_DERIVED_2026_09_23.md §6/§9.1.
1074        -- Minted on first spawn, injected as AGENTMUX_AGENT_TOKEN into that
1075        -- one agent's process, revoked by purge_agent_dependents on delete.
1076        -- Not read by anything until M4; see storage/agent_tokens.rs.
1077        -- No FOREIGN KEY on purpose: a token must be mintable in the same
1078        -- critical section as the row it belongs to without ordering the
1079        -- two inserts, and purge_agent_dependents already owns cleanup.
1080        CREATE TABLE IF NOT EXISTS db_agent_tokens (
1081            agent_id   TEXT PRIMARY KEY,
1082            token      TEXT NOT NULL,
1083            created_at INTEGER NOT NULL DEFAULT 0
1084        );
1085
1086        -- v38: names of deleted agents, lowercased -- identity M4a,
1087        -- SPEC_AGENT_IDENTITY_CARRIED_NOT_DERIVED_2026_09_23.md §6.5.4. A
1088        -- name-keyed signing key under a tombstoned name is never carried to
1089        -- a new agent's UID (M4d); the reuser gets a fresh keypair. Keyed by
1090        -- name because that is what the legacy key tables are keyed by.
1091        CREATE TABLE IF NOT EXISTS db_agent_key_tombstones (
1092            name        TEXT PRIMARY KEY,
1093            deleted_uid TEXT NOT NULL,
1094            deleted_at  INTEGER NOT NULL DEFAULT 0
1095        );
1096
1097        -- Identity M4d-2: each agent's LAN and WAN signing keypairs filed
1098        -- under its UID (db_agents.id), copied from the name-keyed tables on
1099        -- ownership evidence or minted fresh (storage/agent_uid_keys.rs,
1100        -- SPEC_AGENT_IDENTITY_CARRIED_NOT_DERIVED_2026_09_23.md §6.5.10).
1101        -- copied_from is the name a key was copied from, '' when minted.
1102        -- Purged by UID in purge_agent_dependents. Additive, applied on
1103        -- every open, so no version bump.
1104        CREATE TABLE IF NOT EXISTS db_agent_lan_keys_by_uid (
1105            uid         TEXT PRIMARY KEY,
1106            public_key  TEXT NOT NULL,
1107            private_key TEXT NOT NULL,
1108            created_at  INTEGER NOT NULL DEFAULT 0,
1109            copied_from TEXT NOT NULL DEFAULT ''
1110        );
1111        CREATE TABLE IF NOT EXISTS db_agent_wan_keys_by_uid (
1112            uid         TEXT PRIMARY KEY,
1113            public_key  TEXT NOT NULL,
1114            private_key TEXT NOT NULL,
1115            created_at  INTEGER NOT NULL DEFAULT 0,
1116            copied_from TEXT NOT NULL DEFAULT ''
1117        );
1118        -- Identity M4d-6: a remote agent's UID key, pinned per (name, uid) on
1119        -- first sight beside a matching name pin (storage/lan_peer_uid_pins.rs).
1120        -- Additive, applied on every open, so no version bump.
1121        CREATE TABLE IF NOT EXISTS db_lan_peer_uid_pins (
1122            agent_id      TEXT NOT NULL,
1123            uid           TEXT NOT NULL,
1124            public_key    TEXT NOT NULL,
1125            first_seen_at INTEGER NOT NULL DEFAULT 0,
1126            PRIMARY KEY (agent_id, uid)
1127        );
1128
1129        -- Identity M4d-1: every display / instance name an agent has had.
1130        -- Keys are filed under names (and their fallback ids), and every
1131        -- write path that renames an agent — rename, the continuation fold,
1132        -- anything later — leaves keys under the old name, so the purge must
1133        -- still find them. Recorded by trigger so no write path can miss it;
1134        -- never capped; deleted with the row. Additive, applied on every
1135        -- open, so no version bump.
1136        CREATE TABLE IF NOT EXISTS db_agent_former_names (
1137            agent_id    TEXT NOT NULL,
1138            name        TEXT NOT NULL,
1139            recorded_at INTEGER NOT NULL DEFAULT 0,
1140            PRIMARY KEY (agent_id, name),
1141            FOREIGN KEY (agent_id) REFERENCES db_agents(id) ON DELETE CASCADE
1142        );
1143        CREATE TRIGGER IF NOT EXISTS trg_agents_former_name
1144            AFTER UPDATE OF name ON db_agents
1145            WHEN old.is_template = 0 AND old.name <> new.name AND trim(old.name) <> ''
1146        BEGIN
1147            INSERT OR IGNORE INTO db_agent_former_names (agent_id, name, recorded_at)
1148            VALUES (old.id, old.name, CAST(strftime('%s', 'now') AS INTEGER) * 1000);
1149        END;
1150        CREATE TRIGGER IF NOT EXISTS trg_agents_former_instance_name
1151            AFTER UPDATE OF instance_name ON db_agents
1152            WHEN old.is_template = 0
1153             AND COALESCE(old.instance_name, '') <> COALESCE(new.instance_name, '')
1154             AND trim(COALESCE(old.instance_name, '')) <> ''
1155        BEGIN
1156            INSERT OR IGNORE INTO db_agent_former_names (agent_id, name, recorded_at)
1157            VALUES (old.id, old.instance_name, CAST(strftime('%s', 'now') AS INTEGER) * 1000);
1158        END;
1159
1160        -- v40: jekts held for an absent agent of this channel
1161        -- (SPEC_DURABLE_JEKT_DELIVERY_2026_09_24.md). Verdict columns are
1162        -- NULL (unchecked), 0 (failed) or 1 (verified).
1163        CREATE TABLE IF NOT EXISTS db_jekt_held (
1164            request_id       TEXT PRIMARY KEY,
1165            target_uid       TEXT NOT NULL,
1166            target_agent     TEXT NOT NULL DEFAULT '',
1167            source_agent     TEXT NOT NULL DEFAULT '',
1168            audit_source_uid TEXT NOT NULL DEFAULT '',
1169            message          TEXT NOT NULL,
1170            priority         TEXT NOT NULL DEFAULT '',
1171            jekt_tier        TEXT NOT NULL DEFAULT '',
1172            delivery_tier    TEXT NOT NULL DEFAULT '',
1173            sig_verified     INTEGER,
1174            reagent_verified INTEGER,
1175            lan_verified     INTEGER,
1176            channel_verified INTEGER,
1177            is_transcript_request              INTEGER NOT NULL DEFAULT 0,
1178            transcript_request_escalate_forced INTEGER NOT NULL DEFAULT 0,
1179            sent_at_ms       INTEGER NOT NULL,
1180            expires_at_ms    INTEGER NOT NULL,
1181            attempts         INTEGER NOT NULL DEFAULT 0,
1182            last_error       TEXT NOT NULL DEFAULT ''
1183        );
1184        CREATE INDEX IF NOT EXISTS idx_jekt_held_target ON db_jekt_held (target_uid, sent_at_ms);
1185
1186        -- v43: devices paired with the viewer listener. See
1187        -- OBJECT_SCHEMA_VERSION's v43 doc comment above.
1188        CREATE TABLE IF NOT EXISTS db_viewer_devices (
1189            device_id    TEXT PRIMARY KEY,
1190            token_hash   TEXT NOT NULL UNIQUE,
1191            device_name  TEXT NOT NULL,
1192            device_key   TEXT NOT NULL DEFAULT '',
1193            created_ms   INTEGER NOT NULL,
1194            last_seen_ms INTEGER NOT NULL DEFAULT 0
1195        );
1196
1197        -- The bundles picked for an agent, in order; its own bundle
1198        -- (db_agents.default_memory_id) is not listed here
1199        -- (SPEC_RENAME_KNOWLEDGE_TO_MEMORY_2026_10_06.md §3.6). No FK: a
1200        -- bundle lives in the identity store, and an agent's rows go with
1201        -- it in purge_agent_dependents. Additive, applied on every open,
1202        -- so no version bump.
1203        CREATE TABLE IF NOT EXISTS db_agent_bundles (
1204            agent_id  TEXT NOT NULL,
1205            bundle_id TEXT NOT NULL,
1206            position  INTEGER NOT NULL,
1207            PRIMARY KEY (agent_id, bundle_id)
1208        );
1209
1210        -- A widget package's `storage` (SPEC_USER_WIDGETS_AND_WIDGET_API_2026_10_09.md §6.3), JSON values; additive, no version bump.
1211        CREATE TABLE IF NOT EXISTS db_widget_storage (widget_id TEXT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, PRIMARY KEY (widget_id, key));
1212
1213        -- v21: trust-on-first-use pin of a remote agent_id's LAN public key
1214        -- (SPEC_JEKT_LAN_TIER_SIGNING_2026_08_15.md §2.2, reagentx P0).
1215        -- Distinct from db_agent_lan_keys (this instance's OWN agents'
1216        -- keypairs, private half included) — this table holds only public
1217        -- keys OBSERVED from LAN peers for agent_ids this instance does not
1218        -- itself host, pinned on first sight so a later, different key
1219        -- claiming the same agent_id is treated as a mismatch rather than
1220        -- silently trusted.
1221        CREATE TABLE IF NOT EXISTS db_lan_peer_pubkey_pins (
1222            agent_id     TEXT PRIMARY KEY,
1223            public_key   TEXT NOT NULL,
1224            first_seen_at INTEGER NOT NULL DEFAULT 0
1225        );
1226
1227        -- v22: durable declared-long-running-task registry — see
1228        -- OBJECT_SCHEMA_VERSION's v22 doc comment above. `id` mirrors the
1229        -- frontend's dock node_id (usually the originating tool_use_id) so
1230        -- rows join cleanly with DockSnapshotCache/ActivityDock data.
1231        -- `status` is one of 'running' | 'done' | 'error' | 'stopped',
1232        -- matching the vocabulary tool-adapter.ts already uses for a
1233        -- background launch's terminal states.
1234        CREATE TABLE IF NOT EXISTS db_background_tasks (
1235            id            TEXT PRIMARY KEY,
1236            block_id      TEXT NOT NULL,
1237            label         TEXT NOT NULL,
1238            pid           INTEGER,
1239            started_at_ms INTEGER NOT NULL,
1240            status        TEXT NOT NULL DEFAULT 'running',
1241            last_seen_ms  INTEGER NOT NULL,
1242            ended_at_ms   INTEGER
1243        );
1244        CREATE INDEX IF NOT EXISTS idx_background_tasks_block
1245            ON db_background_tasks(block_id);
1246        CREATE INDEX IF NOT EXISTS idx_background_tasks_status
1247            ON db_background_tasks(status);
1248
1249        -- v25: saved fleet-control target groups — see OBJECT_SCHEMA_VERSION's
1250        -- v25 doc comment above. member_ids is a JSON array of block ids
1251        -- (Swarm's native per-agent selection unit); resolved to concrete
1252        -- ids by the frontend before either fleet.broadcast or
1253        -- fleet.bulk-stop executes, so a group's membership can't drift
1254        -- between when a bulk action is confirmed and when it runs.
1255        CREATE TABLE IF NOT EXISTS db_agent_groups (
1256            id         TEXT PRIMARY KEY,
1257            name       TEXT NOT NULL,
1258            member_ids TEXT NOT NULL DEFAULT '[]',
1259            created_at INTEGER NOT NULL DEFAULT 0
1260        );
1261
1262        -- v26: allowlist for a responding agent's 'trusted_peers'
1263        -- conversation_visibility mode — see OBJECT_SCHEMA_VERSION's v26
1264        -- doc comment above. Mirrors db_lan_peer_pubkey_pins's exact shape
1265        -- (own module, get/set-style methods, case-insensitive agent_id
1266        -- lookups). `agent_id` is the RESPONDING agent (whose transcript
1267        -- may be disclosed); `granted_peer_agent_id` is the requester it
1268        -- trusts; `tier` records which delivery tier the grant applies to
1269        -- ('lan'/'wan') since a grant for one tier's cryptographic
1270        -- identity guarantee should not be silently assumed to hold for a
1271        -- weaker one.
1272        CREATE TABLE IF NOT EXISTS db_conversation_trust_grants (
1273            agent_id             TEXT NOT NULL,
1274            granted_peer_agent_id TEXT NOT NULL,
1275            tier                 TEXT NOT NULL,
1276            granted_at           INTEGER NOT NULL DEFAULT 0,
1277            PRIMARY KEY (agent_id, granted_peer_agent_id, tier)
1278        );
1279
1280        -- v28: one-shot, on-demand activity summary per definition — see
1281        -- OBJECT_SCHEMA_VERSION's v28 doc comment above. `summary` is empty
1282        -- only transiently (a row is only ever inserted once generation
1283        -- succeeds); there is no 'pending' state stored here — an absent
1284        -- row IS the pending state.
1285        CREATE TABLE IF NOT EXISTS db_agent_activity_summaries (
1286            definition_id TEXT PRIMARY KEY,
1287            summary       TEXT NOT NULL DEFAULT '',
1288            updated_at    INTEGER NOT NULL DEFAULT 0
1289        );",
1290    )?;
1291
1292    // ---- Additive column migrations (schema v2+) ----
1293    // The flat CREATE batch above covers fresh databases. These ALTERs
1294    // carry an existing database (e.g. a developer's dev DB that persists
1295    // across builds) forward. Idempotent: the "duplicate column" error is
1296    // swallowed. New additive columns append here + bump OBJECT_SCHEMA_VERSION.
1297    //
1298    // db_agent_definitions' own v2/v3+ ALTERs (updated_at, user_hidden,
1299    // container_image/volumes/name, use_ambient_login, model_vendor_base_url,
1300    // auto_continue_enabled, memory_id, conversation_visibility) are run
1301    // separately below, conditionally on the table still existing — see that
1302    // block's own comment for why they can't simply be deleted even though
1303    // the table itself is dropped as of v32 (OBJECT_SCHEMA_VERSION's v32 doc
1304    // comment).
1305    // v5: db_agents.last_block_id — most-recent launch's block (Phase 3c).
1306    //     Defaults to '' for existing rows; the dual-write populates it on
1307    //     the next launch/continuation. Read side (3b.1b) treats '' as
1308    //     "no snapshot" (same as the current empty-block_id fallback).
1309    // v6: Container support (Phase 0 of SPEC_CONTAINER_PANE_SUPPORT_2026_06_11.md).
1310    //     container_image / container_volumes / container_name on both tables.
1311    //     Host-agent rows default to '', '[]', '' respectively — ContainerManager
1312    //     populates them on first container spawn.
1313    // v7: create db_muxbus_credentials if it doesn't exist yet (existing DBs
1314    //     won't have it since it's a new table, not an added column).
1315    // v10: standalone skill + MCP server tables (new tables, handled via
1316    //     CREATE TABLE IF NOT EXISTS in the flat batch above; no ALTER needed).
1317    conn.execute_batch(
1318        "CREATE TABLE IF NOT EXISTS db_muxbus_credentials (
1319            id             TEXT PRIMARY KEY DEFAULT 'global',
1320            cognito_domain TEXT NOT NULL DEFAULT '',
1321            client_id      TEXT NOT NULL DEFAULT '',
1322            access_token   TEXT NOT NULL DEFAULT '',
1323            refresh_token  TEXT NOT NULL DEFAULT '',
1324            id_token       TEXT NOT NULL DEFAULT '',
1325            expires_at     INTEGER NOT NULL DEFAULT 0,
1326            user_email     TEXT NOT NULL DEFAULT '',
1327            user_sub       TEXT NOT NULL DEFAULT ''
1328        )",
1329    )?;
1330
1331    // db_agent_definitions' own additive columns, conditional on the table
1332    // still existing. Removing these outright (rather than gating them) broke
1333    // a real upgrade path: several pending migrations (m0007, m0025, m0026,
1334    // m0028) call Store::open() internally as part of their own up() logic —
1335    // which runs THIS function, under the CURRENT binary's code — before
1336    // m0029 (numbered last) ever drops the table in the same pass. A store
1337    // old enough to still be missing one of these columns (e.g. a genuine
1338    // pre-v2 install jumping straight to the latest binary) would hit a live
1339    // "no such column" error from m0007's consolidate pass or m0025/m0028's
1340    // frozen_repair_def_gaps/project_template_launch_row — both of which
1341    // SELECT several of these columns from db_agent_definitions — well
1342    // before m0029 ever runs. Gating on table existence keeps this a no-op
1343    // once the table is gone (fresh v32+ installs never create it at all;
1344    // upgraded stores lose it only after m0029 runs later in the same pass)
1345    // without reintroducing an unconditional dependency on a table this
1346    // release otherwise no longer declares. Found in review of PR #3096
1347    // (Codex P1).
1348    let legacy_defs_exist: bool = conn.query_row(
1349        "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='db_agent_definitions'",
1350        [],
1351        |row| row.get::<_, i64>(0),
1352    )? == 1;
1353    if legacy_defs_exist {
1354        for stmt in &[
1355            "ALTER TABLE db_agent_definitions ADD COLUMN updated_at INTEGER NOT NULL DEFAULT 0",
1356            "ALTER TABLE db_agent_definitions ADD COLUMN user_hidden INTEGER NOT NULL DEFAULT 0",
1357            "ALTER TABLE db_agent_definitions ADD COLUMN container_image TEXT NOT NULL DEFAULT ''",
1358            "ALTER TABLE db_agent_definitions ADD COLUMN container_volumes TEXT NOT NULL DEFAULT '[]'",
1359            "ALTER TABLE db_agent_definitions ADD COLUMN container_name TEXT NOT NULL DEFAULT ''",
1360            "ALTER TABLE db_agent_definitions ADD COLUMN use_ambient_login INTEGER NOT NULL DEFAULT 0",
1361            "ALTER TABLE db_agent_definitions ADD COLUMN model_vendor_base_url TEXT NOT NULL DEFAULT ''",
1362            "ALTER TABLE db_agent_definitions ADD COLUMN auto_continue_enabled INTEGER NOT NULL DEFAULT 0",
1363            "ALTER TABLE db_agent_definitions ADD COLUMN memory_id TEXT NOT NULL DEFAULT ''",
1364            "ALTER TABLE db_agent_definitions ADD COLUMN conversation_visibility TEXT NOT NULL DEFAULT 'private'",
1365        ] {
1366            if let Err(e) = conn.execute_batch(stmt) {
1367                let msg = e.to_string();
1368                if !msg.contains("duplicate column") {
1369                    return Err(e.into());
1370                }
1371            }
1372        }
1373    }
1374
1375    for stmt in &[
1376        "ALTER TABLE db_agents ADD COLUMN last_block_id TEXT NOT NULL DEFAULT ''",
1377        "ALTER TABLE db_agents ADD COLUMN container_image TEXT NOT NULL DEFAULT ''",
1378        "ALTER TABLE db_agents ADD COLUMN container_volumes TEXT NOT NULL DEFAULT '[]'",
1379        "ALTER TABLE db_agents ADD COLUMN container_name TEXT NOT NULL DEFAULT ''",
1380        // NOTE: targets db_bundles, not db_memory_bundles — adopt_legacy_table_names
1381        // (called at the top of this function) has already renamed the table
1382        // by the time this runs, on every DB that goes through the Phase 4
1383        // rename. See SPEC_ARMORY_PHASE4_STORAGE_RENAME_COMPLETION_2026_07_12.md.
1384        "ALTER TABLE db_bundles ADD COLUMN is_global INTEGER NOT NULL DEFAULT 0",
1385        "ALTER TABLE db_bundles ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0",
1386        // v12: explicit per-agent ambient-login opt-in (fail-by-default spawn
1387        // gating — SPEC_ACCOUNT_DELETE_DEAUTH_LAYERS_2_4_2026_07_14.md §2.3).
1388        "ALTER TABLE db_agents ADD COLUMN use_ambient_login INTEGER NOT NULL DEFAULT 0",
1389        // v15: model vendor override — redirects a harness at a non-default
1390        // backend (e.g. ANTHROPIC_BASE_URL). Formalizes harness vs. model
1391        // vendor as distinct concepts; see ProviderConfig::base_url_env_var.
1392        "ALTER TABLE db_agents ADD COLUMN model_vendor_base_url TEXT NOT NULL DEFAULT ''",
1393        // v16: provider-scoped bundle instructions (ABF v0.2 §2.2) — JSON
1394        // object of {provider_id: content} variants, additive to the
1395        // existing flat `instructions` column.
1396        "ALTER TABLE db_bundles ADD COLUMN instructions_by_provider TEXT NOT NULL DEFAULT '{}'",
1397        // v17: Warden Supervisor auto-continue opt-in (fail-by-default,
1398        // same posture as use_ambient_login) — see
1399        // ANALYSIS_WARDEN_AUTO_CONTROLLER_CONTINUATION_WATCHER_2026_08_12.md.
1400        "ALTER TABLE db_agents ADD COLUMN auto_continue_enabled INTEGER NOT NULL DEFAULT 0",
1401        // v19: the agent's own dedicated ABF bundle, set once at creation.
1402        // db_agents gets a SEPARATE default_memory_id column (not the
1403        // existing instance-scoped memory_id) — see its own CREATE TABLE
1404        // comment for why, and OBJECT_SCHEMA_VERSION's v19 entry.
1405        "ALTER TABLE db_agents ADD COLUMN default_memory_id TEXT NOT NULL DEFAULT ''",
1406        // v26: muxspect Phase B/C per-agent conversation-disclosure policy —
1407        // see OBJECT_SCHEMA_VERSION's v26 doc comment above. Dual-written
1408        // to db_agents under the SAME column name (a simple opt-in-style
1409        // setting, like auto_continue_enabled above — not a differently-
1410        // named-on-db_agents case like memory_id/default_memory_id).
1411        "ALTER TABLE db_agents ADD COLUMN conversation_visibility TEXT NOT NULL DEFAULT 'private'",
1412        // v27: AgentMux-controlled, highest-priority Global Bundle tier —
1413        // see OBJECT_SCHEMA_VERSION's v27 doc comment above.
1414        "ALTER TABLE db_bundles ADD COLUMN is_system INTEGER NOT NULL DEFAULT 0",
1415        // v29: the agent's latest launch state on db_agents — the four
1416        // db_agent_instances columns the consolidated row still lacked. See
1417        // OBJECT_SCHEMA_VERSION's v29 doc comment above and the column doc in
1418        // db_agents' CREATE TABLE. Backfilled from db_agent_instances by
1419        // m0025_agents_launch_state_backfill; kept current by the instance
1420        // dual-write until the legacy table is dropped.
1421        "ALTER TABLE db_agents ADD COLUMN session_id TEXT NOT NULL DEFAULT ''",
1422        "ALTER TABLE db_agents ADD COLUMN status TEXT NOT NULL DEFAULT ''",
1423        "ALTER TABLE db_agents ADD COLUMN started_at INTEGER NOT NULL DEFAULT 0",
1424        "ALTER TABLE db_agents ADD COLUMN ended_at INTEGER NOT NULL DEFAULT 0",
1425        // v39: identity M4c-1 — the Global Memory writer's UID beside its
1426        // name. See OBJECT_SCHEMA_VERSION's v39 doc comment above.
1427        "ALTER TABLE db_bundle_versions ADD COLUMN written_by_uid TEXT NOT NULL DEFAULT ''",
1428        // v41: the subagent that owns a background task. See
1429        // OBJECT_SCHEMA_VERSION's v41 doc comment above.
1430        "ALTER TABLE db_background_tasks ADD COLUMN owner_tool_use_id TEXT",
1431        // v42: the Runtime menu choices the user last made for the agent. See
1432        // OBJECT_SCHEMA_VERSION's v42 doc comment above.
1433        "ALTER TABLE db_agents ADD COLUMN last_runtime TEXT NOT NULL DEFAULT ''",
1434        // v43: an agent hidden from paired devices. See
1435        // OBJECT_SCHEMA_VERSION's v43 doc comment above.
1436        "ALTER TABLE db_agents ADD COLUMN hide_from_devices INTEGER NOT NULL DEFAULT 0",
1437    ] {
1438        if let Err(e) = conn.execute_batch(stmt) {
1439            let msg = e.to_string();
1440            if !msg.contains("duplicate column") {
1441                return Err(e.into());
1442            }
1443        }
1444    }
1445
1446    // ---- Seed blank Bundle singleton ----
1447    // The launch UI renders this as the default option in its Bundle
1448    // dropdown. Fixed id so tests + dev seed data can hard-code
1449    // references.
1450    conn.execute_batch(
1451        "INSERT OR IGNORE INTO db_bundles
1452            (id, name, description, is_blank, created_at, updated_at)
1453         VALUES ('blank', '__blank__', 'Vanilla CLI — no instructions, no context', 1, 0, 0);",
1454    )?;
1455
1456    // Channel-scoped migration tracking (parallel to the global db_migrations
1457    // in store.db). MigrationScope::Channel migrations record completion here
1458    // so each channel tracks its own migration state independently.
1459    conn.execute_batch(
1460        "CREATE TABLE IF NOT EXISTS db_migrations (
1461            id          TEXT PRIMARY KEY,
1462            applied_at  TEXT NOT NULL,
1463            duration_ms INTEGER NOT NULL DEFAULT 0,
1464            scope       TEXT NOT NULL DEFAULT 'channel'
1465        );",
1466    )?;
1467
1468    Ok(())
1469}
1470
1471/// Rename any pre-flatten `objects.db` tables to their de-forged names and
1472/// drop the dead workflow/sentinel tables. Idempotent — on a fresh or
1473/// already-flat database every check is a no-op.
1474///
1475/// This is the single surviving fragment of the old v1–v11 chain: it
1476/// exists only to carry a developer's pre-flatten `objects.db` (always at
1477/// the post-v11 schema, since v11 is merged) forward without data loss.
1478/// SQLite ≥ 3.25 auto-updates foreign-key references in child tables when
1479/// a parent table is renamed, so the agent/identity cascades survive.
1480fn adopt_legacy_table_names(conn: &Connection) -> Result<(), StoreError> {
1481    for (legacy, current) in LEGACY_TABLE_RENAMES {
1482        let legacy_exists: i64 = conn.query_row(
1483            "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?1",
1484            [legacy],
1485            |row| row.get(0),
1486        )?;
1487        if legacy_exists == 0 {
1488            continue;
1489        }
1490        let current_exists: i64 = conn.query_row(
1491            "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?1",
1492            [current],
1493            |row| row.get(0),
1494        )?;
1495        if current_exists == 1 {
1496            // Both present — only reachable on a deliberate
1497            // downgrade-roundtrip dev DB (flat build → pre-flatten build,
1498            // which re-creates the legacy name → flat build again). The
1499            // flatten abandons the downgrade path, but the legacy table
1500            // may hold rows the downgraded build wrote. Do NOT drop it —
1501            // that would be silent data loss (the bug class behind PR
1502            // #933's Codex P1). Leave it on disk and warn loudly so the
1503            // developer can recover or delete it manually.
1504            warn!(
1505                legacy_table = *legacy,
1506                current_table = *current,
1507                "objects.db has both a legacy table and its de-forged \
1508                 replacement — this only happens after a downgrade to a \
1509                 pre-flatten build; the legacy table is left untouched \
1510                 for manual recovery and is otherwise unused",
1511            );
1512        } else {
1513            conn.execute_batch(&format!("ALTER TABLE {legacy} RENAME TO {current};"))?;
1514        }
1515    }
1516
1517    // Drop indexes orphaned by the renames — the flat DDL recreates them
1518    // under the new names.
1519    for idx in LEGACY_INDEX_DROPS {
1520        conn.execute_batch(&format!("DROP INDEX IF EXISTS {idx};"))?;
1521    }
1522
1523    // Drop tables retained only for the abandoned downgrade path.
1524    for table in DEAD_TABLE_DROPS {
1525        conn.execute_batch(&format!("DROP TABLE IF EXISTS {table};"))?;
1526    }
1527
1528    Ok(())
1529}
1530
1531/// Initialize (or re-validate) the `~/.agentmux/shared/store.db` schema.
1532///
1533/// Contains only the durable user content that must survive across
1534/// channels/versions: identity accounts, agent→account links, memory
1535/// bundles, drone definitions, and muxbus credentials. Session state
1536/// (`db_block`, `db_tab`, etc.) and drone run history stay in the
1537/// per-channel `objects.db`.
1538///
1539/// `db_agent_identity_links` drops the FK to `db_agent_definitions` here
1540/// (cross-DB FK enforcement is impossible in SQLite); application code
1541/// enforces referential integrity instead.
1542///
1543/// Idempotent — safe to call on every startup.
1544pub fn run_shared_store_schema(conn: &Connection) -> Result<(), StoreError> {
1545    // Rename any legacy table names (including the Phase 4 accounts/bundles
1546    // rename — LEGACY_TABLE_RENAMES entries that don't apply to this
1547    // connection's tables are safely skipped) before the idempotent
1548    // CREATE TABLE IF NOT EXISTS batch below, same as run_object_schema.
1549    adopt_legacy_table_names(conn)?;
1550
1551    conn.execute_batch(
1552        "CREATE TABLE IF NOT EXISTS db_accounts (
1553            id           TEXT PRIMARY KEY,
1554            name         TEXT NOT NULL,
1555            provider     TEXT NOT NULL,
1556            kind         TEXT NOT NULL,
1557            display_name TEXT NOT NULL DEFAULT '',
1558            secret_ref   TEXT NOT NULL,
1559            context      TEXT NOT NULL DEFAULT '{}',
1560            status       TEXT NOT NULL DEFAULT 'unknown',
1561            created_at   INTEGER NOT NULL DEFAULT 0,
1562            updated_at   INTEGER NOT NULL DEFAULT 0
1563        );
1564        CREATE INDEX IF NOT EXISTS idx_ss_accounts_provider
1565            ON db_accounts(provider);
1566
1567        CREATE TABLE IF NOT EXISTS db_agent_identity_links (
1568            agent_id   TEXT NOT NULL,
1569            account_id TEXT NOT NULL,
1570            provider   TEXT NOT NULL,
1571            PRIMARY KEY (agent_id, provider),
1572            FOREIGN KEY (account_id) REFERENCES db_accounts(id) ON DELETE CASCADE
1573        );
1574        CREATE INDEX IF NOT EXISTS idx_ss_agent_identity_links_account
1575            ON db_agent_identity_links(account_id);
1576
1577        CREATE TABLE IF NOT EXISTS db_bundles (
1578            id            TEXT PRIMARY KEY,
1579            name          TEXT NOT NULL UNIQUE,
1580            description   TEXT NOT NULL DEFAULT '',
1581            is_blank      INTEGER NOT NULL DEFAULT 0,
1582            is_global     INTEGER NOT NULL DEFAULT 0,
1583            provider      TEXT NOT NULL DEFAULT '',
1584            model         TEXT NOT NULL DEFAULT '',
1585            instructions  TEXT NOT NULL DEFAULT '',
1586            instructions_by_provider TEXT NOT NULL DEFAULT '{}',
1587            context_files TEXT NOT NULL DEFAULT '[]',
1588            mcp_servers   TEXT NOT NULL DEFAULT '[]',
1589            skills        TEXT NOT NULL DEFAULT '[]',
1590            sort_order    INTEGER NOT NULL DEFAULT 0,
1591            created_at    INTEGER NOT NULL DEFAULT 0,
1592            updated_at    INTEGER NOT NULL DEFAULT 0,
1593            is_system     INTEGER NOT NULL DEFAULT 0
1594        );
1595        CREATE INDEX IF NOT EXISTS idx_ss_bundles_is_blank
1596            ON db_bundles(is_blank);
1597
1598        CREATE TABLE IF NOT EXISTS db_drone_definitions (
1599            id          TEXT PRIMARY KEY,
1600            name        TEXT NOT NULL,
1601            description TEXT NOT NULL DEFAULT '',
1602            graph       TEXT NOT NULL DEFAULT '{\"nodes\":[],\"edges\":[]}',
1603            viewport    TEXT NOT NULL DEFAULT '{\"x\":0,\"y\":0,\"zoom\":1}',
1604            created_at  INTEGER NOT NULL DEFAULT 0,
1605            updated_at  INTEGER NOT NULL DEFAULT 0
1606        );
1607        CREATE INDEX IF NOT EXISTS idx_ss_drone_definitions_updated
1608            ON db_drone_definitions(updated_at DESC);
1609
1610        CREATE TABLE IF NOT EXISTS db_muxbus_credentials (
1611            id             TEXT PRIMARY KEY DEFAULT 'global',
1612            cognito_domain TEXT NOT NULL DEFAULT '',
1613            client_id      TEXT NOT NULL DEFAULT '',
1614            access_token   TEXT NOT NULL DEFAULT '',
1615            refresh_token  TEXT NOT NULL DEFAULT '',
1616            id_token       TEXT NOT NULL DEFAULT '',
1617            expires_at     INTEGER NOT NULL DEFAULT 0,
1618            user_email     TEXT NOT NULL DEFAULT '',
1619            user_sub       TEXT NOT NULL DEFAULT ''
1620        );
1621
1622        CREATE TABLE IF NOT EXISTS db_migrations (
1623            id          TEXT PRIMARY KEY,
1624            applied_at  TEXT NOT NULL,
1625            duration_ms INTEGER NOT NULL DEFAULT 0,
1626            scope       TEXT NOT NULL DEFAULT 'global'
1627        );
1628
1629        CREATE TABLE IF NOT EXISTS db_cron_jobs (
1630            id            TEXT PRIMARY KEY,
1631            name          TEXT NOT NULL,
1632            expression    TEXT NOT NULL,
1633            prompt        TEXT NOT NULL,
1634            target        TEXT NOT NULL,
1635            created_by    TEXT NOT NULL DEFAULT '',
1636            enabled       INTEGER NOT NULL DEFAULT 1,
1637            last_fired    INTEGER,
1638            fire_count    INTEGER NOT NULL DEFAULT 0,
1639            max_fires     INTEGER,
1640            created_at    INTEGER NOT NULL DEFAULT 0,
1641            max_age_secs  INTEGER,
1642            -- v11 (identity M1b): the target's UID (db_agents.id) beside the
1643            -- slug, resolved ONCE at create time (spec §5.4), dual-written,
1644            -- not yet read -- a job still fires by `target`. Empty when the
1645            -- name did not resolve; never guessed.
1646            target_uid    TEXT NOT NULL DEFAULT '',
1647            -- v12 (identity M4c-1): the creator's UID from the request's
1648            -- Caller, beside the created_by name. Empty when unattributed.
1649            created_by_uid TEXT NOT NULL DEFAULT ''
1650        );
1651        CREATE INDEX IF NOT EXISTS idx_ss_cron_jobs_enabled
1652            ON db_cron_jobs(enabled);
1653
1654        -- v4: per-agent M2M credential for muxbus Tier-4 binding (see
1655        -- SHARED_STORE_SCHEMA_VERSION's doc comment above). client_secret is
1656        -- a Cognito app client secret, not a user password — same trust
1657        -- tier as MUXBUS_TOKEN in db_muxbus_credentials above, stored
1658        -- unencrypted in the same shared store.db for the same reason.
1659        CREATE TABLE IF NOT EXISTS db_agent_credentials (
1660            agent_id       TEXT PRIMARY KEY,
1661            client_id      TEXT NOT NULL DEFAULT '',
1662            client_secret  TEXT NOT NULL DEFAULT '',
1663            token_endpoint TEXT NOT NULL DEFAULT '',
1664            access_token   TEXT NOT NULL DEFAULT '',
1665            expires_at     INTEGER NOT NULL DEFAULT 0,
1666            created_at     INTEGER NOT NULL DEFAULT 0
1667        );
1668
1669        -- v6: durable mirror of each agent's native (Claude Code) memory
1670        -- files, keyed by the stable AgentDefinition.id rather than any live
1671        -- filesystem path — the live path is channel-relative by design
1672        -- (per-build-channel filesystem isolation), so the same agent opened
1673        -- from two channels resolves two different on-disk memory dirs. This
1674        -- table is what makes native memory durable and location-consistent
1675        -- across channels: agent:memory:list/read_file upsert into it on
1676        -- every read, and merge the live-FS view with it in the response —
1677        -- see docs/specs/SPEC_NATIVE_MEMORY_DURABLE_SYNC_2026_08_07.md.
1678        -- Duplicated into run_object_schema (same both-schemas pattern as
1679        -- db_agent_credentials above) for id_store's per-channel fallback
1680        -- before 0011_shared_store_backfill has run.
1681        CREATE TABLE IF NOT EXISTS db_agent_native_memory (
1682            agent_id           TEXT NOT NULL,
1683            filename           TEXT NOT NULL,
1684            content            TEXT NOT NULL,
1685            metadata_type      TEXT NOT NULL DEFAULT '',
1686            size_bytes         INTEGER NOT NULL DEFAULT 0,
1687            updated_at         INTEGER NOT NULL DEFAULT 0,
1688            last_seen_path     TEXT NOT NULL DEFAULT '',
1689            last_seen_mtime_ms INTEGER NOT NULL DEFAULT 0,
1690            PRIMARY KEY (agent_id, filename)
1691        );
1692
1693        -- v8: append-only version history for native memory content — one
1694        -- row per agent:memory:write_file call (plus reverts), so a prior
1695        -- value is never lost the way the mirror above loses it on every
1696        -- overwrite. See docs/specs/SPEC_MEMORY_VERSION_CONTROL_AND_ARMORY_AUDIT_2026_08_19.md.
1697        CREATE TABLE IF NOT EXISTS db_agent_native_memory_versions (
1698            id                 TEXT PRIMARY KEY,
1699            agent_id           TEXT NOT NULL,
1700            filename           TEXT NOT NULL,
1701            content            TEXT NOT NULL,
1702            content_hash       TEXT NOT NULL,
1703            parent_version_id  TEXT,
1704            source             TEXT NOT NULL DEFAULT 'agent_inferred',
1705            source_detail      TEXT NOT NULL DEFAULT '{}',
1706            session_id         TEXT NOT NULL DEFAULT '',
1707            created_at         INTEGER NOT NULL DEFAULT 0
1708        );
1709        CREATE INDEX IF NOT EXISTS idx_ss_native_memory_versions_lookup
1710            ON db_agent_native_memory_versions(agent_id, filename, created_at);
1711
1712        -- v10: append-only version history for Global Memory entry content
1713        -- — see SHARED_STORE_SCHEMA_VERSION's own doc comment for the full
1714        -- rationale (SPEC_AGENT_FACING_GLOBAL_MEMORY_API_2026_09_15.md).
1715        CREATE TABLE IF NOT EXISTS db_bundle_versions (
1716            id                 TEXT PRIMARY KEY,
1717            bundle_id          TEXT NOT NULL,
1718            name               TEXT NOT NULL,
1719            instructions       TEXT NOT NULL,
1720            content_hash       TEXT NOT NULL,
1721            parent_version_id  TEXT,
1722            source             TEXT NOT NULL DEFAULT 'agent_inferred',
1723            source_detail      TEXT NOT NULL DEFAULT '{}',
1724            written_by         TEXT NOT NULL DEFAULT '',
1725            created_at         INTEGER NOT NULL DEFAULT 0,
1726            -- v12 (identity M4c-1): the writer's UID from the request's
1727            -- Caller, beside the written_by name. Empty when unattributed.
1728            written_by_uid     TEXT NOT NULL DEFAULT ''
1729        );
1730        CREATE INDEX IF NOT EXISTS idx_ss_bundle_versions_lookup
1731            ON db_bundle_versions(bundle_id, created_at);",
1732    )?;
1733
1734    // Seed the blank Bundle singleton — same fixed id as objects.db so
1735    // cross-version reads never see a missing blank row.
1736    conn.execute_batch(
1737        "INSERT OR IGNORE INTO db_bundles
1738            (id, name, description, is_blank, created_at, updated_at)
1739         VALUES ('blank', '__blank__', 'Vanilla CLI — no instructions, no context', 1, 0, 0);",
1740    )?;
1741
1742    // ---- Additive column migrations (schema v5+) ----
1743    // Same idempotent pattern as run_object_schema's ALTER-TABLE loop below:
1744    // the flat CREATE batch above covers fresh DBs; these carry an existing
1745    // shared store forward. "duplicate column" is swallowed so this is safe
1746    // to run on every startup regardless of whether the column already exists.
1747    //
1748    // v5: db_cron_jobs.max_age_secs — optional hard expiry bound (seconds
1749    //     since created_at), alongside the existing max_fires bound. NULL
1750    //     for all existing rows = no behavior change for jobs created before
1751    //     this column existed.
1752    for stmt in &[
1753        "ALTER TABLE db_cron_jobs ADD COLUMN max_age_secs INTEGER",
1754        // v7: provider-scoped bundle instructions (ABF v0.2 §2.2).
1755        "ALTER TABLE db_bundles ADD COLUMN instructions_by_provider TEXT NOT NULL DEFAULT '{}'",
1756        // v9: AgentMux-controlled, highest-priority Global Bundle tier —
1757        // see OBJECT_SCHEMA_VERSION's v27 doc comment above.
1758        "ALTER TABLE db_bundles ADD COLUMN is_system INTEGER NOT NULL DEFAULT 0",
1759        // v11: identity M1b — the cron target's UID beside its slug.
1760        "ALTER TABLE db_cron_jobs ADD COLUMN target_uid TEXT NOT NULL DEFAULT ''",
1761        // v12: identity M4c-1 — the actor's UID beside each actor name.
1762        "ALTER TABLE db_cron_jobs ADD COLUMN created_by_uid TEXT NOT NULL DEFAULT ''",
1763        "ALTER TABLE db_bundle_versions ADD COLUMN written_by_uid TEXT NOT NULL DEFAULT ''",
1764    ] {
1765        if let Err(e) = conn.execute_batch(stmt) {
1766            let msg = e.to_string();
1767            if !msg.contains("duplicate column") {
1768                return Err(e.into());
1769            }
1770        }
1771    }
1772
1773    Ok(())
1774}
1775
1776/// `user_version` stamped into the new, permanently-global
1777/// `~/.agentmux/shared/identity-store.db` (distinct file from
1778/// `store.db` — see [`run_identity_store_schema`]'s own doc comment for
1779/// why). Independent counter from `SHARED_STORE_SCHEMA_VERSION`.
1780///   v1 — initial: agent→account links, bundles, drone
1781///        definitions, muxbus credentials, per-agent M2M credentials,
1782///        native memory mirror, cron jobs. Everything
1783///        `run_shared_store_schema` already held EXCEPT `db_accounts` —
1784///        see docs/specs/SPEC_IDENTITY_STORE_SPLIT_2026_08_17.md §3.1.
1785///   v2 — db_accounts, as a FALLBACK MIRROR only (reagentx P0 review on
1786///        PR #2632): the link resolves via this store now, but the
1787///        account row it points to could still only exist in the
1788///        per-channel-isolatable `id_store`, which is empty on a fresh
1789///        channel — the reported bug wasn't actually fixed end to end
1790///        without this. `id_store` remains the PRIMARY, authoritative,
1791///        write path (so Armory's disposable-test-account isolation is
1792///        unchanged); this copy is consulted only when `id_store` doesn't
1793///        have the row. See `identity::resolver::inject`'s account
1794///        resolution for the read-with-fallback + write-back-to-source-store
1795///        logic this enables.
1796///   v3 — db_agent_native_memory_versions, for schema parity with the v1
1797///        native memory mirror this store already carries. Same as that
1798///        table, this store's copy is not an actively-written duplicate
1799///        (id_store/store.db remains the authoritative write path — see
1800///        v2's note above) — added here only so a future always-global
1801///        read path (mirroring what v2 did for db_accounts) is not blocked
1802///        on a missing table. See
1803///        docs/specs/SPEC_MEMORY_VERSION_CONTROL_AND_ARMORY_AUDIT_2026_08_19.md.
1804///   v4 — db_bundles.is_system, for schema parity with OBJECT_SCHEMA_VERSION
1805///        v27 / SHARED_STORE_SCHEMA_VERSION v9 (same column, same reasoning
1806///        as v3 above — this store's `db_bundles` copy is not an actively-
1807///        written duplicate either). Real ALTER TABLE ADD COLUMN added to
1808///        `run_identity_store_schema` in the same change, so this counter
1809///        MUST bump alongside it — an unbumped counter would leave an
1810///        existing identity-store.db stamped at the old user_version
1811///        forever, silently defeating check_schema_compat/stamp_version's
1812///        forward-compat lock for this one store (reagent P1, PR #2782).
1813///        See docs/specs/SPEC_GLOBAL_MEMORY_SYSTEM_TIER_2026_08_24.md.
1814/// v5: `db_work_queue` — the Muxqueue backlog
1815///        (`docs/reports/REPORT_UNIVERSAL_AGENT_WORK_QUEUE_2026_09_01.md`).
1816///        A NEW table, so no ALTER is involved, but the counter still bumps:
1817///        an identity-store.db stamped v4 that a v5 binary opens must be
1818///        recognized as older, not "already current" (same reagent P1 on
1819///        PR #2782 reasoning recorded for v4 above).
1820///
1821///        Deliberately in THIS store, not the per-channel one. A queue whose
1822///        rows are per-channel means "any agent IN THIS CHANNEL can pick it
1823///        up", which is close to useless given every local/dev/portable build
1824///        is its own channel — see the report's §6.2, and
1825///        `docs/analysis/ANALYSIS_PER_CHANNEL_AUTH_BYPASSES_2026_08_31.md`
1826///        for what the per-channel/global seam has already cost once.
1827/// v6: `db_skills`, `db_mcp_servers` — Phase 2 of
1828///        `SPEC_DURABLE_BINDINGS_2026_09_10.md`. These become the
1829///        AUTHORITATIVE catalogs as of this version; the per-channel copies
1830///        in `run_object_schema` keep their declarations (never removed —
1831///        see that spec's §5.1 and §8 item 3) and continue to exist only as
1832///        the same best-effort degraded-mode fallback
1833///        `bootstrap/stores.rs` already substitutes for every other
1834///        identity-store table (`mstore` in
1835///        place of `identity_store` when the identity store can't be
1836///        resolved, created, or opened). Deliberately NOT declared with the
1837///        ref tables (`db_agent_skills_ref`, `db_bundle_skills_ref`, and
1838///        the MCP equivalents) — those stay channel-scoped until Phase 3/6
1839///        promotes them; adding them here now, unpaired with a live writer,
1840///        would be schema for a phase that hasn't shipped.
1841/// v7: partial UNIQUE indexes on `db_skills(name, skill_type)` and
1842///        `db_mcp_servers(name)`, each `WHERE is_global = 1` — global rows
1843///        only; two different owners' private rows sharing a name is still
1844///        fine. Closes a real race in
1845///        `m0031_carry_skills_and_mcp_servers_to_identity_store`: two
1846///        channels' migrations can run concurrently (multiple AgentMux
1847///        instances is an explicitly supported scenario), both read "no
1848///        matching global row yet" before either has inserted, and both
1849///        proceed to insert under different ids — the promised first-wins
1850///        cross-channel dedup silently fails without a DB-level constraint
1851///        to arbitrate the race (Codex P1, PR #3181). The migration's
1852///        insert now handles the resulting `UNIQUE constraint failed` by
1853///        re-querying for whichever row actually won.
1854///   v8 — `idx_ids_skills_global_name` replaces `idx_ids_skills_global_name_type`:
1855///        narrows the skill global-uniqueness index from `(name, skill_type)`
1856///        to `name` alone, matching the invariant `skill_upsert_unique_global`'s
1857///        own dup-check has always enforced (`WHERE name = ?1 AND id <> ?2
1858///        AND is_global = 1` — no `skill_type` filter). Codex P2, PR #3182,
1859///        `SPEC_DURABLE_BINDINGS_2026_09_10.md` §5.4: the v7 index was
1860///        narrower than the invariant it was meant to back, so a
1861///        same-name-different-`skill_type` race between two channels could
1862///        insert two rows the application itself considers duplicates. MCP
1863///        servers don't have this mismatch — `idx_ids_mcp_servers_global_name`
1864///        is already name-only — so this bump is skills-only.
1865///        `m0033_narrow_skill_global_uniqueness_index` defensively collapses
1866///        any existing same-name/different-type pair `m0031`'s own
1867///        `(name, skill_type)`-keyed dedup may already have produced, to the
1868///        earlier-`created_at` row, before the new index is created.
1869///   v9 — db_bundle_versions, for schema parity with OBJECT_SCHEMA_VERSION
1870///        v35 / SHARED_STORE_SCHEMA_VERSION v10 (same table, same reasoning
1871///        as v3 above — this store's copy is not an actively-written
1872///        duplicate either). See
1873///        docs/specs/SPEC_AGENT_FACING_GLOBAL_MEMORY_API_2026_09_15.md.
1874///   v10 — db_work_queue.target_agent_uid / claimed_by_uid, and
1875///        db_cron_jobs.target_uid (parity with SHARED_STORE_SCHEMA_VERSION
1876///        v11): the UID (`db_agents.id`) beside each slug column, dual-written
1877///        and not yet read. Identity M1b of
1878///        SPEC_AGENT_IDENTITY_CARRIED_NOT_DERIVED_2026_09_23.md §9.1.
1879///        `target_agent_uid` is resolved ONCE at enqueue (spec §5.4);
1880///        `claimed_by_uid` is CARRIED from the claimer's own
1881///        `AGENTMUX_AGENT_UID` (M1a, #3548), falling back to resolving its
1882///        `agent_id` with the fallback counted (spec §9.2). Empty when
1883///        unknown; never guessed.
1884///   v11 — db_work_queue.created_by_uid, and db_cron_jobs.created_by_uid /
1885///        db_bundle_versions.written_by_uid (parity with
1886///        SHARED_STORE_SCHEMA_VERSION v12): the actor's UID beside each
1887///        actor-name column, taken from the request's `Caller` (its
1888///        `X-Agent-Token`), never from a body field. Empty for an
1889///        Unattributed request; never guessed. Dual-written, not yet read.
1890///        Identity M4c-1 of
1891///        SPEC_AGENT_IDENTITY_CARRIED_NOT_DERIVED_2026_09_23.md §6.5.9.
1892///
1893/// Added WITHOUT a bump: `db_mcp_server_reach` (OBJECT_SCHEMA_VERSION v44
1894/// has the same table). The catalog's reach switches live with the catalog,
1895/// here, so they survive an upgrade where an agent bind
1896/// (`db_agent_mcp_ref`, in `objects.db`) does not. This store is one file
1897/// shared by every AgentMux on the computer, installed and dev builds alike,
1898/// and `check_schema_compat` makes any build that speaks an older version
1899/// refuse it. So a bump here locks every older AgentMux on the computer out
1900/// as soon as one newer build starts: a dev build of a branch with a bump
1901/// did exactly that on 2026-10-11. A table an older build never reads needs
1902/// no lock: an older build ignores it, a newer one creates it if missing.
1903/// Bump only for a change an older build would misread or corrupt.
1904/// `SPEC_CONNECTORS_MCP_WHAT_AGENTS_RUN_2026_10_10.md` §2.1.
1905pub const IDENTITY_STORE_SCHEMA_VERSION: i64 = 11;
1906
1907/// Initialize (or re-validate) the `~/.agentmux/shared/identity-store.db`
1908/// schema — the permanently-global store introduced by
1909/// `docs/specs/SPEC_IDENTITY_STORE_SPLIT_2026_08_17.md` to fix
1910/// conversation-continuity-across-a-version-switch
1911/// (`docs/specs/REPORT_HISTORY_CONTINUITY_ACROSS_VERSION_UPGRADE_2026_08_17.md`).
1912///
1913/// This is a **near-superset** of [`run_shared_store_schema`]'s tables
1914/// (v2): every one of them, INCLUDING `db_accounts` as of v2 — but
1915/// `db_accounts` here is a read-through FALLBACK MIRROR only, not the
1916/// authoritative write path. Unlike agent→account links, bundles,
1917/// drone definitions, muxbus credentials, and native memory,
1918/// `db_accounts` has one genuine, intentional isolation need (Armory's
1919/// disposable delete-account testing flow): a real per-account
1920/// creation-time scope split (SPEC §3.2) hasn't shipped yet, so
1921/// `id_store`/`resolve_shared_store_path()` stays the primary read/write
1922/// path for accounts, preserving Armory's isolation exactly as before.
1923/// This store's copy exists ONLY so a link resolved here (always global)
1924/// doesn't dead-end on an account row that's stuck in a fresh, per-
1925/// channel-isolated `id_store` — see `identity::resolver::inject`'s
1926/// account resolution (falls back to this store's `identity_get` only
1927/// when `id_store`'s own lookup misses) and
1928/// `migrations::m0022_identity_store_links_backfill` (backfills this
1929/// mirror the same way it backfills links). `db_agent_identity_links.account_id`
1930/// still has NO foreign key here (same reasoning `run_shared_store_schema`
1931/// already uses to drop its FK to `db_agent_definitions`: cross-database
1932/// FK enforcement is impossible in SQLite, and this table's `db_accounts`
1933/// is itself just a mirror, not guaranteed complete — application code
1934/// enforces referential integrity, tolerating a miss, instead).
1935///
1936/// A dedicated store (not a subset of the existing shared store reached
1937/// via a new always-global path) is deliberate, not just a naming choice:
1938/// it makes "can this table ever be reached from a per-channel-isolated
1939/// codepath" a structural fact (a totally separate file/connection)
1940/// rather than a convention someone could violate by calling
1941/// `resolve_shared_store_path()` for a new call site later — the exact
1942/// failure mode `docs/specs/CHECKLIST_AGENT_DATA_SCOPE_ROUTING_2026_08_17.md`
1943/// items 2-4 already document as having bitten this codebase more than
1944/// once.
1945///
1946/// Idempotent — safe to call on every startup.
1947pub fn run_identity_store_schema(conn: &Connection) -> Result<(), StoreError> {
1948    conn.execute_batch(
1949        "CREATE TABLE IF NOT EXISTS db_agent_identity_links (
1950            agent_id   TEXT NOT NULL,
1951            account_id TEXT NOT NULL,
1952            provider   TEXT NOT NULL,
1953            PRIMARY KEY (agent_id, provider)
1954        );
1955        CREATE INDEX IF NOT EXISTS idx_ids_agent_identity_links_account
1956            ON db_agent_identity_links(account_id);
1957
1958        -- v2: fallback mirror only — see this function's own doc comment
1959        -- above. id_store stays the authoritative read/write path.
1960        CREATE TABLE IF NOT EXISTS db_accounts (
1961            id           TEXT PRIMARY KEY,
1962            name         TEXT NOT NULL,
1963            provider     TEXT NOT NULL,
1964            kind         TEXT NOT NULL,
1965            display_name TEXT NOT NULL DEFAULT '',
1966            secret_ref   TEXT NOT NULL,
1967            context      TEXT NOT NULL DEFAULT '{}',
1968            status       TEXT NOT NULL DEFAULT 'unknown',
1969            created_at   INTEGER NOT NULL DEFAULT 0,
1970            updated_at   INTEGER NOT NULL DEFAULT 0
1971        );
1972        CREATE INDEX IF NOT EXISTS idx_ids_accounts_provider
1973            ON db_accounts(provider);
1974
1975        CREATE TABLE IF NOT EXISTS db_bundles (
1976            id            TEXT PRIMARY KEY,
1977            name          TEXT NOT NULL UNIQUE,
1978            description   TEXT NOT NULL DEFAULT '',
1979            is_blank      INTEGER NOT NULL DEFAULT 0,
1980            is_global     INTEGER NOT NULL DEFAULT 0,
1981            provider      TEXT NOT NULL DEFAULT '',
1982            model         TEXT NOT NULL DEFAULT '',
1983            instructions  TEXT NOT NULL DEFAULT '',
1984            instructions_by_provider TEXT NOT NULL DEFAULT '{}',
1985            context_files TEXT NOT NULL DEFAULT '[]',
1986            mcp_servers   TEXT NOT NULL DEFAULT '[]',
1987            skills        TEXT NOT NULL DEFAULT '[]',
1988            sort_order    INTEGER NOT NULL DEFAULT 0,
1989            created_at    INTEGER NOT NULL DEFAULT 0,
1990            updated_at    INTEGER NOT NULL DEFAULT 0,
1991            is_system     INTEGER NOT NULL DEFAULT 0
1992        );
1993        CREATE INDEX IF NOT EXISTS idx_ids_bundles_is_blank
1994            ON db_bundles(is_blank);
1995
1996        CREATE TABLE IF NOT EXISTS db_drone_definitions (
1997            id          TEXT PRIMARY KEY,
1998            name        TEXT NOT NULL,
1999            description TEXT NOT NULL DEFAULT '',
2000            graph       TEXT NOT NULL DEFAULT '{\"nodes\":[],\"edges\":[]}',
2001            viewport    TEXT NOT NULL DEFAULT '{\"x\":0,\"y\":0,\"zoom\":1}',
2002            created_at  INTEGER NOT NULL DEFAULT 0,
2003            updated_at  INTEGER NOT NULL DEFAULT 0
2004        );
2005        CREATE INDEX IF NOT EXISTS idx_ids_drone_definitions_updated
2006            ON db_drone_definitions(updated_at DESC);
2007
2008        CREATE TABLE IF NOT EXISTS db_muxbus_credentials (
2009            id             TEXT PRIMARY KEY DEFAULT 'global',
2010            cognito_domain TEXT NOT NULL DEFAULT '',
2011            client_id      TEXT NOT NULL DEFAULT '',
2012            access_token   TEXT NOT NULL DEFAULT '',
2013            refresh_token  TEXT NOT NULL DEFAULT '',
2014            id_token       TEXT NOT NULL DEFAULT '',
2015            expires_at     INTEGER NOT NULL DEFAULT 0,
2016            user_email     TEXT NOT NULL DEFAULT '',
2017            user_sub       TEXT NOT NULL DEFAULT ''
2018        );
2019
2020        CREATE TABLE IF NOT EXISTS db_migrations (
2021            id          TEXT PRIMARY KEY,
2022            applied_at  TEXT NOT NULL,
2023            duration_ms INTEGER NOT NULL DEFAULT 0,
2024            scope       TEXT NOT NULL DEFAULT 'global'
2025        );
2026
2027        CREATE TABLE IF NOT EXISTS db_cron_jobs (
2028            id            TEXT PRIMARY KEY,
2029            name          TEXT NOT NULL,
2030            expression    TEXT NOT NULL,
2031            prompt        TEXT NOT NULL,
2032            target        TEXT NOT NULL,
2033            created_by    TEXT NOT NULL DEFAULT '',
2034            enabled       INTEGER NOT NULL DEFAULT 1,
2035            last_fired    INTEGER,
2036            fire_count    INTEGER NOT NULL DEFAULT 0,
2037            max_fires     INTEGER,
2038            created_at    INTEGER NOT NULL DEFAULT 0,
2039            max_age_secs  INTEGER,
2040            -- v10 (identity M1b): UID beside the slug, resolved at create
2041            -- time, dual-written, not yet read. Parity with the shared-store
2042            -- copy of this table, which is the one the handlers use today.
2043            target_uid    TEXT NOT NULL DEFAULT '',
2044            -- v11 (identity M4c-1): the creator's UID, parity with the
2045            -- shared-store copy.
2046            created_by_uid TEXT NOT NULL DEFAULT ''
2047        );
2048        CREATE INDEX IF NOT EXISTS idx_ids_cron_jobs_enabled
2049            ON db_cron_jobs(enabled);
2050
2051        -- Muxqueue: the universal agent work queue. One row = one unit of work
2052        -- that ANY agent may claim. See
2053        -- docs/reports/REPORT_UNIVERSAL_AGENT_WORK_QUEUE_2026_09_01.md.
2054        --
2055        -- Deliberately placed directly after db_cron_jobs, because it is that
2056        -- table's sibling: cron is this row with a TIME trigger, this is the
2057        -- same row with a READINESS trigger. `not_before` makes the
2058        -- relationship literal.
2059        --
2060        -- It belongs in THIS schema (the always-global identity store) and not
2061        -- in run_shared_store_schema. A per-channel queue would only ever mean
2062        -- any agent IN THIS CHANNEL can pick it up, which is close to useless
2063        -- when every local/dev/portable build is its own channel -- see the
2064        -- report's section 6.2.
2065        --
2066        -- Editing note: this whole schema is ONE Rust string literal passed to
2067        -- execute_batch, so an UNESCAPED double quote anywhere in it --
2068        -- including inside an SQL comment like this one -- terminates that
2069        -- literal and breaks the build with errors that point at prose rather
2070        -- than at code. Escaped ones are fine and already appear above (see
2071        -- db_drone_definitions default JSON). Apostrophes are fine too: they
2072        -- are ordinary characters inside a Rust double-quoted string, and
2073        -- SQLite does not lex quotes inside a comment.
2074        CREATE TABLE IF NOT EXISTS db_work_queue (
2075            id            TEXT PRIMARY KEY,
2076            title         TEXT NOT NULL,
2077            payload       TEXT NOT NULL,
2078            kind          TEXT NOT NULL DEFAULT '',
2079            target_agent  TEXT NOT NULL DEFAULT '',
2080            target_group  TEXT NOT NULL DEFAULT '',
2081            priority      INTEGER NOT NULL DEFAULT 0,
2082            state         TEXT NOT NULL DEFAULT 'open',
2083            claimed_by    TEXT NOT NULL DEFAULT '',
2084            claim_expires INTEGER,
2085            attempts      INTEGER NOT NULL DEFAULT 0,
2086            max_attempts  INTEGER NOT NULL DEFAULT 3,
2087            created_by    TEXT NOT NULL DEFAULT '',
2088            created_at    INTEGER NOT NULL DEFAULT 0,
2089            updated_at    INTEGER NOT NULL DEFAULT 0,
2090            not_before    INTEGER,
2091            result        TEXT NOT NULL DEFAULT '',
2092            -- v10 (identity M1b): the UID (db_agents.id) beside each slug
2093            -- column, dual-written, not yet read. target_agent_uid is
2094            -- resolved ONCE at enqueue (spec §5.4: resolution happens at
2095            -- authoring time, when a human or model was present, never at
2096            -- claim time); claimed_by_uid is CARRIED from the claimer's own
2097            -- AGENTMUX_AGENT_UID. Empty when unknown -- never guessed.
2098            target_agent_uid TEXT NOT NULL DEFAULT '',
2099            claimed_by_uid   TEXT NOT NULL DEFAULT '',
2100            -- v11 (identity M4c-1): the enqueuer's UID from the request's
2101            -- Caller, beside the created_by name. Empty when unattributed.
2102            created_by_uid   TEXT NOT NULL DEFAULT ''
2103        );
2104        -- The claim query's exact predicate: state + readiness, ordered by
2105        -- priority then age. Without this the claim UPDATE degrades to a table
2106        -- scan under contention, which is when it matters most.
2107        CREATE INDEX IF NOT EXISTS idx_ids_work_queue_claimable
2108            ON db_work_queue(state, not_before, priority DESC, created_at);
2109        -- The reaper's predicate (expired leases).
2110        CREATE INDEX IF NOT EXISTS idx_ids_work_queue_claim_expires
2111            ON db_work_queue(claim_expires)
2112            WHERE claim_expires IS NOT NULL;
2113
2114        CREATE TABLE IF NOT EXISTS db_agent_credentials (
2115            agent_id       TEXT PRIMARY KEY,
2116            client_id      TEXT NOT NULL DEFAULT '',
2117            client_secret  TEXT NOT NULL DEFAULT '',
2118            token_endpoint TEXT NOT NULL DEFAULT '',
2119            access_token   TEXT NOT NULL DEFAULT '',
2120            expires_at     INTEGER NOT NULL DEFAULT 0,
2121            created_at     INTEGER NOT NULL DEFAULT 0
2122        );
2123
2124        CREATE TABLE IF NOT EXISTS db_agent_native_memory (
2125            agent_id           TEXT NOT NULL,
2126            filename           TEXT NOT NULL,
2127            content            TEXT NOT NULL,
2128            metadata_type      TEXT NOT NULL DEFAULT '',
2129            size_bytes         INTEGER NOT NULL DEFAULT 0,
2130            updated_at         INTEGER NOT NULL DEFAULT 0,
2131            last_seen_path     TEXT NOT NULL DEFAULT '',
2132            last_seen_mtime_ms INTEGER NOT NULL DEFAULT 0,
2133            PRIMARY KEY (agent_id, filename)
2134        );
2135
2136        -- v3: schema parity with the v1 native memory mirror above — see
2137        -- IDENTITY_STORE_SCHEMA_VERSION's own doc comment for why this
2138        -- store's copy is not (yet) an actively-written duplicate.
2139        CREATE TABLE IF NOT EXISTS db_agent_native_memory_versions (
2140            id                 TEXT PRIMARY KEY,
2141            agent_id           TEXT NOT NULL,
2142            filename           TEXT NOT NULL,
2143            content            TEXT NOT NULL,
2144            content_hash       TEXT NOT NULL,
2145            parent_version_id  TEXT,
2146            source             TEXT NOT NULL DEFAULT 'agent_inferred',
2147            source_detail      TEXT NOT NULL DEFAULT '{}',
2148            session_id         TEXT NOT NULL DEFAULT '',
2149            created_at         INTEGER NOT NULL DEFAULT 0
2150        );
2151        CREATE INDEX IF NOT EXISTS idx_ids_native_memory_versions_lookup
2152            ON db_agent_native_memory_versions(agent_id, filename, created_at);
2153
2154        -- v9: schema parity with the db_bundle_versions table above (own
2155        -- store) — see IDENTITY_STORE_SCHEMA_VERSION's own doc comment.
2156        CREATE TABLE IF NOT EXISTS db_bundle_versions (
2157            id                 TEXT PRIMARY KEY,
2158            bundle_id          TEXT NOT NULL,
2159            name               TEXT NOT NULL,
2160            instructions       TEXT NOT NULL,
2161            content_hash       TEXT NOT NULL,
2162            parent_version_id  TEXT,
2163            source             TEXT NOT NULL DEFAULT 'agent_inferred',
2164            source_detail      TEXT NOT NULL DEFAULT '{}',
2165            written_by         TEXT NOT NULL DEFAULT '',
2166            created_at         INTEGER NOT NULL DEFAULT 0,
2167            -- v11 (identity M4c-1): parity with the shared-store copy.
2168            written_by_uid     TEXT NOT NULL DEFAULT ''
2169        );
2170        CREATE INDEX IF NOT EXISTS idx_ids_bundle_versions_lookup
2171            ON db_bundle_versions(bundle_id, created_at);
2172
2173        -- v6: authoritative skill/MCP-server catalogs — see this constant's
2174        -- v6 doc comment above. Same column shapes as run_object_schema's
2175        -- copies; no ref tables here yet (Phase 3/6), so no FK targets exist
2176        -- to reference them from this store.
2177        CREATE TABLE IF NOT EXISTS db_skills (
2178            id          TEXT PRIMARY KEY,
2179            name        TEXT NOT NULL,
2180            trigger     TEXT NOT NULL DEFAULT '',
2181            skill_type  TEXT NOT NULL DEFAULT 'prompt',
2182            description TEXT NOT NULL DEFAULT '',
2183            content     TEXT NOT NULL DEFAULT '',
2184            is_global   INTEGER NOT NULL DEFAULT 0,
2185            created_at  INTEGER NOT NULL DEFAULT 0,
2186            updated_at  INTEGER NOT NULL DEFAULT 0
2187        );
2188        CREATE INDEX IF NOT EXISTS idx_ids_skills_is_global ON db_skills(is_global);
2189        -- v8: name-only, matching skill_upsert_unique_global's own dup-check
2190        -- — see this constant's v8 doc comment. Replaces
2191        -- idx_ids_skills_global_name_type (dropped just below for an
2192        -- existing store; a fresh store never creates it in the first
2193        -- place, since this whole batch runs as one execute_batch).
2194        CREATE UNIQUE INDEX IF NOT EXISTS idx_ids_skills_global_name
2195            ON db_skills(name) WHERE is_global = 1;
2196        DROP INDEX IF EXISTS idx_ids_skills_global_name_type;
2197
2198        CREATE TABLE IF NOT EXISTS db_mcp_servers (
2199            id          TEXT PRIMARY KEY,
2200            name        TEXT NOT NULL,
2201            transport   TEXT NOT NULL DEFAULT 'stdio',
2202            config      TEXT NOT NULL DEFAULT '{}',
2203            is_global   INTEGER NOT NULL DEFAULT 0,
2204            created_at  INTEGER NOT NULL DEFAULT 0,
2205            updated_at  INTEGER NOT NULL DEFAULT 0
2206        );
2207        CREATE INDEX IF NOT EXISTS idx_ids_mcp_servers_is_global ON db_mcp_servers(is_global);
2208        CREATE UNIQUE INDEX IF NOT EXISTS idx_ids_mcp_servers_global_name
2209            ON db_mcp_servers(name) WHERE is_global = 1;
2210        -- Who a catalog server reaches beyond its binds. Added without a
2211        -- version bump — see IDENTITY_STORE_SCHEMA_VERSION's doc comment.
2212        CREATE TABLE IF NOT EXISTS db_mcp_server_reach (
2213            mcp_id        TEXT PRIMARY KEY,
2214            all_agents    INTEGER NOT NULL DEFAULT 0,
2215            in_containers INTEGER NOT NULL DEFAULT 0,
2216            updated_at    INTEGER NOT NULL DEFAULT 0
2217        );",
2218    )?;
2219
2220    // Seed the blank Bundle singleton — same fixed id as objects.db/store.db
2221    // so cross-version reads never see a missing blank row.
2222    conn.execute_batch(
2223        "INSERT OR IGNORE INTO db_bundles
2224            (id, name, description, is_blank, created_at, updated_at)
2225         VALUES ('blank', '__blank__', 'Vanilla CLI — no instructions, no context', 1, 0, 0);",
2226    )?;
2227
2228    // Additive columns added after this store's tables were first created —
2229    // same idempotent pattern as run_shared_store_schema's own ALTER loop.
2230    for stmt in &[
2231        // db_bundles.is_system: see OBJECT_SCHEMA_VERSION's v27 doc comment.
2232        "ALTER TABLE db_bundles ADD COLUMN is_system INTEGER NOT NULL DEFAULT 0",
2233        // v10: identity M1b — UID columns beside the slug columns, dual-written.
2234        "ALTER TABLE db_work_queue ADD COLUMN target_agent_uid TEXT NOT NULL DEFAULT ''",
2235        "ALTER TABLE db_work_queue ADD COLUMN claimed_by_uid TEXT NOT NULL DEFAULT ''",
2236        "ALTER TABLE db_cron_jobs ADD COLUMN target_uid TEXT NOT NULL DEFAULT ''",
2237        // v11: identity M4c-1 — the actor's UID beside each actor name.
2238        "ALTER TABLE db_work_queue ADD COLUMN created_by_uid TEXT NOT NULL DEFAULT ''",
2239        "ALTER TABLE db_cron_jobs ADD COLUMN created_by_uid TEXT NOT NULL DEFAULT ''",
2240        "ALTER TABLE db_bundle_versions ADD COLUMN written_by_uid TEXT NOT NULL DEFAULT ''",
2241    ] {
2242        if let Err(e) = conn.execute_batch(stmt) {
2243            let msg = e.to_string();
2244            if !msg.contains("duplicate column") {
2245                return Err(e.into());
2246            }
2247        }
2248    }
2249
2250    Ok(())
2251}
2252
2253/// Initialize the FileStore schema. Creates the wave_file and file_data
2254/// tables. Already a flat single-DDL store — unaffected by the
2255/// `objects.db` flattening.
2256pub fn run_filestore_migrations(conn: &Connection) -> Result<(), StoreError> {
2257    conn.execute_batch(
2258        "CREATE TABLE IF NOT EXISTS db_wave_file (
2259            zoneid TEXT NOT NULL,
2260            name TEXT NOT NULL,
2261            size INTEGER NOT NULL DEFAULT 0,
2262            createdts INTEGER NOT NULL DEFAULT 0,
2263            modts INTEGER NOT NULL DEFAULT 0,
2264            opts TEXT NOT NULL DEFAULT '{}',
2265            meta TEXT NOT NULL DEFAULT '{}',
2266            PRIMARY KEY (zoneid, name)
2267        );
2268
2269        CREATE TABLE IF NOT EXISTS db_file_data (
2270            zoneid TEXT NOT NULL,
2271            name TEXT NOT NULL,
2272            partidx INTEGER NOT NULL,
2273            data BLOB NOT NULL,
2274            PRIMARY KEY (zoneid, name, partidx)
2275        );",
2276    )?;
2277    // Line counter (Phase 5a-2, SPEC_AGENT_PANE_BOUNDED_LIVE_WINDOW_MIGRATION_
2278    // 2026_09_23.md §6.3.7; see `filestore/counter.rs`). Additive and
2279    // nullable, and deliberately WITHOUT a FILESTORE_SCHEMA_VERSION bump: the
2280    // global transcript store is shared by every srv instance on the machine,
2281    // and a bump would make older builds refuse to open it. Older builds
2282    // insert rows without these columns (NULL = not counted) and write
2283    // without maintaining them, which the counter detects. "duplicate
2284    // column" is expected when another instance added them first.
2285    for stmt in &[
2286        "ALTER TABLE db_wave_file ADD COLUMN gen TEXT",
2287        "ALTER TABLE db_wave_file ADD COLUMN lines INTEGER",
2288        "ALTER TABLE db_wave_file ADD COLUMN lines_size INTEGER",
2289        "ALTER TABLE db_wave_file ADD COLUMN lines_tail INTEGER",
2290        "ALTER TABLE db_wave_file ADD COLUMN lines_modts INTEGER",
2291        // Bumped (by the triggers below) by every write that changes bytes
2292        // already in the file — not by appends — and the value an epoch was
2293        // counted at. An epoch is valid only while they are equal.
2294        "ALTER TABLE db_wave_file ADD COLUMN rev INTEGER",
2295        "ALTER TABLE db_wave_file ADD COLUMN lines_rev INTEGER",
2296        // A random identity per row, set by the insert trigger below: a file
2297        // deleted and re-created is a different incarnation even if every
2298        // other column (and the creation millisecond) comes out the same.
2299        "ALTER TABLE db_wave_file ADD COLUMN incarnation BLOB",
2300    ] {
2301        if let Err(e) = conn.execute_batch(stmt) {
2302            if !e.to_string().contains("duplicate column") {
2303                return Err(e.into());
2304            }
2305        }
2306    }
2307    // `rev` is maintained by triggers, not by this code's writes, because
2308    // triggers are part of the database: they fire for every connection,
2309    // including older builds that know nothing about the counter. Any write
2310    // that changes existing bytes bumps it:
2311    //   - a part deleted (write_file's replace, a delete);
2312    //   - a part written over with bytes that aren't a pure extension of it
2313    //     (REPLACE INTO / UPDATE). Appending extends the last part, which
2314    //     keeps it a prefix, so appends — ours or an older build's — don't.
2315    // REPLACE INTO doesn't fire DELETE triggers (recursive_triggers is off,
2316    // SQLite's default; nothing here enables it), so an append can't reach
2317    // the delete trigger. A trigger on a deleted row's parts finds no row.
2318    conn.execute_batch(
2319        "-- Versioned name: CREATE TRIGGER IF NOT EXISTS never updates an
2320         -- existing definition, so a changed rule gets a new name and the
2321         -- old one is dropped.
2322         DROP TRIGGER IF EXISTS db_file_data_rev_insert;
2323         CREATE TRIGGER IF NOT EXISTS db_file_data_rev_insert_v2
2324         BEFORE INSERT ON db_file_data
2325         WHEN EXISTS (
2326             SELECT 1 FROM db_file_data d
2327             WHERE d.zoneid = NEW.zoneid AND d.name = NEW.name AND d.partidx = NEW.partidx
2328               AND substr(NEW.data, 1, length(d.data)) IS NOT d.data
2329         )
2330         -- A NEW part inside the range the file already claims: bytes the
2331         -- file declared (and a reader may have read as missing) are being
2332         -- filled in, not appended. Every writer inserts appended parts
2333         -- before raising `size`, so an append's new parts always start at
2334         -- or past it. Older builds' write_file raises `size` first and
2335         -- inserts after (Codex on #3631). 65536 is PART_DATA_SIZE.
2336         OR (
2337             NOT EXISTS (
2338                 SELECT 1 FROM db_file_data d
2339                 WHERE d.zoneid = NEW.zoneid AND d.name = NEW.name AND d.partidx = NEW.partidx
2340             )
2341             AND NEW.partidx * 65536 < (
2342                 SELECT COALESCE(MAX(w.size), 0) FROM db_wave_file w
2343                 WHERE w.zoneid = NEW.zoneid AND w.name = NEW.name
2344             )
2345         )
2346         BEGIN
2347             UPDATE db_wave_file SET rev = COALESCE(rev, 0) + 1
2348             WHERE zoneid = NEW.zoneid AND name = NEW.name;
2349         END;
2350
2351         CREATE TRIGGER IF NOT EXISTS db_file_data_rev_update
2352         BEFORE UPDATE OF data ON db_file_data
2353         WHEN substr(NEW.data, 1, length(OLD.data)) IS NOT OLD.data
2354         BEGIN
2355             UPDATE db_wave_file SET rev = COALESCE(rev, 0) + 1
2356             WHERE zoneid = OLD.zoneid AND name = OLD.name;
2357         END;
2358
2359         CREATE TRIGGER IF NOT EXISTS db_file_data_rev_delete
2360         AFTER DELETE ON db_file_data
2361         BEGIN
2362             UPDATE db_wave_file SET rev = COALESCE(rev, 0) + 1
2363             WHERE zoneid = OLD.zoneid AND name = OLD.name;
2364         END;
2365
2366         -- Every row, whoever inserts it (older builds included), gets 64
2367         -- random bits of identity. A delete + re-create in the same
2368         -- millisecond with the same size and bytes is still a different
2369         -- incarnation (Codex on #3631).
2370         CREATE TRIGGER IF NOT EXISTS db_wave_file_incarnation
2371         AFTER INSERT ON db_wave_file
2372         BEGIN
2373             UPDATE db_wave_file SET incarnation = randomblob(8)
2374             WHERE zoneid = NEW.zoneid AND name = NEW.name;
2375         END;
2376
2377         UPDATE db_wave_file SET incarnation = randomblob(8) WHERE incarnation IS NULL;",
2378    )?;
2379    Ok(())
2380}
2381
2382/// Initialize the saga durability schema (`saga` + `saga_step` tables and
2383/// their indexes). See `docs/specs/SPEC_SAGA_DURABILITY_2026-05-01.md` §2.2.
2384/// Already a flat single-DDL store.
2385pub fn run_saga_log_migrations(conn: &Connection) -> Result<(), StoreError> {
2386    conn.execute_batch(
2387        "CREATE TABLE IF NOT EXISTS saga (
2388            saga_id        INTEGER PRIMARY KEY,
2389            name           TEXT NOT NULL,
2390            state          TEXT NOT NULL,
2391            started_at     INTEGER NOT NULL,
2392            terminal_at    INTEGER,
2393            failure_reason TEXT,
2394            input_json     TEXT NOT NULL
2395        );
2396
2397        CREATE TABLE IF NOT EXISTS saga_step (
2398            saga_id     INTEGER NOT NULL REFERENCES saga(saga_id),
2399            step_index  INTEGER NOT NULL,
2400            name        TEXT NOT NULL,
2401            state       TEXT NOT NULL,
2402            cmd_json    TEXT NOT NULL,
2403            output_json TEXT,
2404            started_at  INTEGER NOT NULL,
2405            ended_at    INTEGER,
2406            PRIMARY KEY (saga_id, step_index)
2407        );
2408
2409        CREATE INDEX IF NOT EXISTS saga_state_idx
2410            ON saga(state) WHERE state IN ('running', 'compensating');
2411        CREATE INDEX IF NOT EXISTS saga_terminal_idx
2412            ON saga(terminal_at);",
2413    )?;
2414    Ok(())
2415}
2416
2417/// `PRAGMA user_version` tripwire (AUDIT_SQLITE_SYSTEMS §8.5).
2418///
2419/// Compare the file's `user_version` to `current` and refuse to open
2420/// the database if it was written by a NEWER binary.
2421///
2422/// This is the forward-compat **safety lock** from the channels design
2423/// (`SPEC_DATA_CHANNELS_2026_05_24.md` §3.3). Within a channel,
2424/// multiple released AgentMux versions share one data dir; the lock
2425/// keeps an older binary from writing into a schema laid down by a
2426/// newer binary. Same discipline as Chrome's profile-too-new check
2427/// and Postgres's catalog version mismatch.
2428///
2429/// **Must be called BEFORE `run_*_schema` / `run_*_migrations`.** The
2430/// `run_*` functions include mutating steps (legacy-table renames,
2431/// seed inserts, `CREATE TABLE` for new tables the older binary
2432/// doesn't have), so if we ran them first and only then checked the
2433/// version, a downgraded binary could still alter a newer database
2434/// before the error fires — breaking the "reject without touching
2435/// disk" invariant the lock is meant to guarantee (codex P1 on PR
2436/// #1029).
2437///
2438/// Read-only: only `PRAGMA user_version` query, no writes.
2439/// [`stamp_version`] does the corresponding write AFTER migrations
2440/// complete successfully.
2441///
2442/// Recovery for the user when this returns `SchemaTooNew`: upgrade
2443/// AgentMux to a version ≥ `found`, or set
2444/// `AGENTMUX_CHANNEL=<other>` to land in a different channel dir.
2445/// The data on disk is preserved either way — this function never
2446/// modifies the database on the rejected path.
2447pub fn check_schema_compat(
2448    conn: &Connection,
2449    current: i64,
2450    db_label: &str,
2451) -> Result<(), StoreError> {
2452    let found: i64 = conn.query_row("PRAGMA user_version", [], |row| row.get(0))?;
2453    if found > current {
2454        warn!(
2455            db = db_label,
2456            found, expected = current,
2457            "database user_version is newer than this build — refusing \
2458             to open. Upgrade AgentMux or switch channels.",
2459        );
2460        return Err(StoreError::SchemaTooNew {
2461            db: db_label.to_string(),
2462            found,
2463            expected: current,
2464        });
2465    }
2466    Ok(())
2467}
2468
2469/// Stamp the database's `user_version` PRAGMA to `current`. Called
2470/// AFTER `run_*_schema` succeeds, paired with a prior
2471/// [`check_schema_compat`] that gated the migrations on the
2472/// caller-binary speaking a compatible (or newer) schema version.
2473///
2474/// Splitting the read from the write makes the safety-lock order
2475/// explicit at every call site:
2476///
2477/// ```ignore
2478/// check_schema_compat(&conn, OBJECT_SCHEMA_VERSION, "objects.db")?;
2479/// run_object_schema(&conn)?;
2480/// stamp_version(&conn, OBJECT_SCHEMA_VERSION)?;
2481/// ```
2482pub fn stamp_version(conn: &Connection, current: i64) -> Result<(), StoreError> {
2483    conn.execute_batch(&format!("PRAGMA user_version = {current};"))?;
2484    Ok(())
2485}
2486
2487#[cfg(test)]
2488mod tests {
2489    use super::*;
2490
2491    /// Every table the flat `objects.db` schema must contain.
2492    const EXPECTED_TABLES: &[&str] = &[
2493        "db_client",
2494        "db_window",
2495        "db_workspace",
2496        "db_tab",
2497        "db_layout",
2498        "db_block",
2499        "db_temp",
2500        "db_agent_content",
2501        "db_agent_skills",
2502        "db_agent_history",
2503        "db_accounts",
2504        "db_agent_identity_links",
2505        "db_bundles",
2506        "db_agents",
2507        "db_drone_definitions",
2508        "db_drone_runs",
2509    ];
2510
2511    fn table_exists(conn: &Connection, name: &str) -> bool {
2512        let count: i64 = conn
2513            .query_row(
2514                "SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?1",
2515                [name],
2516                |row| row.get(0),
2517            )
2518            .unwrap();
2519        count == 1
2520    }
2521
2522    fn index_exists(conn: &Connection, name: &str) -> bool {
2523        let count: i64 = conn
2524            .query_row(
2525                "SELECT count(*) FROM sqlite_master WHERE type='index' AND name=?1",
2526                [name],
2527                |row| row.get(0),
2528            )
2529            .unwrap();
2530        count == 1
2531    }
2532
2533    fn column_exists(conn: &Connection, table: &str, column: &str) -> bool {
2534        let mut stmt = conn.prepare(&format!("PRAGMA table_info({table})")).unwrap();
2535        let names: Vec<String> = stmt
2536            .query_map([], |row| row.get::<_, String>(1))
2537            .unwrap()
2538            .filter_map(Result::ok)
2539            .collect();
2540        names.iter().any(|n| n == column)
2541    }
2542
2543    #[test]
2544    fn test_object_schema_creates_all_tables_and_singletons() {
2545        let conn = Connection::open_in_memory().unwrap();
2546        conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap();
2547        run_object_schema(&conn).unwrap();
2548
2549        for table in EXPECTED_TABLES {
2550            assert!(table_exists(&conn, table), "{table} should exist");
2551        }
2552        // De-forged + bundle indexes.
2553        for idx in &[
2554            "idx_agent_history_agent_date",
2555            "idx_agent_identity_links_account",
2556            "idx_accounts_provider",
2557            "idx_bundles_is_blank",
2558            "idx_agents_is_template",
2559            "idx_agents_parent_template_id",
2560            "idx_agents_is_seeded",
2561            "idx_drone_definitions_updated",
2562            "idx_drone_runs_status",
2563        ] {
2564            assert!(index_exists(&conn, idx), "{idx} should exist");
2565        }
2566
2567        // Blank singleton seeded.
2568        let mem_blank: i64 = conn
2569            .query_row(
2570                "SELECT count(*) FROM db_bundles WHERE id='blank' AND is_blank=1",
2571                [],
2572                |row| row.get(0),
2573            )
2574            .unwrap();
2575        assert_eq!(mem_blank, 1, "blank Bundle singleton should be seeded");
2576    }
2577
2578    #[test]
2579    fn test_object_schema_idempotent() {
2580        let conn = Connection::open_in_memory().unwrap();
2581        conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap();
2582        run_object_schema(&conn).unwrap();
2583        run_object_schema(&conn).unwrap(); // second pass must not error
2584
2585        // Singleton stays unique.
2586        let mem_count: i64 = conn
2587            .query_row(
2588                "SELECT count(*) FROM db_bundles WHERE id='blank'",
2589                [],
2590                |r| r.get(0),
2591            )
2592            .unwrap();
2593        assert_eq!(mem_count, 1);
2594    }
2595
2596    #[test]
2597    fn test_object_schema_has_model_vendor_base_url_on_db_agents() {
2598        let conn = Connection::open_in_memory().unwrap();
2599        conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap();
2600        run_object_schema(&conn).unwrap();
2601        // Second pass: the ALTER TABLE array's duplicate-column guard must
2602        // not error when the column already exists (fresh installs get it
2603        // via CREATE TABLE; the ALTER is for upgrading pre-v15 databases).
2604        // Was "on_both_tables" — db_agent_definitions is gone as of v32, so
2605        // this is db_agents-only now (see OBJECT_SCHEMA_VERSION's v32 doc
2606        // comment).
2607        run_object_schema(&conn).unwrap();
2608
2609        assert!(column_exists(&conn, "db_agents", "model_vendor_base_url"));
2610    }
2611
2612    #[test]
2613    fn test_object_schema_has_auto_continue_enabled_on_db_agents() {
2614        let conn = Connection::open_in_memory().unwrap();
2615        conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap();
2616        run_object_schema(&conn).unwrap();
2617        // Second pass: the ALTER TABLE array's duplicate-column guard must
2618        // not error when the column already exists (fresh installs get it
2619        // via CREATE TABLE; the ALTER is for upgrading pre-v17 databases).
2620        // Was "on_both_tables" — see the v15 test just above for why.
2621        run_object_schema(&conn).unwrap();
2622
2623        assert!(column_exists(&conn, "db_agents", "auto_continue_enabled"));
2624    }
2625
2626    #[test]
2627    fn test_db_bundles_has_instructions_by_provider_in_both_schemas() {
2628        // ABF v0.2 §2.2 (v16): db_bundles lives in both objects.db (via
2629        // run_object_schema) and the shared store.db (via
2630        // run_shared_store_schema) — mirrors the db_agent_credentials /
2631        // db_agent_native_memory both-schemas precedent (v13/v14).
2632        let object_conn = Connection::open_in_memory().unwrap();
2633        object_conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap();
2634        run_object_schema(&object_conn).unwrap();
2635        run_object_schema(&object_conn).unwrap(); // idempotent second pass
2636        assert!(column_exists(&object_conn, "db_bundles", "instructions_by_provider"));
2637
2638        let shared_conn = Connection::open_in_memory().unwrap();
2639        shared_conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap();
2640        run_shared_store_schema(&shared_conn).unwrap();
2641        run_shared_store_schema(&shared_conn).unwrap();
2642        assert!(column_exists(&shared_conn, "db_bundles", "instructions_by_provider"));
2643    }
2644
2645    #[test]
2646    fn test_db_skills_and_mcp_servers_exist_in_both_object_and_identity_schemas() {
2647        // Phase 2 of SPEC_DURABLE_BINDINGS_2026_09_10.md (v6): the identity
2648        // store gains its own db_skills/db_mcp_servers, becoming
2649        // authoritative — but the channel-store copies (run_object_schema)
2650        // are NOT removed, per that spec's §5.1/§8 item 3. Pins both halves
2651        // of that claim, not just the new one: it would be easy to
2652        // accidentally satisfy only the identity-store half of this and
2653        // silently drop the channel-store fallback.
2654        let object_conn = Connection::open_in_memory().unwrap();
2655        run_object_schema(&object_conn).unwrap();
2656        assert!(table_exists(&object_conn, "db_skills"), "channel store must keep db_skills");
2657        assert!(table_exists(&object_conn, "db_mcp_servers"), "channel store must keep db_mcp_servers");
2658
2659        let identity_conn = Connection::open_in_memory().unwrap();
2660        run_identity_store_schema(&identity_conn).unwrap();
2661        run_identity_store_schema(&identity_conn).unwrap(); // idempotent second pass
2662        assert!(table_exists(&identity_conn, "db_skills"), "identity store must have db_skills");
2663        assert!(table_exists(&identity_conn, "db_mcp_servers"), "identity store must have db_mcp_servers");
2664        assert!(index_exists(&identity_conn, "idx_ids_skills_is_global"));
2665        assert!(index_exists(&identity_conn, "idx_ids_mcp_servers_is_global"));
2666
2667        // No ref tables here yet — those are Phase 3/6, not Phase 2. Schema
2668        // for a phase that hasn't shipped would be its own kind of drift.
2669        assert!(!table_exists(&identity_conn, "db_agent_skills_ref"));
2670        assert!(!table_exists(&identity_conn, "db_bundle_skills_ref"));
2671        assert!(!table_exists(&identity_conn, "db_agent_mcp_ref"));
2672        assert!(!table_exists(&identity_conn, "db_bundle_mcp_ref"));
2673    }
2674
2675    #[test]
2676    fn test_object_schema_omits_dead_tables() {
2677        let conn = Connection::open_in_memory().unwrap();
2678        run_object_schema(&conn).unwrap();
2679        for dead in DEAD_TABLE_DROPS {
2680            assert!(!table_exists(&conn, dead), "{dead} must not be created");
2681        }
2682        // Legacy forge names are never created either.
2683        for (legacy, _) in LEGACY_TABLE_RENAMES {
2684            assert!(
2685                !table_exists(&conn, legacy),
2686                "legacy {legacy} must not be created by the flat schema"
2687            );
2688        }
2689    }
2690
2691    #[test]
2692    fn test_adopt_legacy_renames_forge_tables() {
2693        // Simulate a pre-flatten (post-v11) dev DB: legacy forge table
2694        // names + a dead workflow table, with seeded rows.
2695        let conn = Connection::open_in_memory().unwrap();
2696        conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap();
2697        conn.execute_batch(
2698            "CREATE TABLE db_forge_agents (
2699                id TEXT PRIMARY KEY, slug TEXT NOT NULL DEFAULT '', name TEXT NOT NULL,
2700                icon TEXT NOT NULL DEFAULT '✦', provider TEXT NOT NULL,
2701                description TEXT NOT NULL DEFAULT '', working_directory TEXT NOT NULL DEFAULT '',
2702                shell TEXT NOT NULL DEFAULT '', provider_flags TEXT NOT NULL DEFAULT '',
2703                auto_start INTEGER NOT NULL DEFAULT 0, restart_on_crash INTEGER NOT NULL DEFAULT 0,
2704                idle_timeout_minutes INTEGER NOT NULL DEFAULT 0,
2705                agent_type TEXT NOT NULL DEFAULT 'standalone', environment TEXT NOT NULL DEFAULT '',
2706                agent_bus_id TEXT NOT NULL DEFAULT '', is_seeded INTEGER NOT NULL DEFAULT 0,
2707                accounts TEXT NOT NULL DEFAULT '',
2708                parent_id TEXT NOT NULL DEFAULT '', branch_label TEXT NOT NULL DEFAULT '',
2709                created_at INTEGER NOT NULL DEFAULT 0
2710            );
2711            CREATE UNIQUE INDEX idx_forge_agents_slug ON db_forge_agents(slug);
2712            INSERT INTO db_forge_agents (id, slug, name, provider)
2713                VALUES ('a1', 'coder', 'Coder', 'claude');
2714
2715            CREATE TABLE db_workflow_definitions (id TEXT PRIMARY KEY);",
2716        )
2717        .unwrap();
2718
2719        run_object_schema(&conn).unwrap();
2720
2721        // Renamed, data preserved.
2722        assert!(table_exists(&conn, "db_agent_definitions"));
2723        assert!(!table_exists(&conn, "db_forge_agents"));
2724        let name: String = conn
2725            .query_row(
2726                "SELECT name FROM db_agent_definitions WHERE id='a1'",
2727                [],
2728                |r| r.get(0),
2729            )
2730            .unwrap();
2731        assert_eq!(name, "Coder");
2732        // Old index dropped. No new index takes its place — db_agent_definitions
2733        // is dropped whole as of v32 (OBJECT_SCHEMA_VERSION's v32 doc comment),
2734        // so the flat schema no longer declares a unique index for it; the
2735        // renamed table exists only transiently, until the drop migration
2736        // that runs later in the same upgrade removes it.
2737        assert!(!index_exists(&conn, "idx_forge_agents_slug"));
2738        // Dead table dropped.
2739        assert!(!table_exists(&conn, "db_workflow_definitions"));
2740    }
2741
2742    #[test]
2743    fn test_adopt_legacy_is_noop_on_fresh_db() {
2744        let conn = Connection::open_in_memory().unwrap();
2745        run_object_schema(&conn).unwrap();
2746        // Re-running schema (which re-runs adopt) on the already-flat DB
2747        // leaves the de-forged tables intact and creates no legacy names.
2748        // db_agent_definitions itself is gone from the flat schema as of
2749        // v32, so a live de-forged table that's still declared (db_agents)
2750        // stands in for "the flat schema is present, nothing to adopt".
2751        run_object_schema(&conn).unwrap();
2752        assert!(table_exists(&conn, "db_agents"));
2753        assert!(!table_exists(&conn, "db_forge_agents"));
2754    }
2755
2756    #[test]
2757    fn test_adopt_legacy_both_tables_present_is_non_destructive() {
2758        // Downgrade-roundtrip: a flat DB (db_agent_definitions) where a
2759        // pre-flatten build later re-created db_forge_agents and wrote a
2760        // row. The adopt step must NOT drop the legacy table — silent
2761        // data loss is the bug class behind PR #933's Codex P1.
2762        let conn = Connection::open_in_memory().unwrap();
2763        run_object_schema(&conn).unwrap();
2764        // db_agent_definitions is gone from the flat schema as of v32, so
2765        // this fixture has to construct the "downgrade-era db already has
2766        // the de-forged table too" scenario by hand — minimal shape, this
2767        // test only cares about existence + data survival, not full column
2768        // parity.
2769        conn.execute_batch(
2770            "CREATE TABLE db_agent_definitions (id TEXT PRIMARY KEY, name TEXT NOT NULL);
2771             CREATE TABLE db_forge_agents (id TEXT PRIMARY KEY, name TEXT NOT NULL);
2772             INSERT INTO db_forge_agents (id, name) VALUES ('downgrade-era', 'Recover Me');",
2773        )
2774        .unwrap();
2775
2776        run_object_schema(&conn).unwrap();
2777
2778        // Legacy table left intact — data recoverable, not dropped.
2779        assert!(table_exists(&conn, "db_forge_agents"));
2780        let name: String = conn
2781            .query_row(
2782                "SELECT name FROM db_forge_agents WHERE id='downgrade-era'",
2783                [],
2784                |r| r.get(0),
2785            )
2786            .unwrap();
2787        assert_eq!(name, "Recover Me");
2788        // Flat table still present and authoritative.
2789        assert!(table_exists(&conn, "db_agent_definitions"));
2790    }
2791
2792    #[test]
2793    fn test_adopt_legacy_fk_cascade_survives_rename() {
2794        // A renamed parent must keep cascading into renamed children.
2795        let conn = Connection::open_in_memory().unwrap();
2796        conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap();
2797        conn.execute_batch(
2798            "CREATE TABLE db_forge_agents (
2799                id TEXT PRIMARY KEY, slug TEXT NOT NULL DEFAULT '', name TEXT NOT NULL,
2800                icon TEXT NOT NULL DEFAULT '✦', provider TEXT NOT NULL,
2801                description TEXT NOT NULL DEFAULT '', working_directory TEXT NOT NULL DEFAULT '',
2802                shell TEXT NOT NULL DEFAULT '', provider_flags TEXT NOT NULL DEFAULT '',
2803                auto_start INTEGER NOT NULL DEFAULT 0, restart_on_crash INTEGER NOT NULL DEFAULT 0,
2804                idle_timeout_minutes INTEGER NOT NULL DEFAULT 0,
2805                agent_type TEXT NOT NULL DEFAULT 'standalone', environment TEXT NOT NULL DEFAULT '',
2806                agent_bus_id TEXT NOT NULL DEFAULT '', is_seeded INTEGER NOT NULL DEFAULT 0,
2807                accounts TEXT NOT NULL DEFAULT '',
2808                parent_id TEXT NOT NULL DEFAULT '', branch_label TEXT NOT NULL DEFAULT '',
2809                created_at INTEGER NOT NULL DEFAULT 0
2810            );
2811            CREATE TABLE db_forge_content (
2812                agent_id TEXT NOT NULL, content_type TEXT NOT NULL,
2813                content TEXT NOT NULL DEFAULT '', updated_at INTEGER NOT NULL DEFAULT 0,
2814                PRIMARY KEY (agent_id, content_type),
2815                FOREIGN KEY (agent_id) REFERENCES db_forge_agents(id) ON DELETE CASCADE
2816            );
2817            INSERT INTO db_forge_agents (id, name, provider) VALUES ('a1', 'Coder', 'claude');
2818            INSERT INTO db_forge_content (agent_id, content_type, content)
2819                VALUES ('a1', 'soul', 'hello');",
2820        )
2821        .unwrap();
2822
2823        run_object_schema(&conn).unwrap();
2824
2825        conn.execute("DELETE FROM db_agent_definitions WHERE id='a1'", [])
2826            .unwrap();
2827        let remaining: i64 = conn
2828            .query_row(
2829                "SELECT count(*) FROM db_agent_content WHERE agent_id='a1'",
2830                [],
2831                |r| r.get(0),
2832            )
2833            .unwrap();
2834        assert_eq!(
2835            remaining, 0,
2836            "FK cascade must survive the forge→agent table rename"
2837        );
2838    }
2839
2840    #[test]
2841    fn legacy_column_added_to_a_still_present_db_agent_definitions_via_alter() {
2842        // Codex P1 on #3096: the flat schema no longer CREATEs
2843        // db_agent_definitions (dropped as of v32), but several pending
2844        // migrations (m0007, m0025, m0028) call Store::open() internally —
2845        // running run_object_schema — before m0029 (numbered last) actually
2846        // drops the table in the same upgrade pass. A store old enough to
2847        // still carry the table but missing a column those migrations read
2848        // (e.g. user_hidden) must still get it ALTERed in, or their own
2849        // reads fail with "no such column" before they ever reach m0029.
2850        let conn = Connection::open_in_memory().unwrap();
2851        conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap();
2852        conn.execute_batch(
2853            "CREATE TABLE db_agent_definitions (
2854                id TEXT PRIMARY KEY, slug TEXT NOT NULL DEFAULT '',
2855                name TEXT NOT NULL, icon TEXT NOT NULL DEFAULT '✦',
2856                provider TEXT NOT NULL,
2857                description TEXT NOT NULL DEFAULT '',
2858                working_directory TEXT NOT NULL DEFAULT '',
2859                shell TEXT NOT NULL DEFAULT '',
2860                provider_flags TEXT NOT NULL DEFAULT '',
2861                auto_start INTEGER NOT NULL DEFAULT 0,
2862                restart_on_crash INTEGER NOT NULL DEFAULT 0,
2863                idle_timeout_minutes INTEGER NOT NULL DEFAULT 0,
2864                agent_type TEXT NOT NULL DEFAULT 'standalone',
2865                environment TEXT NOT NULL DEFAULT '',
2866                agent_bus_id TEXT NOT NULL DEFAULT '',
2867                is_seeded INTEGER NOT NULL DEFAULT 0,
2868                accounts TEXT NOT NULL DEFAULT '',
2869                parent_id TEXT NOT NULL DEFAULT '',
2870                branch_label TEXT NOT NULL DEFAULT '',
2871                created_at INTEGER NOT NULL DEFAULT 0
2872            );
2873            INSERT INTO db_agent_definitions (id, name, provider, is_seeded)
2874                VALUES ('pre-existing', 'Old Template', 'claude', 1);",
2875        )
2876        .unwrap();
2877
2878        run_object_schema(&conn).unwrap();
2879        // Idempotent — second pass must not error and must not
2880        // re-default existing rows.
2881        run_object_schema(&conn).unwrap();
2882
2883        let hidden: i64 = conn
2884            .query_row(
2885                "SELECT user_hidden FROM db_agent_definitions WHERE id='pre-existing'",
2886                [],
2887                |r| r.get(0),
2888            )
2889            .unwrap();
2890        assert_eq!(
2891            hidden, 0,
2892            "ALTER must default existing rows to 0 (visible), never to 1",
2893        );
2894    }
2895
2896    #[test]
2897    fn legacy_alter_is_skipped_once_db_agent_definitions_is_gone() {
2898        // The other half of the gate: once the table doesn't exist at all
2899        // (a fresh v32+ install, or an upgraded store after m0029 has
2900        // already dropped it), the conditional ALTER block must not even
2901        // attempt the statement — "no such table" is not "duplicate
2902        // column" and would not be swallowed.
2903        let conn = Connection::open_in_memory().unwrap();
2904        conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap();
2905        run_object_schema(&conn).unwrap();
2906        assert!(!table_exists(&conn, "db_agent_definitions"));
2907    }
2908
2909    #[test]
2910    fn stamp_version_writes_pragma() {
2911        let conn = Connection::open_in_memory().unwrap();
2912        stamp_version(&conn, OBJECT_SCHEMA_VERSION).unwrap();
2913        let v: i64 = conn
2914            .query_row("PRAGMA user_version", [], |r| r.get(0))
2915            .unwrap();
2916        assert_eq!(v, OBJECT_SCHEMA_VERSION);
2917    }
2918
2919    #[test]
2920    fn check_schema_compat_refuses_newer_db_without_writing() {
2921        // `SPEC_DATA_CHANNELS_2026_05_24.md` §3.3 safety lock — if the
2922        // DB on disk was stamped by a newer AgentMux binary, this one
2923        // MUST refuse to open it. The split into
2924        // check_schema_compat + stamp_version (codex P1 on #1029)
2925        // ensures the check runs BEFORE any migration side effects:
2926        // legacy-table rename + seed-insert in `run_object_schema` are
2927        // mutating, and if we ran them first we'd partially alter a
2928        // newer DB before the error fired.
2929        let conn = Connection::open_in_memory().unwrap();
2930        conn.execute_batch("PRAGMA user_version = 99;").unwrap();
2931        let err = check_schema_compat(&conn, OBJECT_SCHEMA_VERSION, "objects.db")
2932            .expect_err("expected refusal");
2933        match err {
2934            StoreError::SchemaTooNew { db, found, expected } => {
2935                assert_eq!(db, "objects.db");
2936                assert_eq!(found, 99);
2937                assert_eq!(expected, OBJECT_SCHEMA_VERSION);
2938            }
2939            other => panic!("expected SchemaTooNew, got {other:?}"),
2940        }
2941        // Crucially, `check_schema_compat` made NO writes. The
2942        // `user_version` is still 99. (Before the split — when the
2943        // single function combined the check with the write — this
2944        // invariant held only by virtue of the early `return Err`
2945        // skipping the stamp; with the split the function is
2946        // structurally read-only, eliminating the risk that a
2947        // future refactor reintroduces a downgrade-corrupt path.)
2948        let v: i64 = conn
2949            .query_row("PRAGMA user_version", [], |r| r.get(0))
2950            .unwrap();
2951        assert_eq!(v, 99, "rejected DB must not have its user_version stamp overwritten");
2952    }
2953
2954    #[test]
2955    fn check_schema_compat_accepts_equal_or_lower_without_writing() {
2956        // check_schema_compat must NEVER write — it just gates
2957        // migrations. The on-disk `user_version` is untouched by it,
2958        // regardless of whether the verdict is accept or reject.
2959        // stamp_version is the only thing that writes, and only after
2960        // migrations succeed.
2961
2962        // Equal: check passes silently.
2963        let conn = Connection::open_in_memory().unwrap();
2964        conn.execute_batch(&format!("PRAGMA user_version = {};", OBJECT_SCHEMA_VERSION))
2965            .unwrap();
2966        check_schema_compat(&conn, OBJECT_SCHEMA_VERSION, "objects.db").unwrap();
2967        let v: i64 = conn
2968            .query_row("PRAGMA user_version", [], |r| r.get(0))
2969            .unwrap();
2970        assert_eq!(v, OBJECT_SCHEMA_VERSION);
2971
2972        // Lower (forward-migration path): check passes, version stays
2973        // at the OLD value — stamp_version is what bumps it after
2974        // migrations.
2975        let conn = Connection::open_in_memory().unwrap();
2976        conn.execute_batch("PRAGMA user_version = 1;").unwrap();
2977        check_schema_compat(&conn, OBJECT_SCHEMA_VERSION, "objects.db").unwrap();
2978        let v: i64 = conn
2979            .query_row("PRAGMA user_version", [], |r| r.get(0))
2980            .unwrap();
2981        assert_eq!(v, 1, "check_schema_compat must not write to user_version");
2982
2983        // Then stamp_version bumps it as the post-migration step.
2984        stamp_version(&conn, OBJECT_SCHEMA_VERSION).unwrap();
2985        let v: i64 = conn
2986            .query_row("PRAGMA user_version", [], |r| r.get(0))
2987            .unwrap();
2988        assert_eq!(v, OBJECT_SCHEMA_VERSION);
2989    }
2990
2991    #[test]
2992    fn test_filestore_migrations_idempotent() {
2993        let conn = Connection::open_in_memory().unwrap();
2994        run_filestore_migrations(&conn).unwrap();
2995        run_filestore_migrations(&conn).unwrap();
2996        assert!(table_exists(&conn, "db_wave_file"));
2997        assert!(table_exists(&conn, "db_file_data"));
2998    }
2999
3000    #[test]
3001    fn test_saga_log_migrations_idempotent() {
3002        let conn = Connection::open_in_memory().unwrap();
3003        run_saga_log_migrations(&conn).unwrap();
3004        run_saga_log_migrations(&conn).unwrap();
3005        assert!(table_exists(&conn, "saga"));
3006        assert!(table_exists(&conn, "saga_step"));
3007    }
3008
3009    /// Identity M4c-1: each store's actor-UID columns exist on a fresh
3010    /// store, and an existing store at the previous version gains them on
3011    /// reopen — through each store's real `open`, so the flat CREATE, the
3012    /// ALTER loop and the version bump are all exercised. A row written
3013    /// before the upgrade reads the column's default, `''` (unknown).
3014    #[test]
3015    fn m4c1_actor_uid_columns_on_fresh_and_upgraded_stores() {
3016        use super::super::store::Store;
3017        type Open = fn(&std::path::Path) -> Result<Store, StoreError>;
3018        let cases: [(&str, Open, i64, i64, &[(&str, &str)]); 3] = [
3019            (
3020                "identity-store.db",
3021                Store::open_identity_store,
3022                10,
3023                IDENTITY_STORE_SCHEMA_VERSION,
3024                &[
3025                    ("db_work_queue", "created_by_uid"),
3026                    ("db_cron_jobs", "created_by_uid"),
3027                    ("db_bundle_versions", "written_by_uid"),
3028                ],
3029            ),
3030            (
3031                "store.db",
3032                Store::open_shared,
3033                11,
3034                SHARED_STORE_SCHEMA_VERSION,
3035                &[
3036                    ("db_cron_jobs", "created_by_uid"),
3037                    ("db_bundle_versions", "written_by_uid"),
3038                ],
3039            ),
3040            (
3041                "objects.db",
3042                Store::open,
3043                38,
3044                OBJECT_SCHEMA_VERSION,
3045                &[("db_bundle_versions", "written_by_uid")],
3046            ),
3047        ];
3048        let dir = tempfile::tempdir().unwrap();
3049        for (file, open, old, new, cols) in cases {
3050            // M4c-1 was one bump each; later bumps (objects v40, durable
3051            // jekt) keep the upgrade from `old` covered.
3052            assert!(new > old, "{file}: the current version is past M4c-1's base");
3053            let user_version = |c: &Connection| -> i64 {
3054                c.query_row("PRAGMA user_version", [], |r| r.get(0))
3055                    .unwrap()
3056            };
3057
3058            // Fresh.
3059            let fresh = dir.path().join(format!("fresh-{file}"));
3060            drop(open(&fresh).unwrap());
3061            let conn = Connection::open(&fresh).unwrap();
3062            for (table, col) in cols {
3063                assert!(
3064                    column_exists(&conn, table, col),
3065                    "fresh {file}: {table}.{col}"
3066                );
3067            }
3068            assert_eq!(user_version(&conn), new);
3069            drop(conn);
3070
3071            // Upgraded: the same store as the previous version left it —
3072            // the columns absent, stamped `old` — with a version row in it.
3073            let path = dir.path().join(file);
3074            drop(open(&path).unwrap());
3075            let conn = Connection::open(&path).unwrap();
3076            for (table, col) in cols {
3077                // Rebuilt without the column: SQLite's `DROP COLUMN` fails
3078                // to re-parse these tables' commented CREATE text.
3079                let keep: Vec<String> = conn
3080                    .prepare(&format!("PRAGMA table_info({table})"))
3081                    .unwrap()
3082                    .query_map([], |r| r.get::<_, String>(1))
3083                    .unwrap()
3084                    .map(Result::unwrap)
3085                    .filter(|c| c != col)
3086                    .collect();
3087                conn.execute_batch(&format!(
3088                    "CREATE TABLE m4c1_old AS SELECT {} FROM {table};
3089                     DROP TABLE {table};
3090                     ALTER TABLE m4c1_old RENAME TO {table};",
3091                    keep.join(", ")
3092                ))
3093                .unwrap();
3094                assert!(!column_exists(&conn, table, col));
3095            }
3096            conn.execute_batch(&format!(
3097                "PRAGMA user_version = {old};
3098                 INSERT INTO db_bundle_versions
3099                     (id, bundle_id, name, instructions, content_hash)
3100                 VALUES ('v-old', 'b', 'n', 'i', 'h');"
3101            ))
3102            .unwrap();
3103            drop(conn);
3104
3105            drop(open(&path).unwrap());
3106            let conn = Connection::open(&path).unwrap();
3107            for (table, col) in cols {
3108                assert!(
3109                    column_exists(&conn, table, col),
3110                    "upgraded {file}: {table}.{col}"
3111                );
3112            }
3113            assert_eq!(user_version(&conn), new, "{file}");
3114            let uid: String = conn
3115                .query_row(
3116                    "SELECT written_by_uid FROM db_bundle_versions WHERE id = 'v-old'",
3117                    [],
3118                    |r| r.get(0),
3119                )
3120                .unwrap();
3121            assert_eq!(
3122                uid, "",
3123                "{file}: a pre-upgrade row is unknown, never guessed"
3124            );
3125        }
3126    }
3127}