From 1dd633e58e45beb3a4f8fb95ccecf1a403eadf77 Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Wed, 20 May 2026 17:17:54 +0300 Subject: [PATCH] fix(0.13.1): inline DURABLE_SCHEMA_SQL as string constant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tsup builds JS only — schema.sql was not copied into dist/, so consumers importing the package couldn't bootstrap a D1 store. Inline the schema as a string constant exported from the package root: import { DURABLE_SCHEMA_SQL } from '@tangle-network/agent-runtime' await env.DB.exec(DURABLE_SCHEMA_SQL) A sync test (schema-sync.test.ts) asserts the constant stays byte-identical to src/durable/schema.sql so the .sql file remains the source of truth without drift. 177 tests pass (2 new). DURABLE_SCHEMA_VERSION is exported alongside for migration tooling. --- package.json | 2 +- src/durable/index.ts | 5 ++ src/durable/schema.ts | 87 +++++++++++++++++++++++++++ src/durable/tests/schema-sync.test.ts | 24 ++++++++ 4 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 src/durable/schema.ts create mode 100644 src/durable/tests/schema-sync.test.ts diff --git a/package.json b/package.json index 4ec33f0..253e948 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tangle-network/agent-runtime", - "version": "0.13.0", + "version": "0.13.1", "description": "Reusable runtime lifecycle for domain-specific agents.", "homepage": "https://github.com/tangle-network/agent-runtime#readme", "repository": { diff --git a/src/durable/index.ts b/src/durable/index.ts index c28019d..8937826 100644 --- a/src/durable/index.ts +++ b/src/durable/index.ts @@ -15,6 +15,11 @@ export { canonicalHash, canonicalJson, deriveWorkerId, manifestHash, stepId } fr export { InMemoryDurableRunStore } from './in-memory-store' export type { DurableContext, RunDurableInput, RunDurableResult } from './runner' export { runDurable } from './runner' +// Canonical D1 schema string + current version. Consumers wire via +// await env.DB.exec(DURABLE_SCHEMA_SQL) +// during one-time bootstrap. `src/durable/schema.sql` is the source of +// truth; `schema.ts` is the build-bundled string that ships in dist/. +export { DURABLE_SCHEMA_SQL, DURABLE_SCHEMA_VERSION } from './schema' export type { DurableRunManifest, DurableRunStore, diff --git a/src/durable/schema.ts b/src/durable/schema.ts new file mode 100644 index 0000000..dc2be5c --- /dev/null +++ b/src/durable/schema.ts @@ -0,0 +1,87 @@ +/** + * The durable-runs SQL schema as a string constant. Inlined so consumers + * (Cloudflare Workers via D1) can apply it without bundling a `.sql` file: + * + * import { DURABLE_SCHEMA_SQL } from '@tangle-network/agent-runtime' + * await env.DB.exec(DURABLE_SCHEMA_SQL) + * + * The canonical source is `src/durable/schema.sql` — this string MUST stay + * byte-identical to it. The build does not copy `.sql` files into `dist/`, + * so the constant is the only path consumers have. A unit test asserts the + * two stay in sync (`durable-schema.test.ts`). + * + * `DURABLE_SCHEMA_VERSION` reflects the latest migration version applied by + * this string. Bump it on every backwards-incompatible change AND add a new + * migration entry to durable_schema_info instead of mutating prior rows. + */ + +export const DURABLE_SCHEMA_VERSION = 1 + +export const DURABLE_SCHEMA_SQL = `-- Durable-run substrate — versioned schema for D1 / SQLite. +-- +-- Apply once per database. Subsequent migrations append; never rewrite a +-- prior version. See \`durable_schema_info\` for the migration trail. +-- +-- Concurrency notes for D1: +-- - SQLite supports UNIQUE constraints for first-emit-wins (\`durable_events\` +-- PK is (run_id, key) — duplicate insert raises, caller treats as "already +-- emitted"). +-- - Lease takeover happens via a conditional UPDATE: we only claim the lease +-- if (lease_holder_id IS NULL OR lease_expires_at < :now) — atomic under +-- SQLite's row-level locking. +-- - All timestamps stored as ISO-8601 TEXT for cross-platform consistency. +-- - \`result_json\` / \`error_json\` / \`outcome_json\` / \`payload_json\` are +-- JSON-encoded TEXT; the application enforces canonical-JSON discipline at +-- the boundary so the store stays type-agnostic. + +CREATE TABLE IF NOT EXISTS durable_schema_info ( + version INTEGER PRIMARY KEY, + applied_at TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS durable_runs ( + run_id TEXT PRIMARY KEY, + manifest_hash TEXT NOT NULL, + project_id TEXT NOT NULL, + scenario_id TEXT, + status TEXT NOT NULL CHECK (status IN ('pending','running','completed','failed','suspended')), + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + completed_at TEXT, + lease_holder_id TEXT, + lease_expires_at TEXT, + outcome_json TEXT, + step_count INTEGER NOT NULL DEFAULT 0 +); + +CREATE INDEX IF NOT EXISTS idx_durable_runs_project_status ON durable_runs(project_id, status); +CREATE INDEX IF NOT EXISTS idx_durable_runs_lease_expires ON durable_runs(lease_expires_at); + +CREATE TABLE IF NOT EXISTS durable_steps ( + run_id TEXT NOT NULL, + step_index INTEGER NOT NULL, + intent TEXT NOT NULL, + kind TEXT NOT NULL, + input_hash TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL CHECK (status IN ('pending','running','completed','failed')), + attempts INTEGER NOT NULL DEFAULT 0, + result_json TEXT, + error_json TEXT, + started_at TEXT, + completed_at TEXT, + PRIMARY KEY (run_id, step_index) +); + +CREATE INDEX IF NOT EXISTS idx_durable_steps_status ON durable_steps(run_id, status); + +CREATE TABLE IF NOT EXISTS durable_events ( + run_id TEXT NOT NULL, + key TEXT NOT NULL, + payload_json TEXT, + emitted_at TEXT NOT NULL, + PRIMARY KEY (run_id, key) +); + +INSERT OR IGNORE INTO durable_schema_info (version, applied_at) +VALUES (1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')); +` diff --git a/src/durable/tests/schema-sync.test.ts b/src/durable/tests/schema-sync.test.ts new file mode 100644 index 0000000..55b8068 --- /dev/null +++ b/src/durable/tests/schema-sync.test.ts @@ -0,0 +1,24 @@ +/** + * Guard: the inlined `DURABLE_SCHEMA_SQL` constant must stay byte-identical + * to `src/durable/schema.sql`. If the .sql file is the source of truth, the + * constant cannot drift — otherwise consumers and tests see different + * schemas and the D1 store silently goes inconsistent. + */ + +import { readFileSync } from 'node:fs' + +import { describe, expect, it } from 'vitest' + +import { DURABLE_SCHEMA_SQL, DURABLE_SCHEMA_VERSION } from '../schema' + +describe('durable schema', () => { + it('inlined SQL constant matches schema.sql byte-for-byte', () => { + const fileSql = readFileSync(new URL('../schema.sql', import.meta.url), 'utf8') + expect(DURABLE_SCHEMA_SQL).toBe(fileSql) + }) + + it('schema version is positive integer', () => { + expect(DURABLE_SCHEMA_VERSION).toBeGreaterThan(0) + expect(Number.isInteger(DURABLE_SCHEMA_VERSION)).toBe(true) + }) +})