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:
weichen
2026-09-15 23:43:37 +00:00
co-authored by Cursor
parent a75aa45129
commit 7dfd6d6f33
2 changed files with 196 additions and 0 deletions
@@ -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;
+36
View File
@@ -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 {