fix: audit fixes for async insight jobs + persist generation params

- Fix query param mismatch: rename GenerationStatusQuery.file_path to
  path so the client's app-resume buildQuery({ path: ... }) resolves
  correctly instead of always getting 400
- Remove dead _lib_id bindings from both generate handlers
- Return 202 Accepted instead of 200 from generate endpoints
- Restore OpenTelemetry span instrumentation on generate handlers
- Remove stale UNIQUE constraint from initial migration (incompatible
  with plain-INSERT DAO)
- Add tests for status guard: complete_job/fail_job are no-ops when
  job is already cancelled, and cancel_job by id
- Persist generation params (num_ctx, temperature, top_p, top_k, min_p,
  system_prompt, persona_id) on the photo_insights table for auditing

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cameron Cordes
2026-05-27 13:02:15 -04:00
parent b87eb4e690
commit 2818936739
14 changed files with 786 additions and 194 deletions
@@ -1,8 +1,7 @@
-- Track async insight generation jobs so the client can poll for
-- completion after the server returns 202 Accepted. The UNIQUE
-- constraint on (library_id, file_path, generation_type) ensures
-- idempotent inserts: if a running job already exists, the caller
-- should return that job_id instead of creating a duplicate.
-- completion after the server returns 202 Accepted. Each generation
-- creates a new row; the application layer cancels prior running
-- jobs before inserting.
CREATE TABLE insight_generation_jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
library_id INTEGER NOT NULL DEFAULT 1,
@@ -12,8 +11,7 @@ CREATE TABLE insight_generation_jobs (
started_at INTEGER NOT NULL,
completed_at INTEGER,
result_insight_id INTEGER,
error_message TEXT,
UNIQUE(library_id, file_path, generation_type)
error_message TEXT
);
-- For the status endpoint: fast lookup by (library_id, file_path)
@@ -0,0 +1,28 @@
-- Restore UNIQUE constraint
CREATE TABLE insight_generation_jobs_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
library_id INTEGER NOT NULL DEFAULT 1,
file_path TEXT NOT NULL,
generation_type TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'running',
started_at INTEGER NOT NULL,
completed_at INTEGER,
result_insight_id INTEGER,
error_message TEXT,
UNIQUE(library_id, file_path, generation_type)
);
INSERT INTO insight_generation_jobs_new
SELECT id, library_id, file_path, generation_type, status, started_at, completed_at, result_insight_id, error_message
FROM insight_generation_jobs;
DROP TABLE insight_generation_jobs;
ALTER TABLE insight_generation_jobs_new RENAME TO insight_generation_jobs;
CREATE INDEX idx_insight_gen_jobs_file
ON insight_generation_jobs(library_id, file_path);
CREATE INDEX idx_insight_gen_jobs_status_cleanup
ON insight_generation_jobs(status, started_at);
@@ -0,0 +1,30 @@
-- Remove UNIQUE(library_id, file_path, generation_type) constraint to allow
-- multiple job rows per file. This enables proper cancel/regenerate semantics:
-- a new job is always inserted on regenerate, and the old job is cancelled
-- independently. The application layer prevents concurrent running jobs.
CREATE TABLE insight_generation_jobs_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
library_id INTEGER NOT NULL DEFAULT 1,
file_path TEXT NOT NULL,
generation_type TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'running',
started_at INTEGER NOT NULL,
completed_at INTEGER,
result_insight_id INTEGER,
error_message TEXT
);
INSERT INTO insight_generation_jobs_new
SELECT id, library_id, file_path, generation_type, status, started_at, completed_at, result_insight_id, error_message
FROM insight_generation_jobs;
DROP TABLE insight_generation_jobs;
ALTER TABLE insight_generation_jobs_new RENAME TO insight_generation_jobs;
CREATE INDEX idx_insight_gen_jobs_file
ON insight_generation_jobs(library_id, file_path);
CREATE INDEX idx_insight_gen_jobs_status_cleanup
ON insight_generation_jobs(status, started_at);
@@ -0,0 +1,11 @@
-- SQLite doesn't support DROP COLUMN before 3.35.0; recreate the table
-- without the new columns. This is only needed for rollback.
CREATE TABLE photo_insights_old AS
SELECT id, library_id, rel_path, title, summary, generated_at,
model_version, is_current, training_messages, approved,
backend, fewshot_source_ids, content_hash
FROM photo_insights;
DROP TABLE photo_insights;
ALTER TABLE photo_insights_old RENAME TO photo_insights;
@@ -0,0 +1,8 @@
-- Persist generation parameters on each insight row for auditing.
ALTER TABLE photo_insights ADD COLUMN num_ctx INTEGER;
ALTER TABLE photo_insights ADD COLUMN temperature REAL;
ALTER TABLE photo_insights ADD COLUMN top_p REAL;
ALTER TABLE photo_insights ADD COLUMN top_k INTEGER;
ALTER TABLE photo_insights ADD COLUMN min_p REAL;
ALTER TABLE photo_insights ADD COLUMN system_prompt TEXT;
ALTER TABLE photo_insights ADD COLUMN persona_id TEXT;