feat(db): add intent plan tables in goose 00044
Persist plans, steps, and append-only decisions with typed CAS, lease, and nonce columns. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
-- +goose Up
|
||||
-- U3: persistent multi-intent plans. Owner is app/intentflow; other packages use the Service port.
|
||||
-- JSONB is only for versioned slots / display snapshots. CAS, lease, and audit columns are typed.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS intent_plans (
|
||||
plan_id TEXT PRIMARY KEY,
|
||||
environment TEXT NOT NULL,
|
||||
project_id BIGINT NOT NULL,
|
||||
session_id TEXT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
client_turn_id TEXT NOT NULL,
|
||||
request_digest TEXT NOT NULL,
|
||||
segmenter_version TEXT NOT NULL DEFAULT '',
|
||||
policy_version TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL,
|
||||
revision BIGINT NOT NULL DEFAULT 1,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
last_fencing_token BIGINT NOT NULL DEFAULT 0,
|
||||
write_plan BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
execution_mode TEXT NOT NULL DEFAULT 'validated_only',
|
||||
safe_summary TEXT NOT NULL DEFAULT '',
|
||||
display_snapshot JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CONSTRAINT chk_intent_plan_status CHECK (status IN (
|
||||
'proposed',
|
||||
'awaiting_clarification',
|
||||
'awaiting_confirmation',
|
||||
'executing',
|
||||
'partially_completed',
|
||||
'completed',
|
||||
'failed',
|
||||
'cancelled',
|
||||
'expired',
|
||||
'superseded'
|
||||
)),
|
||||
CONSTRAINT chk_intent_plan_execution_mode CHECK (execution_mode IN (
|
||||
'validated_only',
|
||||
'live'
|
||||
))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_intent_plans_turn
|
||||
ON intent_plans (environment, session_id, client_turn_id);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_intent_plans_one_active_write
|
||||
ON intent_plans (environment, project_id, session_id, user_id)
|
||||
WHERE write_plan AND status IN (
|
||||
'proposed',
|
||||
'awaiting_clarification',
|
||||
'awaiting_confirmation',
|
||||
'executing',
|
||||
'partially_completed'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_intent_plans_active_scope
|
||||
ON intent_plans (environment, project_id, session_id, user_id, status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_intent_plans_expire
|
||||
ON intent_plans (expires_at)
|
||||
WHERE status IN ('proposed', 'awaiting_clarification', 'awaiting_confirmation');
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_intent_plans_retention
|
||||
ON intent_plans (updated_at)
|
||||
WHERE status IN ('completed', 'failed', 'cancelled', 'expired', 'superseded');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS intent_plan_steps (
|
||||
step_id TEXT PRIMARY KEY,
|
||||
plan_id TEXT NOT NULL REFERENCES intent_plans(plan_id) ON DELETE CASCADE,
|
||||
ordinal INT NOT NULL,
|
||||
action_key TEXT NOT NULL,
|
||||
slots JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
depends_on TEXT[] NOT NULL DEFAULT '{}',
|
||||
status TEXT NOT NULL,
|
||||
expected_resource_version TEXT NOT NULL DEFAULT '',
|
||||
action_digest TEXT NOT NULL DEFAULT '',
|
||||
idempotency_key TEXT NOT NULL DEFAULT '',
|
||||
attempt INT NOT NULL DEFAULT 0,
|
||||
result_ref TEXT NOT NULL DEFAULT '',
|
||||
result_digest TEXT NOT NULL DEFAULT '',
|
||||
safe_summary TEXT NOT NULL DEFAULT '',
|
||||
lease_owner TEXT NOT NULL DEFAULT '',
|
||||
lease_until TIMESTAMPTZ,
|
||||
fencing_token BIGINT NOT NULL DEFAULT 0,
|
||||
version BIGINT NOT NULL DEFAULT 1,
|
||||
execution_mode TEXT NOT NULL DEFAULT 'validated_only',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CONSTRAINT chk_intent_plan_step_status CHECK (status IN (
|
||||
'pending',
|
||||
'eligible',
|
||||
'running',
|
||||
'succeeded',
|
||||
'failed',
|
||||
'denied',
|
||||
'conflicted',
|
||||
'blocked_dependency',
|
||||
'skipped',
|
||||
'cancelled'
|
||||
)),
|
||||
CONSTRAINT chk_intent_plan_step_execution_mode CHECK (execution_mode IN (
|
||||
'validated_only',
|
||||
'live'
|
||||
))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_intent_plan_steps_ordinal
|
||||
ON intent_plan_steps (plan_id, ordinal);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_intent_plan_steps_idempotency
|
||||
ON intent_plan_steps (idempotency_key)
|
||||
WHERE idempotency_key <> '';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_intent_plan_steps_claim
|
||||
ON intent_plan_steps (status, lease_until);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS intent_plan_decisions (
|
||||
decision_id TEXT PRIMARY KEY,
|
||||
plan_id TEXT NOT NULL REFERENCES intent_plans(plan_id) ON DELETE CASCADE,
|
||||
nonce TEXT NOT NULL,
|
||||
plan_revision BIGINT NOT NULL,
|
||||
actor_user_id BIGINT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
step_ids TEXT[] NOT NULL DEFAULT '{}',
|
||||
exclude_unselected BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
decision_digest TEXT NOT NULL,
|
||||
outcome TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CONSTRAINT chk_intent_plan_decision_kind CHECK (kind IN (
|
||||
'confirm',
|
||||
'cancel',
|
||||
'exclude',
|
||||
'clarify',
|
||||
'edit',
|
||||
'confirm_retry',
|
||||
'continue',
|
||||
'replace'
|
||||
))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_intent_plan_decisions_nonce
|
||||
ON intent_plan_decisions (plan_id, nonce);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_intent_plan_decisions_plan
|
||||
ON intent_plan_decisions (plan_id, created_at);
|
||||
|
||||
-- +goose Down
|
||||
DROP INDEX IF EXISTS idx_intent_plan_decisions_plan;
|
||||
DROP INDEX IF EXISTS uq_intent_plan_decisions_nonce;
|
||||
DROP TABLE IF EXISTS intent_plan_decisions;
|
||||
DROP INDEX IF EXISTS idx_intent_plan_steps_claim;
|
||||
DROP INDEX IF EXISTS uq_intent_plan_steps_idempotency;
|
||||
DROP INDEX IF EXISTS uq_intent_plan_steps_ordinal;
|
||||
DROP TABLE IF EXISTS intent_plan_steps;
|
||||
DROP INDEX IF EXISTS idx_intent_plans_retention;
|
||||
DROP INDEX IF EXISTS idx_intent_plans_expire;
|
||||
DROP INDEX IF EXISTS idx_intent_plans_active_scope;
|
||||
DROP INDEX IF EXISTS uq_intent_plans_one_active_write;
|
||||
DROP INDEX IF EXISTS uq_intent_plans_turn;
|
||||
DROP TABLE IF EXISTS intent_plans;
|
||||
@@ -111,6 +111,41 @@ func TestGooseU9_00038_HasUpAndDown(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGooseU3_00044_HasUpAndDown(t *testing.T) {
|
||||
files, err := ListMigrationFiles(ResolveMigrationsDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var found string
|
||||
for _, name := range files {
|
||||
if strings.HasPrefix(name, "00044_") && strings.HasSuffix(name, ".sql") {
|
||||
found = name
|
||||
break
|
||||
}
|
||||
}
|
||||
if found == "" {
|
||||
t.Fatal("expected goose 00044 for intent plans")
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(ResolveMigrationsDir(), found))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
text := string(raw)
|
||||
if !strings.Contains(text, "-- +goose Up") || !strings.Contains(text, "-- +goose Down") {
|
||||
t.Fatal("00044 must have Up and Down")
|
||||
}
|
||||
for _, table := range []string{"intent_plans", "intent_plan_steps", "intent_plan_decisions"} {
|
||||
if !strings.Contains(text, table) {
|
||||
t.Fatalf("00044 must mention %s", table)
|
||||
}
|
||||
}
|
||||
for _, idx := range []string{"uq_intent_plans_turn", "uq_intent_plans_one_active_write", "uq_intent_plan_decisions_nonce"} {
|
||||
if !strings.Contains(text, idx) {
|
||||
t.Fatalf("00044 must mention %s", idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGooseM2_00034to00036_DownThenUp(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("goose down is not part of -short")
|
||||
@@ -132,6 +167,7 @@ func TestGooseM2_00034to00036_DownThenUp(t *testing.T) {
|
||||
"00036_2026-09-14-final-artifact-vault.sql",
|
||||
"00037_2026-09-15-task-run-manifests.sql",
|
||||
"00038_2026-09-15-dataset-lifecycle.sql",
|
||||
"00044_2026-09-15-intent-plans.sql",
|
||||
}
|
||||
joined := strings.Join(files, "\n")
|
||||
for _, name := range want {
|
||||
|
||||
Reference in New Issue
Block a user