-- ============================================================================ -- Krow — immutable published versions -- -- Phase 3. §3: "Immutable versions. Editing publishes a new version. Running -- conversations pin the version they started with." -- -- WHAT WAS WRONG -- -- `agent_definitions` holds one row per (org, definition_id) and editing it -- UPDATEs that row in place. The `version` column moves, but nothing keeps what -- version 2 said — so "which agent answered this?" has no answer once somebody -- saves, and the `agent_version` recorded on every run in `agent_runs` points at -- a definition that no longer exists in that form. -- -- That is tolerable for a curated set shipped with the deployment and wrong the -- moment a tenant edits their own agent, which is exactly the line Phase 3 has -- to cross. -- -- WHY A SECOND TABLE RATHER THAN VERSIONING THE FIRST -- -- The alternative is to widen the unique index to (org_id, definition_id, -- version) and mark one row current. It is fewer tables and it makes every -- existing read ambiguous: a query for "the Activity Agent" would silently -- return however many rows exist, and the ones that forgot the version -- predicate would appear to work until the second version was published. -- -- So `agent_definitions` keeps meaning exactly what it means today — the -- current, editable definition — and every read of it is unchanged. This table -- is the history beside it, and it is APPEND-ONLY: no UPDATE path exists in the -- repository, and the trigger below refuses one at the database. -- -- WHAT PINS A RUN -- -- A run records agent_version already. With this table that number becomes -- resolvable: the loader can load the definition AS IT WAS, which is what makes -- a resumed run — and, more importantly, an approved write — execute against -- the agent the person was actually looking at. A confirmation approved against -- version 3 must not be carried out by version 4's tool list. -- -- WHAT IS DELIBERATELY ABSENT -- -- a diff or patch format Versions are whole snapshots. A patch chain has to -- be replayed to be read, and a corrupted link makes -- every later version unreadable. Markdown is small. -- deletion There is no path to remove a version. A trajectory -- referencing one that had been deleted would be a -- record nobody can explain, which is the thing -- §6 exists to prevent. -- ============================================================================ SET search_path = public; CREATE TABLE definition_versions ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- Which kind of definition. Agents and skills version identically and are -- kept in one table for that reason: two tables with the same columns and the -- same rules is two places to fix the next rule. kind text NOT NULL, org_id uuid NOT NULL REFERENCES organizations (id) ON DELETE CASCADE, -- The author-facing id, NOT a foreign key to agent_definitions. A version -- must outlive the definition it came from: deleting an agent must not -- destroy the record of what it said while it was answering. definition_id text NOT NULL, version integer NOT NULL, -- The whole definition as it was. Snapshot, not patch — see the note above. markdown text NOT NULL, -- Denormalised for listing a history without parsing every blob. name text NOT NULL DEFAULT '', description text NOT NULL DEFAULT '', pages text[] NOT NULL DEFAULT '{}', -- Who published it and when. SET NULL so a departed author's versions stay -- readable — the organization still has to answer for what its agents did. published_by uuid REFERENCES users (id) ON DELETE SET NULL, published_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT definition_versions_kind_check CHECK (kind IN ('agent', 'skill')), CONSTRAINT definition_versions_version_positive CHECK (version >= 1), CONSTRAINT definition_versions_markdown_not_blank CHECK (length(btrim(markdown)) > 0), -- One row per version per definition per tenant. This is what makes a version -- number mean something: publishing the same number twice is a bug, and it -- fails here rather than leaving two rows that disagree. CONSTRAINT definition_versions_unique UNIQUE (org_id, kind, definition_id, version) ); -- "Show me this definition's history", newest first. Also the lookup that -- resolves one specific version, which is the hot path. CREATE INDEX definition_versions_lookup_idx ON definition_versions (org_id, kind, definition_id, version DESC); -- Append-only, enforced here rather than trusted to the repository. -- -- A published version is a record of what a person approved and what an agent -- answered with. Code that edits one is code that rewrites history, and the -- reason to put this in the database is that the repository is not the only -- thing that can reach the table — a migration, a console session and a future -- service all can. CREATE FUNCTION definition_versions_immutable() RETURNS trigger AS $$ BEGIN RAISE EXCEPTION 'definition_versions is append-only: version % of % cannot be % (publish a new version instead)', OLD.version, OLD.definition_id, lower(TG_OP); END; $$ LANGUAGE plpgsql; CREATE TRIGGER definition_versions_no_update BEFORE UPDATE OR DELETE ON definition_versions FOR EACH ROW EXECUTE FUNCTION definition_versions_immutable(); COMMENT ON TABLE definition_versions IS 'Every published version of an agent or skill, as a whole snapshot. Append-only, enforced by ' 'trigger. A run records agent_version; this is what makes that number resolvable back to the ' 'definition that actually answered. See §3.';