Skip to main content

Store

Struct Store 

Source
pub struct Store {
    pub(super) conn: Mutex<Connection>,
    registry: Mutex<Option<Arc<Registry>>>,
    def_registry: Mutex<Option<Arc<DefinitionStore>>>,
    pub(super) token_index: Mutex<Option<Arc<TokenIndex>>>,
    wan_identity: Mutex<Option<Arc<WanIdentityStore>>>,
    registry_agents_base: Mutex<Option<PathBuf>>,
    pub(super) muxbus_save_lock: Mutex<()>,
    global_memory_observer: Mutex<Option<Arc<dyn Fn(GlobalMemoryChange) + Send + Sync>>>,
}
Expand description

SQLite-backed object store for StoreObj types.

Fields§

§conn: Mutex<Connection>

pub(super) so sibling subsystem modules (e.g. bundles) can take the lock. Each per-subsystem file adds methods to Store via impl Store {} and needs the connection.

§registry: Mutex<Option<Arc<Registry>>>

Cross-version named-agent registry. None for in-memory test stores; Some for production srv. Mutations to db_agent_instances parallel-write to this registry when set. See docs/specs/SPEC_SHARED_AGENT_REGISTRY_2026_05_12.md.

§def_registry: Mutex<Option<Arc<DefinitionStore>>>

GLOBAL (cross-channel) agent-definition store. None for in-memory test stores and when the shared dir can’t be resolved; Some for production srv. Definition mutations mirror to it so an agent created in one channel is visible in every channel (cross-channel agent persistence, docs/specs/SPEC_CROSS_CHANNEL_AGENT_PERSISTENCE_2026-06-13.md).

§token_index: Mutex<Option<Arc<TokenIndex>>>

token → UID for attributing requests (identity M4a). None except on the channel’s object store at runtime; see attach_token_index.

§wan_identity: Mutex<Option<Arc<WanIdentityStore>>>

The channel-wide WAN identity store (wan-identity/wan.db). None except on the channel’s object store at runtime, and when the file couldn’t be opened — WAN signing is then off. See wan_identity.rs.

§registry_agents_base: Mutex<Option<PathBuf>>

Base directory that named-instance working_directory values are expressed relative to in the instance registry (write side: registry_mirror; read side: the listnamedagents handler).

This is the current channel’s agents dir (channels/<ch>/agents, i.e. AGENTMUX_AGENTS_DIR). It must be tracked separately from the registry’s own file location because P0.3 re-roots the registry to the global ~/.agentmux/shared/agents/registry/ — once the registry no longer sits under channels/<ch>/agents/, its parent (agents_root()) stops coinciding with the channel agents dir, and using it to strip / re-join working_directory would drop every live instance.

In production it is wired from AGENTMUX_AGENTS_DIR in main.rs (P0.3b), atomically with the re-root to the global shared registry — which is why the wiring waited for the re-root: setting it earlier would diverge in dev mode, where AGENTMUX_AGENTS_DIR ≠ the (then channel-local) registry parent. None only for in-memory test stores and odd envs where the var is unset; the accessor then falls back to the registry’s parent (which equals the channel agents dir in the pre-re-root layout), so existing mirror tests are unchanged. When set, it is passed in explicitly — never read from ambient env inside the Store — so tests running inside an AgentMux pane don’t pick up the host’s AGENTMUX_AGENTS_DIR. See docs/specs/SPEC_CROSS_CHANNEL_AGENT_PERSISTENCE_2026-06-13.md (P0.3).

§muxbus_save_lock: Mutex<()>

Serializes muxbus_save end-to-end (keychain read, keychain write, SQL write, and any rollback) — reagent P1 on #2260: two concurrent muxbus_save calls (the muxbus.login RPC handler and the broker’s registered refresh closure can each call it independently) would otherwise race on the keychain’s “previous state” snapshot; if the second call’s SQL write then failed, its rollback could restore a stale pre-first-call blob, silently reverting a credential the first call already committed. self.conn’s own lock only covers the SQL portion, not the keychain read/write either side of it, so a dedicated lock is needed rather than reusing that one. pub(super) like conn — muxbus.rs is a sibling module under backend::storage.

§global_memory_observer: Mutex<Option<Arc<dyn Fn(GlobalMemoryChange) + Send + Sync>>>

Told about every change to a Global Memory entry or its order, after it commits — the Global Memory record’s write-through (backend::global_memory_record, SPEC_MEMORY_FOLLOWS_THE_AGENT_2026_09_24.md §2.1.6). None except on the channel’s Global Memory store at runtime.

Implementations§

Source§

impl Store

Source

pub fn agent_activity_summary_get( &self, definition_id: &str, ) -> Result<Option<AgentActivitySummary>, StoreError>

Source

pub fn agent_activity_summary_set( &self, definition_id: &str, summary: &str, updated_at: i64, ) -> Result<(), StoreError>

Upsert — a definition’s summary is generated at most once in the common case, but an upsert (rather than plain INSERT) keeps this safe against the rare double-generation race the Ambient Model Call gateway doesn’t fully close (two independent process starts, or a restart mid-generation).

Source§

impl Store

Source

pub fn agent_bundle_ids( &self, agent_id: &str, ) -> Result<Vec<String>, StoreError>

The bundles picked for agent_id, in order. Not the agent’s own.

Source

fn agent_bundles_fold_startup_pick(&self, agent_id: &str)

Append an old Startup-tab pick to the list, then remove it. Best effort: on a failure the pick stays, to be folded on a later read.

Source

fn agent_bundle_ids_stored( &self, agent_id: &str, ) -> Result<Vec<String>, StoreError>

Source

pub fn agent_bundles_set( &self, agent_id: &str, bundle_ids: &[String], ) -> Result<Vec<String>, StoreError>

Replace agent_id’s picks with bundle_ids, in that order. Blank ids, repeats and the agent’s own bundle are dropped. Returns what was stored.

Source

pub fn agent_bundles_forget_bundle( &self, bundle_id: &str, ) -> Result<usize, StoreError>

Take a deleted bundle out of every agent’s list. Returns the rows removed.

Source

pub fn agent_bundle_chain(&self, agent_id: &str) -> Vec<String>

The agent’s whole list: its own bundle first (when it has one), then its picks. What launch unions skills and MCP servers over.

Source§

impl Store

Source

pub fn agent_credential_load( &self, agent_id: &str, ) -> Result<Option<AgentCredential>, StoreError>

Source

pub fn agent_credential_save( &self, agent_id: &str, client_id: &str, client_secret: &str, token_endpoint: &str, ) -> Result<(), StoreError>

Save a newly-provisioned client_id/client_secret (access_token/expires_at left at their defaults — the caller fetches a token separately via agent_credential_save_token).

Source

pub fn agent_credential_save_token( &self, agent_id: &str, access_token: &str, expires_at: i64, ) -> Result<(), StoreError>

Cache a freshly-fetched client_credentials access token against an already-provisioned agent. No-op if the agent has no row yet (the caller should provision first).

Source

pub fn agent_credential_invalidate_token( &self, agent_id: &str, ) -> Result<(), StoreError>

Clear a per-agent credential’s cached access token (client_id/secret are left intact — only the token is suspect). Used when the server rejects a request with 401 despite our cached expires_at still looking valid (revoked/rotated out-of-band): forces the next ensure_agent_credential call to re-fetch via fetch_m2m_token instead of retrying the exact same rejected token. No-op if the agent has no row. reagentx P1 on PR #2342.

Source

pub fn agent_credential_delete(&self, agent_id: &str) -> Result<(), StoreError>

Delete one agent’s credential row outright — for a client the cloud no longer recognizes (invalid_client), where keeping the client_id/secret would only fail again. The next ensure_agent_credential re-provisions it. No-op if there is no row. docs/specs/SPEC_CLOUD_SETTINGS_DISCOVERY_2026_09_27.md §3.4.

Source

pub fn agent_credentials_clear_all(&self) -> Result<(), StoreError>

Wipe every cached per-agent M2M credential. db_agent_credentials carries no account/user_sub column of its own — each row is only ever meaningful under the muxbus account that provisioned it — so a genuine account switch (muxbus_save with a different user_sub) or a logout (muxbus_clear) must wipe this cache wholesale; otherwise a stale row keeps authenticating this agent_id’s requests as the PREVIOUS account after the human has switched to a different one. Re-provisioning is cheap and idempotent on the cloud side, so clearing on every account transition — not just the ones that turn out to matter — is the safe default. reagentx P0 on PR #2342.

Source

pub fn agent_credentials_purge_agent( &self, agent_id: &str, ) -> Result<usize, StoreError>

Every channel’s row for one agent: what deleting the agent removes.

Source

fn agent_credentials_clear_prefix(&self, prefix: &str) -> Result<(), StoreError>

The rows behind prefix (a channel’s <channel>/), or, with an empty prefix, the unprefixed ones.

Source§

impl Store

Source

pub fn agent_group_create( &self, id: &str, name: &str, member_ids: &[String], created_at: i64, ) -> Result<(), StoreError>

Source

pub fn agent_group_list(&self) -> Result<Vec<AgentGroup>, StoreError>

Source

pub fn agent_group_get( &self, id: &str, ) -> Result<Option<AgentGroup>, StoreError>

Source

pub fn agent_group_update( &self, id: &str, name: Option<&str>, member_ids: Option<&[String]>, ) -> Result<bool, StoreError>

Partial update: None fields are left unchanged. Returns false if no row matched id (caller should surface a not-found error).

Source

pub fn agent_group_delete(&self, id: &str) -> Result<bool, StoreError>

Source§

impl Store

Source

pub fn agent_jekt_key_load( &self, agent_id: &str, ) -> Result<Option<Vec<u8>>, StoreError>

Load this agent’s signing key if one has already been minted, without creating one. Returns raw key bytes (decoded from the stored base64).

Source

pub fn agent_jekt_key_expires_at( &self, agent_id: &str, ) -> Result<Option<i64>, StoreError>

When this agent’s jekt key expires (unix seconds): its creation plus JEKT_KEY_TTL_SECS. Until then no ensure rotates it, so a holder of the key needs nothing newer before this time (identity M4d-3).

Source

fn agent_jekt_key_load_with_created_at( &self, agent_id: &str, ) -> Result<Option<(Vec<u8>, i64)>, StoreError>

Load this agent’s signing key together with its mint/last-rotation time, if one has already been minted.

Source

pub fn agent_jekt_key_ensure( &self, agent_id: &str, ) -> Result<Vec<u8>, StoreError>

Return this agent’s signing key, minting and persisting a fresh random one on first use. Race-safe under concurrent first-use: the insert is OR IGNORE, and the winning row (whichever call actually landed first) is always what gets read back and returned, so two concurrent callers for the same never-before-seen agent_id agree on one key instead of each minting and using a different one.

A key older than JEKT_KEY_TTL_SECS is rotated here rather than returned as-is — see docs/specs/SPEC_JEKT_HOST_KEY_TTL_ROTATION_2026_09_14.md.

Source§

impl Store

Source

pub fn attach_token_index(&self) -> Result<Arc<TokenIndex>, StoreError>

Build this store’s TokenIndex from db_agent_tokens and attach it, so mint and revoke keep it current from here on. Called once, on the channel’s object store at boot — never on the shared, identity or migration-time stores, which must not grow an index of their own.

Source

pub fn token_index(&self) -> Option<Arc<TokenIndex>>

The attached TokenIndex, if this store has one.

Source

pub fn agent_token_load(&self, uid: &str) -> Result<Option<String>, StoreError>

This agent’s token if one has been minted, without minting.

The verification read (M4) is the production caller; until it lands only tests read this, hence the dead_code allowance outside test builds. Deletion does not go through here — purge_agent_dependents covers db_agent_tokens directly.

Source

pub fn agent_token_ensure(&self, uid: &str) -> Result<String, StoreError>

This agent’s token, minting and persisting one on first use.

Race-safe under concurrent first use, the same way agent_jekt_key_ensure is: the insert is OR IGNORE and the row is re-read regardless of whether this call’s insert won, so two spawns of a never-before-seen agent agree on one token instead of each holding a different one. An empty uid is refused rather than given a token: a token keyed by "" would be shared by every caller that failed to resolve an identity, which is the shared-credential defect this table exists to remove.

Source§

impl Store

Source

pub fn agent_lan_key_load( &self, agent_id: &str, ) -> Result<Option<AgentLanKeypair>, StoreError>

Load this agent’s LAN keypair if one has already been minted, without creating one.

Source

pub fn agent_lan_public_key_load( &self, agent_id: &str, ) -> Result<Option<String>, StoreError>

Load only this agent’s PUBLIC key — the half safe to hand to a LAN peer’s pubkey-lookup query (see LanDiscovery::find_agent_lan_pubkey and the GET /agentmux/reactive/agent handler it calls). Never loads or touches the private half.

Source

pub fn agent_lan_key_ensure( &self, agent_id: &str, ) -> Result<AgentLanKeypair, StoreError>

Return this agent’s LAN keypair, minting and persisting a fresh random one on first use. Race-safe under concurrent first-use — same INSERT OR IGNORE + re-read pattern as agent_jekt_key_ensure: two concurrent callers for the same never-before-seen agent_id agree on one keypair instead of each minting and using a different one.

Source§

impl Store

Source

pub fn agent_uid_keys_ensure( &self, uid: &str, ) -> Result<Option<(AgentUidKeys, [UidKeyOrigin; 2])>, StoreError>

The UID’s LAN and WAN keypairs, copied or minted on first use (§6.5.10 M4d-2). One critical section: the row is re-read inside the lock, so a row purged since the caller read it gets nothing (Ok(None)) and no private key is left without an owner.

Source

pub fn agent_uid_lan_public_key_load( &self, uid: &str, ) -> Result<Option<String>, StoreError>

The UID’s LAN public key, if it has one; never creates one.

Source§

impl Store

Source

pub fn agent_wan_key_load( &self, agent_id: &str, ) -> Result<Option<AgentWanKeypair>, StoreError>

Load this agent’s WAN keypair if one has already been minted, without creating one.

Source

pub fn agent_wan_key_ensure( &self, agent_id: &str, ) -> Result<AgentWanKeypair, StoreError>

Return this agent’s WAN keypair, minting and persisting a fresh random one on first use. Race-safe under concurrent first-use — same INSERT OR IGNORE + re-read pattern as agent_lan_key_ensure: two concurrent callers for the same never-before-seen agent_id agree on one keypair instead of each minting and using a different one.

Note there is deliberately no TTL/rotation branch here yet, unlike agent_jekt_key_ensure. Rotation needs the continuity chain described in the spec’s §3.6 (a new key signed by its predecessor) to be distinguishable from a registry rewrite; rotating before that exists would trade one gap for a worse one.

Source§

impl Store

Source

pub fn agent_native_memory_list_all_rows( &self, ) -> Result<Vec<NativeMemoryMirrorRowWithAgent>, StoreError>

List every db_agent_native_memory row across every agent, content included — used only by the one-time version-history backfill migration (migrations::m0023_native_memory_versions_backfill). Not for any live RPC path (those are always scoped to one agent_id, and this loads full content for every row at once).

Source

pub fn agent_native_memory_list_meta( &self, agent_id: &str, ) -> Result<Vec<NativeMemoryMirrorRow>, StoreError>

List every mirrored file for agent_id (no content — each row’s content is empty, so a list view never loads full file bodies; agent_native_memory_read fetches one file’s body).

Source

pub fn agent_native_memory_read( &self, agent_id: &str, filename: &str, ) -> Result<Option<String>, StoreError>

Read one mirrored file’s content by (agent_id, filename). Used as the read-path fallback when the file is missing from the live FS (e.g. a different channel wrote it, or the live folder was wiped).

Source

pub fn agent_native_memory_upsert( &self, agent_id: &str, filename: &str, content: &str, metadata_type: Option<&str>, last_seen_path: &str, size_bytes: i64, mtime_ms: i64, ) -> Result<(), StoreError>

Upsert a file’s current live-FS content into the mirror. Called from every agent:memory:list/read_file/write_file RPC on every file it touches — cheap (one local SQLite write already on the request), and is what makes native memory durable across a channel switch.

size_bytes is the file’s REAL on-disk size, not necessarily content.len() — content may be truncated at the RPC layer’s MAX_MEMORY_FILE_BYTES cap for a file Claude Code wrote directly (bypassing write_file’s own size guard). Storing the real size (not the capped one) is what lets agent:memory:list’s change-detection via (size_bytes, last_seen_mtime_ms) still work correctly for an oversized file — comparing against a capped mirror size would never match the live (larger) size, forcing a full re-read + rewrite on every single list() call forever (reagent P1 on PR #2459, fourth pass).

mtime_ms is the live file’s own on-disk modified time (0 if unavailable, e.g. a write_file call that hasn’t re-stat’d the file it just wrote) — stored as last_seen_mtime_ms for the same change-detection purpose.

Source§

impl Store

Source

pub fn agent_native_memory_version_insert( &self, agent_id: &str, filename: &str, content: &str, source: &str, source_detail: &str, session_id: &str, ) -> Result<NativeMemoryVersion, StoreError>

Record a new version of (agent_id, filename)’s content, chained onto whatever version was previously latest for this file (None for the first write ever recorded). Always inserts, even when content is byte-identical to the previous version — simplicity over cleverness; dedup-by-hash is a possible future optimization, not this one.

reagent P2 (re-review): the read-latest-then-insert sequence is a single connection-lock acquisition, not two — an earlier revision released the lock between reading parent_version_id and inserting, so two truly concurrent calls for the same (agent_id, filename) (e.g. two panes of the same agent identity writing at once) could both read the same latest id and insert sibling versions instead of a linear chain.

reagent P2 (second re-review, PR #2674): an in-process Mutex only serializes callers sharing this one Store/Connection — it does nothing for two SEPARATE srv processes (e.g. two channels/ instances of AgentMux) each holding their own Connection to the same shared-store SQLite file. Those two connections’ SELECT-latest and INSERT could still interleave across processes, producing sibling versions with the same parent_version_id. Wrapping the read+insert in a BEGIN IMMEDIATE transaction closes that gap at the SQLite level: it acquires the RESERVED lock up front (before the SELECT runs), so a second connection’s own BEGIN IMMEDIATE blocks (up to busy_timeout, already configured at open) until this one commits — true cross-process mutual exclusion, unlike a plain deferred transaction (whose lock isn’t acquired until the first write, after the read has already happened).

Source

pub fn agent_native_memory_version_insert_if_changed( &self, agent_id: &str, filename: &str, content: &str, source: &str, source_detail: &str, session_id: &str, ) -> Result<Option<NativeMemoryVersion>, StoreError>

Atomic compare-and-insert: within a SINGLE connection-lock acquisition, reads the latest version’s hash for (agent_id, filename) and inserts a new version only if content’s hash differs (or no version exists yet). Returns Ok(None) when nothing was inserted (content unchanged).

Exists specifically for native_memory_drift.rs’s two concurrent detectors (fast fs-watch path, slow reconciliation sweep) — reagent P2 on PR #2675: a separate agent_native_memory_version_latest call followed by a separate agent_native_memory_version_insert call (each independently acquiring and releasing the connection lock) is NOT atomic as a compound operation, even though each individual call is serialized — two concurrent callers can both observe the same stale “latest” between their own read and write, producing duplicate version rows for one actual out-of-band change. A caller that doesn’t need this compare-and-swap guarantee (e.g. an explicit RPC write, where every call is expected to record a new version regardless — see agent_native_memory_version_insert’s own doc) should keep using the plain method above instead.

Same cross-connection hardening as agent_native_memory_version_ insert above (reagent P2 on PR #2674, applied here proactively for the identical read-then-insert shape): the read+insert runs inside a single BEGIN IMMEDIATE transaction, not just a single connection lock, so two separate srv processes/channels racing this same compare-and-insert can’t both observe the same stale “latest” either.

Source

pub fn agent_native_memory_version_list( &self, agent_id: &str, filename: &str, ) -> Result<Vec<NativeMemoryVersionSummary>, StoreError>

List every version of (agent_id, filename), newest first — no content (list-view; see NativeMemoryVersionSummary’s own doc).

Source

pub fn agent_native_memory_version_latest( &self, agent_id: &str, filename: &str, ) -> Result<Option<NativeMemoryVersion>, StoreError>

The most recently recorded version of (agent_id, filename), or None if no version has ever been recorded. Used both to derive a new write’s parent_version_id and (§4.5 drift detection, not yet built) to compare a live file’s current hash against the last known version.

Source

pub fn agent_native_memory_versions_for_agent( &self, agent_id: &str, ) -> Result<Vec<NativeMemoryVersion>, StoreError>

Every version of every file of agent_id, with content, oldest first — the one-time import into the agent’s memory record (SPEC_MEMORY_FOLLOWS_THE_AGENT_2026_09_24.md §2.1.1).

Source

pub fn agent_native_memory_version_list_distinct_files( &self, ) -> Result<Vec<(String, String)>, StoreError>

Every distinct (agent_id, filename) pair with at least one recorded version — the retention sweep’s (native_memory_retention.rs) work list, so it doesn’t need its own separate source of “which files have history” (the version table itself is authoritative for that).

Source

pub fn agent_native_memory_version_relabel_rollout_first_sight( &self, ) -> Result<usize, StoreError>

Prune old versions of (agent_id, filename) per the hybrid age + min-count-floor retention policy (native_memory_retention.rs, spec §7.1): a version is deleted only if it is BOTH older than max_age_ms AND ranked beyond the min_keep most-recent versions for this file — so a rarely-touched file never loses its entire history purely because every version happens to be old, and a hyperactive file never grows unbounded purely because every version happens to be recent. Returns the number of rows deleted. Repair the mislabeled rollout backfill — see migrations::m0024_native_memory_backfill_from_fs for the full story. In short: m0023 was supposed to tag every pre-existing memory file agent_inferred, but it iterated the db_agent_native_memory MIRROR, which is populated lazily and is empty on a typical install — so it backfilled nothing, and the drift sweep (which reads the real filesystem) claimed those same files milliseconds later as external_fs_write, i.e. “detected outside AgentMux — provenance unknown”. Innocent files, alarming label.

The predicate is deliberately narrow, because relabeling a provenance claim is not something to do speculatively. All four must hold:

  • source = 'external_fs_write' and detected_via = 'reconciliation_sweep' — the slow path, which is the only one that can observe a file it has never seen before at startup;
  • parent_version_id IS NULL — a FIRST-EVER version for that (agent_id, filename), i.e. genuine first sight rather than an observed change to something already tracked;
  • created_at inside a short window starting at m0023’s own applied_at — the load-bearing clause, since a memory file created out-of-band next week is also first-sight-via-sweep and that one is a real external write whose label must stand.
§The limit of the window, stated plainly

Inside the window this cannot tell the rollout burst from a genuinely new out-of-band file, and will relabel both (ReAgent P2 on PR #3055 — an earlier revision of this comment claimed an absolute guarantee it does not deliver). Nothing in the row or the file distinguishes them: mtime is no help either, since a burst row’s file may have been legitimately edited since.

What makes the trade acceptable is the shape of the two errors, not their likelihood alone. Getting it wrong costs one just-created file a benign “Agent” label instead of “detected outside AgentMux”, on an install where the user is watching a fresh upgrade. Not acting costs every pre-existing memory file on every affected install a permanent security-flavored warning it did not earn — which is the bug this repairs. The window is kept as small as the evidence allows so the ambiguous region stays tiny.

source_detail keeps the original detected_via and records that the row was relabeled, so the audit trail says what happened rather than quietly presenting the new claim as if it were the original observation.

Returns the number of rows relabeled. A no-op (returns 0) when m0023 never ran — nothing to repair on a fresh install.

Source

pub fn agent_native_memory_version_prune( &self, agent_id: &str, filename: &str, min_keep: u32, max_age_ms: i64, ) -> Result<usize, StoreError>

Source

pub fn agent_native_memory_version_get( &self, version_id: &str, ) -> Result<Option<NativeMemoryVersion>, StoreError>

Read one version’s full content by id — for diff/revert, which need exactly one or two full bodies, not a whole file’s history.

Source§

impl Store

Source

pub fn jekt_held_insert( &self, held: &HeldJekt, ) -> Result<HoldOutcome, StoreError>

Hold held, unless its request_id is already held or a cap is reached — decided under the one connection lock, so two concurrent holds cannot both slip under a cap.

Source

pub fn jekt_held_delete_expired(&self, now_ms: i64) -> Result<usize, StoreError>

Delete held messages past their expiry; returns how many.

Source

pub fn jekt_held_targets(&self) -> Result<Vec<String>, StoreError>

Every target UID with a held message, the one waiting longest first.

Source

pub fn jekt_held_for_target( &self, target_uid: &str, limit: usize, ) -> Result<Vec<HeldJekt>, StoreError>

One target’s held messages, oldest first, at most limit.

Source

pub fn jekt_held_delete(&self, request_id: &str) -> Result<(), StoreError>

Remove one held message (delivered, or dropped).

Source

pub fn jekt_held_record_failure( &self, request_id: &str, error: &str, ) -> Result<i64, StoreError>

Count a failed replay; returns the new attempt count.

Source§

impl Store

Source

pub fn lan_peer_pubkey_pin_get_or_set( &self, agent_id: &str, observed_key_b64: &str, ) -> Result<String, StoreError>

Get-or-pin: if a pin already exists for this agent_id, returns it UNCHANGED regardless of what observed_key_b64 says (the whole point — a later, different key must never silently overwrite an established pin). If no pin exists yet, pins observed_key_b64 and returns it. Callers compare the return value against observed_key_b64: equal means “trusted, use it”; a mismatch means “this claimed sender’s key just changed unexpectedly” — a red flag, not a value to fall back to. Race-safe under concurrent first-use via the same INSERT OR IGNORE + re-read pattern agent_jekt_key_ensure/agent_lan_key_ensure already establish.

Source§

impl Store

Source

pub fn lan_peer_uid_pin_get_or_set( &self, agent_id: &str, uid: &str, observed_key_b64: &str, ) -> Result<String, StoreError>

Get-or-pin, as lan_peer_pubkey_pin_get_or_set: an existing pin for (agent_id, uid) is returned unchanged, otherwise observed_key_b64 is pinned and returned. A return that differs from observed_key_b64 means the UID’s key changed under that name.

Source§

impl Store

Source

pub fn agent_def_get( &self, id: &str, ) -> Result<Option<AgentDefinition>, StoreError>

Fetch a single agent definition by primary key. Reads db_agents directly (the same source agent_def_list reads, via the shared private agent_row_get) — as of the definition flip this is no longer scoped to definitions/templates only: any db_agents row, including a bare template launch with no db_agent_definitions counterpart, resolves here, matching what agent_def_list already returns for it. Falls back to the global cross-channel registry when the row doesn’t exist locally at all.

Used by the template_promote migration’s deterministic-id idempotency check (see migrate_promote_template_sessions_v1): every retry asks “does the promote-target clone for this template already exist?” and either reuses it or inserts it.

Source

pub(crate) fn agent_def_get_strict( &self, id: &str, ) -> Result<Option<AgentDefinition>, String>

agent_def_get, but a failed read of the global definition registry is an error rather than “not found”. For callers that must refuse instead of acting on a partial answer — the name resolver treats “no such UID” as “send the bare name”, which a hidden fault must not trigger (Codex P2 on #3568).

Source

fn shared_def_get( &self, id: &str, ) -> Result<Option<AgentDefinition>, DefStoreError>

The global cross-channel definition for id, errors preserved.

Source

pub fn agent_def_list(&self) -> Result<Vec<AgentDefinition>, StoreError>

List all agent definitions, most-recently-used first.

Reads from the consolidated db_agents table, ordered by updated_at DESC then created_at ASC. Every definition mutation AND every instance lifecycle touch bumps db_agents.updated_at, so recency on a row tracks the last time the agent was either edited or launched.

Result-set shape: every db_agents row is returned — templates (is_template = 1) and user-clone projections (is_template = 0) each appear once. parent_id is sourced from db_agents.parent_template_id.

Source

pub fn agent_def_count(&self) -> Result<i64, StoreError>

Count agent rows (used by seed engine to check if seeding is needed). Reads from the consolidated db_agents table. The seed engine only cares about == 0 to decide “fresh database, seed templates”.

Source

pub(in backend::storage) fn agent_def_exists_local( &self, id: &str, ) -> Result<bool, StoreError>

Whether a definition with id exists in the LOCAL channel’s SQLite (db_agents). Gates the cross-channel content/skills fallback: a locally-known agent with genuinely empty content/skills must NOT resurrect them from the global record. (reagent P1 on #1385.)

Source

pub fn agent_def_delete_seeded(&self) -> Result<usize, StoreError>

Delete all seeded agents (is_seeded=1). Used by reseed to clear built-in agents.

No project-instruction cleanup here, unlike agent_def_delete: this removes template rows, and a template is never launched — observations are recorded by agent.open, so a template has none to forget (Codex, PR #3162).

Source

pub fn agent_def_insert( &self, agent: &mut AgentDefinition, ) -> Result<(), StoreError>

Insert a new agent definition. Auto-derives slug from name if empty, resolves collisions by appending -2, -3, etc., and mutates agent.slug so the caller sees the resolved value (important for handlers that serialize the struct back to the frontend after insert).

The collision check + insert run under a single mutex lock, so this is race-safe against concurrent inserts on the same connection.

Source

pub(in backend::storage) fn agent_def_insert_local_only( &self, agent: &mut AgentDefinition, updated_at_override: Option<i64>, ) -> Result<(), StoreError>

Insert into db_agents only — does NOT mirror to the global registry. Used directly by agent_def_insert (which mirrors right after) and by agent_def_backfill_local_from_registry (which must NOT mirror — see that function’s doc comment for why re-mirroring immediately after this call would wipe the registry’s real content/skills).

updated_at_override: None stamps updated_at = created_at (the original, unchanged behavior for genuinely new definitions). Some(ts) stamps updated_at = ts instead — used by the backfill path to preserve the registry record’s real updated_at rather than resetting it.

is_seeded = 1 rows become is_template = 1 (a template; bindings stay empty). is_seeded = 0 rows become is_template = 0 with parent_template_id = parent_id — same is_template/parent_template_id derivation the old dual-write mirror used (frozen intent, no longer a second write).

Source

pub(in backend::storage) fn agent_def_backfill_local_from_registry( &self, record: &DefinitionRecord, ) -> Result<(), StoreError>

Materialize a local shadow of a registry-only definition — this channel’s db_agents row plus a best-effort local copy of content/skills — so instance_create’s FK can succeed for an agent whose definition exists cross-channel but was never created in THIS channel’s SQLite.

Deliberately does NOT call registry_def_upsert (unlike agent_def_insert, which does): the registry is already authoritative for this record. registry_def_upsert rebuilds the registry record from agent_content_get_all_local/ agent_skill_list_local — LOCAL-only reads, by design (see that function’s own comment) — so calling it immediately after this insert-with-no-content-yet would overwrite the registry’s real content/skills with empty arrays. Content/skill rows are instead copied here via direct INSERTs (bypassing agent_content_set/ agent_skill_insert, which each call registry_def_refresh_if_mirrored themselves) so nothing re-mirrors mid-backfill.

Copy failures for content/skills are logged and otherwise ignored — the definition row alone satisfies the FK, and reads still resolve content/skills correctly via the existing cross-channel fallback even if this best-effort local copy is incomplete. A slug collision against an unrelated local agent will rename the local slug (existing agent_def_insert_local_only behavior) — low-probability and non-fatal, since slug isn’t the FK target.

Source

pub fn agent_def_find_or_insert( &self, agent: &mut AgentDefinition, ) -> Result<Option<AgentDefinition>, StoreError>

Atomic check-then-insert for agent.define.

Looks up an existing user-owned definition by name (case-insensitive) or derived slug under the SAME mutex guard that protects the INSERT — preventing TOCTOU when two concurrent agent.define calls arrive for the same name.

Returns:

  • Ok(Some(def)) — an existing row matched; agent was NOT inserted.
  • Ok(None) — no match; agent was inserted and agent.slug now holds the collision-resolved slug.
Source

pub fn agent_def_set_hidden( &self, id: &str, hidden: bool, ) -> Result<bool, StoreError>

Set the user_hidden flag on a single agent definition. Phase 2 of the two-tier picker (Q2 Decision Y). Returns: Ok(true) — row updated. Ok(false) — no row with that id exists. Err(...) — the row exists but is NOT a seeded template (is_seeded != 1). User-owned definitions go through agent_def_delete, not hide.

Does NOT bump updated_at: hide is a per-user view-state flag, not a definition-content edit. Keeps updated_at faithful to the agent’s payload (the manifest re-sync compares description etc. against the canonical row).

Source

pub fn agent_def_has_local_row(&self, id: &str) -> Result<bool, StoreError>

Whether this channel’s store has its own row for definition id, as opposed to the definition only resolving through the global registry overlay. Only a local row can have its default_memory_id set.

Source

pub fn agent_def_set_memory_id_if_empty( &self, id: &str, memory_id: &str, ) -> Result<bool, StoreError>

Set db_agents.default_memory_id on a LOCAL agent — but only if it’s currently empty. Exists because agent_def_update’s SET clause deliberately never touches this column (readonly-after-creation, see its own field doc comment) — this is the one narrow, explicit path allowed to set it, and only the FIRST time, for m0021’s backfill and definition-time provisioning to use. Returns Ok(true) if the row existed and had an empty default_memory_id (write applied), Ok(false) if the row doesn’t exist locally OR already has a non-empty value (no-op, not an error — matches the “set once” semantic silently rather than requiring every caller to pre-check).

LOCAL ONLY, deliberately — a cross-channel (global-registry-only) definition can’t be reached this way today because DefinitionRecordV1 doesn’t carry memory_id yet (same accepted gap as model_vendor_base_url, see def_registry_mirror.rs). Callers backfilling across the whole agent population must check agent_def_get first and skip (with a log) anything that only resolves via the global registry.

Source

pub(crate) fn resolve_effective_vendor( provider: &str, model_vendor_base_url: &str, ) -> String

The provider’s default vendor, or "custom" when the agent has a non-empty model_vendor_base_url override — same rule the frontend’s resolveEffectiveVendor (frontend/app/view/agent/providers/catalog.ts) already uses for the dual-icon vendor badge. Falls back to the provider id itself if the provider isn’t in the registry (e.g. a stale/custom provider string), same fallback the frontend uses.

Derived wherever it’s needed rather than stored: a Claude agent pointed at a custom endpoint is "custom", not "anthropic" (#2587).

Source

pub fn resolve_effective_provider_id(&self, agent: &AgentDefinition) -> String

The harness/provider id agent runs: its own provider. A bundle carries no harness (SPEC_AGENT_BUNDLE_FORMAT_V0_3_2026_10_05.md §3.1); the agent owns it, read-only after creation (updateagent and agent.define refuse a change). The bound bundle’s provider is a hint and is consulted only when the agent has none at all, a definition the §3.3 migration couldn’t reach; there it is the value the agent has always run with.

Call this on the EFFECTIVE identity/memory store (AppState.id_store), never on the per-channel mstore — that fallback reads the bundle, which lives in the shared store when one is configured. One shared implementation for spawn (agent_open.rs), the credential gate (identity/resolver/inject.rs), clone, fork and export, so they can’t disagree.

Source

pub fn bundle_provision_for_new_agent( &self, agent: &AgentDefinition, now: i64, ) -> Result<String, StoreError>

Provision a fresh, dedicated ABF bundle for a NEW agent definition — the definition-time half of docs/specs/ARCHITECTURE_MANDATORY_ABF_RETHINK_2026_08_14.md §3.2 (m0021 is the backfill half, for agents that already existed before this shipped). The bundle carries no harness: the agent owns its provider (SPEC_AGENT_BUNDLE_FORMAT_V0_3_2026_10_05.md §3.2); the agent is passed for the bundle’s name.

Call this on the EFFECTIVE identity/memory store (AppState.id_store), never on the per-channel mstore directly. Bundles live in the shared store when one is configured (the normal case) — every real bundle-read path (listmemories/getmemory/ the Memory editor/the bundle-summary panel) reads through id_store, so a bundle created via mstore.bundle_upsert directly would be written to a different SQLite file and be invisible everywhere else (P1 finding, Codex review on PR #2587, live in the shipped code this fixes: agent_def_provision_and_ bind_bundle’s callers all used to invoke this method ON mstore).

Does NOT mutate agent or touch db_agent_definitions — returns the new bundle’s id; callers set agent.memory_id themselves before their own insert (so the existing agent_def_insert* INSERT statements, which already include memory_id, pick it up with no separate write).

Source

pub(crate) fn resolve_unique_bundle_name( &self, base_name: &str, ) -> Result<String, StoreError>

Resolve a db_bundles.name guaranteed not to collide with an existing row — that column is TEXT NOT NULL UNIQUE, but only AgentDefinition.slug is guaranteed unique, not the display name, so a naive "{agent.name} — ABF" collides whenever two agents share a display name. Appends a numeric suffix on collision, same shape agent_def_insert_local_only already uses for slug collisions.

P1 fix (2026-08-15, ReAgent review on PR #2587 round 4): before this, a collision here silently left a runtime-provisioned agent unbound (the caller’s best-effort error handling swallowed it) and — worse — unconditionally ABORTED m0021’s entire backfill loop via ? on the very first collision, permanently stalling backfill for every agent processed after it, on every subsequent boot, until the underlying name collision was manually resolved.

Source

pub fn agent_def_provision_and_bind_bundle( &self, bundle_store: &Store, agent: &mut AgentDefinition, now: i64, )

Provision + bind a fresh bundle to an ALREADY-INSERTED agent definition — the two-step, post-insert counterpart to bundle_provision_for_new_agent above. Deliberately separate from the insert itself (rather than setting memory_id on the struct before calling agent_def_insert*/agent_def_find_or_insert): agent_def_find_or_insert in particular uses its AgentDefinition argument as BOTH the lookup key and the conditional-insert payload, so a bundle built up-front would go to waste (leaked, unbound) on every if_exists=skip/update call against an already-existing name — and agent.define is meant to be called repeatedly/ idempotently. Binding after the fact, only once the caller has confirmed a row was genuinely freshly inserted, avoids that leak.

Best-effort by design (matches createagent’s own color-assignment comment: a failure here shouldn’t fail agent creation) — logs and returns on either step’s failure rather than propagating, since the agent row itself is already durably committed by the time this runs, and a still-unbound agent is exactly the pre-existing memory_id='' state m0021 already knows how to backfill later.

Takes &mut AgentDefinition and writes the new bundle id back into agent.memory_id on success — same “caller’s struct reflects what landed” convention agent_def_update documents just below — so RPC handlers that serialize agent straight back to the caller (e.g. createagent, importagentfromclaw) return the real value instead of the empty string the struct held before this call.

self (the definition store, i.e. mstore) and bundle_store (the effective identity/memory store, i.e. AppState.id_store) are deliberately two SEPARATE parameters, not the same store used for both writes — see bundle_provision_for_new_agent’s own doc comment for why (P1 fix, Codex review on PR #2587: they used to be the same store, silently writing every provisioned bundle somewhere the rest of the app can’t see it).

Source

pub fn agent_def_update( &self, agent: &mut AgentDefinition, ) -> Result<bool, StoreError>

Update an existing agent definition (all fields except id, created_at, is_seeded, parent_id). parent_id is NOT updatable post-insert — it describes the agent’s lineage; re-parenting is done by creating a new fork, not mutating the original.

branch_label IS written here (unlike parent_id) — this storage- layer function persists whatever is on agent. Immutability for most callers is enforced one layer up, at the RPC handler: updateagent always passes back old.branch_label.clone() unchanged, so it’s a no-op for that caller. renameagentdefinitiontitle is the one deliberate exception that supplies a real change — see crates/srv/src/server/agent_handlers/template.rs and docs/specs/SPEC_PANE_TAB_STRIP_COMPACT_SIZING_AND_RENAME_2026_07_22.md §4.

Self-stamps updated_at with the current time and writes it back into agent.updated_at, so the caller’s struct (e.g. an RPC response body) reflects exactly what landed in the database.

Source

pub fn agent_def_delete(&self, id: &str) -> Result<bool, StoreError>

Delete a agent definition by id. Returns true if a row was deleted.

Source

pub(in backend::storage) fn scope_for_row( conn: &Connection, id: &str, ) -> RecordScope

The RecordScope every registry operation on id must use.

Read this before adding another registry call. The wide scope matches records by definition_id, and a TEMPLATE’s id is the definition_id of every legacy, never-re-keyed launch record belonging to a REAL agent launched from it — so a wide match on a template deletes / retires / renames those agents’ records along with the template, making them vanish from the picker. ReAgent found exactly that omission at four separate call sites on PR #3262 (agent_def_delete, instance_delete, instance_set_hidden, instance_rename), which is why the rule lives here rather than as a fourth hand-written if.

An id with no row at all is NOT a template: the global definition store only holds is_seeded == 0 user agents, so a cross-channel id is always an agent — which is what lets the delete paths still sweep on a retry after the local row is already gone.

Takes the connection rather than &self because every caller needs this read in the SAME critical section as its own UPDATE/DELETE — otherwise the row could change kind in between. A &self wrapper existed briefly and was dead on arrival for exactly that reason (ReAgent P2 on PR #3262).

Source

pub fn agent_dependents_purge(&self, id: &str) -> Result<usize, StoreError>

Purge this store’s rows keyed to a deleted agent, without touching db_agents itself. For the OTHER physical database an agent has rows in: agent_def_delete covers the object store it runs against, and the deleteagent handler calls this on state.identity_store, which is where the live db_agent_identity_links / db_agent_credentials / native-memory rows actually are (SPEC_IDENTITY_STORE_SPLIT_2026_08_17.md). See purge_agent_dependents for the table list and why one list serves both stores. Returns rows removed.

Source

pub(in backend::storage) fn purge_agent_side_effects( &self, id: &str, caller: &'static str, scope: RecordScope, )

The non-SQLite half of deleting an agent row: observations keyed to it, and its records in the host-global instance registry. Shared by Self::agent_def_delete and Self::instance_delete — the two names one deletion is reached by since the definition flip (see instance_delete’s own doc comment). Best-effort throughout: SQLite has already committed, and neither side failing is a reason to report the delete as failed. caller only labels the logs.

scope must come from scope_for_row, read BEFORE the row was deleted — afterwards there is nothing left to ask.

Source

pub fn agents_grandfather_ambient_login( &self, linked_agent_ids: &HashSet<String>, ) -> Result<(usize, usize), StoreError>

One-shot grandfather pass for the layer-3 ambient-login opt-in (m0017, spec §2.4 of SPEC_ACCOUNT_DELETE_DEAUTH_LAYERS_2_4_2026_07_14.md).

Agents WITHOUT any db_agent_identity_links row at migration time were de-facto ambient users → use_ambient_login = 1; agents WITH links opted into managed accounts → 0 (honest failure is the new behavior for them). linked_agent_ids comes from the SHARED store (the live links table); this method writes db_agents — the definition flip’s only remaining caller of this is m0017 on an install upgrading through the whole migration chain for the first time, where db_agents is already the sole live source. Returns (rows set to 1, rows set to 0).

Source§

impl Store

Source

pub fn instance_list( &self, definition_id: Option<&str>, status: Option<&str>, ) -> Result<Vec<AgentInstance>, StoreError>

List agents as instances. Both filters are optional — pass None to scan all. Ordered by updated_at descending, created_at as a tiebreaker (most recent activity first — every launch, continuation and lifecycle write bumps updated_at).

The definition_id filter matches the agent’s own id only: templates are not agents and user agents derived from one template are separate agents (see instance_list_named for the one caller that wants “everything derived from this template”). The status filter reads the row’s latest launch status (v29).

Source

pub fn instance_get( &self, id: &str, ) -> Result<Option<AgentInstance>, StoreError>

One agent by id, as an instance. None for a template or an unknown id.

Source

pub fn agent_last_runtime_get(&self, id: &str) -> Result<String, StoreError>

The Runtime menu choices the user last made for agent id (JSON, see normalize_last_runtime), or ‘’ when there are none or the agent is unknown or a template.

Source

pub fn agent_last_runtime_set( &self, id: &str, runtime: &str, ) -> Result<bool, StoreError>

Replace agent id’s remembered runtime. Returns false for an unknown agent or a template (templates are not agents anyone continues, so they remember nothing). Deliberately leaves updated_at alone: this is a note about how the agent was last run, not an edit of its definition.

Source

pub(in backend::storage) fn agent_row_get( &self, id: &str, ) -> Result<Option<AgentDefinition>, StoreError>

A single db_agents row in the AgentDefinition shape, no registry overlay — the definition a launch resolves against. Templates and user agents alike.

Source

pub fn instance_create( &self, inst: &AgentInstance, ) -> Result<AgentInstance, StoreError>

Record a launch. Returns the agent row the launch now lives on, as an instance — its id may differ from inst.id: a launch of a user agent folds into that agent’s row, and a continuation (non-empty parent_instance_id naming an existing row) folds into the row it continues. Only a fresh launch of a TEMPLATE creates a new row, keyed by inst.id. Callers must persist the returned id, not the one they passed (the frontend stores it as the block’s agentInstanceId).

inst.definition_id must name a db_agents row; when it only exists in the global cross-channel definition registry (an agent created in another channel), the local definition is backfilled first, exactly as before (REPORT_AGENT_DEFINITION_DB_GAP_2026_07_27.md). A definition that exists nowhere is StoreError::NotFound — the error the legacy FK used to raise, minus the FK.

Source

fn monotonic_updated_at(conn: &Connection, floor: i64) -> i64

An updated_at strictly greater than any already on db_agents, so recency ordering survives fast successive writes within one millisecond (reagent P2 round 3 on #1013 — the rule the dual-write used; inherited here).

Source

pub fn instance_set_hidden( &self, id: &str, hidden: bool, ) -> Result<bool, StoreError>

Set the hidden flag on an agent row. Used by the “Forget agent” affordance — soft-delete only; the row + working directory remain on disk for audit + recovery.

Cross-version case: an agent migrated into the registry from another version’s SQLite won’t have a row in the current version’s SQLite. The UPDATE returns 0 rows, but the registry still needs to flip — otherwise “Forget agent” silently no-ops for those. Returns true when either side acted.

Source

pub fn instance_rename( &self, id: &str, instance_name: &str, ) -> Result<bool, StoreError>

Rename the agent’s PICKER-VISIBLE display name (instance_name), in SQLite and across every registry record for it.

renameagentdefinitiontitle on its own writes name (or branch_label for a fork) — the fields the pane tab strip’s titleOf() reads. “My Agents” rows render instance_name || definition_name instead, and instance_name is non-empty for every launched or registry-backed row, so a rename closed the editor, refetched, and redisplayed the OLD name; for a fork it changed branch_label, which no picker row shows at all (codex P1 on PR #3262). Renaming an agent has to move the name the user is actually looking at, so the handler calls this too.

Scoped to rows that ALREADY have an instance_name: writing one onto an agent that has never had a named launch would make it appear in instance_list_named (which filters instance_name <> '') — conjuring a picker row rather than renaming one. Those rows fall back to definition_name, which the title update already moved.

Returns whether anything changed, counting the registry: a cross-channel agent has no local row to update but does have records.

Source

pub fn instance_list_named( &self, limit: usize, definition_id: Option<&str>, identity_id: Option<&str>, _include_continuations: bool, ) -> Result<Vec<AgentInstance>, StoreError>

Named agents for the launch modal’s “Continue agent” dropdown and the picker’s “My Agents” surface: non-hidden rows with an instance_name, newest launch first, capped by limit.

definition_id restricts the result to that agent itself OR every agent derived from it (parent_template_id) — the launch modal opens per template, and what a user wants to continue there is any agent they launched from it. identity_id filters on the row’s identity binding.

include_continuations is accepted for API compatibility but no longer changes the query: chains are pre-collapsed (one row per agent carrying its latest launch), so the legacy “heads only” and “latest per chain” modes both yield exactly this list.

Source

pub fn instance_get_by_slug( &self, slug: &str, ) -> Result<Option<AgentInstance>, StoreError>

Resolve an agent by its persisted slug — the App-API self-lookup (AGENTMUX_AGENT_ID is the slug) — only if exactly one agent has it. Matches ONLY slug; deliberately single-purpose after reagentx P1 on PR #2428 (round 2): a lookup that also matched another column let a coincidental cross-namespace collision return an unrelated agent’s memory, accounts or bundle. Hidden rows are excluded.

None means “no agent” or “more than one agent” — callers already treat both as “fall back to another source”, so they are not distinguished. The fallbacks are themselves fail-closed (agent_registry_lookup::find_active_record_by_slug), so refusing here routes to another refusal rather than to a guess.

Why ambiguity returns None rather than the most recently updated row: db_agents.slug has no UNIQUE constraint — migrations.rs declines the index deliberately. Both write paths now suffix-resolve (#3497 §4), so neither mints a duplicate, but nothing at the schema level prevents one: a future writer, a migration, or hand-edited data still can. (An earlier revision of this comment claimed instance_create copied its template’s slug onto every launch, so two launches collided. That was never true of the real launch flow — see the spec’s §2.5.2 — and is no longer true of the function either.) This function used to ORDER BY updated_at DESC LIMIT 1 and hand the winner’s definition_id and working_directory to the caller. HistoryService::sessions_for_agent consults it to resolve an agent’s conversation history, so guessing wrong does not degrade gracefully — it discloses one agent’s transcripts to another, and deterministically: the same wrong agent won every time. A slug simply does not identify an agent when it collides, so the honest answer is “unknown”. Same rule and same reasoning as crate::backend::agent_registry_lookup::find_active_record_by_slug (PR #3480), which this mirrors for db_agents.

Source

pub fn agents_matching_name( &self, typed: &str, ) -> Result<Vec<AgentNameMatch>, StoreError>

Every non-template, non-hidden agent a typed name could mean (identity M3, spec §5): exact slug, or name/instance_name case-insensitively. Lists rather than picks — the caller decides what to do with several.

Source

pub fn rows_answering_to( &self, name: &str, ) -> Result<Vec<AgentNameMatch>, StoreError>

Every row a name selects, templates and hidden rows included — the identity M4a-2 ambiguity check (spec §6.5.3). Unlike Self::agents_matching_name, which lists only the agents a typed name could mean, this asks whether the name is anyone else’s: slug collision suffixing (resolve_slug_collision) counts template rows, so a “Claude” made from the “Claude” template is claude-2, and a name its MCP sends as claude belongs to the template (#3573).

Source

pub fn agent_names_by_id( &self, id: &str, ) -> Result<Option<AgentNameMatch>, StoreError>

The names row id answers to — the by-id counterpart of Self::agents_matching_name, for checking a body’s actor name against the calling agent’s own row (identity M4a-2, spec §6.5.3). Any row, template or hidden: the caller’s token already names it.

Source

pub fn agent_was_deleted(&self, id: &str) -> bool

Whether id names an agent that was deleted: no local row (template or not), and the shared definition registry holds a record for it that is retired rather than active — deleting a user agent retires its global record (registry_def_retire). A template, a removed template and a live cross-channel agent all read false: templates are never mirrored, and a cross-channel agent’s record is active. With no shared registry this cannot be told and reads false. For the identity spawn gate (#3577).

Source

pub fn agent_row_ensure_local(&self, id: &str) -> Result<bool, StoreError>

Ensure a user agent id has a local db_agents row, backfilling it from the shared definition registry when it exists only there (a cross-channel agent) — the same backfill instance_create runs, without its fold. Identity M4b-4 (spec §6.5.8): so a block naming a cross-channel agent resolves to a local row at its spawn. Returns whether the row exists afterwards.

Source

pub fn instance_record_launch( &self, id: &str, block_id: &str, started_at: i64, ) -> Result<bool, StoreError>

Record a launch of user agent id on block_id: a lifecycle-only update — last_block_id, status = running, started_at, ended_at = 0, a monotonic updated_at and the registry mirror — the columns instance_create’s fold moves, and none of the ones it would overwrite from a caller’s record (name, identity_id, memory_id, instance_name, github_context, user_hidden). Identity M4b-4 (spec §6.5.8), for agent.open, which has no such record. Returns false for a template or a missing row.

Source

pub fn instance_update_partial( &self, id: &str, upd: &InstanceUpdate, ) -> Result<Option<AgentInstance>, StoreError>

Partial update of an agent’s launch state. Only Some fields are written; None leaves that column untouched. Some("") explicitly clears (the updateagentinstance command contract, and poison_resume’s deliberate stale-session clear).

Returns the post-update row. None is reserved for not-found; an all-None update on an existing id is a no-op that returns the unchanged row, so callers can tell “nothing to change” from “no such agent”. See SPEC_UPDATEAGENTINSTANCE_PARTIAL_UPDATE_2026_05_29.md.

Source

pub fn instance_repoint_definition( &self, old_def_id: &str, new_def_id: &str, ) -> Result<usize, StoreError>

Repoint every agent derived from old_def_id at new_def_id. Used by the Phase 1 two-tier-picker migration (SPEC_AGENT_PICKER_TWO_TIER_2026_05_24.md): when a seeded template has been used directly, the migration clones it into a user agent and repoints its launches so the existing reattach flow keeps working. Returns the number of rows updated.

Source

pub fn instance_delete(&self, id: &str) -> Result<bool, StoreError>

Delete an agent row (never a template). The row IS the agent in the consolidated model, so this removes the agent — previously a launch row of a user-clone definition could be deleted while the definition stayed; there is no such split any more. As of the definition flip, this and agent_def_delete are the same operation reached from two names: neither writes db_agent_definitions, and nothing reads it per-boot any more to resurrect what either one deletes (the startup gap-repair that once could is gone — codex P2 on #3080 closed the resurrection path this way rather than by re-adding a legacy-table delete here).

Source

pub fn instance_get_active_for_block( &self, block_id: &str, ) -> Result<Option<AgentInstance>, StoreError>

Resolve the agent bindings tied to a block.

Resolves through block.meta.agentId (or legacy agent:id) first — the block says which agent it shows — returning that agent’s row with block_id set to the asked-for block. Falls back to the agent whose latest launch is on this block and still active, which is what a block from before the block meta carried agentId needs.

block.meta.agentInstanceId never overrides agentId: codex P1 on PR #1114 round 3 surfaced that pane reuse can leave a stale one behind. It is consulted only for a block whose agentId names no agent row, and only within what that block names, while it is still the block’s latest launch (see the fallback below).

Source

pub fn instance_get_by_block_id( &self, block_id: &str, ) -> Result<Option<AgentInstance>, StoreError>

The agent whose latest launch is on block_id, regardless of status — with its real session_id, so callers that write back to that exact row (keeping session_id live as the CLI emits it — see persist_session_id / SPEC_PANE_CLOSE_REOPEN_CONTINUITY_GUARANTEE_2026_07_27.md §4.1) have the right id to pass to instance_update_partial.

When a pane is reused, several rows can name the block. The one the pane runs is the block’s own agentId (or legacy agent:id), while its latest launch is still this block; otherwise the most recent launch (started_at). Never the most recently updated row: a rename or definition edit bumps updated_at, and a session id then landed on the renamed agent instead of the one running (#3580).

Source§

impl Store

Source

pub fn retired_agent_names(&self) -> Result<Vec<String>, StoreError>

The names deleted agents were known by, lowercased as they were tombstoned before each delete. A fork’s automatic number counts them, so a deleted fork’s number is never handed out again, even the highest one (suggest_fork_name).

Source

pub fn agent_former_names(&self, id: &str) -> Result<Vec<String>, StoreError>

The names agent id had before its renames. A lineage root’s former names keep its deleted forks’ numbers counted after it is renamed (suggest_fork_name).

Source

pub fn agent_slugs_folded(&self, name: &str) -> Result<Vec<String>, StoreError>

The slugs of every row whose slug equals name trimmed and folded (identity M4d-3’s drift count; slug uniqueness is case-sensitive, so “Aria” and “aria” are two rows).

Source§

impl Store

Source

pub fn agent_ids_for_slug_folded( &self, name: &str, ) -> Result<Vec<String>, StoreError>

The ids of every row whose slug is name, trimmed and folded with Rust’s to_lowercase (identity M4d-6: which agent this instance knows by a name a jekt claims).

Source§

impl Store

Source

pub fn agent_defs_for_target( &self, target: &str, ) -> Result<Vec<AgentDefinition>, StoreError>

The definitions a message’s target names: the one whose slug it is, else every one whose display name routes to it (agent_route_name: a fork “Loap #3” is Loap-3, and a root Claude may hold the slug claude-2) (#4744). Folded like the key tables.

Source§

impl Store

Source

pub fn background_task_observe( &self, id: &str, block_id: &str, label: &str, started_at_ms: i64, now_ms: i64, ) -> Result<(), StoreError>

Create the row if it doesn’t exist yet (status running), and refresh last_seen_ms — but only while the row is still running. Never resurrects a row a concurrent background_task_complete call already marked terminal; the terminal status is left untouched.

Source

pub fn background_task_set_pid( &self, id: &str, pid: i64, ) -> Result<bool, StoreError>

Record the OS pid once known (bashwrap/process_tracker learn it after the task has already been observed as declared-background). No-op (returns false) if the row doesn’t exist.

Source

pub fn background_task_complete( &self, id: &str, status: BackgroundTaskStatus, ended_at_ms: i64, ) -> Result<bool, StoreError>

Mark a task terminal. Idempotent — calling this again on an already- terminal row just overwrites the status/ended_at (last write wins), which only matters if two different terminal signals race, an edge case not worth a CAS for a status transition with no live consumer depending on “first terminal write wins” today.

Source

pub fn background_task_get( &self, id: &str, ) -> Result<Option<BackgroundTask>, StoreError>

Source

pub fn background_task_list_for_block( &self, block_id: &str, ) -> Result<Vec<BackgroundTask>, StoreError>

Source

pub fn background_task_list_running( &self, ) -> Result<Vec<BackgroundTask>, StoreError>

Global — every still-running row across every block. Used by Swarm-style fleet views and reconnect/history-restore, where the caller doesn’t yet know which block(s) to ask about.

Source

pub fn background_task_list_fleet( &self, ended_since_ms: i64, ) -> Result<Vec<BackgroundTask>, StoreError>

Every block’s running rows plus those that ended at or after ended_since_ms — what a fleet view (Swarm) shows: live work, and finished work for as long as it keeps showing a finished row.

Source

pub fn background_task_describe( &self, id: &str, label: &str, owner_tool_use_id: Option<&str>, ) -> Result<bool, StoreError>

Record what the CLI says about a task: its description (replacing a generic label such as the renderer’s “Bash”) and the subagent that owns it. The owner is only ever filled in, never cleared or changed. No-op (false) if the row doesn’t exist.

Source§

impl Store

Source

pub fn agent_content_get( &self, agent_id: &str, content_type: &str, ) -> Result<Option<AgentContent>, StoreError>

Source

pub fn agent_content_set( &self, content: &AgentContent, ) -> Result<(), StoreError>

Upsert a content blob for an agent.

Source

pub(super) fn agent_content_get_all_local( &self, agent_id: &str, ) -> Result<Vec<AgentContent>, StoreError>

Get all content blobs for an agent. LOCAL channel’s content blobs only — NO cross-channel fallback. Used by the def-registry mirror (which always operates on a local agent, so must never read the global record) and by agent_content_get_all.

Source

pub fn agent_content_get_all( &self, agent_id: &str, ) -> Result<Vec<AgentContent>, StoreError>

Source

pub fn agent_content_delete( &self, agent_id: &str, content_type: &str, ) -> Result<bool, StoreError>

Delete a specific content blob. Returns true if a row was deleted.

Source§

impl Store

Source

pub fn conversation_trust_grant_check( &self, agent_id: &str, requester_agent_id: &str, tier: &str, ) -> Result<bool, StoreError>

True if agent_id has explicitly granted requester_agent_id access to its conversation content on tier (“lan” or “wan”). A grant for one tier’s cryptographic identity guarantee is never assumed to cover another — a caller must check the grant for the ACTUAL tier the incoming transcript_request arrived on.

Source

pub fn conversation_trust_grant_add( &self, agent_id: &str, requester_agent_id: &str, tier: &str, ) -> Result<(), StoreError>

Grant requester_agent_id access to agent_id’s conversation content on tier. Idempotent — granting an already-granted peer is a no-op, not an error or a duplicate row (the compound primary key on (agent_id, granted_peer_agent_id, tier) enforces this at the schema level too; INSERT OR REPLACE keeps this call site simple rather than requiring callers to distinguish “insert” from “already exists”).

Source

pub fn conversation_trust_grant_revoke( &self, agent_id: &str, requester_agent_id: &str, tier: &str, ) -> Result<(), StoreError>

Revoke a previously-granted peer. A no-op (not an error) if no such grant exists — revoking is idempotent, same as granting.

Source

pub fn conversation_trust_grant_list( &self, agent_id: &str, ) -> Result<Vec<(String, String, i64)>, StoreError>

List every peer agent_id has granted access to, across all tiers — for a settings UI/CLI to show the current allowlist.

Source§

impl Store

Source

pub fn cron_create(&self, job: &CronJob) -> Result<(), StoreError>

Source

pub fn cron_list(&self) -> Result<Vec<CronJob>, StoreError>

Source

pub fn cron_list_enabled(&self) -> Result<Vec<CronJob>, StoreError>

Source

pub fn cron_get(&self, id: &str) -> Result<Option<CronJob>, StoreError>

Source

pub fn cron_set_enabled( &self, id: &str, enabled: bool, ) -> Result<bool, StoreError>

Source

pub fn cron_record_fire(&self, id: &str, now: i64) -> Result<(), StoreError>

Source

pub fn cron_delete(&self, id: &str) -> Result<bool, StoreError>

Source§

impl Store

Source

pub(super) fn registry_def_upsert(&self, def_id: &str)

Mirror a definition (by id) into the global store, reading its full row + content + skills from SQLite. Best-effort: a missing global store or any failure is logged, never propagated (SQLite stays authoritative). No-op when the definition no longer exists.

upsert itself refuses to resurrect a tombstoned id, so a stale mirror call for a deleted agent won’t un-delete it.

Source

pub(super) fn registry_def_refresh_if_mirrored(&self, def_id: &str)

Same mirror as registry_def_upsert, but never CREATES a global record — only refreshes one that already exists. Used by agent_content_set/agent_content_delete/the skills equivalents: their job is “keep an EXISTING mirror’s content/skills in sync,” never “make this agent globally mirrored for the first time” (that is agent_def_insert/agent_def_update/agent_def_find_or_insert’s job, and their own registry_def_upsert calls already cover it).

Load-bearing as of the definition flip (SPEC_AGENT_ARCHITECTURE_2026_05_27.md Phase 3d, reagent P1 x2 on #3092): agent_def_get now resolves a template LAUNCH’s own row too (is_seeded = 0, indistinguishable by column shape from a genuine user agent — instance_create’s fresh-launch INSERT sets both is_template/is_seeded to 0 the same way agent_def_insert does for a real one). A launch never goes through agent_def_insert, so it never has an existing global record — routing content/skill writes through registry_def_upsert’s unconditional create-or-update would mirror the launch into the shared cross-channel registry the first time anything wrote its content (e.g. a routine ui:zoom setting), making it appear as a new, permanent “My Agents” entry in every other channel — one ghost per launch, accumulating forever. This is not a template-vs-launch distinction this function has to make: skipping creation entirely is correct for BOTH (a genuine template’s own content writes are also a no-op here, same as registry_def_upsert’s explicit is_seeded check already intends).

Source

pub(super) fn registry_def_retire(&self, def_id: &str) -> bool

Mirror a user-agent deletion as a global tombstone (retired/) so another channel’s stale SQLite can’t resurrect it via upsert. Best-effort.

Source

pub(super) fn registry_def_update_definition_fields( &self, agent: &AgentDefinition, ) -> bool

Apply a definition edit directly to the global record’s definition fields, PRESERVING its existing content + skills. Used when editing a cross-channel agent that has no local SQLite row to update. Returns whether a global record was found + updated. Best-effort. Tombstoned (retired) agents are not resurrected — get reads only the active tree.

Source

pub(super) fn registry_def_update_content_field( &self, agent_id: &str, content_type: &str, value: Option<&str>, ) -> Result<(), StoreError>

Update a single content blob in the global registry record for a cross-channel agent, preserving all other definition fields, content, and skills. Used when the local channel has no db_agent_definitions row for the agent (FK would prevent a local SQLite write).

value = Some(s) upserts the blob; value = None removes it. Returns Err if the global registry is unavailable or the agent is absent.

Source§

impl Store

Source

pub fn agent_history_append( &self, agent_id: &str, entry: &str, ) -> Result<AgentHistory, StoreError>

Append a history entry for an agent. Auto-sets session_date (today) and timestamp.

Source

pub fn agent_history_list( &self, agent_id: &str, session_date: Option<&str>, limit: i64, offset: i64, ) -> Result<Vec<AgentHistory>, StoreError>

List history entries for an agent, with optional date filter and pagination.

Search history entries for an agent using LIKE-based matching.

Source§

impl Store

Source

pub fn identity_list( &self, provider: Option<&str>, ) -> Result<Vec<IdentityAccount>, StoreError>

List identity accounts. If provider is Some, filter to that provider; otherwise return every account, ordered by most recent update first (so the identity panel shows live accounts on top).

A row whose secret_ref/context JSON fails to parse is skipped (logged with its account id) rather than aborting the whole query — one corrupted row must never hide every other account. This is the isolation the oauth_config_dir tag-alias comment above already flagged as missing (“identity_list() aborts entirely on the first unparseable row”) after that incident; this closes it at the source instead of only patching the one wire-format mismatch that triggered it. See ANALYSIS_ARMORY_STASH_CREDENTIAL_VISIBILITY_GAP_2026_08_04 (never committed to this repo).

Source

pub fn identity_get( &self, id: &str, ) -> Result<Option<IdentityAccount>, StoreError>

Source

pub fn identity_set_context_email( &self, id: &str, email: &str, ) -> Result<bool, StoreError>

Upsert an identity account. If account.id is empty the caller must generate one first (we don’t silently mint ids here — callers should know whether they’re creating vs updating). Set one account’s context.email, and nothing else: no other column, no updated_at, and no row created — an account deleted since it was read stays deleted, and a status or secret_ref written meanwhile stands (SPEC_ACCOUNT_EMAIL_IN_ARMORY_2026_09_23.md §4’s backfill runs inside a list). Returns whether a row was updated.

Source

pub fn identity_upsert( &self, account: &IdentityAccount, ) -> Result<(), StoreError>

Source

pub fn identity_upsert_with_mirror( &self, mirror: &Store, account: &IdentityAccount, ) -> Result<(), StoreError>

identity_upsert(self) — the authoritative, primary write — followed by a BEST-EFFORT mirror write into identity_store, so a link resolved from the always-global identity store (§SPEC_IDENTITY_STORE_ SPLIT_2026_08_17.md) doesn’t dead-end resolving the account it points at on a fresh, per-channel-isolated self after a version/ channel switch — see identity::resolver::inject::resolve_account.

Call this instead of plain identity_upsert at every LIVE account- creation/update call site (reagentx P0 review on PR #2632 — the original fix only backfilled the mirror once, via migration, so any account created/updated AFTER the fix shipped still had no mirror entry and reproduced the exact reported bug on its own next channel switch).

self is the primary (id_store) — its result is authoritative and propagated to the caller. mirror’s (identity_store’s) result is logged on failure and otherwise ignored: the mirror existing at all is a durability nicety for THIS specific continuity scenario, not a correctness requirement the primary write should ever be blocked or failed by.

Source

pub fn identity_repair_malformed_secret_refs(&self) -> Result<usize, StoreError>

Migration-only repair pass (m0019_repair_malformed_secret_ref): scan every db_accounts.secret_ref for JSON that fails to parse and attempt to fix the one known corruption shape — a Windows path pasted into the JSON string with its backslashes never escaped (found live: {"backend":"oauth_config_dir","dir":"C:\Users\..."}, not reproducible from any write path in this codebase — every real caller goes through identity_upsert above, which always serde_json::to_strings correctly; this shape only arises from an out-of-band edit of the database file). Rows that still don’t parse after the repair attempt are left untouched and logged — never deleted, since a broken row may still be linked via db_agent_identity_links and identity_list’s per-row skip (above) already makes leaving it alone safe. Returns the number of rows repaired.

Source

pub fn identity_delete( &self, id: &str, ) -> Result<IdentityDeleteOutcome, StoreError>

Delete an identity account AND every db_agent_identity_links row referencing it, atomically (single transaction under the connection lock).

The cascade is EXPLICIT even though the current DDL carries FOREIGN KEY (account_id) … ON DELETE CASCADE: databases whose links table arrived via legacy ALTER TABLE … RENAME (forge era) never got the FK clause retrofitted — CREATE TABLE IF NOT EXISTS can’t alter an existing table — so on real user databases the DDL-level cascade silently doesn’t exist and dangling links survive the account row (auth-lifecycle gap §2.4, docs/analysis/ANALYSIS_ACCOUNT_DELETE_AUTH_LIFECYCLE_GAP_2026_07_14.md). db_identity_bindings (named by the report) was retired in Phase 4c and no longer exists; links are the sole junction on accounts.

Delete every db_agent_identity_links row for account_id, on WHATEVER store this is called on — added for docs/specs/SPEC_IDENTITY_STORE_SPLIT_2026_08_17.md: since links now live in a different physical store (identity_store) than accounts (id_store), identity_delete’s own same-connection cascade above no longer reaches them (it only ever deleted rows in the SAME store as the account, which was always true before this split). Callers deleting an account must call this on identity_store in addition to identity_delete on id_store — see the deleteidentityaccount handler. Returns the same (count, affected_agents) shape identity_delete did, captured before the delete in one transaction for the same race-safety reason.

Link an agent to an identity for a given provider. Overwrites any existing link for the same (agent_id, provider) — each agent has at most one account per provider.

Remove the identity link for a given (agent_id, provider). Returns true iff a link existed.

Source

pub fn agent_identity_list_all( &self, ) -> Result<Vec<AgentIdentityLink>, StoreError>

List every (agent_id, account_id, provider) row in the table. Used by the startup backfill to seed the shared store.

Source

pub fn agent_identity_list_for_agent( &self, agent_id: &str, ) -> Result<Vec<AgentIdentityLink>, StoreError>

List all (agent_id, account_id, provider) triples for an agent.

Every distinct (agent_id, provider) link pair. Used by the m0017/m0018 ambient-login grandfather migrations, which must reason about provider CLASS: only oauth-class links mean an agent “opted into managed CLI login” (flag stays 0) — an api-key link (e.g. a github PAT) is never spawn-gated, so it must not forfeit grandfathering (spec §2.4 of SPEC_ACCOUNT_DELETE_DEAUTH_LAYERS_2_4_2026_07_14.md). Class filtering happens at the caller via identity::resolver::provider_class; the storage layer stays class-agnostic.

Source§

impl Store

Source

fn managed_bound_ids<R: ManagedResource>( &self, owner: Owner, owner_id: &str, ) -> Result<HashSet<String>, StoreError>

The ref ids owner_id holds for one primitive, on self.

Source

fn managed_bound_counts<R: ManagedResource>( &self, ) -> Result<HashMap<String, i64>, StoreError>

REF_COL -> count across every agent-level ref row, on self.

Source

fn managed_catalog_global<R: ManagedResource>( &self, ) -> Result<Vec<R>, StoreError>

Every global row of a primitive, on self acting as the CATALOG store — newest first.

Source

fn managed_catalog_global_and_ids<R: ManagedResource>( &self, ids: &HashSet<String>, ) -> Result<Vec<R>, StoreError>

Every global row, plus (if ids is non-empty) every row in ids — global or private — on self acting as the CATALOG store. Unordered; callers sort as needed.

Source

pub(super) fn managed_list<R: ManagedResource>( &self, catalog: &Store, owner: Owner, owner_id: &str, ) -> Result<Vec<(R, bool)>, StoreError>

Rows visible to owner_id — its own referenced rows plus every global row — each paired with whether this owner holds the ref. Global rows first, newest first.

Source

pub(super) fn managed_list_global_for_agent<R: ManagedResource>( &self, catalog: &Store, agent_id: &str, ) -> Result<Vec<(R, bool)>, StoreError>

Global rows only, each paired with whether agent_id holds the agent-level ref. Newest first. Never returns private rows — the caller-supplied agent_id is unverified on the catalog RPCs that use this (reagentx P0 on PR #2329).

Source

pub(super) fn managed_list_global<R: ManagedResource>( &self, catalog: &Store, ) -> Result<Vec<(R, i64)>, StoreError>

Global rows with how many agents hold an agent-level ref to each — the catalog’s “used by N agents” in Connectors and Memory. Newest first.

Source

pub(super) fn managed_get<R: ManagedResource>( &self, id: &str, ) -> Result<Option<R>, StoreError>

One row by id. Genuinely single-table — callers invoke this directly on whichever store owns R::TABLE (catalog for Skill/McpServer).

Source

pub(super) fn managed_delete<R: ManagedResource>( &self, catalog: &Store, id: &str, ) -> Result<bool, StoreError>

Delete the catalog row on catalog FIRST, then purge id’s ref rows on self. True if a catalog row was deleted.

Order matters (Codex P1, PR #3183): with the ref-purge running first, a catalog-delete failure AFTER the ref purge already committed (disk full, the identity store locked, any I/O error) stranded the row — still present in catalog, but no ref points at it and it is not necessarily global, so no access-check path can find it again, and the ordinary delete RPC’s own pre-check (skill_is_bound_to) now rejects a retry too, since the ref that check depends on is already gone. Catalog-first fixes the failure mode entirely: if the catalog delete itself errors, ? returns before the ref purge ever runs, so the row keeps its ref(s) and stays exactly as accessible/retryable as before the call. If the catalog delete SUCCEEDS (including “0 rows, already gone”) and the SUBSEQUENT ref purge then fails or a process crashes between the two, the worst case is a dangling ref pointing at an id the identity store no longer holds — the same already-accepted, gracefully-degrading gap SPEC_DURABLE_BINDINGS_2026_09_10.md §5.4 documents for cross-channel dangling refs (skill_get on a missing id returns None, which every read path already handles), not a new failure mode.

Source

pub(super) fn managed_unbind_all_for_bundle<R: ManagedResource>( &self, bundle_id: &str, ) -> Result<usize, StoreError>

Purge every bundle-level ref for bundle_id, for one resource kind.

The mirror of managed_delete’s ref purge, from the other side: that one runs when a catalog row goes away, this one when a bundle does. Neither can rely on foreign keys: the ref tables declare none to the catalog rows or to db_bundles. The bundle side cannot have an enforcing FK at all, since db_bundles lives in a different physical database from the ref tables (see the db_bundle_skills_ref comment in migrations.rs).

Returns the number of refs removed.

Source

pub(super) fn managed_bind_agent<R: ManagedResource>( &self, catalog: &Store, agent_id: &str, id: &str, ) -> Result<(), StoreError>

Insert the agent-level ref (idempotent). Errors if agent_id is not a LOCAL agent: the ref table’s agent_id → db_agents FK would otherwise swallow the INSERT OR IGNORE silently for a cross-channel agent, which is indistinguishable by row count from “already bound” (reagentx P1 on PR #2315; REPORT_ARMORY_SKILLS_MARKDOWN_AND_BIND_BUG_2026_07_27.md).

Checks db_agents, not db_agent_definitions: R::AGENT_REF_TABLE’s FK targets db_agents as of Phase 3c (#3088), so checking the legacy table here would reject a bind for any agent that exists ONLY as a db_agents row (a template launch), even though the FK the INSERT below actually depends on would accept it.

Additionally errors if id has no row in catalog — Part A of SPEC_DURABLE_BINDINGS_2026_09_10.md §5.4 drops the ref table’s skill_id/mcp_id → db_skills/db_mcp_servers FK entirely (it used to target THIS channel’s local, now-stale catalog copy), so the existence check that FK used to provide is now explicit here instead — mirroring managed_bind_bundle’s existing id_store check.

Source

pub(super) fn managed_bind_bundle<R: ManagedResource>( &self, catalog: &Store, id_store: &Store, bundle_id: &str, id: &str, ) -> Result<(), StoreError>

Insert the bundle-level ref (idempotent). Bundle existence is checked in id_store, not self — bundles are authoritatively written through the shared store, and self’s local db_bundles copy is essentially always empty in production (reagentx P0 on PR #2639).

Also checks id exists in catalog — same Part A FK-removal reason as managed_bind_agent’s new check (db_bundle_skills_ref also lost its skill_id FK, and never had a bundle_id FK at all — see migrations.rs’s own comment on these tables). Added now for the same reason as the agent-level check, even though bundle_skill_bind’s current call sites already pre-check existence via skill_get before calling — see this crate’s report for why this is still worth doing as a second line of defense at the store layer rather than trusting every call site forever.

Source

fn bundle_exists(&self, bundle_id: &str) -> Result<bool, StoreError>

Source

pub(super) fn managed_unbind<R: ManagedResource>( &self, owner: Owner, owner_id: &str, id: &str, ) -> Result<bool, StoreError>

Remove the owner-level ref. True if a row was removed.

Source

pub(super) fn managed_upsert_unique<R: ManagedResource>( &self, catalog: &Store, owner: Owner, owner_id: &str, row: &R, bind_new: bool, force_global: Option<bool>, ) -> Result<(), StoreError>

Upsert row into catalog enforcing owner-scoped name uniqueness (no other row visible to the owner — bound or global — may share the name), and bind it into self when bind_new.

No longer atomic (Phase 2 of SPEC_DURABLE_BINDINGS_2026_09_10.md §5.4, accepted interim gap): the catalog insert and the ref-table bind are two separate connections now, so a narrow same-name race between two concurrent upserts for a brand-new name is possible (single-owner, single-submit UI flows are not a realistic multi-writer scenario — not closed until Phase 3/6 restores single-connection atomicity). What this DOES still guarantee: if the ref-bind fails after the catalog insert already committed, a best-effort COMPENSATING DELETE removes the just-inserted catalog row before the bind error propagates, so the caller’s failure and the durable state agree (“nothing was created”) instead of leaving a permanent, undiscoverable orphan catalog row (no ordinary list/access/delete path can find a row that is neither bound nor global). The compensating delete’s own failure is logged, never allowed to mask the original bind error.

Source

pub(super) fn managed_upsert_unique_for_bundle<R: ManagedResource>( &self, catalog: &Store, id_store: &Store, bundle_id: &str, row: &R, bind_new: bool, ) -> Result<(), StoreError>

Bundle-scoped upsert: create a NEW, PRIVATE (never global) row bound directly to a bundle, enforcing bundle-scoped name uniqueness. Bundle existence is checked in id_store (see managed_bind_bundle). managed_bind_bundle may only bind EXISTING global rows — binding an existing private row would let one bundle “steal” read access to whatever entity that row belongs to — but globals already reach every agent, so this is how a bundle gets a genuinely bundle-specific row that has never belonged to anyone else (reagentx P1 on PR #2639).

Source

pub(super) fn managed_upsert_unique_global<R: ManagedResource>( &self, row: &R, ) -> Result<(), StoreError>

Atomically upsert a GLOBAL row enforcing catalog-wide name uniqueness among every global row (not just those visible to one owner). Forces is_global = 1 on INSERT; the caller is expected to have set it too.

Pure single-table operation — callers invoke this directly on whichever store owns R::TABLE (identity_store in production).

Source

pub(super) fn managed_is_accessible_to<R: ManagedResource>( &self, catalog: &Store, owner: Owner, owner_id: &str, id: &str, ) -> Result<bool, StoreError>

True when the row is global (in catalog) or the owner holds a ref to it (in self) — the read/mutation access check.

Source

pub(super) fn managed_is_bound_to<R: ManagedResource>( &self, owner: Owner, owner_id: &str, id: &str, ) -> Result<bool, StoreError>

True when the owner holds a direct ref to the row (global rows are not implied) — the ownership check for edit/delete. Pure ref-table operation on self.

Source

pub(super) fn managed_union_bundle_refs<R: ManagedResource>( &self, catalog: &Store, agent_id: &str, visible: &mut Vec<R>, )

Union the rows referenced by agent_id’s bundles into visible, in list order (its own bundle first, then its picks: agent_bundle_chain), deduped by id, so the first bundle to name a row wins and a global row already in visible isn’t added twice. Composable model v2 — without this, bundle-level refs would be exactly as inert at launch as the bundle’s old inline JSON columns were (GH issue #2024 item 3). Silent on lookup failure. Skills only now: a bundle carries no MCP servers (SPEC_BUNDLE_CONTENTS_MEMORY_NOT_MCP_2026_10_07.md §3.1).

Source§

impl Store

Source

pub fn project_instructions_record( &self, agent_id: &str, observations: &[ProjectInstructionObservation], ) -> Result<(), StoreError>

Record what was observed, replacing the previous observation.

Last-write-wins per (agent_id, path): this is a “what did it look like most recently” record, not a history. Version history for these files would mean storing their content, which is exactly what this table declines to do — the repository’s own version control already owns that job for the repository’s own files.

Source

pub fn project_instructions_list( &self, agent_id: &str, ) -> Result<Vec<ProjectInstructionObservation>, StoreError>

Every recorded observation for agent_id, path-ordered.

Source

pub fn project_instructions_forget( &self, agent_id: &str, ) -> Result<usize, StoreError>

Forget everything recorded for agent_id.

Called by agent_def_delete, beside the row these observations belong to. There is no foreign key to enforce it — they record files an agent reads, which is not a relationship SQLite can express — so without that call the rows outlive the agent and a future agent reusing the id inherits somebody else’s baseline, every file reporting unchanged against observations never made about it (ReAgent, PR #3162).

Nothing else should call it: dropping observations resets every file to FirstSeen, which silently discards the drift this table exists to surface.

Source§

impl Store

Source

pub fn bundle_unbind_all_components( &self, bundle_id: &str, ) -> Result<(usize, usize), StoreError>

Drop every skill and MCP ref belonging to bundle_id. Bundles no longer carry MCP servers (m0036_drop_bundle_mcp), but an older build sharing the channel can still write a bundle MCP ref, so those go too.

Call after deleting the bundle itself. bundle_delete lives on the store that owns db_bundles, which does not have the ref tables at all — they sit beside the catalog tables they key into — so the cleanup cannot happen inside it and has to be driven from the handler, where both stores are in scope. Without it a deleted bundle leaves its refs behind, and a future bundle reusing that id would inherit them.

Returns (skill_refs_removed, mcp_refs_removed).

Source§

impl Store

Source

pub fn mcp_server_list( &self, catalog: &Store, agent_id: &str, ) -> Result<Vec<McpServerListItem>, StoreError>

List all MCP servers visible to an agent: own (referenced) + global, each annotated with whether this specific agent holds the bind ref.

Source

pub fn effective_mcp_servers( &self, catalog: &Store, agent_id: &str, container: bool, ) -> Vec<McpServer>

Every MCP server an agent launches with, on both launch paths (agent.open and the picker’s writeagentconfig): its own private servers, plus the catalog servers bound to it or switched on for all agents. A container agent gets only the catalog servers switched on for containers: a catalog server is usually a host command. Its own servers are delivered as configured. Not its bundles’: a bundle carries no MCP servers (SPEC_BUNDLE_CONTENTS_MEMORY_NOT_MCP_2026_10_07.md §3.1). SPEC_CONNECTORS_MCP_WHAT_AGENTS_RUN_2026_10_10.md §2.1.

Source

pub fn mcp_server_reach_all( &self, ) -> Result<HashMap<String, McpServerReach>, StoreError>

Every catalog server’s reach switches, by server id. Call on the catalog store (the identity store), where db_mcp_server_reach sits beside db_mcp_servers.

Source

pub fn mcp_server_set_reach( &self, mcp_id: &str, reach: McpServerReach, now_ms: i64, ) -> Result<(), StoreError>

Set a catalog server’s reach switches. Call on the catalog store; the caller checks the server is a global row.

Source

pub fn mcp_server_list_global( &self, catalog: &Store, ) -> Result<Vec<McpServerCatalogItem>, StoreError>

List every GLOBAL MCP server — Connectors → MCP servers. Unlike mcp_server_list, this takes no agent_id and never includes an agent’s private servers; it backs the window-scoped mcp.catalog.* MuxAPI (no check_s1, so there is no agent context to scope by). Each row carries bound_count — how many agents currently hold a db_agent_mcp_ref to it — per SPEC_V1_MCP_SKILLS_PRIMITIVES_2026_06_30.md §8 (“used by N agents”), tracked as gap #2 of #1960.

Source

pub fn mcp_server_list_global_for_agent( &self, catalog: &Store, agent_id: &str, ) -> Result<Vec<McpServerListItem>, StoreError>

Catalog-tier sibling of mcp_server_list (above) — same bound_to_agent shape, but deliberately GLOBAL ROWS ONLY, unlike mcp_server_list’s UNION with agent_id’s own private servers. Backs mcp.catalog.list_for_agent, which — like every other mcp.catalog.* command — has no check_s1, so agent_id here is caller-supplied and unverified. Returning private server rows (whose config can carry secrets: API keys, auth headers, env vars) for an arbitrary caller-chosen agent_id would let any window connection read any agent’s private server config. Global rows carry no per-agent secret — they’re already fully visible via mcp_server_list_global (Connectors → MCP servers) — so exposing them alongside a caller-chosen agent’s bind status is safe. reagentx P0 on PR #2329.

Source

pub fn mcp_server_get(&self, id: &str) -> Result<Option<McpServer>, StoreError>

Get a standalone MCP server by id.

Source

pub fn mcp_server_delete( &self, catalog: &Store, id: &str, ) -> Result<bool, StoreError>

Delete a standalone MCP server and purge ref rows (both agent- and bundle-level — FK cascades may be off on some builds, same reasoning as skill_delete). Returns true if deleted.

Source

pub fn mcp_server_bind( &self, catalog: &Store, agent_id: &str, mcp_id: &str, ) -> Result<(), StoreError>

Bind an MCP server to an agent (insert ref row). Idempotent — binding an already-bound pair is a silent no-op success.

Errors if agent_id isn’t a LOCAL agent: db_agent_mcp_ref.agent_id has an ON-enforced FK to db_agents(id) (store.rs; was db_agent_definitions(id) before Phase 3c, #3088), but Connectors’ agent picker (ListAgentDefinitionsCommand → agent_def_list()) also lists cross-channel agents that only exist in another channel’s local database. Binding one of those would otherwise have the FK silently swallow the INSERT OR IGNORE — indistinguishable, by affected-row-count alone, from the equally-silent “already bound” case — reporting success while creating nothing. Same fix as skill_bind — see docs/reports/REPORT_ARMORY_SKILLS_MARKDOWN_AND_BIND_BUG_2026_07_27.md.

Source

pub fn mcp_server_unbind( &self, agent_id: &str, mcp_id: &str, ) -> Result<bool, StoreError>

Unbind an MCP server from an agent. Returns true if a row was removed.

Source

pub fn mcp_server_upsert_unique( &self, catalog: &Store, agent_id: &str, server: &McpServer, bind_new: bool, ) -> Result<(), StoreError>

Atomically upsert an MCP server enforcing per-agent name uniqueness, and (when bind_new) bind it — all in one transaction so concurrent mcp.upsert calls for the same name can’t both pass a check and insert duplicate-named bindings. Returns an error if another server visible to the agent (bound or global) already uses the name.

Source

pub fn mcp_server_upsert_unique_global( &self, server: &McpServer, ) -> Result<(), StoreError>

Atomically upsert a GLOBAL MCP server enforcing catalog-wide name uniqueness (no agent_id — unlike mcp_server_upsert_unique, this checks for a duplicate name among every global row, not just those visible to one agent). Reagent P1 on #1948: agent_config.rs’s build_mcp_config_from_refs merges servers into a JSON object keyed by server.name — two same-named global servers would silently clobber each other’s config for every agent that has either bound. server.is_global must already be true; caller’s job.

Source

pub(crate) fn mcp_server_list_all_raw( &self, ) -> Result<Vec<McpServer>, StoreError>

Source

pub(crate) fn mcp_server_insert_raw( &self, server: &McpServer, ) -> Result<usize, StoreError>

Returns 1 if a row was actually inserted, 0 if OR IGNORE found one already there — see Skill::skill_insert_raw’s doc comment.

Source

pub(crate) fn mcp_server_find_global_by_name( &self, name: &str, ) -> Result<Option<McpServer>, StoreError>

Source

pub(crate) fn mcp_server_rewrite_ref_id( &self, old_id: &str, new_id: &str, ) -> Result<usize, StoreError>

Mirrors Skill::skill_rewrite_ref_id exactly, for the MCP ref tables — including its insert-then-delete merge semantics (Codex P2, PR #3183). Returns the number of ref rows actually repointed — see Skill::skill_rewrite_ref_id’s doc comment.

Source

pub fn mcp_server_is_accessible_to( &self, catalog: &Store, agent_id: &str, mcp_id: &str, ) -> Result<bool, StoreError>

Return true if the given MCP server is accessible to the agent (global or bound). Used for read and mutation access checks.

Source

pub fn mcp_server_is_bound_to( &self, agent_id: &str, mcp_id: &str, ) -> Result<bool, StoreError>

Return true if the agent has a direct ref binding to this MCP server. Used for delete access — an agent may only delete servers it directly bound.

Source§

impl Store

Source

pub fn bundle_list(&self) -> Result<Vec<Bundle>, StoreError>

Source

pub fn bundle_list_global(&self) -> Result<Vec<Bundle>, StoreError>

Returns only the global bundles (is_global = 1), is_system rows first (regardless of sort_order — see docs/specs/SPEC_GLOBAL_MEMORY_SYSTEM_TIER_2026_08_24.md), then by explicit sort_order (then name as a stable tiebreak). Called at agent launch to inject workspace-wide rules into every agent in the order the user arranged them in Memory → Global.

Source

pub fn bundle_get(&self, id: &str) -> Result<Option<Bundle>, StoreError>

Source

fn bundle_is_system( &self, conn: &Connection, id: &str, ) -> Result<Option<bool>, StoreError>

Look up just the is_system flag for id, without decoding a full Bundle row. Shared by every guard in this file that needs to know “is the EXISTING row (if any) a system entry” before deciding whether to allow a write through the generic path.

Source

pub fn bundle_upsert(&self, memory: &Bundle) -> Result<(), StoreError>

Self::bundle_upsert_unobserved, then tell the Global Memory record’s observer (GlobalMemoryChange).

Source

fn bundle_upsert_unobserved(&self, memory: &Bundle) -> Result<(), StoreError>

Generic Global Bundle upsert — used by the ordinary Memory editor, the per-agent Bundle editor, ABF import, and internal seeding. Refuses outright to touch an existing is_system=1 row (content included, not just the flag) — see docs/specs/SPEC_GLOBAL_MEMORY_SYSTEM_TIER_2026_08_24.md §3.2. Use bundle_upsert_system to create/edit a system entry.

Source

pub fn bundle_upsert_with_version( &self, memory: &Bundle, written_by: &str, written_by_uid: &str, source: &str, source_detail: &str, ) -> Result<Option<BundleVersion>, StoreError>

Same as bundle_upsert, but when memory.is_global is true, also appends a db_bundle_versions row for it — atomically, in the SAME transaction as the db_bundles write, not as two separate calls. codex P2, PR #3237: two separate lock/transaction acquisitions (the original shape — call bundle_upsert, then separately call bundle_version_insert) let a concurrent writer interleave between them: writer A upserts, writer B upserts AND versions, then A’s own (now-stale) version-insert lands last — the history would then claim A’s content is current while the live row actually holds B’s. One transaction makes that interleaving impossible.

written_by is the caller’s own TRUSTED identity (an agent’s real AGENTMUX_AGENT_ID, or "armory-ui" for the human-facing Memory editor) — see BundleVersion::written_by’s own doc comment for why this is a separate field from source/source_detail. written_by_uid is the writer’s UID from the request’s Caller (identity M4c-1, spec §6.5.9) — "" when Unattributed, and for the Memory pane. Returns None (no version recorded) when memory.is_global is false — versioning is scoped to Global Memory specifically, matching this table’s whole purpose; a private, per-agent bundle upsert still writes normally but has nothing to audit here.

Shares bundle_upsert’s exact guard (refuses an existing is_system=1 row) and exact db_bundles SQL — kept as a literal duplicate rather than having one call the other, since self.conn is a plain (non-reentrant) Mutex: calling bundle_upsert (which takes its own lock) from inside a transaction already holding that same lock would deadlock.

Source

pub fn bundle_upsert_with_version_based( &self, memory: &Bundle, written_by: &str, written_by_uid: &str, source: &str, source_detail: &str, base_sha256: Option<&str>, ) -> Result<Option<BundleVersion>, StoreError>

Self::bundle_upsert_with_version_based_unobserved, then tell the Global Memory record’s observer (GlobalMemoryChange).

Source

fn bundle_upsert_with_version_based_unobserved( &self, memory: &Bundle, written_by: &str, written_by_uid: &str, source: &str, source_detail: &str, base_sha256: Option<&str>, ) -> Result<Option<BundleVersion>, StoreError>

bundle_upsert_with_version with an optional base: when base_sha256 is Some, the row’s CURRENT name + instructions must still hash to it (bundle_versions::content_hash — the same value as the latest version’s content_hash), checked inside the same IMMEDIATE transaction as the write, else StoreError::Conflict and nothing is written. A missing row is a conflict too. None behaves exactly like bundle_upsert_with_version. Memory → Global’s editor sends it (SPEC_MEMORY_FOLLOWS_THE_AGENT_2026_09_24.md §2.4).

Source

pub fn bundle_upsert_system_if_changed( &self, memory: &Bundle, written_by: &str, source: &str, source_detail: &str, ) -> Result<Option<BundleVersion>, StoreError>

Same as bundle_upsert_system_with_version, but skips recording a version when the write is byte-identical (name + instructions) to what’s already stored — the case where an operator opens a seeded Operator Config entry in Memory and clicks Save without changing anything (the UI does not suppress this request). Recording a version for a true no-op would permanently mark the row as human-owned even though nothing actually changed, breaking bundle_ reseed_system_if_owned’s ownership signal for no reason (Codex P2, PR #3244).

The existing-content comparison happens in the SAME transaction as the resulting write, for the identical check-then-act reason bundle_ reseed_system_if_owned documents on itself — an earlier revision of upsertsystemmemory (bundle.rs) composed this from a separate bundle_get call plus a conditional branch, which raced a concurrent writer between the read and the write (ReAgent P2, PR #3244).

Returns Ok(None) when the save was a no-op (still refreshes every other field via the plain upsert; just records no version); Ok(Some( version)) otherwise, including for a brand-new row (never a no-op).

Source

pub fn bundle_upsert_system_if_changed_based( &self, memory: &Bundle, written_by: &str, source: &str, source_detail: &str, base_sha256: Option<&str>, ) -> Result<Option<BundleVersion>, StoreError>

Self::bundle_upsert_system_if_changed_based_unobserved, then tell the Global Memory record’s observer (GlobalMemoryChange).

Source

fn bundle_upsert_system_if_changed_based_unobserved( &self, memory: &Bundle, written_by: &str, source: &str, source_detail: &str, base_sha256: Option<&str>, ) -> Result<Option<BundleVersion>, StoreError>

bundle_upsert_system_if_changed with the same optional base check bundle_upsert_with_version_based documents — the system-tier half of Memory → Global’s conditional save.

Source

pub fn bundle_reseed_system_if_owned( &self, memory: &Bundle, seeder_identity: &str, source: &str, source_detail: &str, ) -> Result<BundleReseedOutcome, StoreError>

Self::bundle_reseed_system_if_owned_unobserved, then tell the Global Memory record’s observer (GlobalMemoryChange).

Source

fn bundle_reseed_system_if_owned_unobserved( &self, memory: &Bundle, seeder_identity: &str, source: &str, source_detail: &str, ) -> Result<BundleReseedOutcome, StoreError>

Atomically decide-and-apply an Operator Config reseed for one system row — the ownership check (is this row still owned by AgentMux’s own seeder, or has a human/another writer claimed it) and the resulting write happen inside the SAME transaction, not as two separate Store calls composed by the caller. A caller-composed version (read latest version via bundle_version_list, decide, then separately call bundle_upsert_system_with_version) is a check-then-act race: a concurrent writer — a human editing this row in Memory, or another AgentMux instance’s own startup reseed, since multiple instances can run in parallel by design — could land a new version between the read and the write, and the caller’s decision would be based on data that was already stale by the time it acted (muxreview P1, PR #3244).

seeder_identity is the reserved written_by value that marks a row as still owned by the seeder (see operator_config_seed::WRITTEN_BY) — a latest version written by anything else means a human (or some other writer) has claimed this row, and it is left alone.

  • No existing db_bundles row at all -> (re)create, UNLESS the latest version for this id is a RETIREMENT_TOMBSTONE_SOURCE marker whose recorded generation is newer than source_detail’s own (SkippedRetired in that case) — see bundle_delete_system_ if_owned’s own doc comment for why the tombstone exists: without it, an older AgentMux build (or a rollback) sharing this store.db would read “no row” as “never existed” and resurrect content a newer build’s prune deliberately retired (ReAgent/Codex P2, PR #3244). An ordinary human delete (bundle_delete_system, which records no version at all) leaves no tombstone, so a manually deleted entry still comes back exactly as before — only a generation-aware prune leaves a tombstone behind.
  • Existing row, latest version’s written_by != seeder_identity -> SkippedLocalEdit, no write at all.
  • Existing row, latest version’s content_hash already matches the target (name+instructions) -> Unchanged, no write.
  • Otherwise -> upsert + version insert, Updated.
Source

pub fn bundle_delete(&self, id: &str) -> Result<bool, StoreError>

Self::bundle_delete_unobserved, then tell the Global Memory record’s observer (GlobalMemoryChange).

Source

fn bundle_delete_unobserved(&self, id: &str) -> Result<bool, StoreError>

Delete a Bundle. Refuses to delete the blank singleton, a seeded bundle, or (new) a system entry — use bundle_delete_system for the last case.

Source

pub fn bundle_delete_system(&self, id: &str) -> Result<bool, StoreError>

Self::bundle_delete_system_unobserved, then tell the Global Memory record’s observer (GlobalMemoryChange).

Source

fn bundle_delete_system_unobserved(&self, id: &str) -> Result<bool, StoreError>

The ONLY path that can remove an is_system=1 row — structurally incapable of deleting anything else, even if misused.

Source

pub fn bundle_list_system_ids(&self) -> Result<Vec<String>, StoreError>

All is_system=1 row ids — the full universe operator_config_seed’s startup prune pass checks its manifest against, to find rows for an entry AgentMux itself removed or renamed in a later release (Codex P2, PR #3244). Ids only, not full Bundles: the prune pass needs nothing else before deciding whether a given id is still in the manifest.

Source

pub fn bundle_delete_system_if_owned( &self, id: &str, seeder_identity: &str, caller_manifest_version: u32, ) -> Result<bool, StoreError>

Self::bundle_delete_system_if_owned_unobserved, then tell the Global Memory record’s observer (GlobalMemoryChange).

Source

fn bundle_delete_system_if_owned_unobserved( &self, id: &str, seeder_identity: &str, caller_manifest_version: u32, ) -> Result<bool, StoreError>

Atomically deletes an is_system=1 row ONLY if its latest version is still owned by seeder_identity AND that version’s own recorded manifest generation is <= caller_manifest_version — the delete-side counterpart of bundle_reseed_system_if_owned’s ownership check, used to prune a row whose manifest entry AgentMux itself removed in a later release.

The generation gate exists for the same reason bundle_reseed_ system_if_owned has one: on a machine sharing store.db across multiple AgentMux builds/versions, an OLDER build’s own manifest simply doesn’t mention an entry a NEWER build already added — that absence means “I don’t know about this yet,” not “this was removed.” Without gating on generation, an older build’s prune pass would delete a newer build’s own addition on every one of its startups. Only a row whose generation this process’s OWN manifest has already caught up to (or exceeds) is eligible for pruning at all.

A row a human has edited (written_by on the latest version is anything else) is left in place — same “never clobber a local edit” posture as the reseed path; an orphaned-but-human-owned entry is a human’s to clean up, not this seeder’s. A row with NO version history at all (should not normally happen — every write path that can create an is_system row also records one) is conservatively treated as NOT owned by the seeder and left alone, rather than guessing. Returns true only when a deletion actually happened.

Also appends a RETIREMENT_TOMBSTONE_SOURCE-marked version row in the SAME transaction as the delete — without it, an older AgentMux build (or a rollback to one) sharing this store.db, whose own manifest still lists this id, would see no db_bundles row and treat that as “never existed, safe to create,” silently resurrecting content this deletion deliberately retired. bundle_reseed_system_ if_owned’s missing-row branch checks for this tombstone before recreating anything (ReAgent/Codex P2, PR #3244).

Also validates the LIVE row’s content against what the latest version claims before trusting written_by at all — the delete-side mirror of the same check bundle_reseed_system_if_owned gained earlier in this PR. Without it, a compatible-schema OLDER build’s unversioned write (still possible during a rolling upgrade — see that method’s own doc comment) would leave written_by stale at the seeder’s identity, and this method would DELETE the unrecorded edit outright — strictly worse than the overwrite the reseed-path fix was built to prevent, since deletion cannot be undone from db_bundles alone. ReAgent P1, PR #3244.

Source

pub fn bundle_reorder( &self, ordered_ids: &[String], ) -> Result<usize, StoreError>

Self::bundle_reorder_unobserved, then tell the Global Memory record’s observer (GlobalMemoryChange).

Source

fn bundle_reorder_unobserved( &self, ordered_ids: &[String], ) -> Result<usize, StoreError>

Assign sort_order to the given bundle ids in the order supplied (position 0, 1, 2, …). Drives the Memory → Global ordering, which in turn controls CLAUDE.md injection order. Ids not present in the table, OR present but is_system=1, are skipped silently — a system row’s position is fixed (always first, see bundle_list_global) and never disturbed by the generic reorder command. Runs in a single transaction so a partial reorder never lands. Returns the number of rows updated.

Source§

impl Store

Source

pub fn bundle_version_list( &self, bundle_id: &str, ) -> Result<Vec<BundleVersionSummary>, StoreError>

History for one bundle, newest first — metadata only, no content (mirrors agent_native_memory_version_list). Not called by the Operator Config reseed path — Store::bundle_reseed_system_if_owned (bundles.rs) performs its own inline SELECT written_by, content_hash, source_detail ... LIMIT 1 query instead, so its ownership check and the write it gates happen in one transaction (see that method’s own doc comment for why). Backs the GlobalMemoryHistory MCP tool (global_memory_history_impl, server/app_api/mod.rs) as of SPEC_AGENT_FACING_GLOBAL_MEMORY_API_2026_09_15.md’s Phase-3 follow-up — also still exercised directly by this file’s own tests.

Source

pub fn bundle_version_get( &self, version_id: &str, ) -> Result<Option<BundleVersion>, StoreError>

One version by id, full content included — the lookup diff/revert build on (mirrors agent_native_memory_version_get). Called by global_memory_diff_impl/global_memory_revert_impl (app_api/mod.rs), which back the GlobalMemoryDiff/ GlobalMemoryRevert MCP tools.

Source§

impl Store

Source

pub fn muxbus_load(&self) -> Result<Option<MuxBusCredentials>, StoreError>

Source

pub fn muxbus_is_fresh(&self) -> bool

Side-effect-free (no keychain/SQL WRITE, see allow_migration below) freshness check: valid, non-empty access token that isn’t nearing expiry. Safe for RefreshScheduler::register’s is_fresh closure, called as it is under the per-id lock on every ensure_fresh/sweep tick — the concern that closure must avoid (reagent P2 on #2260) is an unexpected WRITE happening inside a nominally read-only check, not lock acquisition itself; muxbus_load_impl below still takes muxbus_save_lock (reagent P1 on this PR — see its comment) purely to serialize this read against a concurrent writer, with no side effect of its own performed while holding it. Any error (including a genuinely corrupted keychain entry) is treated as “not fresh” rather than propagated — a freshness check has no error channel to propagate through, and “not fresh” just means the broker will attempt a refresh, which surfaces the real problem through THAT path instead.

Source

fn muxbus_load_impl( &self, allow_migration: bool, ) -> Result<Option<MuxBusCredentials>, StoreError>

allow_migration gates only the lazy-migration self-heal WRITES in muxbus_load_tokens below (false for muxbus_is_fresh’s side-effect-free contract, true for the normal muxbus_load path) — it does NOT gate the lock below, which every call takes.

Source

fn muxbus_load_tokens( &self, allow_migration: bool, legacy_access: &str, legacy_refresh: &str, legacy_id: &str, user_sub: &str, ) -> Result<MuxBusTokens, StoreError>

Resolve the three token fields, checking sources in order:

  1. the current split-entry keychain layout (fast path),
  2. the legacy (pre-2026-08-03) single combined-blob keychain entry — migrated in place to the split layout when found,
  3. legacy plaintext SQL columns from rows that predate keychain storage entirely — migrated in place to the split layout too. A keychain READ failure at any step falls back to the legacy plaintext SQL columns when they have data, same as the pre-split-entry code did — a transient storage failure must not present as a full logout when we still have usable data sitting right there.
Source

pub fn muxbus_refresh_lock( &self, ) -> Result<Option<CrossProcessGuard>, StoreError>

One process at a time refreshes this channel’s sign-in; the others wait, then find it already fresh. Its own lock, apart from the store’s, so reads aren’t held up by the token request.

Source

pub fn muxbus_save(&self, creds: &MuxBusCredentials) -> Result<(), StoreError>

Source

pub fn muxbus_clear(&self) -> Result<(), StoreError>

Source§

impl Store

Source

pub(super) fn registry_upsert_if_named( &self, inst: &AgentInstance, wrote_session: bool, )

Mirror a db_agent_instances mutation into the cross-version registry. Only fires for named rows. Routes by display_hidden so the registry file ends up in the tree matching SQLite’s dropdown filter:

  • hidden = true → upsert (atomic write to active/) then retire (atomic rename to retired/). Net: file lives in retired/<id>.json with the freshest content. Prevents instance_update on a previously-hidden row from resurrecting an active registry file, AND keeps the retired tombstone’s content current.
  • hidden = false → unretire (no-op if not retired) then upsert. Net: file in active/<id>.json, no orphan retired.

wrote_session says whether the caller’s own write set the row’s session_id. When it didn’t (a launch), the row’s session id may be older than one another channel has since mirrored, so a session id the registry already holds is kept (#3586).

Failures are logged, never propagated: SQLite remains authoritative.

Source

pub(super) fn registry_propagate_continuation_session_id( &self, inst: &AgentInstance, )

Propagate a fresh session_id onto the chain-root registry record when inst is a continuation row (parent_instance_id set).

registry_upsert_if_named above deliberately excludes continuation rows from the mirror (see its filter comment — kept symmetric with instance_list_named’s legacy dropdown mode so a resume chain doesn’t fragment into N registry files). That’s correct for most fields, but it leaves the registry’s session_id — the ONE field a cross-channel/cross-build reader consults to --resume this agent — stuck at whatever the chain head had when first mirrored, never updated by any later resume. Root cause of docs/retro/retro-agent-resumed-9-day-stale-session-2026-08-22.md: reopening the agent in a different channel silently resumed a stale-but-valid session instead of the live one.

Read-modify-write against the existing root record (never constructs a partial one — Registry::upsert’s merge overwrites every field present in the struct). Best-effort: logs and returns on any lookup/write failure, since the SQLite write already succeeded and remains authoritative.

Callers decide WHETHER to invoke this at all based on whether inst.session_id reflects a genuine write, not just this function — an empty session_id is ambiguous in isolation: it can mean “never captured yet” (a fresh continuation — must NOT propagate, would clobber a still-valid root pointer, reagent P1 on PR #2755) or “deliberately cleared” (poison_resume’s stale-resume handling, via persist_session_id(block_id, "", ...) on an EXISTING row — MUST propagate, otherwise the shared registry keeps serving a session_id already proven dead, reagent P1 round 2). Only the caller knows which case it’s in (create vs. an explicit InstanceUpdate.session_id write), so this function itself never special-cases emptiness — it always mirrors whatever it’s given.

Source

fn find_chain_root_id(&self, inst: &AgentInstance) -> String

Walk parent_instance_id upward from inst to find the chain root — the row with no parent. Bounded to guard against a corrupted cyclic chain; real chains are a handful of user-driven resumes deep.

When a lookup misses locally, returns current.parent_instance_id (the id we couldn’t resolve), NOT current.id. Two cases produce a local miss and both want that: (1) the id is a genuinely orphaned continuation (its head was hard-deleted, no FK cascade) — the registry has no file for it either, so the caller’s reg.get harmlessly no-ops; (2) the id is the chain root living in a DIFFERENT channel’s local SQLite — this store never has that row, but the shared registry still does, under exactly this id. That second case is the primary scenario this propagation exists for (reopening an agent in another channel/build): current.id is a continuation’s own id, which never has a registry file (reagent P1 on PR #2755 — the original fallback made cross-channel propagation always no-op, defeating the fix for its own target case).

Source§

impl Store

Source

pub(super) fn agent_skill_list_local( &self, agent_id: &str, ) -> Result<Vec<AgentSkill>, StoreError>

List all skills for an agent, ordered by created_at ascending. LOCAL channel’s skills only — NO cross-channel fallback. Used by the def-registry mirror (which always operates on a local agent) and by agent_skill_list.

Source

pub fn agent_skill_list( &self, agent_id: &str, ) -> Result<Vec<AgentSkill>, StoreError>

Source

pub fn agent_skill_get( &self, id: &str, ) -> Result<Option<AgentSkill>, StoreError>

Get a single skill by id.

Source

pub fn agent_skill_insert(&self, skill: &AgentSkill) -> Result<(), StoreError>

Insert a new skill.

Source

pub fn agent_skill_update(&self, skill: &AgentSkill) -> Result<bool, StoreError>

Update an existing skill (all fields except id, agent_id, created_at).

Source

pub fn agent_skill_delete(&self, id: &str) -> Result<bool, StoreError>

Delete a legacy skill by id. Returns true if a row was deleted.

Source§

impl Store

Source

pub fn skill_list( &self, catalog: &Store, agent_id: &str, ) -> Result<Vec<SkillListItem>, StoreError>

List all skills visible to an agent: own (referenced) + global, each annotated with whether this specific agent holds the bind ref. catalog is where db_skills is authoritative — identity_store in production (Phase 2 of SPEC_DURABLE_BINDINGS_2026_09_10.md §5.4).

Source

pub fn effective_skills( &self, catalog: &Store, agent_id: &str, ) -> Vec<AgentSkill>

Effective skills for an agent at launch/config-materialization time: this agent’s own ref-bound skills (via skill_list) take over entirely when present (globals still included), otherwise fall back to the legacy db_agent_skills table with globals layered on top. The fallback decision is gated on own refs only — not the global-inclusive list — so adding a global skill never discards a legacy-only agent’s skills.

Single source of truth for two call sites that must stay consistent: write_agent_config_files (the authoritative Rust materialization path) and the listagentskills RPC handler, which the frontend’s pre-launch ListAgentSkillsCommand call depends on for its own buildConfigFiles mirror. Before this was extracted, listagentskills returned only legacy skills — silently hiding every standalone/Memory → Skills skill (including any Agent-Skills-format one, see SKILL_TYPE_AGENT_SKILL) from the actual “click Launch” flow, since that RPC is window-scoped (no check_s1) and can’t call the agent-scoped skill.list RPC the way an already-running, authenticated agent connection could (reagent P0 on PR #2322 — the launch UI is not an authenticated agent connection, so it was never able to reach the standalone Skill primitive via the RPC layer at all).

Source

pub fn skill_list_global( &self, catalog: &Store, ) -> Result<Vec<SkillCatalogItem>, StoreError>

List every GLOBAL skill — Memory → Skills. Unlike skill_list, this takes no agent_id and never includes an agent’s private skills; it backs the window-scoped skill.catalog.* MuxAPI (no check_s1, so there is no agent context to scope by). Each row carries bound_count — how many agents currently hold a db_agent_skills_ref to it — per SPEC_V1_MCP_SKILLS_PRIMITIVES_2026_06_30.md §8 (“used by N agents”), tracked as gap #2 of #1960.

Source

pub fn skill_list_global_for_agent( &self, catalog: &Store, agent_id: &str, ) -> Result<Vec<SkillListItem>, StoreError>

Catalog-tier sibling of skill_list (above) — same bound_to_agent shape, but deliberately GLOBAL ROWS ONLY, unlike skill_list’s UNION with agent_id’s own private skills. Backs skill.catalog.list_for_agent, which — like every other skill.catalog.* command — has no check_s1, so agent_id here is caller-supplied and unverified. Returning private skill rows (whose content/description/trigger can carry sensitive agent-authored material) for an arbitrary caller-chosen agent_id would let any window connection read any agent’s private skills. Global rows carry nothing per-agent-secret — they’re already fully visible via skill_list_global (Memory → Skills) — so exposing them alongside a caller-chosen agent’s bind status is safe. reagentx P0 on PR #2329.

Source

pub fn skill_get(&self, id: &str) -> Result<Option<Skill>, StoreError>

Get a standalone skill by id.

Source

pub fn skill_delete( &self, catalog: &Store, id: &str, ) -> Result<bool, StoreError>

Delete a standalone skill and purge its ref rows (both agent- and bundle-level). Returns true if deleted.

Source

pub fn skill_bind( &self, catalog: &Store, agent_id: &str, skill_id: &str, ) -> Result<(), StoreError>

Bind a skill to an agent (insert ref row). Idempotent — binding an already-bound pair is a silent no-op success.

Errors if agent_id isn’t a LOCAL agent: db_agent_skills_ref.agent_id has an ON-enforced FK to db_agents(id) (store.rs; was db_agent_definitions(id) before Phase 3c, #3088), but Memory’s agent picker (ListAgentDefinitionsCommand → agent_def_list()) also lists cross-channel agents that only exist in another channel’s local database. Binding one of those would otherwise have the FK silently swallow the INSERT OR IGNORE — indistinguishable, by affected-row-count alone, from the equally-silent “already bound” case — reporting success while creating nothing. reagentx P1, PR #2315.

Source

pub fn skill_unbind( &self, agent_id: &str, skill_id: &str, ) -> Result<bool, StoreError>

Unbind a skill from an agent. Returns true if a row was removed.

Source

pub fn skill_upsert_unique( &self, catalog: &Store, agent_id: &str, skill: &Skill, bind_new: bool, ) -> Result<(), StoreError>

Atomically upsert a skill enforcing per-agent name uniqueness, and (when bind_new) bind it to the agent — all in one transaction so concurrent skill.upsert calls for the same name can’t both pass a separate check and insert duplicates (the check+write is not split across lock releases). Returns StoreError::Other (“… already bound to this agent”) if another skill visible to the agent (bound or global) already uses the name.

Source

pub fn skill_upsert_unique_global( &self, skill: &Skill, ) -> Result<(), StoreError>

Atomically upsert a GLOBAL skill enforcing catalog-wide name uniqueness (no agent_id — unlike skill_upsert_unique, this checks for a duplicate name among every global row, not just those visible to one agent). Same defense-in-depth as McpServer::mcp_server_upsert_unique_global (reagent P1 on #1948) — two same-named global skills would at minimum produce a confusing duplicate bullet in the assembled CLAUDE.md skills index. skill.is_global must already be true; caller’s job.

Source

pub(crate) fn skill_list_all_raw(&self) -> Result<Vec<Skill>, StoreError>

Every row in this store’s db_skills, global and private alike, with no accessibility filtering. Ordinary app code never needs this — it is always scoped by agent/bundle accessibility — which is exactly why no such method existed before the carry-across migration needed one.

Source

pub(crate) fn skill_insert_raw( &self, skill: &Skill, ) -> Result<usize, StoreError>

Insert skill verbatim — including its is_global flag, which skill_upsert_unique_global would otherwise force to true. INSERT OR IGNORE: idempotent against a re-run migrating the same row twice (by id), never overwrites an existing row. Returns 1 if a row was actually inserted, 0 if OR IGNORE found one already there under this id — the caller’s signal for whether this was a genuinely new carry or an idempotent re-run no-op.

Source

pub(crate) fn skill_find_global_by_name( &self, name: &str, ) -> Result<Option<Skill>, StoreError>

The first existing GLOBAL row matching name alone, for the migration’s first-wins dedup among global skills that aren’t a recognized starter (those converge on starter_skill_id instead — see skill_seed::starter_skill_trigger_for_name).

Name-only, not (name, skill_type) — matching idx_ids_skills_global_name (IDENTITY_STORE_SCHEMA_VERSION v8, SPEC_DURABLE_BINDINGS_2026_09_10.md §5.4, Codex P1 on PR #3183): a (name, skill_type)-scoped re-query would miss the actual winner whenever two channels’ carries collide on name but not type — the insert that failed did so because of the name-only constraint, so the re-query must match the constraint that actually fired, or it finds nothing, falls through to the unrelated-PK-collision branch, and mints a fresh id whose insert SILENTLY no-ops against the same name constraint (INSERT OR IGNORE), leaving a local ref rewritten to an id the identity store never actually holds — the skill vanishes the moment reads redirect there.

Source

pub(crate) fn skill_rewrite_ref_id( &self, old_id: &str, new_id: &str, ) -> Result<usize, StoreError>

Repoint every ref row (agent- and bundle-level) naming old_id to new_id instead, on THIS store only. Called by the carry-across migration after it decides a skill’s row must live under a different id in the identity store than it does locally — without this, the ref row would name an id that no longer resolves anywhere once catalog reads redirect to the identity store (Phase 2’s whole point).

INSERT-survivor-then-DELETE-old, not a blind UPDATE: the PK (agent_id, skill_id)/(bundle_id, skill_id) collides whenever this exact owner already independently held a ref to new_id too — not vanishingly unlikely once two DIFFERENT catalog rows (not just two copies of the same one) can converge onto one survivor, which is exactly what m0033_narrow_skill_global_uniqueness_index’s dedup does (an owner can easily have bound BOTH of two same-name, different-skill_type skills before the collision was discovered). A plain UPDATE OR IGNORE left that case’s old-id row untouched (documented at the time as an acceptable no-op — ReAgent P1, PR #3181, round 2) — but a caller that then deletes the OLD catalog row (as m0033 does, right after this rewrite) turns that “no-op” into a permanently dangling ref pointing at an id nothing resolves anymore, even within the current channel (Codex P2, PR #3183). Insert-then- delete instead: INSERT OR IGNORE the survivor binding for every owner who held old_id (already-bound owners no-op harmlessly, same as before), then unconditionally DELETE every old_id row — so the owner ends up bound to the survivor either way, never left pointing at a name that’s about to stop existing. Returns the number of old_id ref rows resolved (0 if none named old_id, distinct from “the call itself no-oped”).

Source

pub fn skill_is_accessible_to( &self, catalog: &Store, agent_id: &str, skill_id: &str, ) -> Result<bool, StoreError>

Return true if the given skill is accessible to the agent (global or bound). Used for read and delete access checks.

Source

pub fn skill_is_bound_to( &self, agent_id: &str, skill_id: &str, ) -> Result<bool, StoreError>

Return true if the agent has a direct ref binding to this skill (for delete/mutation). Global skills are excluded — use the is_global guard separately.

Source

pub fn bundle_skill_list( &self, catalog: &Store, bundle_id: &str, ) -> Result<Vec<SkillBundleListItem>, StoreError>

Bundle-level sibling of skill_list — this bundle’s own (referenced) and global skills, each annotated with whether this specific bundle holds the db_bundle_skills_ref row.

Source

pub fn bundle_skill_bind( &self, catalog: &Store, id_store: &Store, bundle_id: &str, skill_id: &str, ) -> Result<(), StoreError>

Bind a skill to a bundle (insert ref row). Idempotent.

id_store (NOT self) is where bundle existence is checked: bundles are written through the shared store, and self’s own db_bundles copy is essentially always empty in production, so checking there made “bundle not found” fire for every real bundle (PR #2639). catalog is where db_skills is authoritative — see managed_bind_bundle’s doc comment for why it now ALSO checks skill existence there (Part A of SPEC_DURABLE_BINDINGS_2026_09_10.md §5.4 dropped this table’s skill_id FK).

Source

pub fn bundle_skill_upsert_unique( &self, catalog: &Store, id_store: &Store, bundle_id: &str, skill: &Skill, bind_new: bool, ) -> Result<(), StoreError>

Atomically create a NEW, PRIVATE (never global) skill scoped and bound directly to a bundle, enforcing bundle-scoped name uniqueness — the bundle-level analog of skill_upsert_unique. Binding can only reference global skills (binding another entity’s private one would hand this bundle read access to it), and a global skill already reaches every agent, so this is how a bundle gets a skill of its own (PR #2639).

Source

pub fn bundle_skill_unbind( &self, bundle_id: &str, skill_id: &str, ) -> Result<bool, StoreError>

Unbind a skill from a bundle. Returns true if a row was removed.

Source

pub fn bundle_skill_is_accessible_to( &self, catalog: &Store, bundle_id: &str, skill_id: &str, ) -> Result<bool, StoreError>

Return true if the given skill is accessible to the bundle (global or bundle-bound). Mirrors skill_is_accessible_to.

Source

pub fn bundle_skill_is_bound_to( &self, bundle_id: &str, skill_id: &str, ) -> Result<bool, StoreError>

Return true if the bundle has a direct ref binding to this skill (global excluded). Mirrors skill_is_bound_to — used for edit/delete-ownership checks, as opposed to bundle_skill_is_accessible_to’s broader read-access check.

Source§

impl Store

Source

pub fn open(path: &Path) -> Result<Self, StoreError>

Open a Store backed by a file on disk. Configures WAL mode and 5s busy timeout (matching Go).

Source

pub fn open_read_only(path: &Path) -> Result<Self, StoreError>

Open an existing store read-only: SQLITE_OPEN_READ_ONLY, no WAL/journal pragma, no schema setup, no version stamp — nothing that could write. For doctor checks (migrate --verify, muxspect migrations) that need the typed read API against a store they must not touch, including one on a read-only backup (codex P2 on #3070). Any read of a table the store predates simply errors. Fails if the file does not exist — a doctor must never conjure a store.

Source

pub fn open_in_memory() -> Result<Self, StoreError>

Open an in-memory Store for testing.

Source

pub fn origin_id(&self) -> String

Which store this is, stable for its lifetime: the database file, or — for an in-memory store — its address. Lets a record written into a machine-wide zone say which channel’s store wrote it.

Source

pub fn open_shared(path: &Path) -> Result<Self, StoreError>

Open the GLOBAL shared store at path (~/.agentmux/shared/store.db).

Uses the same WAL + busy-timeout config as open() but runs run_shared_store_schema instead of run_object_schema so only the durable-user-content tables are created (identity, memory, drone definitions, muxbus creds). Per-channel session tables are intentionally absent — calling per-channel CRUD methods on this store will error.

Source

pub fn open_identity_store(path: &Path) -> Result<Self, StoreError>

Open the permanently-global identity store at path (~/.agentmux/shared/identity-store.db) — see docs/specs/SPEC_IDENTITY_STORE_SPLIT_2026_08_17.md.

Same WAL + busy-timeout config as open_shared, but runs run_identity_store_schema — a strict subset of open_shared’s tables (everything except db_accounts) — instead of run_shared_store_schema. Crucially, this store’s PATH is never gated by isolated_auth_enabled() (see its resolver, registry::resolve_identity_store_path): unlike open_shared, there is no isolated/per-channel variant of this store at all.

Source

pub fn open_source_readonly(path: &Path) -> Result<Self, StoreError>

Open a sibling objects.db file for read-only backfill access.

Does NOT run schema migrations or stamp a version — the source DB is never modified (spec §3.1). WAL mode is not set either: the file is opened with SQLITE_OPEN_READ_ONLY | SQLITE_OPEN_NO_MUTEX so concurrent writers on the same DB are unaffected. Tables absent in older schemas (e.g. db_drone_definitions) return empty results via the normal StoreError path; callers use .unwrap_or_default().

Source

pub(crate) fn conn(&self) -> &Mutex<Connection>

Crate-internal accessor for sibling modules that maintain their own per-table CRUD via the DroneStore extension trait pattern (see crates/srv/src/drone/storage.rs). Outside callers must use the typed methods on this impl.

Source

pub fn run_agents_consolidate( &self, data_dir: Option<&Path>, ) -> Result<ConsolidateStats, StoreError>

Run the db_agents consolidation backfill under the mstore’s exclusive connection lock. Idempotent — gated by a marker file in data_dir (skip with None for tests).

Source

pub fn checkpoint(&self) -> Result<(), StoreError>

Run PRAGMA wal_checkpoint(TRUNCATE) to fold the WAL back into the main DB file. Called periodically and on clean shutdown. The 5s busy_timeout set at open handles transient reader contention — if it can’t fully truncate it returns partial progress (safe; picked up on the next pass).

Source

pub fn agents_consolidate_looks_incomplete(&self) -> Result<bool, StoreError>

True when db_agent_definitions/db_agent_instances have rows but db_agents doesn’t — i.e. the consolidation backfill’s marker/stamp can’t be trusted as proof it actually ran. See agents_consolidate::consolidate_looks_incomplete.

Source

fn configure_and_migrate(conn: Connection) -> Result<Self, StoreError>

Source

pub fn set_registry(&self, registry: Arc<Registry>)

Attach a shared cross-version agent registry. Called once on srv startup after Store::open and before the store is wrapped in Arc. Mutations to db_agent_instances will then parallel-write to the registry; the SQLite table remains the authoritative read path for PR A.

Source

pub fn set_global_memory_observer( &self, observer: Arc<dyn Fn(GlobalMemoryChange) + Send + Sync>, )

Attach the Global Memory record’s observer (see the field).

Source

pub(super) fn notify_global_memory(&self, change: GlobalMemoryChange)

Tell the observer, if any, about a committed change. Callers invoke it once their connection lock is released.

Source

pub(super) fn registry(&self) -> Option<Arc<Registry>>

Source

pub fn set_registry_agents_base(&self, base: PathBuf)

Set the channel agents dir that instance working_directory values are stored relative to (see the registry_agents_base field). Wired from AGENTMUX_AGENTS_DIR in main.rs in P0.3b (atomically with the registry re-root); until then it is exercised only by tests.

Source

pub fn registry_agents_base(&self) -> Option<PathBuf>

The base dir for instance working_directory relative paths.

Returns the explicitly-set channel agents dir ([set_registry_agents_base]) when present; otherwise falls back to the registry’s parent (agents_root()), which equals the channel agents dir in the pre-re-root layout. Used symmetrically by the write mirror and the read handler so the two never disagree on the anchor.

Source

pub fn shared_agent_registry(&self) -> Option<Arc<Registry>>

Public accessor for the cross-version named-agent registry. Returns None when the registry couldn’t be resolved at startup (CI / unusual envs); callers must handle the absent case by falling back to SQLite.

Source

pub fn set_wan_identity(&self, wan_identity: Arc<WanIdentityStore>)

Attach the GLOBAL (cross-channel) agent-definition store. Called once on srv startup after Store::open, before the store is wrapped in Arc. Definition mutations then mirror to it.

Source

pub fn wan_identity(&self) -> Option<Arc<WanIdentityStore>>

The channel-wide WAN identity store, if attached. None means WAN signing is off for this process — never fall back to another store.

Source

pub fn set_def_registry(&self, def_registry: Arc<DefinitionStore>)

Source

pub fn shared_def_registry(&self) -> Option<Arc<DefinitionStore>>

Public accessor for the global agent-definition store. None when it couldn’t be resolved at startup (CI / unusual envs / in-memory tests); callers fall back to SQLite.

Source

fn table_name<T: StoreObj>() -> String

Table name for a StoreObj type: db_<otype>.

Source

pub fn get<T: StoreObj>(&self, oid: &str) -> Result<Option<T>, StoreError>

Get a single object by OID. Returns None if not found.

Source

pub fn must_get<T: StoreObj>(&self, oid: &str) -> Result<T, StoreError>

Get a single object, returning StoreError::NotFound if missing.

Source

pub fn get_raw( &self, otype: &str, oid: &str, ) -> Result<Option<Value>, StoreError>

Get a single object as raw JSON Value by otype and OID. Used by GetObject/GetObjects to return data without strict struct deserialization.

Source

pub fn exists_raw(&self, otype: &str, oid: &str) -> Result<bool, StoreError>

Check if an object exists (by otype and OID).

Source

pub fn insert<T: StoreObj>(&self, obj: &mut T) -> Result<(), StoreError>

Insert a new object. Sets version to 1.

Source

pub fn update<T: StoreObj>(&self, obj: &mut T) -> Result<i64, StoreError>

Update an existing object. Increments version atomically. Returns the new version number.

Source

pub fn update_raw( &self, otype: &str, oid: &str, value: &Value, ) -> Result<i64, StoreError>

Update an object using raw JSON (bypasses struct deserialization). Used by UpdateObject where the frontend sends the full replacement object. This matches Go’s generic map-based UpdateObject behavior.

Source

pub fn delete<T: StoreObj>(&self, oid: &str) -> Result<(), StoreError>

Delete an object by OID.

Source

pub fn delete_by_otype(&self, otype: &str, oid: &str) -> Result<(), StoreError>

Delete by otype string and OID (for dynamic dispatch). Validates otype against VALID_OTYPES to prevent SQL injection.

Source

pub fn get_all<T: StoreObj>(&self) -> Result<Vec<T>, StoreError>

Get all objects of a given type.

Source

pub fn count<T: StoreObj>(&self) -> Result<i64, StoreError>

Count objects of a given type.

Source

pub fn with_tx<F, R>(&self, f: F) -> Result<R, StoreError>
where F: FnOnce(&StoreTx<'_>) -> Result<R, StoreError>,

Execute multiple operations in a single SQLite transaction. Acquires the Mutex once, wraps all operations in BEGIN/COMMIT. On error, rolls back and returns the error.

This is the key performance primitive — reduces N lock acquisitions and N fsyncs to 1 each.

Source

pub fn migration_is_applied(&self, id: &str) -> bool

Source

pub fn migration_mark_applied( &self, id: &str, scope: &str, duration_ms: u64, ) -> Result<(), StoreError>

Source

pub fn migrations_list_applied(&self) -> Result<Vec<String>, StoreError>

Source§

impl Store

Source

pub fn viewer_device_insert( &self, device_id: &str, token_hash: &str, device_name: &str, device_key: &str, created_ms: i64, ) -> Result<(), StoreError>

Record a new paired device.

Source

pub fn viewer_device_list(&self) -> Result<Vec<ViewerDevice>, StoreError>

Every paired device, oldest first.

Source

pub fn viewer_device_token_hashes( &self, ) -> Result<Vec<(String, String)>, StoreError>

(device_id, token_hash) of every paired device, for the bearer check, which compares each hash in constant time rather than looking one up.

Source

pub fn viewer_device_touch( &self, device_id: &str, last_seen_ms: i64, ) -> Result<bool, StoreError>

Stamp a device’s last request. False when it was revoked meanwhile.

Source

pub fn viewer_device_delete(&self, device_id: &str) -> Result<bool, StoreError>

Revoke a device. False when there was no such device.

Source

pub fn agent_hide_from_devices_get(&self, id: &str) -> Result<bool, StoreError>

Whether agent id (a definition id) is hidden from paired devices. False for an unknown agent.

Source

pub fn agent_hide_from_devices_set( &self, id: &str, hidden: bool, ) -> Result<bool, StoreError>

Hide agent id from paired devices, or show it again. False for an unknown agent. Leaves updated_at alone, like agent_last_runtime_set: who may watch the agent is not an edit of how it runs.

Source§

impl Store

Source

pub fn widget_storage_get( &self, widget_id: &str, key: &str, ) -> Result<Option<String>, StoreError>

Source

pub fn widget_storage_set( &self, widget_id: &str, key: &str, value: &str, max_total: usize, ) -> Result<bool, StoreError>

Sets key, unless the package’s keys and values would then take more than max_total bytes; returns whether it was set.

Source

pub fn widget_storage_delete( &self, widget_id: &str, key: &str, ) -> Result<(), StoreError>

Source

pub fn widget_storage_keys( &self, widget_id: &str, prefix: &str, ) -> Result<Vec<String>, StoreError>

The package’s keys that start with prefix, sorted.

Source

pub fn widget_storage_purge(&self, widget_id: &str) -> Result<(), StoreError>

Everything the package stored (when it’s uninstalled).

Source§

impl Store

Source

pub fn work_claim_put( &self, claim: &WorkClaim, now_ms: i64, ) -> Result<WorkClaim, StoreError>

Record claim, or renew the holder’s existing claim on the same target: its id and created_at stay, its note and expiry are replaced. Expired claims are deleted first. Returns the stored claim.

Source

pub fn work_claims_live( &self, now_ms: i64, ) -> Result<Vec<WorkClaim>, StoreError>

Every unexpired claim, oldest first.

Source

pub fn work_claim_release( &self, agent: &str, agent_uid: &str, id: Option<&str>, now_ms: i64, ) -> Result<Vec<WorkClaim>, StoreError>

Release the holder’s claims: the one with id, or all of them when id is None. Returns what was released; another agent’s claim is never touched.

Source§

impl Store

Source

pub fn work_queue_enqueue(&self, item: &WorkItem) -> Result<(), StoreError>

Insert a new open item. id is caller-supplied so an enqueue can be made idempotent by the caller (re-enqueueing the same id is a conflict, not a duplicate).

Source

pub fn work_queue_claim( &self, filter: &ClaimFilter, now_ms: i64, lease_ms: i64, ) -> Result<Option<WorkItem>, StoreError>

Atomically claim the highest-priority eligible item, or None when nothing is claimable.

The entire claim is ONE conditional UPDATE ... RETURNING. That is what makes concurrent claims safe: SQLite serializes writers, so exactly one caller can observe a given row in open and transition it. Do NOT refactor this into a SELECT-then-UPDATE — that reintroduces the read-modify-write race this shape exists to avoid, and the race is invisible in single-threaded tests.

now_ms and lease_ms are injected rather than read from the clock here so tests can drive expiry deterministically.

Source

pub fn work_queue_heartbeat( &self, id: &str, agent_id: &str, caller_uid: &str, attempt: i64, now_ms: i64, lease_ms: i64, ) -> Result<Option<HolderMatch>, StoreError>

Extend a live lease.

attempt is the fence token — the attempts value returned by the claim this call belongs to. Holder identity alone is NOT sufficient (Codex P1 on PR #2898): agent_id is stable across claims, so after expire → reap → the SAME agent reclaims, a delayed call left over from the FIRST claim still satisfies a holder-and-state predicate and would silently operate on the SECOND claim. That is a textbook ABA. attempts increments on every claim, so comparing it fences each claim instance apart with no extra column.

Source

pub fn work_queue_complete( &self, id: &str, agent_id: &str, caller_uid: &str, attempt: i64, result: &str, now_ms: i64, ) -> Result<Option<HolderMatch>, StoreError>

Mark a claimed item finished. Same holder + fence guard as heartbeat — a stale completion from a previous claim must not close out a newer one with an old result.

Source

pub fn work_queue_release( &self, id: &str, agent_id: &str, caller_uid: &str, attempt: i64, reason: &str, now_ms: i64, ) -> Result<Option<HolderMatch>, StoreError>

Give a claim back voluntarily.

attempts is NOT decremented — a release still consumed an attempt. And once those attempts are exhausted the item goes to failed, not back to open (Codex P2 on PR #2898): reopening unconditionally let a hot-potato item cycle claim→release→claim forever, because the reaper only parks rows that are still claimed and a released row isn’t. That silently defeated the documented attempt bound for exactly the case the bound exists for.

Fenced by attempt for the same ABA reason as work_queue_heartbeat.

Source

pub fn work_queue_reap(&self, now_ms: i64) -> Result<(usize, usize), StoreError>

Reclaim every item whose lease expired: back to open, or to failed once attempts >= max_attempts.

Returns (reopened, failed). Safe to call on any interval; it is idempotent because an already-reaped row is no longer claimed.

Source

pub fn work_queue_cancel( &self, id: &str, reason: &str, now_ms: i64, ) -> Result<bool, StoreError>

Explicitly abandon an item regardless of holder (operator action).

Source

pub fn work_queue_get(&self, id: &str) -> Result<Option<WorkItem>, StoreError>

Source

pub fn work_queue_claimed_by( &self, agent_id: &str, uid: &str, ) -> Result<Vec<WorkItem>, StoreError>

List items, newest-updated first. state empty = all states. Every item currently claimed by this agent — by name, or by UID when one is given — with no row cap. A superset of the store’s holder rule (HOLDER_BY_UID); callers narrow it with the exact rule. Used when an agent quits, to release its claims at once (SPEC_AGENT_SELF_QUIT §4.3): a capped fleet-wide list could miss them on a busy queue.

Source

pub fn work_queue_list( &self, state: &str, limit: usize, ) -> Result<Vec<WorkItem>, StoreError>

Trait Implementations§

Source§

impl DroneStore for Store

Source§

fn drone_list(&self) -> Result<Vec<DroneDefinition>, StoreError>

Source§

fn drone_get(&self, id: &str) -> Result<Option<DroneDefinition>, StoreError>

Source§

fn drone_upsert(&self, wf: &DroneDefinition) -> Result<(), StoreError>

Source§

fn drone_delete(&self, id: &str) -> Result<bool, StoreError>

Source§

fn drone_run_insert(&self, run: &DroneRun) -> Result<(), StoreError>

Source§

fn drone_run_update(&self, run: &DroneRun) -> Result<usize, StoreError>

Update an existing run row in place. Returns rows affected (0 if no row matched run.id). Used to flip a placeholder running row into its terminal state once the drain task completes, so the row exists from the moment the RPC returns.
Source§

fn drone_runs_for( &self, drone_id: &str, limit: i64, ) -> Result<Vec<DroneRun>, StoreError>

Auto Trait Implementations§

§

impl !Freeze for Store

§

impl RefUnwindSafe for Store

§

impl Send for Store

§

impl Sync for Store

§

impl Unpin for Store

§

impl UnsafeUnpin for Store

§

impl UnwindSafe for Store

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
§

impl<T> Downcast for T
where T: Any,

§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.
§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Convert Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be further downcast into Rc<ConcreteType> where ConcreteType implements Trait.
§

fn as_any(&self) -> &(dyn Any + 'static)

Convert &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Convert &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Send + Sync> ⓘ

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
§

impl<T> PolicyExt for T
where T: ?Sized,

§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] only if self and other return Action::Follow. Read more
§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

§

fn vzip(self) -> V

§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more