-- AwoooP Phase 4: Platform Shell in Shadow Mode -- Run State Machine 持久化表 -- 2026-05-04 ogt + Claude Sonnet 4.6(ADR-114/ADR-119) -- -- 前置:Phase 1 control plane(awooop_projects)必須已執行 -- -- 三表: -- awooop_run_state — Run FSM 主表(lease + heartbeat + SKIP LOCKED) -- awooop_run_step_journal — SAGA step journal(tool call + 補償指令,ADR-119) -- awooop_run_idempotency — 去重冪等表(ADR-114) -- ========================================================= -- STEP 1: awooop_run_state -- ========================================================= CREATE TABLE IF NOT EXISTS awooop_run_state ( run_id UUID PRIMARY KEY, project_id VARCHAR(64) NOT NULL REFERENCES awooop_projects(project_id), agent_id VARCHAR(128) NOT NULL, -- FSM 狀態 state VARCHAR(32) NOT NULL DEFAULT 'pending' CHECK (state IN ( 'pending','running','waiting_tool', 'waiting_approval','completed','failed', 'cancelled','timeout' )), -- Worker lease(SKIP LOCKED 防 double-pickup) lease_until TIMESTAMPTZ, heartbeat_at TIMESTAMPTZ, worker_id VARCHAR(128), -- Retry 計數 attempt_count SMALLINT NOT NULL DEFAULT 0, max_attempts SMALLINT NOT NULL DEFAULT 3, -- Observability trace_id VARCHAR(128), -- Trigger 來源 trigger_type VARCHAR(32), trigger_ref VARCHAR(256), -- channel_event_id / schedule_id / etc. -- Shadow mode flag is_shadow BOOLEAN NOT NULL DEFAULT TRUE, -- Artifact integrity(ADR-112) input_sha256 CHAR(64), output_sha256 CHAR(64), -- Budget cost_usd NUMERIC(10, 4) NOT NULL DEFAULT 0.0000, step_count SMALLINT NOT NULL DEFAULT 0, -- 結果 error_code VARCHAR(64), error_detail TEXT, -- 時間戳記 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, timeout_at TIMESTAMPTZ ); COMMENT ON TABLE awooop_run_state IS 'ADR-114: Run FSM 主表,SKIP LOCKED worker lease'; COMMENT ON COLUMN awooop_run_state.is_shadow IS 'Phase 4 shadow mode:TRUE = 不產生 user response,不執行 destructive tool'; -- Index: worker 掃 PENDING(SKIP LOCKED 用) CREATE INDEX IF NOT EXISTS idx_run_state_pending ON awooop_run_state (project_id, created_at) WHERE state = 'pending' AND lease_until IS NULL; -- Index: stale run reaper(找 lease 過期的 running run) CREATE INDEX IF NOT EXISTS idx_run_state_stale ON awooop_run_state (lease_until) WHERE state = 'running' AND lease_until IS NOT NULL; -- Index: project timeline(dashboard 查詢) CREATE INDEX IF NOT EXISTS idx_run_state_project_timeline ON awooop_run_state (project_id, created_at DESC); -- Index: trace_id(跨系統追蹤) CREATE INDEX IF NOT EXISTS idx_run_state_trace_id ON awooop_run_state (trace_id) WHERE trace_id IS NOT NULL; -- ========================================================= -- STEP 2: awooop_run_step_journal(SAGA step journal,ADR-119) -- ========================================================= CREATE TABLE IF NOT EXISTS awooop_run_step_journal ( step_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), run_id UUID NOT NULL REFERENCES awooop_run_state(run_id) ON DELETE CASCADE, project_id VARCHAR(64) NOT NULL, -- Step 順序(每個 run 內遞增) step_seq SMALLINT NOT NULL, -- Tool call 資訊 tool_name VARCHAR(128) NOT NULL, mcp_gateway_id VARCHAR(128), -- Artifact integrity(ADR-112) input_hash CHAR(64), output_hash CHAR(64), -- SAGA 補償指令(JSON) compensation_json JSONB, -- 執行結果 result_status VARCHAR(16) NOT NULL DEFAULT 'pending' CHECK (result_status IN ('pending','success','failed','compensated')), error_code VARCHAR(64), -- Shadow 攔截記錄 was_blocked BOOLEAN NOT NULL DEFAULT FALSE, block_reason VARCHAR(128), -- 時間 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), completed_at TIMESTAMPTZ, latency_ms INTEGER ); COMMENT ON TABLE awooop_run_step_journal IS 'ADR-119 SAGA step journal:每個 tool call 獨立記錄 + 補償指令'; CREATE UNIQUE INDEX IF NOT EXISTS uix_run_step_seq ON awooop_run_step_journal (run_id, step_seq); CREATE INDEX IF NOT EXISTS idx_run_step_run_id ON awooop_run_step_journal (run_id, step_seq); -- ========================================================= -- STEP 3: awooop_run_idempotency(ADR-114 去重冪等) -- ========================================================= CREATE TABLE IF NOT EXISTS awooop_run_idempotency ( idempotency_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), project_id VARCHAR(64) NOT NULL, channel_type VARCHAR(32) NOT NULL, provider_event_id VARCHAR(256) NOT NULL, -- 映射到的 run run_id UUID NOT NULL REFERENCES awooop_run_state(run_id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); COMMENT ON TABLE awooop_run_idempotency IS 'ADR-114: (project_id, channel_type, provider_event_id) → run_id 去重'; CREATE UNIQUE INDEX IF NOT EXISTS uix_run_idempotency_key ON awooop_run_idempotency (project_id, channel_type, provider_event_id); CREATE INDEX IF NOT EXISTS idx_run_idempotency_run_id ON awooop_run_idempotency (run_id); -- ========================================================= -- STEP 4: RLS(ADR-118 多租戶隔離) -- ========================================================= ALTER TABLE awooop_run_state ENABLE ROW LEVEL SECURITY; ALTER TABLE awooop_run_state FORCE ROW LEVEL SECURITY; ALTER TABLE awooop_run_step_journal ENABLE ROW LEVEL SECURITY; ALTER TABLE awooop_run_step_journal FORCE ROW LEVEL SECURITY; ALTER TABLE awooop_run_idempotency ENABLE ROW LEVEL SECURITY; ALTER TABLE awooop_run_idempotency FORCE ROW LEVEL SECURITY; DROP POLICY IF EXISTS run_state_tenant_isolation ON awooop_run_state; CREATE POLICY run_state_tenant_isolation ON awooop_run_state FOR ALL TO awooop_app USING (project_id = current_setting('app.project_id', TRUE)) WITH CHECK (project_id = current_setting('app.project_id', TRUE)); DROP POLICY IF EXISTS run_step_journal_tenant_isolation ON awooop_run_step_journal; CREATE POLICY run_step_journal_tenant_isolation ON awooop_run_step_journal FOR ALL TO awooop_app USING (project_id = current_setting('app.project_id', TRUE)) WITH CHECK (project_id = current_setting('app.project_id', TRUE)); DROP POLICY IF EXISTS run_idempotency_tenant_isolation ON awooop_run_idempotency; CREATE POLICY run_idempotency_tenant_isolation ON awooop_run_idempotency FOR ALL TO awooop_app USING (project_id = current_setting('app.project_id', TRUE)) WITH CHECK (project_id = current_setting('app.project_id', TRUE)); -- ========================================================= -- STEP 5: GRANT -- ========================================================= GRANT SELECT, INSERT, UPDATE ON awooop_run_state TO awooop_app; GRANT SELECT, INSERT, UPDATE ON awooop_run_step_journal TO awooop_app; GRANT SELECT, INSERT ON awooop_run_idempotency TO awooop_app; -- ========================================================= -- 驗收查詢 -- ========================================================= -- SELECT tablename, rowsecurity FROM pg_tables -- WHERE tablename IN ('awooop_run_state','awooop_run_step_journal','awooop_run_idempotency'); -- 預期:所有 rowsecurity = true