-- Migration: 20260502-001_async_job_model -- Purpose: Pattern 1 async job model — sidecar processes ingest jobs serially -- via Postgres-backed queue. Worker submits and polls rather than -- blocking on synchronous HTTP response. -- -- Architectural rationale: tonight's smoke test (2026-05-02 ~01:40-01:50 UTC) -- diagnosed that bulk ingest against a 4,222-entity graph commits successfully -- but the worker's HTTP read-timeout fires before the response returns. Three -- days of "saga deadlock" failures were false negatives — the work succeeded; -- the worker just stopped listening. Pattern 1 separates submission from -- completion observation so the worker can't false-negative this way. -- -- The job model is also the natural data source for Phase A items 6-7 -- (metrics tables) — graphiti_jobs records duration, status transitions, -- and per-job summary that those tables will aggregate. -- -- Idempotent: safe to re-run. -- Job state for sidecar's async ingest queue. -- One row per submitted bulk-or-single ingest. Sidecar reads queued jobs -- on startup to resume after restart. Worker polls status until terminal. CREATE TABLE IF NOT EXISTS graphiti_jobs ( job_id UUID PRIMARY KEY, job_type TEXT NOT NULL CHECK (job_type IN ('bulk', 'single')), payload JSONB NOT NULL, -- full submitted request body status TEXT NOT NULL DEFAULT 'queued' -- 'queued'|'running'|'committed'|'failed' CHECK (status IN ('queued', 'running', 'committed', 'failed')), enqueued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), started_at TIMESTAMPTZ, finished_at TIMESTAMPTZ, error TEXT, -- non-null when status='failed' summary JSONB, -- {nodes: N, edges: N, episodes: N} submitted_by TEXT -- worker name for traceability ); -- Index supporting sidecar's "pick next queued job" query CREATE INDEX IF NOT EXISTS idx_graphiti_jobs_queued ON graphiti_jobs (enqueued_at) WHERE status = 'queued'; -- Index supporting worker's "poll my job by id" query (PK already does this, -- but explicit index aids ANALYZE behavior on small tables) CREATE INDEX IF NOT EXISTS idx_graphiti_jobs_status ON graphiti_jobs (status); -- Stage 3 queue gains a reference to the sidecar job processing the row. -- When set, worker polls graphiti_jobs.status rather than blocking on HTTP. -- NULL means: row not yet submitted, or pre-Pattern-1 row. ALTER TABLE stage_3_queue ADD COLUMN IF NOT EXISTS external_job_id UUID; -- Index for "find rows that submitted but didn't complete" recovery scans CREATE INDEX IF NOT EXISTS idx_stage_3_queue_external_job ON stage_3_queue (external_job_id) WHERE external_job_id IS NOT NULL AND completed_at IS NULL AND failed_at IS NULL;