@happyvertical/smrt-core 0.60.9 → 0.60.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/browser-entry.md +10 -0
- package/agents/change-feed.md +33 -0
- package/agents/object-runtime.md +8 -0
- package/agents/schema-paths.md +6 -0
- package/dist/browser.d.ts +2 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +3 -2
- package/dist/cascade.d.ts +17 -3
- package/dist/cascade.d.ts.map +1 -1
- package/dist/cascade.js +30 -15
- package/dist/cascade.js.map +1 -1
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +8 -4
- package/dist/collection.js.map +1 -1
- package/dist/embedded-write-queue.d.ts +18 -7
- package/dist/embedded-write-queue.d.ts.map +1 -1
- package/dist/embedded-write-queue.js +40 -10
- package/dist/embedded-write-queue.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/manifest/static-manifest.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +25 -7
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest.json +39 -7
- package/dist/migrations/differ.d.ts +4 -0
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +25 -9
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/orchestrate.d.ts +1 -1
- package/dist/migrations/orchestrate.d.ts.map +1 -1
- package/dist/migrations/orchestrate.js +5 -14
- package/dist/migrations/orchestrate.js.map +1 -1
- package/dist/native-duckdb.d.ts.map +1 -1
- package/dist/native-duckdb.js +5 -1
- package/dist/native-duckdb.js.map +1 -1
- package/dist/object.d.ts +26 -0
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +215 -55
- package/dist/object.js.map +1 -1
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +8 -4
- package/dist/schema/generator.js.map +1 -1
- package/dist/scripts/browser-entry-evaluation.mjs +63 -8
- package/dist/scripts/browser-unbundled-dev.mjs +5 -2
- package/dist/scripts/fixtures/browser-pglite-cascade.ts +107 -0
- package/dist/scripts/fixtures/pglite-peer/package.json +9 -0
- package/dist/scripts/fixtures/pglite-peer/peer.js +1 -0
- package/dist/scripts/fixtures/pglite-peer/pnpm-lock.yaml +22 -0
- package/dist/scripts/fixtures/pglite-peer/pnpm-workspace.yaml +1 -0
- package/dist/smrt-knowledge.json +19 -11
- package/dist/smrt-knowledge.okf.md +181 -0
- package/dist/sync-device.d.ts +6 -0
- package/dist/sync-device.d.ts.map +1 -0
- package/dist/sync-device.js +18 -0
- package/dist/sync-device.js.map +1 -0
- package/dist/sync-tombstones.d.ts +30 -0
- package/dist/sync-tombstones.d.ts.map +1 -0
- package/dist/sync-tombstones.js +112 -0
- package/dist/sync-tombstones.js.map +1 -0
- package/dist/system/schema.d.ts +5 -1
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +21 -4
- package/dist/system/schema.js.map +1 -1
- package/package.json +11 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","names":[],"sources":["../../src/system/schema.ts"],"sourcesContent":["/**\n * SMRT System Tables Schema\n *\n * System tables use _smrt_ prefix to avoid conflicts with user tables.\n * All system tables are created in the same database as user data.\n */\n\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\n\n/**\n * Context memory storage\n * Stores remembered context (learned strategies, patterns, selectors) for reuse\n */\nexport const CREATE_SMRT_CONTEXTS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_contexts (\n id TEXT PRIMARY KEY,\n owner_class TEXT NOT NULL,\n owner_id TEXT NOT NULL,\n scope TEXT NOT NULL,\n key TEXT NOT NULL,\n value TEXT,\n metadata TEXT,\n version INTEGER DEFAULT 1,\n confidence REAL DEFAULT 1.0,\n success_count INTEGER DEFAULT 0,\n failure_count INTEGER DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_used_at TIMESTAMP,\n expires_at TIMESTAMP,\n UNIQUE(owner_class, owner_id, scope, key, version)\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_owner\n ON _smrt_contexts(owner_class, owner_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_scope\n ON _smrt_contexts(scope);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_confidence\n ON _smrt_contexts(confidence);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_last_used\n ON _smrt_contexts(last_used_at);\n\n-- Retention predicate for pruneExpiredContexts() (#2375). Rows that never\n-- expire carry NULL here, so the index only has to cover the ones that do.\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_expires_at\n ON _smrt_contexts(expires_at);\n`;\n\n/**\n * Schema version tracking\n * Records which SMRT framework versions have been applied\n */\nexport const CREATE_SMRT_MIGRATIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_migrations (\n id TEXT PRIMARY KEY,\n version TEXT NOT NULL UNIQUE,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n description TEXT,\n checksum TEXT\n);\n`;\n\n/**\n * Schema migration tracking\n * Tracks applied schema migrations for idempotency, audit, and rollback\n */\nexport const CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_schema_migrations (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n version TEXT NOT NULL,\n checksum TEXT NOT NULL,\n applied_checksum TEXT,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n execution_time_ms INTEGER,\n package_name TEXT,\n source_file TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n error_message TEXT,\n attempts INTEGER DEFAULT 0,\n is_reversible INTEGER DEFAULT 1,\n rolled_back_at TIMESTAMP,\n applied_by TEXT,\n batch INTEGER\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_status\n ON _smrt_schema_migrations(status);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_applied_at\n ON _smrt_schema_migrations(applied_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_batch\n ON _smrt_schema_migrations(batch);\n`;\n\n/**\n * Embedding storage for semantic search\n * Stores embedding vectors for SMRT objects to enable vector similarity search\n */\nexport const CREATE_SMRT_EMBEDDINGS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_embeddings (\n id TEXT PRIMARY KEY,\n object_class TEXT NOT NULL,\n object_id TEXT NOT NULL,\n field_name TEXT NOT NULL,\n content_hash TEXT NOT NULL,\n embedding TEXT NOT NULL,\n model TEXT NOT NULL,\n dimensions INTEGER NOT NULL,\n provider TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n UNIQUE(object_class, object_id, field_name, model)\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_object\n ON _smrt_embeddings(object_class, object_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_class\n ON _smrt_embeddings(object_class);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_hash\n ON _smrt_embeddings(content_hash);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_model\n ON _smrt_embeddings(model);\n`;\n\n/**\n * Dispatch queue for inter-agent communication\n * Stores dispatch messages for asynchronous agent-to-agent signaling\n */\nexport const CREATE_SMRT_DISPATCH_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_dispatch (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n source TEXT NOT NULL,\n source_id TEXT,\n payload TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n attempts INTEGER DEFAULT 0,\n last_error TEXT,\n processed_at TIMESTAMP,\n processed_by TEXT,\n target_subscriber TEXT,\n correlation_id TEXT,\n tenant_id TEXT,\n metadata TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_status\n ON _smrt_dispatch(status);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_tenant_id\n ON _smrt_dispatch(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_type\n ON _smrt_dispatch(type);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_source\n ON _smrt_dispatch(source);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_created\n ON _smrt_dispatch(created_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_target\n ON _smrt_dispatch(target_subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_correlation\n ON _smrt_dispatch(correlation_id);\n\n-- Retention predicates of DispatchCollection.cleanup() (#2375): completed rows\n-- are aged out on processed_at, failed rows on updated_at. The status-only\n-- index above cannot serve either range.\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_status_processed\n ON _smrt_dispatch(status, processed_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_status_updated\n ON _smrt_dispatch(status, updated_at);\n`;\n\n/**\n * Dispatch subscriptions for persistent handlers\n * Stores subscriptions to dispatch types for agent processing\n */\nexport const CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_dispatch_subscriptions (\n id TEXT PRIMARY KEY,\n signal_type TEXT NOT NULL,\n subscriber TEXT NOT NULL,\n handler TEXT NOT NULL DEFAULT 'handleDispatch',\n delivery TEXT NOT NULL DEFAULT 'compete',\n enabled INTEGER DEFAULT 1,\n tenant_id TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\n-- Subscription identity is tenant-scoped (S5 #1398): the same\n-- (signal_type, subscriber) pair may exist independently in different tenants,\n-- so tenant B can no longer overwrite/delete/enable tenant A's subscription.\n-- A named UNIQUE index (rather than an inline UNIQUE constraint) is used so the\n-- compatibility migration can additively reshape existing tables. NULL tenant_id\n-- (global subscriptions) is deduped at the application layer by the NULL-aware\n-- upsert in @happyvertical/sql.\nCREATE UNIQUE INDEX IF NOT EXISTS uq_smrt_dispatch_subs_tenant_signal_subscriber\n ON _smrt_dispatch_subscriptions(tenant_id, signal_type, subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_subscriber\n ON _smrt_dispatch_subscriptions(subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_tenant_id\n ON _smrt_dispatch_subscriptions(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_signal_type\n ON _smrt_dispatch_subscriptions(signal_type);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_enabled\n ON _smrt_dispatch_subscriptions(enabled);\n`;\n\n/**\n * AI usage telemetry storage\n * Stores normalized AI usage records for reporting and billing hooks\n */\nexport const CREATE_SMRT_AI_USAGE_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_ai_usage (\n id TEXT PRIMARY KEY,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n operation TEXT NOT NULL,\n prompt_tokens INTEGER,\n completion_tokens INTEGER,\n total_tokens INTEGER,\n estimated_cost REAL,\n duration INTEGER NOT NULL,\n class_name TEXT,\n tenant_id TEXT,\n tags TEXT,\n created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_created\n ON _smrt_ai_usage(created_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_class\n ON _smrt_ai_usage(class_name);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_tenant\n ON _smrt_ai_usage(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_provider_model\n ON _smrt_ai_usage(provider, model);\n\n-- Serves both the tenant-scoped retention predicate of pruneAiUsage() and the\n-- subscriptions billing meter's tenant_id + created_at range scan, neither of\n-- which the single-column indexes above can satisfy (#2375).\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_tenant_created\n ON _smrt_ai_usage(tenant_id, created_at);\n`;\n\n/**\n * Append-only change feed (issue #1758)\n *\n * One row per framework save/delete: monotonic per-database sequence,\n * table, row id, operation (create/update/delete — deletes are tombstones),\n * tenant, timestamp. Written by the change-feed interceptor and read\n * through `getChangesSince()`.\n *\n * `seq` is deliberately a plain BIGINT PRIMARY KEY rather than a native\n * AUTOINCREMENT/identity/serial column: the appender allocates\n * `COALESCE(MAX(seq), 0) + 1` inside the INSERT (with a conflict retry),\n * which keeps committed sequences contiguous so commit order equals\n * sequence order on every engine. Native identity columns allocate before\n * commit, so under concurrent writers on MVCC engines a reader could\n * observe seq N+1 while seq N is still uncommitted and advance its cursor\n * past it — breaking the feed's no-missed-changes cursor guarantee. The\n * plain column also keeps this DDL portable across SQLite, Postgres and\n * DuckDB with no per-engine branching. See `change-feed.ts`.\n *\n * `row_id` is nullable: manual bumps (`bumpChangeFeed`) may record a\n * table-level change without a specific row.\n */\nexport const CREATE_SMRT_CHANGES_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_changes (\n seq BIGINT PRIMARY KEY,\n table_name TEXT NOT NULL,\n row_id TEXT,\n operation TEXT NOT NULL,\n tenant_id TEXT,\n created_at TIMESTAMP NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_table_seq\n ON _smrt_changes(table_name, seq);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_tenant_seq\n ON _smrt_changes(tenant_id, seq);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_created_at\n ON _smrt_changes(created_at);\n`;\n\n/**\n * PostgreSQL-only staging table for change-feed appends made inside a\n * caller-managed transaction (issue #2649).\n *\n * `_smrt_changes.seq` is allocated `COALESCE(MAX(seq), 0) + 1`, so two\n * concurrent appends contend on the same primary-key value and the loser\n * *waits for the winner's transaction to end*. When the winner is a long write\n * transaction that goes on to take row locks the loser already holds, that\n * wait closes a genuine lock cycle and PostgreSQL aborts one side with\n * `40P01` — an ordinary concurrent request killing a legitimate long write.\n *\n * The staging table breaks the cycle by removing the wait: an append issued\n * inside a caller transaction inserts here instead, where its identity value\n * conflicts with nothing and it never waits on another transaction. The row is\n * still fate-shared with the caller (a rollback removes it, exactly as\n * before). A later *drain* — {@link CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION}\n * — moves committed staged rows into `_smrt_changes` with contiguous\n * sequences, so the feed's cursor guarantee is unchanged.\n *\n * PostgreSQL-only on purpose: SQLite and DuckDB do not reach the defect\n * (SQLite serializes writers outright), and keeping the portable DDL untouched\n * keeps the embedded path byte-identical.\n */\nexport const CREATE_POSTGRES_SMRT_CHANGES_PENDING_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_changes_pending (\n pending_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n table_name TEXT NOT NULL,\n row_id TEXT,\n operation TEXT NOT NULL,\n tenant_id UUID,\n created_at TIMESTAMPTZ NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_pending_table\n ON _smrt_changes_pending(table_name);\n`;\n\n/** Name of the PostgreSQL staging table for deferred change-feed appends. */\nexport const POSTGRES_CHANGE_FEED_PENDING_TABLE = '_smrt_changes_pending';\n\n/** PostgreSQL materialization of the portable change-feed table DDL. */\nexport const CREATE_POSTGRES_SMRT_CHANGES_TABLE =\n CREATE_SMRT_CHANGES_TABLE.replace(/\\bTIMESTAMP\\b/g, 'TIMESTAMPTZ').replace(\n /\\btenant_id TEXT\\b/g,\n 'tenant_id UUID',\n );\n\n/** PostgreSQL helper used to isolate best-effort feed appends (#2026). */\nexport const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME = '_smrt_append_change';\n\n/** PostgreSQL one-statement batch wrapper around the isolated append helper. */\nexport const POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_NAME =\n '_smrt_append_changes';\n\n/** Legacy helper identity used before PostgreSQL Date values became instants. */\nexport const LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(text,text,text,text,timestamp without time zone)`;\n\n/** Exact PostgreSQL identity used for catalog lookup of the append helper. */\nexport const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(text,text,text,text,timestamp with time zone)`;\n\n/** PostgreSQL identity of the one-statement batch append helper. */\nexport const POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_NAME}(jsonb)`;\n\n/**\n * Body marker stamped into both PostgreSQL change-feed helpers (#2649).\n *\n * Existence is not currency: an install can hold a helper of the right name\n * and signature whose *body* predates a fix. The probe in `change-feed.ts`\n * matches this token against `pg_proc.prosrc`, so a stale body is detected and\n * replaced. Bump it whenever either helper's body changes in a way an existing\n * database must pick up.\n */\nexport const POSTGRES_CHANGE_FEED_HELPER_MARKER = 'smrt-change-feed-helpers:v5';\n\n/**\n * The previous body marker, still stamped into every v5 body (#3297).\n *\n * v5 only changes how a tenant id is coerced: it is converted to whatever type\n * the live `tenant_id` column has (TEXT before `db:migrate-uuid`, native uuid\n * after), so a v5 body is a drop-in for v4 callers. Older SMRT processes probe\n * for the v4 marker; carrying it stops them from replacing a v5 body with a\n * v4 one (which inserts text into a converted uuid column) during a\n * mixed-version rollout.\n */\nexport const POSTGRES_CHANGE_FEED_HELPER_COMPAT_MARKER =\n 'smrt-change-feed-helpers:v4';\n\n/**\n * Coerce a text tenant id to the live type of `<table>.tenant_id` (#3297).\n *\n * `jsonb_populate_record` builds the table's row type at execution time, so\n * the same body works before and after the column converts to uuid, and a\n * cached plan is invalidated by the ALTER like any other. Empty strings are\n * NULL, matching the TypeScript writers.\n */\nfunction postgresTenantIdAs(table: string, textExpression: string): string {\n return `(jsonb_populate_record(NULL::${table}, jsonb_build_object('tenant_id', NULLIF(${textExpression}, '')))).tenant_id`;\n}\n\n/** PostgreSQL helper that sequences staged change-feed appends (#2649). */\nexport const POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME = '_smrt_drain_changes';\n\n/**\n * Exact PostgreSQL identity of the drain helper (#2982): the batch limit plus\n * the live set of sensitive tables, which the drain purges from staging and\n * never sequences.\n */\nexport const POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME}(integer,text[])`;\n\n/**\n * Identity of the pre-#2982 single-argument drain, kept as a compatibility\n * entry point so replicas of an older SMRT that still call\n * `_smrt_drain_changes()` keep working during a mixed-version rollout. It\n * delegates to the two-argument drain with no exclusions — exactly the old\n * behavior — because it has no way to know the live sensitive set.\n */\nexport const POSTGRES_CHANGE_FEED_COMPAT_DRAIN_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME}(integer)`;\n\n/** How many staged entries one drain call sequences. */\nexport const POSTGRES_CHANGE_FEED_DRAIN_BATCH = 1000;\n\n/**\n * PostgreSQL-only drain: move committed staged appends into `_smrt_changes`.\n *\n * This is where the feed's sequence is actually allocated for entries written\n * inside a caller transaction. It runs in its own short transaction (an\n * autocommit append's, or a reader's `drainChangeFeed()` call), holds no user\n * row locks, and never waits on another transaction:\n *\n * - it only ever sees *committed* staged rows — an in-flight transaction's\n * staged row is invisible under MVCC, so the drain neither reads nor locks\n * it, and it is picked up by a later drain once that transaction commits;\n * - concurrent drains are excluded by a `pg_try_advisory_xact_lock`, which\n * *skips* rather than waits, so a drain can never become an edge in a lock\n * cycle either.\n *\n * Because exactly one drain runs at a time and it numbers rows\n * `COALESCE(MAX(seq), 0) + row_number()` in staged order, committed sequences\n * stay contiguous and no reader can observe seq N before seq N-1 — the change\n * feed's cursor guarantee is preserved verbatim (see `change-feed.ts`).\n *\n * Failures are returned as data, like the append helper, so a drain problem\n * never aborts the caller's transaction. A failed drain leaves the staged rows\n * in place for the next attempt.\n *\n * Sensitive tables (#2982): `p_excluded_tables` is the caller's live\n * sensitive set. Under the drain lock, staged rows for those tables are\n * deleted before any sequence is allocated, and the sequencing batch skips\n * them too, so an entry staged by an older replica (or left over from before\n * an upgrade) never reaches `_smrt_changes`. Neither parameter has a default:\n * a call with no arguments resolves only to the compatibility overload.\n */\nexport const CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION = `\nCREATE OR REPLACE FUNCTION ${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME}(\n p_limit INTEGER,\n p_excluded_tables TEXT[]\n)\nRETURNS TABLE(\n drained_seq BIGINT,\n drained_table TEXT,\n drained_row_id TEXT,\n drained_operation TEXT,\n drained_tenant_id TEXT,\n error_code TEXT,\n error_message TEXT\n)\nLANGUAGE plpgsql\nSECURITY INVOKER\nAS $smrt_change_feed_drain$\n-- ${POSTGRES_CHANGE_FEED_HELPER_MARKER}\n-- ${POSTGRES_CHANGE_FEED_HELPER_COMPAT_MARKER}\nDECLARE\n v_base BIGINT;\n v_error_code TEXT;\n v_error_message TEXT;\nBEGIN\n -- EVERY statement below sits inside this exception boundary, preflight\n -- included. The caller may own the surrounding transaction, and an error\n -- escaping this function would abort it — the #2026 contract. A probe that\n -- blocks on a table lock until statement_timeout is exactly that: it is not\n -- the insert, but it aborts the caller just the same.\n BEGIN\n IF to_regclass('_smrt_changes_pending') IS NULL THEN\n RETURN;\n END IF;\n\n -- Never sequence from inside a transaction that has already written. Doing\n -- so would hold freshly allocated sequences uncommitted for the rest of\n -- that transaction, which is precisely the wait edge #2649 removes — a\n -- reader calling getChangesSince() inside a long write transaction must\n -- not put it back. The staged rows keep for the next drain.\n IF pg_current_xact_id_if_assigned() IS NOT NULL THEN\n RETURN;\n END IF;\n\n -- Cheap common case: nothing staged, so an autocommit append pays one\n -- index probe rather than a drain.\n IF NOT EXISTS (SELECT 1 FROM _smrt_changes_pending LIMIT 1) THEN\n RETURN;\n END IF;\n\n -- Try, never wait: a drain that waited could become an edge in a cycle.\n IF NOT pg_try_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('change-feed-drain')\n ) THEN\n RETURN;\n END IF;\n\n -- The feed never records a sensitive table (#2982). Staged entries for\n -- one — from an older replica, or staged before the table was declared\n -- sensitive — are dropped here, before a sequence is allocated.\n -- Bounded like the sequencing batch, so a large legacy backlog cannot make\n -- one pass outlive statement_timeout and stall the feed; the remainder is\n -- purged by later passes, and the batch below skips it meanwhile.\n IF COALESCE(cardinality(p_excluded_tables), 0) > 0 THEN\n DELETE FROM _smrt_changes_pending AS sensitive\n WHERE sensitive.pending_id IN (\n SELECT staged.pending_id\n FROM _smrt_changes_pending AS staged\n WHERE staged.table_name = ANY(p_excluded_tables)\n ORDER BY staged.pending_id\n LIMIT GREATEST(COALESCE(p_limit, ${POSTGRES_CHANGE_FEED_DRAIN_BATCH}), 1)\n );\n END IF;\n\n SELECT COALESCE(MAX(changes.seq), 0) INTO v_base FROM _smrt_changes AS changes;\n\n RETURN QUERY\n WITH removed AS (\n DELETE FROM _smrt_changes_pending AS pending\n WHERE pending.pending_id IN (\n SELECT candidate.pending_id\n FROM _smrt_changes_pending AS candidate\n -- A sensitive row committed after the purge above is skipped, not\n -- sequenced; the next drain purges it.\n WHERE NOT (\n candidate.table_name = ANY(COALESCE(p_excluded_tables, ARRAY[]::TEXT[]))\n )\n ORDER BY candidate.pending_id\n LIMIT GREATEST(COALESCE(p_limit, ${POSTGRES_CHANGE_FEED_DRAIN_BATCH}), 1)\n )\n RETURNING\n pending.pending_id,\n pending.table_name,\n pending.row_id,\n pending.operation,\n pending.tenant_id,\n pending.created_at\n ),\n numbered AS (\n SELECT\n removed.*,\n v_base + row_number() OVER (ORDER BY removed.pending_id) AS new_seq\n FROM removed\n ),\n inserted AS (\n INSERT INTO _smrt_changes (\n seq,\n table_name,\n row_id,\n operation,\n tenant_id,\n created_at\n )\n SELECT\n numbered.new_seq,\n numbered.table_name,\n numbered.row_id,\n numbered.operation,\n ${postgresTenantIdAs('_smrt_changes', 'numbered.tenant_id::TEXT')},\n numbered.created_at\n FROM numbered\n RETURNING\n _smrt_changes.seq,\n _smrt_changes.table_name,\n _smrt_changes.row_id,\n _smrt_changes.operation,\n _smrt_changes.tenant_id\n )\n SELECT\n inserted.seq,\n inserted.table_name,\n inserted.row_id,\n inserted.operation,\n inserted.tenant_id::TEXT,\n NULL::TEXT,\n NULL::TEXT\n FROM inserted\n ORDER BY inserted.seq;\n EXCEPTION WHEN query_canceled OR assert_failure OR OTHERS THEN\n GET STACKED DIAGNOSTICS\n v_error_code = RETURNED_SQLSTATE,\n v_error_message = MESSAGE_TEXT;\n RETURN QUERY SELECT\n NULL::BIGINT,\n NULL::TEXT,\n NULL::TEXT,\n NULL::TEXT,\n NULL::TEXT,\n v_error_code,\n v_error_message;\n END;\nEND;\n$smrt_change_feed_drain$;\n`;\n\n/**\n * Compatibility single-argument drain (#2982): see\n * {@link POSTGRES_CHANGE_FEED_COMPAT_DRAIN_FUNCTION_IDENTITY}. Current SMRT\n * never calls it.\n */\nexport const CREATE_POSTGRES_CHANGE_FEED_COMPAT_DRAIN_FUNCTION = `\nCREATE OR REPLACE FUNCTION ${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME}(\n p_limit INTEGER DEFAULT ${POSTGRES_CHANGE_FEED_DRAIN_BATCH}\n)\nRETURNS TABLE(\n drained_seq BIGINT,\n drained_table TEXT,\n drained_row_id TEXT,\n drained_operation TEXT,\n drained_tenant_id TEXT,\n error_code TEXT,\n error_message TEXT\n)\nLANGUAGE plpgsql\nSECURITY INVOKER\nAS $smrt_change_feed_drain_compat$\n-- ${POSTGRES_CHANGE_FEED_HELPER_MARKER}\nDECLARE\n v_error_code TEXT;\n v_error_message TEXT;\nBEGIN\n -- Its own boundary, like the drain it delegates to: a resolution or\n -- privilege error on the inner call must come back as data, never abort an\n -- older replica's caller transaction (#2026).\n BEGIN\n RETURN QUERY\n SELECT * FROM ${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME}(p_limit, ARRAY[]::TEXT[]);\n EXCEPTION WHEN query_canceled OR assert_failure OR OTHERS THEN\n GET STACKED DIAGNOSTICS\n v_error_code = RETURNED_SQLSTATE,\n v_error_message = MESSAGE_TEXT;\n RETURN QUERY SELECT\n NULL::BIGINT,\n NULL::TEXT,\n NULL::TEXT,\n NULL::TEXT,\n NULL::TEXT,\n v_error_code,\n v_error_message;\n END;\nEND;\n$smrt_change_feed_drain_compat$;\n`;\n\n/**\n * PostgreSQL-only change-feed append function.\n *\n * A PL/pgSQL block with an EXCEPTION handler runs its body in an internal\n * subtransaction. Returning SQLSTATE as data lets the caller log or retry a\n * failed best-effort append without leaving its surrounding transaction in\n * PostgreSQL's aborted (25P02) state. This statement contains dollar-quoted\n * semicolons, so callers must execute it whole rather than adding it to\n * {@link ALL_SYSTEM_TABLES}, whose portable DDL entries are semicolon-split.\n */\nexport const CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nCREATE OR REPLACE FUNCTION ${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(\n p_table_name TEXT,\n p_row_id TEXT,\n p_operation TEXT,\n p_tenant_id TEXT,\n p_created_at TIMESTAMPTZ\n)\nRETURNS TABLE(\n allocated_seq BIGINT,\n error_code TEXT,\n error_message TEXT\n)\nLANGUAGE plpgsql\nSECURITY INVOKER\nAS $smrt_change_feed$\n-- ${POSTGRES_CHANGE_FEED_HELPER_MARKER}\n-- ${POSTGRES_CHANGE_FEED_HELPER_COMPAT_MARKER}\nDECLARE\n v_seq BIGINT;\n v_error_code TEXT;\n v_error_message TEXT;\n v_deferred BOOLEAN;\nBEGIN\n -- Has the caller already written in this transaction?\n --\n -- PostgreSQL assigns a transaction id at the first write, so a non-NULL\n -- pg_current_xact_id_if_assigned() means the caller may be holding row\n -- locks. Allocating the feed head here would then make a concurrent appender\n -- wait for this whole transaction to end — the wait edge that closes the\n -- #2649 cycle. An autocommit append reports NULL: its caller's row write\n -- committed as its own statement, so it holds no row locks and its inline\n -- allocation cannot be part of a cycle. (Requires PostgreSQL 13+.)\n --\n -- This is exactly the condition that makes a cycle possible, not an\n -- approximation of \"inside BEGIN\": a transaction that appends *before* its\n -- first write still allocates inline, but it holds no row locks at that\n -- moment, so nothing that waits on its feed row can also be waited on by it.\n -- Preflight inside its own exception boundary for the same reason the drain\n -- wraps its own: an error here would abort a caller-owned transaction\n -- (#2026), and a catalog probe is still a statement that can fail.\n BEGIN\n v_deferred := pg_current_xact_id_if_assigned() IS NOT NULL\n AND to_regclass('_smrt_changes_pending') IS NOT NULL;\n EXCEPTION WHEN query_canceled OR assert_failure OR OTHERS THEN\n GET STACKED DIAGNOSTICS\n v_error_code = RETURNED_SQLSTATE,\n v_error_message = MESSAGE_TEXT;\n RETURN QUERY SELECT NULL::BIGINT, v_error_code, v_error_message;\n RETURN;\n END;\n\n IF v_deferred THEN\n BEGIN\n -- Staged, not sequenced: an identity value conflicts with nothing, so\n -- this insert never waits on another transaction. It stays fate-shared\n -- with the caller (rollback removes it) and is moved into\n -- _smrt_changes, in order and with a contiguous sequence, by the next\n -- drain after the caller commits.\n INSERT INTO _smrt_changes_pending (\n table_name,\n row_id,\n operation,\n tenant_id,\n created_at\n )\n VALUES (\n p_table_name,\n p_row_id,\n p_operation,\n ${postgresTenantIdAs('_smrt_changes_pending', 'p_tenant_id')},\n p_created_at\n );\n\n -- NULL sequence with NULL error code is the staged marker.\n RETURN QUERY SELECT NULL::BIGINT, NULL::TEXT, NULL::TEXT;\n RETURN;\n EXCEPTION WHEN query_canceled OR assert_failure OR OTHERS THEN\n GET STACKED DIAGNOSTICS\n v_error_code = RETURNED_SQLSTATE,\n v_error_message = MESSAGE_TEXT;\n RETURN QUERY SELECT NULL::BIGINT, v_error_code, v_error_message;\n RETURN;\n END;\n END IF;\n\n -- Autocommit append. The caller's own row write already committed as its own\n -- statement, so this transaction holds no user row locks and allocating the\n -- head here cannot join a lock cycle.\n --\n -- This deliberately does NOT drain: a drain performed here would sequence\n -- staged entries that the JavaScript caller never sees, so no live SSE\n -- signal would be published for them while this append's own signal carries\n -- a HIGHER sequence -- and an EventSource that stores that id as its\n -- Last-Event-ID resumes above them and never receives them. Draining is\n -- driven from JavaScript (drainChangeFeed) so every sequenced entry gets\n -- its signal.\n BEGIN\n INSERT INTO _smrt_changes (\n seq,\n table_name,\n row_id,\n operation,\n tenant_id,\n created_at\n )\n SELECT\n COALESCE(MAX(changes.seq), 0) + 1,\n p_table_name,\n p_row_id,\n p_operation,\n ${postgresTenantIdAs('_smrt_changes', 'p_tenant_id')},\n p_created_at\n FROM _smrt_changes AS changes\n RETURNING _smrt_changes.seq INTO v_seq;\n\n RETURN QUERY SELECT v_seq, NULL::TEXT, NULL::TEXT;\n EXCEPTION WHEN query_canceled OR assert_failure OR OTHERS THEN\n GET STACKED DIAGNOSTICS\n v_error_code = RETURNED_SQLSTATE,\n v_error_message = MESSAGE_TEXT;\n RETURN QUERY SELECT NULL::BIGINT, v_error_code, v_error_message;\n END;\nEND;\n$smrt_change_feed$;\n`;\n\n/**\n * PostgreSQL batch append helper. It makes the staged/direct decision before\n * its own writes, then appends every element through that one path. This is\n * necessary because a direct first insert assigns the transaction id that\n * later entries must not mistake for a caller-owned write transaction.\n */\nexport const CREATE_POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION = `\nCREATE OR REPLACE FUNCTION ${POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_NAME}(\n p_entries JSONB\n)\nRETURNS TABLE(\n entry_index INTEGER,\n allocated_seq BIGINT,\n error_code TEXT,\n error_message TEXT\n)\nLANGUAGE plpgsql\nSECURITY INVOKER\nAS $smrt_change_feed_batch$\n-- ${POSTGRES_CHANGE_FEED_HELPER_MARKER}\n-- ${POSTGRES_CHANGE_FEED_HELPER_COMPAT_MARKER}\nDECLARE\n v_deferred BOOLEAN;\n v_error_code TEXT;\n v_error_message TEXT;\nBEGIN\n BEGIN\n v_deferred := pg_current_xact_id_if_assigned() IS NOT NULL\n AND to_regclass('_smrt_changes_pending') IS NOT NULL;\n\n IF v_deferred THEN\n RETURN QUERY\n WITH input AS (\n SELECT entries.ordinality::INTEGER - 1 AS entry_index, entries.value\n FROM jsonb_array_elements(COALESCE(p_entries, '[]'::jsonb))\n WITH ORDINALITY AS entries(value, ordinality)\n ), staged AS (\n INSERT INTO _smrt_changes_pending (\n table_name, row_id, operation, tenant_id, created_at\n )\n SELECT\n input.value->>'table',\n input.value->>'rowId',\n input.value->>'operation',\n ${postgresTenantIdAs('_smrt_changes_pending', \"input.value->>'tenantId'\")},\n (input.value->>'timestamp')::TIMESTAMPTZ\n FROM input\n ORDER BY input.entry_index\n RETURNING 1\n )\n SELECT input.entry_index, NULL::BIGINT, NULL::TEXT, NULL::TEXT\n FROM input\n ORDER BY input.entry_index;\n RETURN;\n END IF;\n\n RETURN QUERY\n WITH input AS (\n SELECT entries.ordinality::INTEGER - 1 AS entry_index, entries.value\n FROM jsonb_array_elements(COALESCE(p_entries, '[]'::jsonb))\n WITH ORDINALITY AS entries(value, ordinality)\n ), numbered AS (\n SELECT\n input.*,\n COALESCE((SELECT MAX(seq) FROM _smrt_changes), 0)\n + row_number() OVER (ORDER BY input.entry_index) AS new_seq\n FROM input\n ), inserted AS (\n INSERT INTO _smrt_changes (\n seq, table_name, row_id, operation, tenant_id, created_at\n )\n SELECT\n numbered.new_seq,\n numbered.value->>'table',\n numbered.value->>'rowId',\n numbered.value->>'operation',\n ${postgresTenantIdAs('_smrt_changes', \"numbered.value->>'tenantId'\")},\n (numbered.value->>'timestamp')::TIMESTAMPTZ\n FROM numbered\n ORDER BY numbered.entry_index\n RETURNING seq\n )\n SELECT\n numbered.entry_index,\n numbered.new_seq,\n NULL::TEXT,\n NULL::TEXT\n FROM numbered\n ORDER BY numbered.entry_index;\n EXCEPTION WHEN query_canceled OR assert_failure OR OTHERS THEN\n GET STACKED DIAGNOSTICS\n v_error_code = RETURNED_SQLSTATE,\n v_error_message = MESSAGE_TEXT;\n RETURN QUERY SELECT NULL::INTEGER, NULL::BIGINT, v_error_code, v_error_message;\n END;\nEND;\n$smrt_change_feed_batch$;\n`;\n\nconst POSTGRES_CHANGE_FEED_PENDING_DDL =\n CREATE_POSTGRES_SMRT_CHANGES_PENDING_TABLE.split(';')\n .map((statement) => statement.trim())\n .filter((statement) => statement.length > 0)\n .map(\n (statement) => `EXECUTE $smrt_change_feed_pending_ddl$\n${statement}\n$smrt_change_feed_pending_ddl$;`,\n )\n .join('\\n');\n\n/**\n * Install both drain overloads inside a helper DO block (#2982).\n *\n * The two-argument drain is the first framework routine identity added to\n * databases that already exist, and a new function gets PostgreSQL's default\n * ACL — which a hardened deployment (PUBLIC EXECUTE revoked, the runtime role\n * granted explicitly by the permissions planner) would leave the runtime\n * unable to call. When this block creates it, it therefore inherits the\n * compatibility drain's reconciled EXECUTE grants. A role that may not grant\n * keeps the default ACL; the permissions planner reports and repairs it.\n */\nconst POSTGRES_CHANGE_FEED_DRAIN_INSTALL = ` DECLARE\n v_drain_created BOOLEAN :=\n to_regprocedure('${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY}') IS NULL;\n v_grant RECORD;\n BEGIN\n EXECUTE $smrt_change_feed_drain_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION.trim()}\n$smrt_change_feed_drain_ddl$;\n EXECUTE $smrt_change_feed_drain_compat_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_COMPAT_DRAIN_FUNCTION.trim()}\n$smrt_change_feed_drain_compat_ddl$;\n IF v_drain_created AND (\n SELECT p.proacl FROM pg_proc AS p\n WHERE p.oid = to_regprocedure('${POSTGRES_CHANGE_FEED_COMPAT_DRAIN_FUNCTION_IDENTITY}')\n ) IS NOT NULL THEN\n BEGIN\n EXECUTE 'REVOKE ALL ON FUNCTION ${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY} FROM PUBLIC';\n FOR v_grant IN\n SELECT acl.grantee\n FROM pg_proc AS p, aclexplode(p.proacl) AS acl\n WHERE p.oid = to_regprocedure('${POSTGRES_CHANGE_FEED_COMPAT_DRAIN_FUNCTION_IDENTITY}')\n AND acl.privilege_type = 'EXECUTE'\n LOOP\n IF v_grant.grantee = 0 THEN\n EXECUTE 'GRANT EXECUTE ON FUNCTION ${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY} TO PUBLIC';\n ELSE\n EXECUTE format(\n 'GRANT EXECUTE ON FUNCTION ${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY} TO %I',\n pg_get_userbyid(v_grant.grantee)\n );\n END IF;\n END LOOP;\n EXCEPTION WHEN insufficient_privilege THEN\n NULL;\n END;\n END IF;\n END;`;\n\n/**\n * Serialize PostgreSQL helper replacement inside one server-side statement.\n *\n * The transaction-scoped advisory lock prevents concurrent bootstraps from\n * racing on PostgreSQL's `pg_proc` uniqueness constraint. The nested dollar\n * quote keeps the complete function DDL atomic from the client's perspective.\n */\nexport const REPLACE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nDO $smrt_replace_change_feed$\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n${POSTGRES_CHANGE_FEED_PENDING_DDL}\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n${POSTGRES_CHANGE_FEED_DRAIN_INSTALL}\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n EXECUTE $smrt_change_feed_batch_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION.trim()}\n$smrt_change_feed_batch_ddl$;\nEND;\n$smrt_replace_change_feed$;\n`;\n\n/**\n * Install the PostgreSQL helpers only when missing, serialized server-side.\n *\n * A client-side catalog probe remains the fast path for already-initialized\n * read handles. This guarded statement is the cold-path race boundary: both\n * the advisory lock and the post-lock catalog check run before function DDL.\n *\n * The staging table and the drain helper (#2649) are created unconditionally\n * (`IF NOT EXISTS` / `CREATE OR REPLACE`) because an install that already has\n * the append helper from an older SMRT may still be missing them; the append\n * helper itself is replaced when the staging table was absent, which is how an\n * upgraded database picks up the deferring version.\n */\nexport const ENSURE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nDO $smrt_ensure_change_feed$\nDECLARE\n v_append_stale BOOLEAN;\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n v_append_stale := to_regclass('${POSTGRES_CHANGE_FEED_PENDING_TABLE}') IS NULL\n OR NOT EXISTS (\n SELECT 1\n FROM pg_proc\n WHERE oid = to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}')\n AND prosrc LIKE '%${POSTGRES_CHANGE_FEED_HELPER_MARKER}%'\n ) OR NOT EXISTS (\n SELECT 1 FROM pg_proc\n WHERE oid = to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_IDENTITY}')\n AND prosrc LIKE '%${POSTGRES_CHANGE_FEED_HELPER_MARKER}%'\n );\n${POSTGRES_CHANGE_FEED_PENDING_DDL}\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n${POSTGRES_CHANGE_FEED_DRAIN_INSTALL}\n IF v_append_stale THEN\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n EXECUTE $smrt_change_feed_batch_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION.trim()}\n$smrt_change_feed_batch_ddl$;\n END IF;\nEND;\n$smrt_ensure_change_feed$;\n`;\n\nconst POSTGRES_CHANGE_FEED_SCHEMA_DDL =\n CREATE_POSTGRES_SMRT_CHANGES_TABLE.split(';')\n .map((statement) => statement.trim())\n .filter((statement) => statement.length > 0)\n .map(\n (statement) => `EXECUTE $smrt_change_feed_schema_ddl$\n${statement}\n$smrt_change_feed_schema_ddl$;`,\n )\n .join('\\n');\n\n/**\n * Install the complete PostgreSQL change-feed schema under the bootstrap lock.\n *\n * The lock is deliberately acquired before table or index DDL. Framework\n * bootstrap uses the same ordering, so a raw-handle cold start cannot retain\n * catalog locks while waiting behind a framework bootstrap transaction.\n */\nexport const ENSURE_POSTGRES_CHANGE_FEED_SCHEMA = `\nDO $smrt_ensure_change_feed_schema$\nDECLARE\n v_append_stale BOOLEAN;\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n v_append_stale := to_regclass('${POSTGRES_CHANGE_FEED_PENDING_TABLE}') IS NULL\n OR NOT EXISTS (\n SELECT 1\n FROM pg_proc\n WHERE oid = to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}')\n AND prosrc LIKE '%${POSTGRES_CHANGE_FEED_HELPER_MARKER}%'\n ) OR NOT EXISTS (\n SELECT 1 FROM pg_proc\n WHERE oid = to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_IDENTITY}')\n AND prosrc LIKE '%${POSTGRES_CHANGE_FEED_HELPER_MARKER}%'\n );\n${POSTGRES_CHANGE_FEED_SCHEMA_DDL}\n${POSTGRES_CHANGE_FEED_PENDING_DDL}\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n${POSTGRES_CHANGE_FEED_DRAIN_INSTALL}\n IF v_append_stale THEN\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n EXECUTE $smrt_change_feed_batch_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION.trim()}\n$smrt_change_feed_batch_ddl$;\n END IF;\nEND;\n$smrt_ensure_change_feed_schema$;\n`;\n\n/**\n * Data backfill tracking\n *\n * Distinct from `_smrt_schema_migrations` — backfills are app-specific\n * data corrections (slug rewrites, model splits, lookup-table seeds) that\n * don't have schema diffs or rollback semantics. Apps register backfills\n * by name and the tracker handles idempotency.\n */\nexport const CREATE_SMRT_BACKFILLS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_backfills (\n name TEXT PRIMARY KEY,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n description TEXT,\n package_name TEXT\n);\n`;\n\n/** The insert-only claim table backing `runOnce()` (`../run-once.ts`). Browser-safe: retention imports it. */\nexport const RUN_ONCE_CLAIMS_TABLE = '_smrt_run_once_claims';\n\n/**\n * Idempotency claims for `runOnce()` (#3080).\n *\n * `claim_key` is the sha256 hex digest of the sorted-key-JSON encoding of\n * `(tenantId, actor, token, contentDigest)` — see `../run-once.ts` for the\n * derivation and why the token alone is not enough (it collapses distinct\n * submissions that reuse a form's token). It is the table's PRIMARY KEY, so\n * a second `INSERT` for the same key always raises a unique-constraint\n * violation instead of silently adopting the first caller's row: exactly the\n * \"insert-only against a UNIQUE column\" claim the issue asks for.\n *\n * Neither the raw token nor the raw submitted content is stored — only\n * digests — so this table cannot leak submitted business data. `status`\n * moves from `'in_progress'` to `'completed'` inside the SAME transaction\n * that runs the caller's work, so `result` (the caller's JSON-serialized,\n * ready-to-replay return value) and `completed_at` are only ever set once,\n * atomically with whatever else that transaction wrote. If the work throws,\n * `runOnce()` never marks the claim completed, and the transaction that\n * inserted it rolls back the insert along with it — a claim is never left\n * permanently `'in_progress'` by a failed attempt; a retry with the same key\n * can always run again.\n */\nexport const CREATE_SMRT_RUN_ONCE_CLAIMS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_run_once_claims (\n claim_key TEXT PRIMARY KEY,\n tenant_id TEXT NOT NULL,\n actor TEXT NOT NULL,\n content_digest TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'in_progress',\n result TEXT,\n created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n completed_at TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_run_once_claims_tenant\n ON _smrt_run_once_claims(tenant_id, created_at);\n\n-- Retention predicate for pruneRunOnceClaims() (#3080): completed claims are\n-- aged out on completed_at, scoped to status = 'completed' so an in_progress\n-- claim (completed_at IS NULL) is never matched by the sweep.\nCREATE INDEX IF NOT EXISTS idx_smrt_run_once_claims_status_completed\n ON _smrt_run_once_claims(status, completed_at);\n`;\n\n/**\n * All system table creation statements\n */\nexport const ALL_SYSTEM_TABLES = [\n CREATE_SMRT_CONTEXTS_TABLE,\n CREATE_SMRT_MIGRATIONS_TABLE,\n CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE,\n CREATE_SMRT_BACKFILLS_TABLE,\n CREATE_SMRT_RUN_ONCE_CLAIMS_TABLE,\n CREATE_SMRT_EMBEDDINGS_TABLE,\n CREATE_SMRT_DISPATCH_TABLE,\n CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE,\n CREATE_SMRT_AI_USAGE_TABLE,\n CREATE_SMRT_CHANGES_TABLE,\n];\n\n/**\n * Framework-owned tables that are backed by `@smrt()` models rather than by\n * {@link ALL_SYSTEM_TABLES}, and whose write volume disqualifies them from the\n * change feed.\n *\n * This is the jobs runner's own state: the claim loop and the forge projection\n * poll every second and rewrite these rows several times per job, so feeding\n * them into an append-only log whose pruning is not scheduled (issue #2375)\n * would dominate it with data no client syncs.\n *\n * The list is deliberately short and biased toward *observing*. A table wrongly\n * excluded is an invisible bug — exactly the one issue #2376 fixes — while a\n * table wrongly included costs a few extra feed rows. Everything else keeping\n * the `_smrt_` prefix (feature flags, prompt overrides, subscription plans,\n * field policies, agent and report schedules, report state) is observed.\n */\nexport const FRAMEWORK_OPERATIONAL_TABLES: readonly string[] = [\n '_smrt_jobs',\n '_smrt_job_events',\n '_smrt_workers',\n '_smrt_forge_deliveries',\n '_smrt_forge_projection_checkpoints',\n];\n\n/**\n * Framework tables that are backed by `@smrt()` models *and* reshaped by\n * `system/compatibility.ts`.\n *\n * They are dual-owned: `db:migrate` creates them from the jobs manifest, and\n * the compatibility pass adds columns/indexes that predate the manifest. On a\n * fresh install the manifest wins the race — bootstrap runs before the tables\n * exist — so their compatibility pass is deferred and re-checked until the\n * tables show up (issue #2376).\n */\nexport const DEFERRED_COMPATIBILITY_TABLES: readonly string[] = [\n '_smrt_jobs',\n '_smrt_job_events',\n];\n\n/**\n * System tables SMRT used to create on every database and no longer does.\n *\n * `_smrt_signals` never had a writer and `_smrt_registry` was only written by\n * an `ObjectRegistry.persistToDatabase()` helper that nothing ever called, so\n * both were empty on every deployment. New databases no longer get them.\n * Existing databases keep the empty tables until an operator drops them:\n *\n * ```sql\n * DROP TABLE IF EXISTS _smrt_signals;\n * DROP TABLE IF EXISTS _smrt_registry;\n * ```\n *\n * The framework deliberately does not issue those drops itself — dropping a\n * table is not reversible and a deployment may have repurposed the name.\n */\nexport const RETIRED_SYSTEM_TABLES: readonly string[] = [\n '_smrt_registry',\n '_smrt_signals',\n];\n\n/**\n * System-table columns that hold a real tenant id (#3297): a tenant UUID from\n * the tenant context, or NULL for global rows. On PostgreSQL they materialize\n * as native `uuid`, like every application `tenant_id`; other engines keep\n * TEXT, where `UUID` renders as TEXT anyway. Existing PostgreSQL TEXT columns\n * convert through `smrt db:migrate-uuid` in a maintenance window.\n *\n * Deliberately NOT here (they stay TEXT):\n * - text identity encodings that are not tenant ids: `_smrt_contexts.owner_id`\n * (`__collection__` / `__collection__:<tenantId>` scope keys, object/agent\n * row ids), `_smrt_run_once_claims.actor` (an arbitration namespace),\n * `_smrt_dispatch.processed_by` (a subscriber name) and\n * `_smrt_schema_migrations.applied_by` (a hostname);\n * - `_smrt_run_once_claims.tenant_id`: a NOT NULL claim scope folded into the\n * claim key; a caller without a tenant must pass a scope string, so it is a\n * scope key, not a tenant FK;\n * - `_smrt_ai_usage.tenant_id`: model usage writes tenant UUIDs, but the\n * published external-agent usage API accepts any non-empty text tenant id,\n * so converting it is a contract change tracked separately.\n */\nexport const SYSTEM_TABLE_TENANT_UUID_COLUMNS: ReadonlyArray<{\n table: string;\n column: string;\n}> = Object.freeze([\n { table: '_smrt_dispatch', column: 'tenant_id' },\n { table: '_smrt_dispatch_subscriptions', column: 'tenant_id' },\n { table: '_smrt_changes', column: 'tenant_id' },\n { table: '_smrt_changes_pending', column: 'tenant_id' },\n]);\n\n/**\n * Materialize the portable system-table schema for a target engine.\n *\n * PostgreSQL must use TIMESTAMPTZ because every system Date is an instant,\n * and native UUID for the tenant columns in\n * {@link SYSTEM_TABLE_TENANT_UUID_COLUMNS} (#3297).\n * PostgreSQL and DuckDB also materialize integer storage as BIGINT, matching\n * application-table DDL; SQLite keeps its variable-width INTEGER.\n */\nexport function getSystemTableDDL(engine: DatabaseEngine): string[] {\n return ALL_SYSTEM_TABLES.map((ddl) =>\n getSystemTableDDLForEngine(ddl, engine),\n );\n}\n\n/** Materialize one portable system-table statement for a target engine. */\nexport function getSystemTableDDLForEngine(\n ddl: string,\n engine: DatabaseEngine,\n): string {\n const integerDdl =\n engine === 'postgres' || engine === 'duckdb' || engine === 'json'\n ? ddl.replace(/\\bINTEGER\\b/g, 'BIGINT')\n : ddl;\n return engine === 'postgres'\n ? toPostgresTenantUuid(integerDdl.replace(/\\bTIMESTAMP\\b/g, 'TIMESTAMPTZ'))\n : integerDdl;\n}\n\nconst POSTGRES_UUID_TENANT_TABLES = new Set(\n SYSTEM_TABLE_TENANT_UUID_COLUMNS.map(({ table }) => table),\n);\n\n/**\n * Render `tenant_id TEXT` as native uuid on PostgreSQL in the DDL of a table\n * listed in {@link SYSTEM_TABLE_TENANT_UUID_COLUMNS} (each system DDL\n * constant creates one table).\n */\nfunction toPostgresTenantUuid(ddl: string): string {\n const table = ddl.match(\n /CREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?([A-Za-z_][\\w$]*)/i,\n )?.[1];\n return table && POSTGRES_UUID_TENANT_TABLES.has(table)\n ? ddl.replace(/\\btenant_id TEXT\\b/g, 'tenant_id UUID')\n : ddl;\n}\n\n/**\n * Current SMRT system schema version.\n *\n * `bootstrapSystemTables()` skips replaying system DDL once this exact version\n * is recorded in `_smrt_migrations`, so every change to {@link ALL_SYSTEM_TABLES}\n * must bump it — otherwise existing databases never see the change. New\n * *columns* additionally need an `addColumnIfMissing()` entry in\n * `system/compatibility.ts`, because `CREATE TABLE IF NOT EXISTS` is a no-op on\n * a table that already exists.\n *\n * 1.10.1 retires the never-written `_smrt_registry` / `_smrt_signals` tables\n * (see {@link RETIRED_SYSTEM_TABLES}) and adds the retention-predicate indexes\n * from #2375 (`_smrt_contexts.expires_at`, `_smrt_ai_usage(tenant_id,\n * created_at)`, `_smrt_dispatch(status, processed_at)` and\n * `(status, updated_at)`) in the same replay. (`1.10.0` briefly existed only\n * on two unreleased, unmerged branches with two different, incompatible DDL\n * sets — never published, never stamped in any real `_smrt_migrations` table\n * — so merging them reuses no version an existing install could have already\n * recorded; see the note on {@link SMRT_SCHEMA_DDL_CHECKSUMS}.)\n *\n * 1.11.0 adds `_smrt_run_once_claims` (#3080), the insert-only claim table\n * behind `runOnce()` (`../run-once.ts`).\n *\n * 1.11.1 adds `idx_smrt_run_once_claims_status_completed` (#3080), the\n * `(status, completed_at)` index backing `pruneRunOnceClaims()`'s retention\n * predicate (`system/retention.ts`).\n */\nexport const SMRT_SCHEMA_VERSION = '1.11.1';\n\n/**\n * Canonical form of the system DDL that {@link SMRT_SCHEMA_DDL_CHECKSUMS} covers.\n *\n * Whitespace is normalized so reformatting alone does not trip the guard while\n * any token change does.\n */\nexport function getSystemSchemaChecksumInput(): string {\n return ALL_SYSTEM_TABLES.join('\\n').replace(/\\s+/g, ' ').trim();\n}\n\n/**\n * SHA-256 of {@link getSystemSchemaChecksumInput} for each system schema\n * version, oldest first.\n *\n * The discipline that keeps fresh installs and upgraded installs identical was\n * documented but unenforced (issue #2376): a contributor could edit the DDL and\n * ship it without bumping the version, and no existing database would ever\n * apply the change. `system-schema-evolution.test.ts` recomputes the current\n * hash and requires it to equal this map's entry for\n * {@link SMRT_SCHEMA_VERSION}.\n *\n * It is a *history*, not a single constant, so the guard cannot be silenced by\n * overwriting one value: every recorded hash must stay distinct, which means a\n * changed DDL has nowhere to be recorded except under a new version key. Adding\n * that key is the bump. This is also why merging two branches that each\n * independently bumped to the same version string (both `1.10.0`, covering two\n * different DDL sets — #2376's table retirement and #2375's retention indexes)\n * must land under a *new* key rather than picking a \"final\" checksum for the\n * shared number: nothing published or otherwise reachable ever stamped\n * `1.10.0`, so recording the merged DDL as `1.10.1` costs nothing and removes\n * any doubt about a version string meaning two different things to two\n * installs.\n *\n * Entries before 1.10.1 are not recorded — the guard starts here.\n */\nexport const SMRT_SCHEMA_DDL_CHECKSUMS: Readonly<Record<string, string>> =\n Object.freeze({\n '1.10.1':\n 'f796ee3b3f7ab8b9dc659ecaa68884ec01277408c3dd6edea540853fce369c16',\n '1.11.0':\n 'c51351a76c96beb64fa7e5922f78d776367af882ed1ff37229f9e9ffacee8372',\n '1.11.1':\n '98112937a3bbb3883c3db1fd5e836751f91a479c1c5da64d4410263a6581ef1f',\n });\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"schema.js","names":[],"sources":["../../src/system/schema.ts"],"sourcesContent":["/**\n * SMRT System Tables Schema\n *\n * System tables use _smrt_ prefix to avoid conflicts with user tables.\n * All system tables are created in the same database as user data.\n */\n\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\n\n/**\n * Context memory storage\n * Stores remembered context (learned strategies, patterns, selectors) for reuse\n */\nexport const CREATE_SMRT_CONTEXTS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_contexts (\n id TEXT PRIMARY KEY,\n owner_class TEXT NOT NULL,\n owner_id TEXT NOT NULL,\n scope TEXT NOT NULL,\n key TEXT NOT NULL,\n value TEXT,\n metadata TEXT,\n version INTEGER DEFAULT 1,\n confidence REAL DEFAULT 1.0,\n success_count INTEGER DEFAULT 0,\n failure_count INTEGER DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_used_at TIMESTAMP,\n expires_at TIMESTAMP,\n UNIQUE(owner_class, owner_id, scope, key, version)\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_owner\n ON _smrt_contexts(owner_class, owner_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_scope\n ON _smrt_contexts(scope);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_confidence\n ON _smrt_contexts(confidence);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_last_used\n ON _smrt_contexts(last_used_at);\n\n-- Retention predicate for pruneExpiredContexts() (#2375). Rows that never\n-- expire carry NULL here, so the index only has to cover the ones that do.\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_expires_at\n ON _smrt_contexts(expires_at);\n`;\n\n/**\n * Schema version tracking\n * Records which SMRT framework versions have been applied\n */\nexport const CREATE_SMRT_MIGRATIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_migrations (\n id TEXT PRIMARY KEY,\n version TEXT NOT NULL UNIQUE,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n description TEXT,\n checksum TEXT\n);\n`;\n\n/**\n * Schema migration tracking\n * Tracks applied schema migrations for idempotency, audit, and rollback\n */\nexport const CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_schema_migrations (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n version TEXT NOT NULL,\n checksum TEXT NOT NULL,\n applied_checksum TEXT,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n execution_time_ms INTEGER,\n package_name TEXT,\n source_file TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n error_message TEXT,\n attempts INTEGER DEFAULT 0,\n is_reversible INTEGER DEFAULT 1,\n rolled_back_at TIMESTAMP,\n applied_by TEXT,\n batch INTEGER\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_status\n ON _smrt_schema_migrations(status);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_applied_at\n ON _smrt_schema_migrations(applied_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_batch\n ON _smrt_schema_migrations(batch);\n`;\n\n/**\n * Embedding storage for semantic search\n * Stores embedding vectors for SMRT objects to enable vector similarity search\n */\nexport const CREATE_SMRT_EMBEDDINGS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_embeddings (\n id TEXT PRIMARY KEY,\n object_class TEXT NOT NULL,\n object_id TEXT NOT NULL,\n field_name TEXT NOT NULL,\n content_hash TEXT NOT NULL,\n embedding TEXT NOT NULL,\n model TEXT NOT NULL,\n dimensions INTEGER NOT NULL,\n provider TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n UNIQUE(object_class, object_id, field_name, model)\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_object\n ON _smrt_embeddings(object_class, object_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_class\n ON _smrt_embeddings(object_class);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_hash\n ON _smrt_embeddings(content_hash);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_model\n ON _smrt_embeddings(model);\n`;\n\n/**\n * Dispatch queue for inter-agent communication\n * Stores dispatch messages for asynchronous agent-to-agent signaling\n */\nexport const CREATE_SMRT_DISPATCH_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_dispatch (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n source TEXT NOT NULL,\n source_id TEXT,\n payload TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n attempts INTEGER DEFAULT 0,\n last_error TEXT,\n processed_at TIMESTAMP,\n processed_by TEXT,\n target_subscriber TEXT,\n correlation_id TEXT,\n tenant_id TEXT,\n metadata TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_status\n ON _smrt_dispatch(status);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_tenant_id\n ON _smrt_dispatch(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_type\n ON _smrt_dispatch(type);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_source\n ON _smrt_dispatch(source);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_created\n ON _smrt_dispatch(created_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_target\n ON _smrt_dispatch(target_subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_correlation\n ON _smrt_dispatch(correlation_id);\n\n-- Retention predicates of DispatchCollection.cleanup() (#2375): completed rows\n-- are aged out on processed_at, failed rows on updated_at. The status-only\n-- index above cannot serve either range.\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_status_processed\n ON _smrt_dispatch(status, processed_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_status_updated\n ON _smrt_dispatch(status, updated_at);\n`;\n\n/**\n * Dispatch subscriptions for persistent handlers\n * Stores subscriptions to dispatch types for agent processing\n */\nexport const CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_dispatch_subscriptions (\n id TEXT PRIMARY KEY,\n signal_type TEXT NOT NULL,\n subscriber TEXT NOT NULL,\n handler TEXT NOT NULL DEFAULT 'handleDispatch',\n delivery TEXT NOT NULL DEFAULT 'compete',\n enabled INTEGER DEFAULT 1,\n tenant_id TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\n-- Subscription identity is tenant-scoped (S5 #1398): the same\n-- (signal_type, subscriber) pair may exist independently in different tenants,\n-- so tenant B can no longer overwrite/delete/enable tenant A's subscription.\n-- A named UNIQUE index (rather than an inline UNIQUE constraint) is used so the\n-- compatibility migration can additively reshape existing tables. NULL tenant_id\n-- (global subscriptions) is deduped at the application layer by the NULL-aware\n-- upsert in @happyvertical/sql.\nCREATE UNIQUE INDEX IF NOT EXISTS uq_smrt_dispatch_subs_tenant_signal_subscriber\n ON _smrt_dispatch_subscriptions(tenant_id, signal_type, subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_subscriber\n ON _smrt_dispatch_subscriptions(subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_tenant_id\n ON _smrt_dispatch_subscriptions(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_signal_type\n ON _smrt_dispatch_subscriptions(signal_type);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_enabled\n ON _smrt_dispatch_subscriptions(enabled);\n`;\n\n/**\n * AI usage telemetry storage\n * Stores normalized AI usage records for reporting and billing hooks\n */\nexport const CREATE_SMRT_AI_USAGE_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_ai_usage (\n id TEXT PRIMARY KEY,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n operation TEXT NOT NULL,\n prompt_tokens INTEGER,\n completion_tokens INTEGER,\n total_tokens INTEGER,\n estimated_cost REAL,\n duration INTEGER NOT NULL,\n class_name TEXT,\n tenant_id TEXT,\n tags TEXT,\n created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_created\n ON _smrt_ai_usage(created_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_class\n ON _smrt_ai_usage(class_name);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_tenant\n ON _smrt_ai_usage(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_provider_model\n ON _smrt_ai_usage(provider, model);\n\n-- Serves both the tenant-scoped retention predicate of pruneAiUsage() and the\n-- subscriptions billing meter's tenant_id + created_at range scan, neither of\n-- which the single-column indexes above can satisfy (#2375).\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_tenant_created\n ON _smrt_ai_usage(tenant_id, created_at);\n`;\n\n/**\n * Append-only change feed (issue #1758)\n *\n * One row per framework save/delete: monotonic per-database sequence,\n * table, row id, operation (create/update/delete — deletes are tombstones),\n * tenant, timestamp. Written by the change-feed interceptor and read\n * through `getChangesSince()`.\n *\n * `seq` is deliberately a plain BIGINT PRIMARY KEY rather than a native\n * AUTOINCREMENT/identity/serial column: the appender allocates\n * `COALESCE(MAX(seq), 0) + 1` inside the INSERT (with a conflict retry),\n * which keeps committed sequences contiguous so commit order equals\n * sequence order on every engine. Native identity columns allocate before\n * commit, so under concurrent writers on MVCC engines a reader could\n * observe seq N+1 while seq N is still uncommitted and advance its cursor\n * past it — breaking the feed's no-missed-changes cursor guarantee. The\n * plain column also keeps this DDL portable across SQLite, Postgres and\n * DuckDB with no per-engine branching. See `change-feed.ts`.\n *\n * `row_id` is nullable: manual bumps (`bumpChangeFeed`) may record a\n * table-level change without a specific row.\n */\nexport const CREATE_SMRT_CHANGES_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_changes (\n seq BIGINT PRIMARY KEY,\n table_name TEXT NOT NULL,\n row_id TEXT,\n operation TEXT NOT NULL,\n tenant_id TEXT,\n created_at TIMESTAMP NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_table_seq\n ON _smrt_changes(table_name, seq);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_tenant_seq\n ON _smrt_changes(tenant_id, seq);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_created_at\n ON _smrt_changes(created_at);\n`;\n\n/**\n * PostgreSQL-only staging table for change-feed appends made inside a\n * caller-managed transaction (issue #2649).\n *\n * `_smrt_changes.seq` is allocated `COALESCE(MAX(seq), 0) + 1`, so two\n * concurrent appends contend on the same primary-key value and the loser\n * *waits for the winner's transaction to end*. When the winner is a long write\n * transaction that goes on to take row locks the loser already holds, that\n * wait closes a genuine lock cycle and PostgreSQL aborts one side with\n * `40P01` — an ordinary concurrent request killing a legitimate long write.\n *\n * The staging table breaks the cycle by removing the wait: an append issued\n * inside a caller transaction inserts here instead, where its identity value\n * conflicts with nothing and it never waits on another transaction. The row is\n * still fate-shared with the caller (a rollback removes it, exactly as\n * before). A later *drain* — {@link CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION}\n * — moves committed staged rows into `_smrt_changes` with contiguous\n * sequences, so the feed's cursor guarantee is unchanged.\n *\n * PostgreSQL-only on purpose: SQLite and DuckDB do not reach the defect\n * (SQLite serializes writers outright), and keeping the portable DDL untouched\n * keeps the embedded path byte-identical.\n */\nexport const CREATE_POSTGRES_SMRT_CHANGES_PENDING_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_changes_pending (\n pending_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n table_name TEXT NOT NULL,\n row_id TEXT,\n operation TEXT NOT NULL,\n tenant_id UUID,\n created_at TIMESTAMPTZ NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_pending_table\n ON _smrt_changes_pending(table_name);\n`;\n\n/** Name of the PostgreSQL staging table for deferred change-feed appends. */\nexport const POSTGRES_CHANGE_FEED_PENDING_TABLE = '_smrt_changes_pending';\n\n/** PostgreSQL materialization of the portable change-feed table DDL. */\nexport const CREATE_POSTGRES_SMRT_CHANGES_TABLE =\n CREATE_SMRT_CHANGES_TABLE.replace(/\\bTIMESTAMP\\b/g, 'TIMESTAMPTZ').replace(\n /\\btenant_id TEXT\\b/g,\n 'tenant_id UUID',\n );\n\n/** PostgreSQL helper used to isolate best-effort feed appends (#2026). */\nexport const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME = '_smrt_append_change';\n\n/** PostgreSQL one-statement batch wrapper around the isolated append helper. */\nexport const POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_NAME =\n '_smrt_append_changes';\n\n/** Legacy helper identity used before PostgreSQL Date values became instants. */\nexport const LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(text,text,text,text,timestamp without time zone)`;\n\n/** Exact PostgreSQL identity used for catalog lookup of the append helper. */\nexport const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(text,text,text,text,timestamp with time zone)`;\n\n/** PostgreSQL identity of the one-statement batch append helper. */\nexport const POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_NAME}(jsonb)`;\n\n/**\n * Body marker stamped into both PostgreSQL change-feed helpers (#2649).\n *\n * Existence is not currency: an install can hold a helper of the right name\n * and signature whose *body* predates a fix. The probe in `change-feed.ts`\n * matches this token against `pg_proc.prosrc`, so a stale body is detected and\n * replaced. Bump it whenever either helper's body changes in a way an existing\n * database must pick up.\n */\nexport const POSTGRES_CHANGE_FEED_HELPER_MARKER = 'smrt-change-feed-helpers:v5';\n\n/**\n * The previous body marker, still stamped into every v5 body (#3297).\n *\n * v5 only changes how a tenant id is coerced: it is converted to whatever type\n * the live `tenant_id` column has (TEXT before `db:migrate-uuid`, native uuid\n * after), so a v5 body is a drop-in for v4 callers. Older SMRT processes probe\n * for the v4 marker; carrying it stops them from replacing a v5 body with a\n * v4 one (which inserts text into a converted uuid column) during a\n * mixed-version rollout.\n */\nexport const POSTGRES_CHANGE_FEED_HELPER_COMPAT_MARKER =\n 'smrt-change-feed-helpers:v4';\n\n/**\n * Coerce a text tenant id to the live type of `<table>.tenant_id` (#3297).\n *\n * `jsonb_populate_record` builds the table's row type at execution time, so\n * the same body works before and after the column converts to uuid, and a\n * cached plan is invalidated by the ALTER like any other. Empty strings are\n * NULL, matching the TypeScript writers.\n */\nfunction postgresTenantIdAs(table: string, textExpression: string): string {\n return `(jsonb_populate_record(NULL::${table}, jsonb_build_object('tenant_id', NULLIF(${textExpression}, '')))).tenant_id`;\n}\n\n/** PostgreSQL helper that sequences staged change-feed appends (#2649). */\nexport const POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME = '_smrt_drain_changes';\n\n/**\n * Exact PostgreSQL identity of the drain helper (#2982): the batch limit plus\n * the live set of sensitive tables, which the drain purges from staging and\n * never sequences.\n */\nexport const POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME}(integer,text[])`;\n\n/**\n * Identity of the pre-#2982 single-argument drain, kept as a compatibility\n * entry point so replicas of an older SMRT that still call\n * `_smrt_drain_changes()` keep working during a mixed-version rollout. It\n * delegates to the two-argument drain with no exclusions — exactly the old\n * behavior — because it has no way to know the live sensitive set.\n */\nexport const POSTGRES_CHANGE_FEED_COMPAT_DRAIN_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME}(integer)`;\n\n/** How many staged entries one drain call sequences. */\nexport const POSTGRES_CHANGE_FEED_DRAIN_BATCH = 1000;\n\n/**\n * PostgreSQL-only drain: move committed staged appends into `_smrt_changes`.\n *\n * This is where the feed's sequence is actually allocated for entries written\n * inside a caller transaction. It runs in its own short transaction (an\n * autocommit append's, or a reader's `drainChangeFeed()` call), holds no user\n * row locks, and never waits on another transaction:\n *\n * - it only ever sees *committed* staged rows — an in-flight transaction's\n * staged row is invisible under MVCC, so the drain neither reads nor locks\n * it, and it is picked up by a later drain once that transaction commits;\n * - concurrent drains are excluded by a `pg_try_advisory_xact_lock`, which\n * *skips* rather than waits, so a drain can never become an edge in a lock\n * cycle either.\n *\n * Because exactly one drain runs at a time and it numbers rows\n * `COALESCE(MAX(seq), 0) + row_number()` in staged order, committed sequences\n * stay contiguous and no reader can observe seq N before seq N-1 — the change\n * feed's cursor guarantee is preserved verbatim (see `change-feed.ts`).\n *\n * Failures are returned as data, like the append helper, so a drain problem\n * never aborts the caller's transaction. A failed drain leaves the staged rows\n * in place for the next attempt.\n *\n * Sensitive tables (#2982): `p_excluded_tables` is the caller's live\n * sensitive set. Under the drain lock, staged rows for those tables are\n * deleted before any sequence is allocated, and the sequencing batch skips\n * them too, so an entry staged by an older replica (or left over from before\n * an upgrade) never reaches `_smrt_changes`. Neither parameter has a default:\n * a call with no arguments resolves only to the compatibility overload.\n */\nexport const CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION = `\nCREATE OR REPLACE FUNCTION ${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME}(\n p_limit INTEGER,\n p_excluded_tables TEXT[]\n)\nRETURNS TABLE(\n drained_seq BIGINT,\n drained_table TEXT,\n drained_row_id TEXT,\n drained_operation TEXT,\n drained_tenant_id TEXT,\n error_code TEXT,\n error_message TEXT\n)\nLANGUAGE plpgsql\nSECURITY INVOKER\nAS $smrt_change_feed_drain$\n-- ${POSTGRES_CHANGE_FEED_HELPER_MARKER}\n-- ${POSTGRES_CHANGE_FEED_HELPER_COMPAT_MARKER}\nDECLARE\n v_base BIGINT;\n v_error_code TEXT;\n v_error_message TEXT;\nBEGIN\n -- EVERY statement below sits inside this exception boundary, preflight\n -- included. The caller may own the surrounding transaction, and an error\n -- escaping this function would abort it — the #2026 contract. A probe that\n -- blocks on a table lock until statement_timeout is exactly that: it is not\n -- the insert, but it aborts the caller just the same.\n BEGIN\n IF to_regclass('_smrt_changes_pending') IS NULL THEN\n RETURN;\n END IF;\n\n -- Never sequence from inside a transaction that has already written. Doing\n -- so would hold freshly allocated sequences uncommitted for the rest of\n -- that transaction, which is precisely the wait edge #2649 removes — a\n -- reader calling getChangesSince() inside a long write transaction must\n -- not put it back. The staged rows keep for the next drain.\n IF pg_current_xact_id_if_assigned() IS NOT NULL THEN\n RETURN;\n END IF;\n\n -- Cheap common case: nothing staged, so an autocommit append pays one\n -- index probe rather than a drain.\n IF NOT EXISTS (SELECT 1 FROM _smrt_changes_pending LIMIT 1) THEN\n RETURN;\n END IF;\n\n -- Try, never wait: a drain that waited could become an edge in a cycle.\n IF NOT pg_try_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('change-feed-drain')\n ) THEN\n RETURN;\n END IF;\n\n -- The feed never records a sensitive table (#2982). Staged entries for\n -- one — from an older replica, or staged before the table was declared\n -- sensitive — are dropped here, before a sequence is allocated.\n -- Bounded like the sequencing batch, so a large legacy backlog cannot make\n -- one pass outlive statement_timeout and stall the feed; the remainder is\n -- purged by later passes, and the batch below skips it meanwhile.\n IF COALESCE(cardinality(p_excluded_tables), 0) > 0 THEN\n DELETE FROM _smrt_changes_pending AS sensitive\n WHERE sensitive.pending_id IN (\n SELECT staged.pending_id\n FROM _smrt_changes_pending AS staged\n WHERE staged.table_name = ANY(p_excluded_tables)\n ORDER BY staged.pending_id\n LIMIT GREATEST(COALESCE(p_limit, ${POSTGRES_CHANGE_FEED_DRAIN_BATCH}), 1)\n );\n END IF;\n\n SELECT COALESCE(MAX(changes.seq), 0) INTO v_base FROM _smrt_changes AS changes;\n\n RETURN QUERY\n WITH removed AS (\n DELETE FROM _smrt_changes_pending AS pending\n WHERE pending.pending_id IN (\n SELECT candidate.pending_id\n FROM _smrt_changes_pending AS candidate\n -- A sensitive row committed after the purge above is skipped, not\n -- sequenced; the next drain purges it.\n WHERE NOT (\n candidate.table_name = ANY(COALESCE(p_excluded_tables, ARRAY[]::TEXT[]))\n )\n ORDER BY candidate.pending_id\n LIMIT GREATEST(COALESCE(p_limit, ${POSTGRES_CHANGE_FEED_DRAIN_BATCH}), 1)\n )\n RETURNING\n pending.pending_id,\n pending.table_name,\n pending.row_id,\n pending.operation,\n pending.tenant_id,\n pending.created_at\n ),\n numbered AS (\n SELECT\n removed.*,\n v_base + row_number() OVER (ORDER BY removed.pending_id) AS new_seq\n FROM removed\n ),\n inserted AS (\n INSERT INTO _smrt_changes (\n seq,\n table_name,\n row_id,\n operation,\n tenant_id,\n created_at\n )\n SELECT\n numbered.new_seq,\n numbered.table_name,\n numbered.row_id,\n numbered.operation,\n ${postgresTenantIdAs('_smrt_changes', 'numbered.tenant_id::TEXT')},\n numbered.created_at\n FROM numbered\n RETURNING\n _smrt_changes.seq,\n _smrt_changes.table_name,\n _smrt_changes.row_id,\n _smrt_changes.operation,\n _smrt_changes.tenant_id\n )\n SELECT\n inserted.seq,\n inserted.table_name,\n inserted.row_id,\n inserted.operation,\n inserted.tenant_id::TEXT,\n NULL::TEXT,\n NULL::TEXT\n FROM inserted\n ORDER BY inserted.seq;\n EXCEPTION WHEN query_canceled OR assert_failure OR OTHERS THEN\n GET STACKED DIAGNOSTICS\n v_error_code = RETURNED_SQLSTATE,\n v_error_message = MESSAGE_TEXT;\n RETURN QUERY SELECT\n NULL::BIGINT,\n NULL::TEXT,\n NULL::TEXT,\n NULL::TEXT,\n NULL::TEXT,\n v_error_code,\n v_error_message;\n END;\nEND;\n$smrt_change_feed_drain$;\n`;\n\n/**\n * Compatibility single-argument drain (#2982): see\n * {@link POSTGRES_CHANGE_FEED_COMPAT_DRAIN_FUNCTION_IDENTITY}. Current SMRT\n * never calls it.\n */\nexport const CREATE_POSTGRES_CHANGE_FEED_COMPAT_DRAIN_FUNCTION = `\nCREATE OR REPLACE FUNCTION ${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME}(\n p_limit INTEGER DEFAULT ${POSTGRES_CHANGE_FEED_DRAIN_BATCH}\n)\nRETURNS TABLE(\n drained_seq BIGINT,\n drained_table TEXT,\n drained_row_id TEXT,\n drained_operation TEXT,\n drained_tenant_id TEXT,\n error_code TEXT,\n error_message TEXT\n)\nLANGUAGE plpgsql\nSECURITY INVOKER\nAS $smrt_change_feed_drain_compat$\n-- ${POSTGRES_CHANGE_FEED_HELPER_MARKER}\nDECLARE\n v_error_code TEXT;\n v_error_message TEXT;\nBEGIN\n -- Its own boundary, like the drain it delegates to: a resolution or\n -- privilege error on the inner call must come back as data, never abort an\n -- older replica's caller transaction (#2026).\n BEGIN\n RETURN QUERY\n SELECT * FROM ${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME}(p_limit, ARRAY[]::TEXT[]);\n EXCEPTION WHEN query_canceled OR assert_failure OR OTHERS THEN\n GET STACKED DIAGNOSTICS\n v_error_code = RETURNED_SQLSTATE,\n v_error_message = MESSAGE_TEXT;\n RETURN QUERY SELECT\n NULL::BIGINT,\n NULL::TEXT,\n NULL::TEXT,\n NULL::TEXT,\n NULL::TEXT,\n v_error_code,\n v_error_message;\n END;\nEND;\n$smrt_change_feed_drain_compat$;\n`;\n\n/**\n * PostgreSQL-only change-feed append function.\n *\n * A PL/pgSQL block with an EXCEPTION handler runs its body in an internal\n * subtransaction. Returning SQLSTATE as data lets the caller log or retry a\n * failed best-effort append without leaving its surrounding transaction in\n * PostgreSQL's aborted (25P02) state. This statement contains dollar-quoted\n * semicolons, so callers must execute it whole rather than adding it to\n * {@link ALL_SYSTEM_TABLES}, whose portable DDL entries are semicolon-split.\n */\nexport const CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nCREATE OR REPLACE FUNCTION ${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(\n p_table_name TEXT,\n p_row_id TEXT,\n p_operation TEXT,\n p_tenant_id TEXT,\n p_created_at TIMESTAMPTZ\n)\nRETURNS TABLE(\n allocated_seq BIGINT,\n error_code TEXT,\n error_message TEXT\n)\nLANGUAGE plpgsql\nSECURITY INVOKER\nAS $smrt_change_feed$\n-- ${POSTGRES_CHANGE_FEED_HELPER_MARKER}\n-- ${POSTGRES_CHANGE_FEED_HELPER_COMPAT_MARKER}\nDECLARE\n v_seq BIGINT;\n v_error_code TEXT;\n v_error_message TEXT;\n v_deferred BOOLEAN;\nBEGIN\n -- Has the caller already written in this transaction?\n --\n -- PostgreSQL assigns a transaction id at the first write, so a non-NULL\n -- pg_current_xact_id_if_assigned() means the caller may be holding row\n -- locks. Allocating the feed head here would then make a concurrent appender\n -- wait for this whole transaction to end — the wait edge that closes the\n -- #2649 cycle. An autocommit append reports NULL: its caller's row write\n -- committed as its own statement, so it holds no row locks and its inline\n -- allocation cannot be part of a cycle. (Requires PostgreSQL 13+.)\n --\n -- This is exactly the condition that makes a cycle possible, not an\n -- approximation of \"inside BEGIN\": a transaction that appends *before* its\n -- first write still allocates inline, but it holds no row locks at that\n -- moment, so nothing that waits on its feed row can also be waited on by it.\n -- Preflight inside its own exception boundary for the same reason the drain\n -- wraps its own: an error here would abort a caller-owned transaction\n -- (#2026), and a catalog probe is still a statement that can fail.\n BEGIN\n v_deferred := pg_current_xact_id_if_assigned() IS NOT NULL\n AND to_regclass('_smrt_changes_pending') IS NOT NULL;\n EXCEPTION WHEN query_canceled OR assert_failure OR OTHERS THEN\n GET STACKED DIAGNOSTICS\n v_error_code = RETURNED_SQLSTATE,\n v_error_message = MESSAGE_TEXT;\n RETURN QUERY SELECT NULL::BIGINT, v_error_code, v_error_message;\n RETURN;\n END;\n\n IF v_deferred THEN\n BEGIN\n -- Staged, not sequenced: an identity value conflicts with nothing, so\n -- this insert never waits on another transaction. It stays fate-shared\n -- with the caller (rollback removes it) and is moved into\n -- _smrt_changes, in order and with a contiguous sequence, by the next\n -- drain after the caller commits.\n INSERT INTO _smrt_changes_pending (\n table_name,\n row_id,\n operation,\n tenant_id,\n created_at\n )\n VALUES (\n p_table_name,\n p_row_id,\n p_operation,\n ${postgresTenantIdAs('_smrt_changes_pending', 'p_tenant_id')},\n p_created_at\n );\n\n -- NULL sequence with NULL error code is the staged marker.\n RETURN QUERY SELECT NULL::BIGINT, NULL::TEXT, NULL::TEXT;\n RETURN;\n EXCEPTION WHEN query_canceled OR assert_failure OR OTHERS THEN\n GET STACKED DIAGNOSTICS\n v_error_code = RETURNED_SQLSTATE,\n v_error_message = MESSAGE_TEXT;\n RETURN QUERY SELECT NULL::BIGINT, v_error_code, v_error_message;\n RETURN;\n END;\n END IF;\n\n -- Autocommit append. The caller's own row write already committed as its own\n -- statement, so this transaction holds no user row locks and allocating the\n -- head here cannot join a lock cycle.\n --\n -- This deliberately does NOT drain: a drain performed here would sequence\n -- staged entries that the JavaScript caller never sees, so no live SSE\n -- signal would be published for them while this append's own signal carries\n -- a HIGHER sequence -- and an EventSource that stores that id as its\n -- Last-Event-ID resumes above them and never receives them. Draining is\n -- driven from JavaScript (drainChangeFeed) so every sequenced entry gets\n -- its signal.\n BEGIN\n INSERT INTO _smrt_changes (\n seq,\n table_name,\n row_id,\n operation,\n tenant_id,\n created_at\n )\n SELECT\n COALESCE(MAX(changes.seq), 0) + 1,\n p_table_name,\n p_row_id,\n p_operation,\n ${postgresTenantIdAs('_smrt_changes', 'p_tenant_id')},\n p_created_at\n FROM _smrt_changes AS changes\n RETURNING _smrt_changes.seq INTO v_seq;\n\n RETURN QUERY SELECT v_seq, NULL::TEXT, NULL::TEXT;\n EXCEPTION WHEN query_canceled OR assert_failure OR OTHERS THEN\n GET STACKED DIAGNOSTICS\n v_error_code = RETURNED_SQLSTATE,\n v_error_message = MESSAGE_TEXT;\n RETURN QUERY SELECT NULL::BIGINT, v_error_code, v_error_message;\n END;\nEND;\n$smrt_change_feed$;\n`;\n\n/**\n * PostgreSQL batch append helper. It makes the staged/direct decision before\n * its own writes, then appends every element through that one path. This is\n * necessary because a direct first insert assigns the transaction id that\n * later entries must not mistake for a caller-owned write transaction.\n */\nexport const CREATE_POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION = `\nCREATE OR REPLACE FUNCTION ${POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_NAME}(\n p_entries JSONB\n)\nRETURNS TABLE(\n entry_index INTEGER,\n allocated_seq BIGINT,\n error_code TEXT,\n error_message TEXT\n)\nLANGUAGE plpgsql\nSECURITY INVOKER\nAS $smrt_change_feed_batch$\n-- ${POSTGRES_CHANGE_FEED_HELPER_MARKER}\n-- ${POSTGRES_CHANGE_FEED_HELPER_COMPAT_MARKER}\nDECLARE\n v_deferred BOOLEAN;\n v_error_code TEXT;\n v_error_message TEXT;\nBEGIN\n BEGIN\n v_deferred := pg_current_xact_id_if_assigned() IS NOT NULL\n AND to_regclass('_smrt_changes_pending') IS NOT NULL;\n\n IF v_deferred THEN\n RETURN QUERY\n WITH input AS (\n SELECT entries.ordinality::INTEGER - 1 AS entry_index, entries.value\n FROM jsonb_array_elements(COALESCE(p_entries, '[]'::jsonb))\n WITH ORDINALITY AS entries(value, ordinality)\n ), staged AS (\n INSERT INTO _smrt_changes_pending (\n table_name, row_id, operation, tenant_id, created_at\n )\n SELECT\n input.value->>'table',\n input.value->>'rowId',\n input.value->>'operation',\n ${postgresTenantIdAs('_smrt_changes_pending', \"input.value->>'tenantId'\")},\n (input.value->>'timestamp')::TIMESTAMPTZ\n FROM input\n ORDER BY input.entry_index\n RETURNING 1\n )\n SELECT input.entry_index, NULL::BIGINT, NULL::TEXT, NULL::TEXT\n FROM input\n ORDER BY input.entry_index;\n RETURN;\n END IF;\n\n RETURN QUERY\n WITH input AS (\n SELECT entries.ordinality::INTEGER - 1 AS entry_index, entries.value\n FROM jsonb_array_elements(COALESCE(p_entries, '[]'::jsonb))\n WITH ORDINALITY AS entries(value, ordinality)\n ), numbered AS (\n SELECT\n input.*,\n COALESCE((SELECT MAX(seq) FROM _smrt_changes), 0)\n + row_number() OVER (ORDER BY input.entry_index) AS new_seq\n FROM input\n ), inserted AS (\n INSERT INTO _smrt_changes (\n seq, table_name, row_id, operation, tenant_id, created_at\n )\n SELECT\n numbered.new_seq,\n numbered.value->>'table',\n numbered.value->>'rowId',\n numbered.value->>'operation',\n ${postgresTenantIdAs('_smrt_changes', \"numbered.value->>'tenantId'\")},\n (numbered.value->>'timestamp')::TIMESTAMPTZ\n FROM numbered\n ORDER BY numbered.entry_index\n RETURNING seq\n )\n SELECT\n numbered.entry_index,\n numbered.new_seq,\n NULL::TEXT,\n NULL::TEXT\n FROM numbered\n ORDER BY numbered.entry_index;\n EXCEPTION WHEN query_canceled OR assert_failure OR OTHERS THEN\n GET STACKED DIAGNOSTICS\n v_error_code = RETURNED_SQLSTATE,\n v_error_message = MESSAGE_TEXT;\n RETURN QUERY SELECT NULL::INTEGER, NULL::BIGINT, v_error_code, v_error_message;\n END;\nEND;\n$smrt_change_feed_batch$;\n`;\n\nconst POSTGRES_CHANGE_FEED_PENDING_DDL =\n CREATE_POSTGRES_SMRT_CHANGES_PENDING_TABLE.split(';')\n .map((statement) => statement.trim())\n .filter((statement) => statement.length > 0)\n .map(\n (statement) => `EXECUTE $smrt_change_feed_pending_ddl$\n${statement}\n$smrt_change_feed_pending_ddl$;`,\n )\n .join('\\n');\n\n/**\n * Install both drain overloads inside a helper DO block (#2982).\n *\n * The two-argument drain is the first framework routine identity added to\n * databases that already exist, and a new function gets PostgreSQL's default\n * ACL — which a hardened deployment (PUBLIC EXECUTE revoked, the runtime role\n * granted explicitly by the permissions planner) would leave the runtime\n * unable to call. When this block creates it, it therefore inherits the\n * compatibility drain's reconciled EXECUTE grants. A role that may not grant\n * keeps the default ACL; the permissions planner reports and repairs it.\n */\nconst POSTGRES_CHANGE_FEED_DRAIN_INSTALL = ` DECLARE\n v_drain_created BOOLEAN :=\n to_regprocedure('${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY}') IS NULL;\n v_grant RECORD;\n BEGIN\n EXECUTE $smrt_change_feed_drain_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION.trim()}\n$smrt_change_feed_drain_ddl$;\n EXECUTE $smrt_change_feed_drain_compat_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_COMPAT_DRAIN_FUNCTION.trim()}\n$smrt_change_feed_drain_compat_ddl$;\n IF v_drain_created AND (\n SELECT p.proacl FROM pg_proc AS p\n WHERE p.oid = to_regprocedure('${POSTGRES_CHANGE_FEED_COMPAT_DRAIN_FUNCTION_IDENTITY}')\n ) IS NOT NULL THEN\n BEGIN\n EXECUTE 'REVOKE ALL ON FUNCTION ${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY} FROM PUBLIC';\n FOR v_grant IN\n SELECT acl.grantee\n FROM pg_proc AS p, aclexplode(p.proacl) AS acl\n WHERE p.oid = to_regprocedure('${POSTGRES_CHANGE_FEED_COMPAT_DRAIN_FUNCTION_IDENTITY}')\n AND acl.privilege_type = 'EXECUTE'\n LOOP\n IF v_grant.grantee = 0 THEN\n EXECUTE 'GRANT EXECUTE ON FUNCTION ${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY} TO PUBLIC';\n ELSE\n EXECUTE format(\n 'GRANT EXECUTE ON FUNCTION ${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY} TO %I',\n pg_get_userbyid(v_grant.grantee)\n );\n END IF;\n END LOOP;\n EXCEPTION WHEN insufficient_privilege THEN\n NULL;\n END;\n END IF;\n END;`;\n\n/**\n * Serialize PostgreSQL helper replacement inside one server-side statement.\n *\n * The transaction-scoped advisory lock prevents concurrent bootstraps from\n * racing on PostgreSQL's `pg_proc` uniqueness constraint. The nested dollar\n * quote keeps the complete function DDL atomic from the client's perspective.\n */\nexport const REPLACE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nDO $smrt_replace_change_feed$\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n${POSTGRES_CHANGE_FEED_PENDING_DDL}\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n${POSTGRES_CHANGE_FEED_DRAIN_INSTALL}\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n EXECUTE $smrt_change_feed_batch_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION.trim()}\n$smrt_change_feed_batch_ddl$;\nEND;\n$smrt_replace_change_feed$;\n`;\n\n/**\n * Install the PostgreSQL helpers only when missing, serialized server-side.\n *\n * A client-side catalog probe remains the fast path for already-initialized\n * read handles. This guarded statement is the cold-path race boundary: both\n * the advisory lock and the post-lock catalog check run before function DDL.\n *\n * The staging table and the drain helper (#2649) are created unconditionally\n * (`IF NOT EXISTS` / `CREATE OR REPLACE`) because an install that already has\n * the append helper from an older SMRT may still be missing them; the append\n * helper itself is replaced when the staging table was absent, which is how an\n * upgraded database picks up the deferring version.\n */\nexport const ENSURE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nDO $smrt_ensure_change_feed$\nDECLARE\n v_append_stale BOOLEAN;\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n v_append_stale := to_regclass('${POSTGRES_CHANGE_FEED_PENDING_TABLE}') IS NULL\n OR NOT EXISTS (\n SELECT 1\n FROM pg_proc\n WHERE oid = to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}')\n AND prosrc LIKE '%${POSTGRES_CHANGE_FEED_HELPER_MARKER}%'\n ) OR NOT EXISTS (\n SELECT 1 FROM pg_proc\n WHERE oid = to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_IDENTITY}')\n AND prosrc LIKE '%${POSTGRES_CHANGE_FEED_HELPER_MARKER}%'\n );\n${POSTGRES_CHANGE_FEED_PENDING_DDL}\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n${POSTGRES_CHANGE_FEED_DRAIN_INSTALL}\n IF v_append_stale THEN\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n EXECUTE $smrt_change_feed_batch_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION.trim()}\n$smrt_change_feed_batch_ddl$;\n END IF;\nEND;\n$smrt_ensure_change_feed$;\n`;\n\nconst POSTGRES_CHANGE_FEED_SCHEMA_DDL =\n CREATE_POSTGRES_SMRT_CHANGES_TABLE.split(';')\n .map((statement) => statement.trim())\n .filter((statement) => statement.length > 0)\n .map(\n (statement) => `EXECUTE $smrt_change_feed_schema_ddl$\n${statement}\n$smrt_change_feed_schema_ddl$;`,\n )\n .join('\\n');\n\n/**\n * Install the complete PostgreSQL change-feed schema under the bootstrap lock.\n *\n * The lock is deliberately acquired before table or index DDL. Framework\n * bootstrap uses the same ordering, so a raw-handle cold start cannot retain\n * catalog locks while waiting behind a framework bootstrap transaction.\n */\nexport const ENSURE_POSTGRES_CHANGE_FEED_SCHEMA = `\nDO $smrt_ensure_change_feed_schema$\nDECLARE\n v_append_stale BOOLEAN;\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n v_append_stale := to_regclass('${POSTGRES_CHANGE_FEED_PENDING_TABLE}') IS NULL\n OR NOT EXISTS (\n SELECT 1\n FROM pg_proc\n WHERE oid = to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}')\n AND prosrc LIKE '%${POSTGRES_CHANGE_FEED_HELPER_MARKER}%'\n ) OR NOT EXISTS (\n SELECT 1 FROM pg_proc\n WHERE oid = to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_IDENTITY}')\n AND prosrc LIKE '%${POSTGRES_CHANGE_FEED_HELPER_MARKER}%'\n );\n${POSTGRES_CHANGE_FEED_SCHEMA_DDL}\n${POSTGRES_CHANGE_FEED_PENDING_DDL}\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n${POSTGRES_CHANGE_FEED_DRAIN_INSTALL}\n IF v_append_stale THEN\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n EXECUTE $smrt_change_feed_batch_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION.trim()}\n$smrt_change_feed_batch_ddl$;\n END IF;\nEND;\n$smrt_ensure_change_feed_schema$;\n`;\n\n/**\n * Data backfill tracking\n *\n * Distinct from `_smrt_schema_migrations` — backfills are app-specific\n * data corrections (slug rewrites, model splits, lookup-table seeds) that\n * don't have schema diffs or rollback semantics. Apps register backfills\n * by name and the tracker handles idempotency.\n */\nexport const CREATE_SMRT_BACKFILLS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_backfills (\n name TEXT PRIMARY KEY,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n description TEXT,\n package_name TEXT\n);\n`;\n\n/** The insert-only claim table backing `runOnce()` (`../run-once.ts`). Browser-safe: retention imports it. */\nexport const RUN_ONCE_CLAIMS_TABLE = '_smrt_run_once_claims';\n\n/**\n * Idempotency claims for `runOnce()` (#3080).\n *\n * `claim_key` is the sha256 hex digest of the sorted-key-JSON encoding of\n * `(tenantId, actor, token, contentDigest)` — see `../run-once.ts` for the\n * derivation and why the token alone is not enough (it collapses distinct\n * submissions that reuse a form's token). It is the table's PRIMARY KEY, so\n * a second `INSERT` for the same key always raises a unique-constraint\n * violation instead of silently adopting the first caller's row: exactly the\n * \"insert-only against a UNIQUE column\" claim the issue asks for.\n *\n * Neither the raw token nor the raw submitted content is stored — only\n * digests — so this table cannot leak submitted business data. `status`\n * moves from `'in_progress'` to `'completed'` inside the SAME transaction\n * that runs the caller's work, so `result` (the caller's JSON-serialized,\n * ready-to-replay return value) and `completed_at` are only ever set once,\n * atomically with whatever else that transaction wrote. If the work throws,\n * `runOnce()` never marks the claim completed, and the transaction that\n * inserted it rolls back the insert along with it — a claim is never left\n * permanently `'in_progress'` by a failed attempt; a retry with the same key\n * can always run again.\n */\nexport const CREATE_SMRT_RUN_ONCE_CLAIMS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_run_once_claims (\n claim_key TEXT PRIMARY KEY,\n tenant_id TEXT NOT NULL,\n actor TEXT NOT NULL,\n content_digest TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'in_progress',\n result TEXT,\n created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n completed_at TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_run_once_claims_tenant\n ON _smrt_run_once_claims(tenant_id, created_at);\n\n-- Retention predicate for pruneRunOnceClaims() (#3080): completed claims are\n-- aged out on completed_at, scoped to status = 'completed' so an in_progress\n-- claim (completed_at IS NULL) is never matched by the sweep.\nCREATE INDEX IF NOT EXISTS idx_smrt_run_once_claims_status_completed\n ON _smrt_run_once_claims(status, completed_at);\n`;\n\n/** Durable sync deletion revisions, separate from live natural keys. */\nexport const CREATE_SMRT_SYNC_TOMBSTONES_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_sync_tombstones (\n table_name TEXT NOT NULL,\n row_id TEXT NOT NULL,\n model TEXT NOT NULL,\n tenant_id TEXT,\n origin_device_id TEXT NOT NULL,\n revision INTEGER NOT NULL,\n deleted_at TIMESTAMP NOT NULL,\n PRIMARY KEY (table_name, row_id)\n);\n`;\n\n/**\n * All system table creation statements\n */\nexport const ALL_SYSTEM_TABLES = [\n CREATE_SMRT_CONTEXTS_TABLE,\n CREATE_SMRT_MIGRATIONS_TABLE,\n CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE,\n CREATE_SMRT_BACKFILLS_TABLE,\n CREATE_SMRT_RUN_ONCE_CLAIMS_TABLE,\n CREATE_SMRT_EMBEDDINGS_TABLE,\n CREATE_SMRT_DISPATCH_TABLE,\n CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE,\n CREATE_SMRT_AI_USAGE_TABLE,\n CREATE_SMRT_CHANGES_TABLE,\n CREATE_SMRT_SYNC_TOMBSTONES_TABLE,\n];\n\n/**\n * Framework-owned tables that are backed by `@smrt()` models rather than by\n * {@link ALL_SYSTEM_TABLES}, and whose write volume disqualifies them from the\n * change feed.\n *\n * This is the jobs runner's own state: the claim loop and the forge projection\n * poll every second and rewrite these rows several times per job, so feeding\n * them into an append-only log whose pruning is not scheduled (issue #2375)\n * would dominate it with data no client syncs.\n *\n * The list is deliberately short and biased toward *observing*. A table wrongly\n * excluded is an invisible bug — exactly the one issue #2376 fixes — while a\n * table wrongly included costs a few extra feed rows. Everything else keeping\n * the `_smrt_` prefix (feature flags, prompt overrides, subscription plans,\n * field policies, agent and report schedules, report state) is observed.\n */\nexport const FRAMEWORK_OPERATIONAL_TABLES: readonly string[] = [\n '_smrt_jobs',\n '_smrt_job_events',\n '_smrt_workers',\n '_smrt_forge_deliveries',\n '_smrt_forge_projection_checkpoints',\n];\n\n/**\n * Framework tables that are backed by `@smrt()` models *and* reshaped by\n * `system/compatibility.ts`.\n *\n * They are dual-owned: `db:migrate` creates them from the jobs manifest, and\n * the compatibility pass adds columns/indexes that predate the manifest. On a\n * fresh install the manifest wins the race — bootstrap runs before the tables\n * exist — so their compatibility pass is deferred and re-checked until the\n * tables show up (issue #2376).\n */\nexport const DEFERRED_COMPATIBILITY_TABLES: readonly string[] = [\n '_smrt_jobs',\n '_smrt_job_events',\n];\n\n/**\n * System tables SMRT used to create on every database and no longer does.\n *\n * `_smrt_signals` never had a writer and `_smrt_registry` was only written by\n * an `ObjectRegistry.persistToDatabase()` helper that nothing ever called, so\n * both were empty on every deployment. New databases no longer get them.\n * Existing databases keep the empty tables until an operator drops them:\n *\n * ```sql\n * DROP TABLE IF EXISTS _smrt_signals;\n * DROP TABLE IF EXISTS _smrt_registry;\n * ```\n *\n * The framework deliberately does not issue those drops itself — dropping a\n * table is not reversible and a deployment may have repurposed the name.\n */\nexport const RETIRED_SYSTEM_TABLES: readonly string[] = [\n '_smrt_registry',\n '_smrt_signals',\n];\n\n/**\n * System-table columns that hold a real tenant id (#3297): a tenant UUID from\n * the tenant context, or NULL for global rows. On PostgreSQL they materialize\n * as native `uuid`, like every application `tenant_id`; other engines keep\n * TEXT, where `UUID` renders as TEXT anyway. Existing PostgreSQL TEXT columns\n * convert through `smrt db:migrate-uuid` in a maintenance window.\n *\n * Deliberately NOT here (they stay TEXT):\n * - text identity encodings that are not tenant ids: `_smrt_contexts.owner_id`\n * (`__collection__` / `__collection__:<tenantId>` scope keys, object/agent\n * row ids), `_smrt_run_once_claims.actor` (an arbitration namespace),\n * `_smrt_dispatch.processed_by` (a subscriber name) and\n * `_smrt_schema_migrations.applied_by` (a hostname);\n * - `_smrt_run_once_claims.tenant_id`: a NOT NULL claim scope folded into the\n * claim key; a caller without a tenant must pass a scope string, so it is a\n * scope key, not a tenant FK;\n * - `_smrt_ai_usage.tenant_id`: model usage writes tenant UUIDs, but the\n * published external-agent usage API accepts any non-empty text tenant id,\n * so converting it is a contract change tracked separately.\n */\nexport const SYSTEM_TABLE_TENANT_UUID_COLUMNS: ReadonlyArray<{\n table: string;\n column: string;\n}> = Object.freeze([\n { table: '_smrt_dispatch', column: 'tenant_id' },\n { table: '_smrt_dispatch_subscriptions', column: 'tenant_id' },\n { table: '_smrt_changes', column: 'tenant_id' },\n { table: '_smrt_changes_pending', column: 'tenant_id' },\n]);\n\n/**\n * Materialize the portable system-table schema for a target engine.\n *\n * PostgreSQL must use TIMESTAMPTZ because every system Date is an instant,\n * and native UUID for the tenant columns in\n * {@link SYSTEM_TABLE_TENANT_UUID_COLUMNS} (#3297).\n * PostgreSQL and DuckDB also materialize integer storage as BIGINT, matching\n * application-table DDL; SQLite keeps its variable-width INTEGER.\n */\nexport function getSystemTableDDL(engine: DatabaseEngine): string[] {\n return ALL_SYSTEM_TABLES.map((ddl) =>\n getSystemTableDDLForEngine(ddl, engine),\n );\n}\n\n/** Materialize one portable system-table statement for a target engine. */\nexport function getSystemTableDDLForEngine(\n ddl: string,\n engine: DatabaseEngine,\n): string {\n const integerDdl =\n engine === 'postgres' || engine === 'duckdb' || engine === 'json'\n ? ddl.replace(/\\bINTEGER\\b/g, 'BIGINT')\n : ddl;\n return engine === 'postgres'\n ? toPostgresTenantUuid(integerDdl.replace(/\\bTIMESTAMP\\b/g, 'TIMESTAMPTZ'))\n : integerDdl;\n}\n\nconst POSTGRES_UUID_TENANT_TABLES = new Set(\n SYSTEM_TABLE_TENANT_UUID_COLUMNS.map(({ table }) => table),\n);\n\n/**\n * Render `tenant_id TEXT` as native uuid on PostgreSQL in the DDL of a table\n * listed in {@link SYSTEM_TABLE_TENANT_UUID_COLUMNS} (each system DDL\n * constant creates one table).\n */\nfunction toPostgresTenantUuid(ddl: string): string {\n const table = ddl.match(\n /CREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?([A-Za-z_][\\w$]*)/i,\n )?.[1];\n return table && POSTGRES_UUID_TENANT_TABLES.has(table)\n ? ddl.replace(/\\btenant_id TEXT\\b/g, 'tenant_id UUID')\n : ddl;\n}\n\n/**\n * Current SMRT system schema version.\n *\n * `bootstrapSystemTables()` skips replaying system DDL once this exact version\n * is recorded in `_smrt_migrations`, so every change to {@link ALL_SYSTEM_TABLES}\n * must bump it — otherwise existing databases never see the change. New\n * *columns* additionally need an `addColumnIfMissing()` entry in\n * `system/compatibility.ts`, because `CREATE TABLE IF NOT EXISTS` is a no-op on\n * a table that already exists.\n *\n * 1.10.1 retires the never-written `_smrt_registry` / `_smrt_signals` tables\n * (see {@link RETIRED_SYSTEM_TABLES}) and adds the retention-predicate indexes\n * from #2375 (`_smrt_contexts.expires_at`, `_smrt_ai_usage(tenant_id,\n * created_at)`, `_smrt_dispatch(status, processed_at)` and\n * `(status, updated_at)`) in the same replay. (`1.10.0` briefly existed only\n * on two unreleased, unmerged branches with two different, incompatible DDL\n * sets — never published, never stamped in any real `_smrt_migrations` table\n * — so merging them reuses no version an existing install could have already\n * recorded; see the note on {@link SMRT_SCHEMA_DDL_CHECKSUMS}.)\n *\n * 1.11.0 adds `_smrt_run_once_claims` (#3080), the insert-only claim table\n * behind `runOnce()` (`../run-once.ts`).\n *\n * 1.11.1 adds `idx_smrt_run_once_claims_status_completed` (#3080), the\n * `(status, completed_at)` index backing `pruneRunOnceClaims()`'s retention\n * predicate (`system/retention.ts`).\n *\n * 1.12.0 adds the durable tracked-deletion ledger.\n */\nexport const SMRT_SCHEMA_VERSION = '1.12.0';\n\n/**\n * Canonical form of the system DDL that {@link SMRT_SCHEMA_DDL_CHECKSUMS} covers.\n *\n * Whitespace is normalized so reformatting alone does not trip the guard while\n * any token change does.\n */\nexport function getSystemSchemaChecksumInput(): string {\n return ALL_SYSTEM_TABLES.join('\\n').replace(/\\s+/g, ' ').trim();\n}\n\n/**\n * SHA-256 of {@link getSystemSchemaChecksumInput} for each system schema\n * version, oldest first.\n *\n * The discipline that keeps fresh installs and upgraded installs identical was\n * documented but unenforced (issue #2376): a contributor could edit the DDL and\n * ship it without bumping the version, and no existing database would ever\n * apply the change. `system-schema-evolution.test.ts` recomputes the current\n * hash and requires it to equal this map's entry for\n * {@link SMRT_SCHEMA_VERSION}.\n *\n * It is a *history*, not a single constant, so the guard cannot be silenced by\n * overwriting one value: every recorded hash must stay distinct, which means a\n * changed DDL has nowhere to be recorded except under a new version key. Adding\n * that key is the bump. This is also why merging two branches that each\n * independently bumped to the same version string (both `1.10.0`, covering two\n * different DDL sets — #2376's table retirement and #2375's retention indexes)\n * must land under a *new* key rather than picking a \"final\" checksum for the\n * shared number: nothing published or otherwise reachable ever stamped\n * `1.10.0`, so recording the merged DDL as `1.10.1` costs nothing and removes\n * any doubt about a version string meaning two different things to two\n * installs.\n *\n * Entries before 1.10.1 are not recorded — the guard starts here.\n */\nexport const SMRT_SCHEMA_DDL_CHECKSUMS: Readonly<Record<string, string>> =\n Object.freeze({\n '1.10.1':\n 'f796ee3b3f7ab8b9dc659ecaa68884ec01277408c3dd6edea540853fce369c16',\n '1.11.0':\n 'c51351a76c96beb64fa7e5922f78d776367af882ed1ff37229f9e9ffacee8372',\n '1.11.1':\n '98112937a3bbb3883c3db1fd5e836751f91a479c1c5da64d4410263a6581ef1f',\n '1.12.0':\n '6aa42e368e7d57191de9b0c040c289edf38a1ffa7ae0097f57188ecd5a5b8f9c',\n });\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-core",
|
|
3
|
-
"version": "0.60.
|
|
3
|
+
"version": "0.60.10",
|
|
4
4
|
"description": "Core SMRT framework for business objects, persistence, and generated REST, CLI, MCP, and AI interfaces",
|
|
5
5
|
"author": "HappyVertical",
|
|
6
6
|
"type": "module",
|
|
@@ -200,15 +200,15 @@
|
|
|
200
200
|
"./dist/utils/json.js": "./dist/utils/json.browser.js"
|
|
201
201
|
},
|
|
202
202
|
"dependencies": {
|
|
203
|
-
"@happyvertical/ai": "^0.111.
|
|
204
|
-
"@happyvertical/files": "^0.111.
|
|
205
|
-
"@happyvertical/json": "^0.111.
|
|
206
|
-
"@happyvertical/logger": "^0.111.
|
|
207
|
-
"@happyvertical/smrt-config": "0.60.
|
|
208
|
-
"@happyvertical/smrt-scanner": "0.60.
|
|
209
|
-
"@happyvertical/smrt-types": "0.60.
|
|
210
|
-
"@happyvertical/sql": "^0.111.
|
|
211
|
-
"@happyvertical/utils": "^0.111.
|
|
203
|
+
"@happyvertical/ai": "^0.111.5",
|
|
204
|
+
"@happyvertical/files": "^0.111.5",
|
|
205
|
+
"@happyvertical/json": "^0.111.5",
|
|
206
|
+
"@happyvertical/logger": "^0.111.5",
|
|
207
|
+
"@happyvertical/smrt-config": "0.60.10",
|
|
208
|
+
"@happyvertical/smrt-scanner": "0.60.10",
|
|
209
|
+
"@happyvertical/smrt-types": "0.60.10",
|
|
210
|
+
"@happyvertical/sql": "^0.111.5",
|
|
211
|
+
"@happyvertical/utils": "^0.111.5",
|
|
212
212
|
"@noble/hashes": "^2.4.0",
|
|
213
213
|
"@oxc-project/runtime": "^0.150.0",
|
|
214
214
|
"cheerio": "^1.2.0",
|
|
@@ -261,7 +261,7 @@
|
|
|
261
261
|
"generate": "node scripts/generate-manifest.js",
|
|
262
262
|
"generate:test": "node scripts/generate-test-manifest.js",
|
|
263
263
|
"test": "node scripts/generate-test-manifest.js && vitest run",
|
|
264
|
-
"test:browser": "node scripts/browser-plain-json-regression.mjs && node scripts/browser-entry-evaluation.mjs && node scripts/browser-unbundled-dev.mjs",
|
|
264
|
+
"test:browser": "node scripts/browser-plain-json-regression.mjs && node scripts/browser-entry-evaluation.mjs && node scripts/browser-entry-evaluation.mjs --pglite-cascade && node scripts/browser-unbundled-dev.mjs",
|
|
265
265
|
"test:postgres": "node scripts/generate-test-manifest.js && NODE_OPTIONS=--trace-deprecation node ../../scripts/run-with-ci-postgres.mjs -- pnpm exec vitest run optional.test.ts",
|
|
266
266
|
"test:integration": "TEST_INTEGRATION=1 vitest run src/__tests__/full-registry-integration.test.ts src/__tests__/manifest-no-leak.test.ts",
|
|
267
267
|
"test:watch": "node scripts/generate-test-manifest.js && vitest",
|