You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
review-database today stores per-node config (applied + draft) and a coarse config-reloadStatus. It has no notion of installed software: no version, no install/run lifecycle, no package-operation record. This effort adds that state additively, so that review (D2) and review-web (D3) have the types they need to install, update, and display packages on hosts.
The effort delivers, in dependency order:
a Lifecycle value type — the install/run state of a package on a host, distinct from and orthogonal to the existing config-reload Status — together with its first users, the installed_version, installed_commit, lifecycle and bound_addrs fields on Agent and ExternalService;
a core-component registry table keyed by (component, host) for host-fixed infrastructure (REView / aice-web-next / roxyd / bootroot) — these are not agents and not external services;
the operation_attempt ledger record — a durable, crash-safe record of a package operation, keyed uniquely by idempotency_key;
the ledger's secondary indexes, expiry sweep and retention rule, which are what make it usable as an orchestration guard;
the migration (migrate_0_46_to_0_47), the COMPATIBLE_VERSION_REQ bump to ">=0.47.0,<0.48.0", and registration of the two new column families — all in one slice;
a public format-version writer so a rolled-back REView can restore the VERSION markers together with a restored snapshot.
The source of truth is docs/rfcs/0001-install-update-state-schema.md in this repo. Each child issue restates the contract it needs and is implementable from its own text.
Children
Add the Lifecycle type and install/update fields to Agent and ExternalService — Value types
Add the core-component registry table — New tables
Add the operation_attempt ledger record and idempotency-keyed CRUD — New tables
Add operation_attempt secondary indexes, expiry sweep, and retention — New tables
Migrate to DB format 0.47.0 and register the new column families — Migration and format bump
Add a public format-version marker writer for rollback — Migration and format bump
Shared background
This repo only defines and persists types. Store layout, signature verification, GraphQL, and roxyd control belong to review (D2) and review-web (D3). Nothing here calls out to a host or a registrar.
No desired state. Install/update is an immediate imperative action, not a stored intent. There is no desired_version and no reconcile loop. Only actual state is stored, for display and for crash-safe resume. UpdateAvailable is computed by review (installed (version, commit) differs from the store's latest build) and is never stored here.
Build identity is (version, commit), never version alone. The same version may carry different commits — a pre-release rebuilt from a new commit, or a hotfix without a version bump. version is an opaque display label and is not required to be semver. Every place that records an installed or resolved build records both.
Where the data lives today. The public Node struct (src/tables/node.rs) exposes agents: Vec<Agent> and external_services: Vec<ExternalService>, but that is an assembled view. The persisted node record is the private Inner struct, which holds only the agent/external-service string keys. The Agent and ExternalServicevalues live in their own column families, AGENTS ("agents") and EXTERNAL_SERVICES ("external services"), listed in MAP_NAMES in src/tables.rs. Anything that has to touch those values — notably the migration — walks those two column families, not the node records.
Multi-instance is already expressible.Agent.key is <instance>.<service> and is unique per node, so two piglet instances on one node are two rows, 001.piglet and 002.piglet, under the same node_id and the same kind. No schema change is needed for that. What it does invalidate is any assumption that a node holds at most one row per AgentKind; no code path may assume that.
v1 pins the instance number to 1. At most one instance per (component, host) is stored and the number is always 1. There is deliberately no counter, reservation table, or release rule — a second install for a (component, host) that already has a row is refused, not given another number. The schema nevertheless carries the instance dimension (operation_attempt.instance, the (host, target, instance) index) so a later allocator is an extension rather than a migration.
Column families are registered only with the format bump.StateDb::open auto-creates every CF named in MAP_NAMES, while migrate_data_dir returns early for a data dir that is already at a compatible version. Adding a new CF name to MAP_NAMES before COMPATIBLE_VERSION_REQ bumps would silently mutate a 0.46.0 data dir with a new CF and no version change. So the new-table issues implement their table modules without touching MAP_NAMES, and the CF registration lands in the same change as the version bump.
Do not cut a release from the middle of this effort. Between the first child merging and the format-bump child merging, main carries a changed persisted record shape for Agent and ExternalService while COMPATIBLE_VERSION_REQ and the crate version still read 0.46.0 — so a build from main in that window advertises a 0.46.0 compatibility it no longer has, and would fail to read a real 0.46.0 data dir. The window is deliberate: the RFC's own ordering requires the shapes to exist before the migration that converts them, and it is CI-safe because no existing test builds a persisted Agent / ExternalService fixture from bytes. But no release may be cut from inside it. The effort is releasable again only once Migrate to DB format 0.47.0 and register the new column families has merged.
Naming trap.operation_attempt is not aice-web-next's apply_attempts table, and neither replaces the other. apply_attempts tracks one operator's config-Apply click through the UI (which drafts it intended to dispatch, whether the browser-side run still holds the lock). operation_attempt tracks the package operation REView is executing on a host, survives a REView restart, and carries the compensation owed to bootroot. They sit on different sides of the API, key on different things, and have different lifetimes. Do not collapse them or drive one from the other.
Execution order
Issues in the same wave have no unmet dependencies among these children and can run in parallel.
graph TD
issue825["#825 Add the `Lifecycle` type and install/update fields to `Agent` and `ExternalService` [phase: Value types]"]
issue826["#826 Add the core-component registry table [phase: New tables]"]
issue827["#827 Add the `operation_attempt` ledger record and idempotency-keyed CRUD [phase: New tables]"]
issue828["#828 Add `operation_attempt` secondary indexes, expiry sweep, and retention [phase: New tables]"]
issue829["#829 Migrate to DB format 0.47.0 and register the new column families [phase: Migration and format bump]"]
issue830["#830 Add a public format-version marker writer for rollback [phase: Migration and format bump]"]
issue858["#858 Protect operation attempt iteration [phase: unphased]"]
issue825 --> issue826
issue827 --> issue828
issue825 --> issue829
issue826 --> issue829
issue827 --> issue829
issue828 --> issue829
issue829 --> issue830
Install/update state schema (RFC D1)
Summary
review-databasetoday stores per-node config (applied + draft) and a coarse config-reloadStatus. It has no notion of installed software: no version, no install/run lifecycle, no package-operation record. This effort adds that state additively, so thatreview(D2) andreview-web(D3) have the types they need to install, update, and display packages on hosts.The effort delivers, in dependency order:
Lifecyclevalue type — the install/run state of a package on a host, distinct from and orthogonal to the existing config-reloadStatus— together with its first users, theinstalled_version,installed_commit,lifecycleandbound_addrsfields onAgentandExternalService;(component, host)for host-fixed infrastructure (REView / aice-web-next / roxyd / bootroot) — these are not agents and not external services;operation_attemptledger record — a durable, crash-safe record of a package operation, keyed uniquely byidempotency_key;migrate_0_46_to_0_47), theCOMPATIBLE_VERSION_REQbump to">=0.47.0,<0.48.0", and registration of the two new column families — all in one slice;VERSIONmarkers together with a restored snapshot.The source of truth is
docs/rfcs/0001-install-update-state-schema.mdin this repo. Each child issue restates the contract it needs and is implementable from its own text.Children
Lifecycletype and install/update fields toAgentandExternalService— Value typesoperation_attemptledger record and idempotency-keyed CRUD — New tablesoperation_attemptsecondary indexes, expiry sweep, and retention — New tablesShared background
This repo only defines and persists types. Store layout, signature verification, GraphQL, and roxyd control belong to
review(D2) andreview-web(D3). Nothing here calls out to a host or a registrar.No desired state. Install/update is an immediate imperative action, not a stored intent. There is no
desired_versionand no reconcile loop. Only actual state is stored, for display and for crash-safe resume.UpdateAvailableis computed byreview(installed(version, commit)differs from the store's latest build) and is never stored here.Build identity is
(version, commit), neverversionalone. The sameversionmay carry differentcommits — a pre-release rebuilt from a new commit, or a hotfix without a version bump.versionis an opaque display label and is not required to be semver. Every place that records an installed or resolved build records both.Where the data lives today. The public
Nodestruct (src/tables/node.rs) exposesagents: Vec<Agent>andexternal_services: Vec<ExternalService>, but that is an assembled view. The persisted node record is the privateInnerstruct, which holds only the agent/external-service string keys. TheAgentandExternalServicevalues live in their own column families,AGENTS("agents") andEXTERNAL_SERVICES("external services"), listed inMAP_NAMESinsrc/tables.rs. Anything that has to touch those values — notably the migration — walks those two column families, not the node records.Multi-instance is already expressible.
Agent.keyis<instance>.<service>and is unique per node, so twopigletinstances on one node are two rows,001.pigletand002.piglet, under the samenode_idand the samekind. No schema change is needed for that. What it does invalidate is any assumption that a node holds at most one row perAgentKind; no code path may assume that.v1 pins the instance number to 1. At most one instance per
(component, host)is stored and the number is always1. There is deliberately no counter, reservation table, or release rule — a second install for a(component, host)that already has a row is refused, not given another number. The schema nevertheless carries the instance dimension (operation_attempt.instance, the(host, target, instance)index) so a later allocator is an extension rather than a migration.Column families are registered only with the format bump.
StateDb::openauto-creates every CF named inMAP_NAMES, whilemigrate_data_dirreturns early for a data dir that is already at a compatible version. Adding a new CF name toMAP_NAMESbeforeCOMPATIBLE_VERSION_REQbumps would silently mutate a0.46.0data dir with a new CF and no version change. So the new-table issues implement their table modules without touchingMAP_NAMES, and the CF registration lands in the same change as the version bump.Do not cut a release from the middle of this effort. Between the first child merging and the format-bump child merging,
maincarries a changed persisted record shape forAgentandExternalServicewhileCOMPATIBLE_VERSION_REQand the crate version still read0.46.0— so a build frommainin that window advertises a0.46.0compatibility it no longer has, and would fail to read a real0.46.0data dir. The window is deliberate: the RFC's own ordering requires the shapes to exist before the migration that converts them, and it is CI-safe because no existing test builds a persistedAgent/ExternalServicefixture from bytes. But no release may be cut from inside it. The effort is releasable again only once Migrate to DB format 0.47.0 and register the new column families has merged.Naming trap.
operation_attemptis not aice-web-next'sapply_attemptstable, and neither replaces the other.apply_attemptstracks one operator's config-Apply click through the UI (which drafts it intended to dispatch, whether the browser-side run still holds the lock).operation_attempttracks the package operation REView is executing on a host, survives a REView restart, and carries the compensation owed to bootroot. They sit on different sides of the API, key on different things, and have different lifetimes. Do not collapse them or drive one from the other.Execution order
Issues in the same wave have no unmet dependencies among these children and can run in parallel.
Lifecycletype and install/update fields toAgentandExternalService#825, Add theoperation_attemptledger record and idempotency-keyed CRUD #827, Protect operation attempt iteration #858operation_attemptsecondary indexes, expiry sweep, and retention #828