Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/cli/migrations/0014_task_objective_backfill.sql
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
-- Backfill task-level kind='task_objective' artifact rows for every task
-- that existed before the shared code-worker prompt composer landed.
--
-- The composer's loadOriginalTaskObjective() requires a kind='task_objective'
-- artifact with attempt_id IS NULL, written once at enqueue time. Without
-- The composer's loadOriginalTaskObjective() requires at least one
-- kind='task_objective' artifact with attempt_id IS NULL. Without
-- this backfill, any pre-existing active task would throw on its next CI /
-- crash / stale / wall-clock / malformed retry, on review/conflict respawn,
-- or on orchestrator submit-brief.
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/admin/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -877,7 +877,7 @@ function missionControlTaskRows(db: DB): MissionControlTaskRow[] {
WHERE ar.task_id = t.task_id
AND ar.kind = 'task_objective'
AND ar.attempt_id IS NULL
ORDER BY ar.artifact_id ASC
ORDER BY ar.artifact_id DESC
LIMIT 1
) AS objective_file_path,
(
Expand All @@ -886,7 +886,7 @@ function missionControlTaskRows(db: DB): MissionControlTaskRow[] {
WHERE ar.task_id = t.task_id
AND ar.kind = 'ticket_snapshot'
AND ar.attempt_id IS NULL
ORDER BY ar.artifact_id ASC
ORDER BY ar.artifact_id DESC
LIMIT 1
) AS ticket_snapshot_file_path,
CASE
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/build/embedded.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import type { Migration } from "../db/migrate.ts";

export const QUAY_VERSION = "dev+3afbf26+dirty";
export const QUAY_VERSION = "dev+2623a9f+dirty";

export const EMBEDDED_MIGRATIONS: readonly Migration[] = [
{ name: "0001_init.sql", sql: "-- Slice 0 schema: persistence contract for Quay (per quay-spec.md §9).\n-- Foreign keys must be enabled at the connection level (PRAGMA foreign_keys = ON).\n\nCREATE TABLE repos (\n repo_id TEXT PRIMARY KEY,\n repo_url TEXT NOT NULL,\n base_branch TEXT NOT NULL,\n package_manager TEXT NOT NULL,\n install_cmd TEXT NOT NULL,\n test_cmd TEXT,\n ci_workflow_name TEXT,\n contribution_guide_path TEXT,\n archived_at TEXT,\n created_at TEXT NOT NULL\n);\n\nCREATE TABLE preambles (\n preamble_id INTEGER PRIMARY KEY AUTOINCREMENT,\n body TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\n\nCREATE TABLE retry_templates (\n template_id INTEGER PRIMARY KEY AUTOINCREMENT,\n kind TEXT NOT NULL,\n body TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\n\nCREATE TABLE tasks (\n task_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n external_ref TEXT,\n state TEXT NOT NULL,\n branch_name TEXT NOT NULL,\n tmux_id TEXT NOT NULL,\n worktree_path TEXT NOT NULL,\n pr_number INTEGER,\n pr_url TEXT,\n head_sha TEXT,\n base_sha TEXT,\n attempts_consumed INTEGER NOT NULL DEFAULT 0,\n retry_budget INTEGER NOT NULL,\n budget_exhausted INTEGER NOT NULL DEFAULT 0 CHECK (budget_exhausted IN (0, 1)),\n tick_error TEXT,\n slack_thread_ref TEXT,\n claimed_at TEXT,\n claim_id TEXT,\n claim_expirations_consecutive INTEGER NOT NULL DEFAULT 0,\n last_review_id_acted_on TEXT,\n last_conflict_observation TEXT,\n non_budget_respawns_consumed INTEGER NOT NULL DEFAULT 0,\n next_escalation_seq INTEGER NOT NULL DEFAULT 1,\n cancel_requested_at TEXT,\n cancel_close_pr INTEGER NOT NULL DEFAULT 0 CHECK (cancel_close_pr IN (0, 1)),\n cancel_keep_worktree INTEGER NOT NULL DEFAULT 0 CHECK (cancel_keep_worktree IN (0, 1)),\n spawn_failures_consecutive INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE INDEX tasks_state_idx ON tasks(state);\n\nCREATE TABLE attempts (\n attempt_id INTEGER PRIMARY KEY AUTOINCREMENT,\n task_id TEXT NOT NULL REFERENCES tasks(task_id),\n attempt_number INTEGER NOT NULL,\n preamble_id INTEGER NOT NULL REFERENCES preambles(preamble_id),\n template_id INTEGER REFERENCES retry_templates(template_id),\n reason TEXT NOT NULL,\n consumed_budget INTEGER NOT NULL CHECK (consumed_budget IN (0, 1)),\n tmux_session TEXT,\n spawned_at TEXT,\n remote_sha_at_spawn TEXT,\n remote_sha_at_exit TEXT,\n pr_existed_at_spawn INTEGER NOT NULL DEFAULT 0 CHECK (pr_existed_at_spawn IN (0, 1)),\n ended_at TEXT,\n exit_kind TEXT,\n kill_intent TEXT,\n UNIQUE (task_id, attempt_number)\n);\n\nCREATE UNIQUE INDEX one_pending_attempt_per_task\n ON attempts(task_id)\n WHERE spawned_at IS NULL;\n\nCREATE TABLE artifacts (\n artifact_id INTEGER PRIMARY KEY AUTOINCREMENT,\n task_id TEXT NOT NULL REFERENCES tasks(task_id),\n attempt_id INTEGER REFERENCES attempts(attempt_id),\n kind TEXT NOT NULL,\n file_path TEXT NOT NULL,\n content_hash TEXT,\n escalation_seq INTEGER,\n escalation_nonce TEXT,\n slack_pre_post_fence_ts TEXT,\n slack_post_ts TEXT,\n slack_recovered_post_ts TEXT,\n captured_at TEXT NOT NULL\n);\n\nCREATE UNIQUE INDEX artifact_recovery_idempotency\n ON artifacts(task_id, attempt_id, kind, content_hash)\n WHERE content_hash IS NOT NULL AND attempt_id IS NOT NULL;\n\nCREATE INDEX artifacts_task_kind_attempt_idx\n ON artifacts(task_id, kind, attempt_id);\n\nCREATE TABLE events (\n event_id INTEGER PRIMARY KEY AUTOINCREMENT,\n task_id TEXT NOT NULL REFERENCES tasks(task_id),\n attempt_id INTEGER REFERENCES attempts(attempt_id),\n event_type TEXT NOT NULL,\n from_state TEXT,\n to_state TEXT,\n payload_artifact_id INTEGER REFERENCES artifacts(artifact_id),\n occurred_at TEXT NOT NULL\n);\n\nCREATE INDEX events_task_occurred_idx ON events(task_id, occurred_at);\n" },
Expand All @@ -19,7 +19,7 @@ export const EMBEDDED_MIGRATIONS: readonly Migration[] = [
{ name: "0011_orchestrator_handoffs.sql", sql: "-- Durable orchestrator handoff queue for tasks that enter\n-- awaiting-next-brief and need judgment outside `quay tick`.\n\nCREATE TABLE orchestrator_handoffs (\n handoff_id INTEGER PRIMARY KEY AUTOINCREMENT,\n task_id TEXT NOT NULL REFERENCES tasks(task_id),\n reason TEXT NOT NULL CHECK (\n reason IN (\n 'worker_blocker',\n 'budget_exhausted',\n 'human_reply_ingested',\n 'manual_resume'\n )\n ),\n state_event_id INTEGER NOT NULL REFERENCES events(event_id),\n idempotency_key TEXT NOT NULL,\n payload_json TEXT,\n status TEXT NOT NULL DEFAULT 'pending' CHECK (\n status IN ('pending', 'claimed', 'completed', 'cancelled')\n ),\n claim_id TEXT,\n claimed_at TEXT,\n completed_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n UNIQUE (idempotency_key),\n UNIQUE (task_id, state_event_id, reason)\n);\n\nCREATE INDEX orchestrator_handoffs_status_created_idx\n ON orchestrator_handoffs(status, created_at, handoff_id);\n\nCREATE INDEX orchestrator_handoffs_task_status_idx\n ON orchestrator_handoffs(task_id, status, handoff_id);\n" },
{ name: "0012_agent_model_selection.sql", sql: "-- First-class agent/model selection snapshots.\n--\n-- Repo columns are role defaults. Task columns are immutable snapshots taken\n-- at enqueue / synthetic review scheduling time, so later config changes do\n-- not alter already-queued work. Attempt column records the intended model\n-- that was passed to the agent invocation.\n\nALTER TABLE repos ADD COLUMN model_worker TEXT;\nALTER TABLE repos ADD COLUMN model_reviewer TEXT;\n\nALTER TABLE tasks ADD COLUMN worker_agent TEXT;\nALTER TABLE tasks ADD COLUMN worker_model TEXT;\nALTER TABLE tasks ADD COLUMN reviewer_agent TEXT;\nALTER TABLE tasks ADD COLUMN reviewer_model TEXT;\n\nALTER TABLE attempts ADD COLUMN agent_model TEXT;\n" },
{ name: "0013_review_requests.sql", sql: "-- Durable review enrollment queue consumed by tick.\nCREATE TABLE review_requests (\n request_id INTEGER PRIMARY KEY AUTOINCREMENT,\n task_id TEXT NOT NULL REFERENCES tasks(task_id),\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n pr_number INTEGER NOT NULL,\n head_sha TEXT NOT NULL,\n source TEXT NOT NULL DEFAULT 'review-pr',\n requested_by TEXT,\n delivery_id TEXT,\n tags_json TEXT,\n reviewer_agent TEXT,\n reviewer_model TEXT,\n status TEXT NOT NULL CHECK (\n status IN ('pending_ci', 'scheduled', 'superseded', 'discarded_terminal')\n ),\n scheduled_attempt_id INTEGER REFERENCES attempts(attempt_id),\n superseded_by_request_id INTEGER REFERENCES review_requests(request_id),\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n terminal_state TEXT\n);\n\nCREATE UNIQUE INDEX review_requests_unique_head\n ON review_requests(task_id, head_sha);\n\nCREATE INDEX review_requests_pending_idx\n ON review_requests(status, repo_id, pr_number, created_at);\n" },
{ name: "0014_task_objective_backfill.sql", sql: "-- Backfill task-level kind='task_objective' artifact rows for every task\n-- that existed before the shared code-worker prompt composer landed.\n--\n-- The composer's loadOriginalTaskObjective() requires a kind='task_objective'\n-- artifact with attempt_id IS NULL, written once at enqueue time. Without\n-- this backfill, any pre-existing active task would throw on its next CI /\n-- crash / stale / wall-clock / malformed retry, on review/conflict respawn,\n-- or on orchestrator submit-brief.\n--\n-- For legacy tasks, the raw original brief lives in the first attempt's\n-- (`attempt_number=1`, `reason='initial'`) brief artifact. The backfilled\n-- row points at the same on-disk file and copies the content_hash — no file\n-- writes are required. The `artifact_recovery_idempotency` unique index\n-- excludes `attempt_id IS NULL`, so the new task-level row never collides\n-- with the per-attempt brief it shadows.\n--\n-- The NOT EXISTS clause makes this migration safe to re-run.\n\nINSERT INTO artifacts (task_id, attempt_id, kind, file_path, content_hash, captured_at)\nSELECT\n ar.task_id,\n NULL,\n 'task_objective',\n ar.file_path,\n ar.content_hash,\n strftime('%Y-%m-%dT%H:%M:%fZ', 'now')\nFROM artifacts ar\nJOIN attempts a ON a.attempt_id = ar.attempt_id\nWHERE ar.kind = 'brief'\n AND a.attempt_number = 1\n AND a.reason = 'initial'\n AND NOT EXISTS (\n SELECT 1\n FROM artifacts ao\n WHERE ao.task_id = ar.task_id\n AND ao.kind = 'task_objective'\n AND ao.attempt_id IS NULL\n );\n" },
{ name: "0014_task_objective_backfill.sql", sql: "-- Backfill task-level kind='task_objective' artifact rows for every task\n-- that existed before the shared code-worker prompt composer landed.\n--\n-- The composer's loadOriginalTaskObjective() requires at least one\n-- kind='task_objective' artifact with attempt_id IS NULL. Without\n-- this backfill, any pre-existing active task would throw on its next CI /\n-- crash / stale / wall-clock / malformed retry, on review/conflict respawn,\n-- or on orchestrator submit-brief.\n--\n-- For legacy tasks, the raw original brief lives in the first attempt's\n-- (`attempt_number=1`, `reason='initial'`) brief artifact. The backfilled\n-- row points at the same on-disk file and copies the content_hash — no file\n-- writes are required. The `artifact_recovery_idempotency` unique index\n-- excludes `attempt_id IS NULL`, so the new task-level row never collides\n-- with the per-attempt brief it shadows.\n--\n-- The NOT EXISTS clause makes this migration safe to re-run.\n\nINSERT INTO artifacts (task_id, attempt_id, kind, file_path, content_hash, captured_at)\nSELECT\n ar.task_id,\n NULL,\n 'task_objective',\n ar.file_path,\n ar.content_hash,\n strftime('%Y-%m-%dT%H:%M:%fZ', 'now')\nFROM artifacts ar\nJOIN attempts a ON a.attempt_id = ar.attempt_id\nWHERE ar.kind = 'brief'\n AND a.attempt_number = 1\n AND a.reason = 'initial'\n AND NOT EXISTS (\n SELECT 1\n FROM artifacts ao\n WHERE ao.task_id = ar.task_id\n AND ao.kind = 'task_objective'\n AND ao.attempt_id IS NULL\n );\n" },
{ name: "0015_task_goals.sql", sql: "-- Task-level goal worker mode.\n-- quay: foreign_keys_off\n--\n-- A goal is owned by its Quay task, not scheduled independently. The task\n-- stays the scheduling unit; task_goals carries durable objective/status and\n-- accounting state across normal attempts.\n\nALTER TABLE tasks ADD COLUMN worker_execution TEXT NOT NULL DEFAULT 'oneshot'\n CHECK (worker_execution IN ('oneshot', 'goal'));\n\nALTER TABLE attempts ADD COLUMN goal_id TEXT;\nALTER TABLE attempts ADD COLUMN goal_report_processed_at TEXT;\n\n-- Add the goal-mode no-progress handoff reason. SQLite cannot alter a CHECK\n-- constraint in place, so rebuild the table while preserving rows.\nALTER TABLE orchestrator_handoffs RENAME TO orchestrator_handoffs_old;\n\nCREATE TABLE orchestrator_handoffs (\n handoff_id INTEGER PRIMARY KEY AUTOINCREMENT,\n task_id TEXT NOT NULL REFERENCES tasks(task_id),\n reason TEXT NOT NULL CHECK (\n reason IN (\n 'worker_blocker',\n 'budget_exhausted',\n 'human_reply_ingested',\n 'manual_resume',\n 'no_progress'\n )\n ),\n state_event_id INTEGER NOT NULL REFERENCES events(event_id),\n idempotency_key TEXT NOT NULL,\n payload_json TEXT,\n status TEXT NOT NULL DEFAULT 'pending' CHECK (\n status IN ('pending', 'claimed', 'completed', 'cancelled')\n ),\n claim_id TEXT,\n claimed_at TEXT,\n completed_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n UNIQUE (idempotency_key),\n UNIQUE (task_id, state_event_id, reason)\n);\n\nINSERT INTO orchestrator_handoffs (\n handoff_id, task_id, reason, state_event_id, idempotency_key,\n payload_json, status, claim_id, claimed_at, completed_at, created_at,\n updated_at\n)\nSELECT\n handoff_id, task_id, reason, state_event_id, idempotency_key,\n payload_json, status, claim_id, claimed_at, completed_at, created_at,\n updated_at\nFROM orchestrator_handoffs_old;\n\nDROP TABLE orchestrator_handoffs_old;\n\nCREATE INDEX orchestrator_handoffs_status_created_idx\n ON orchestrator_handoffs(status, created_at, handoff_id);\n\nCREATE INDEX orchestrator_handoffs_task_status_idx\n ON orchestrator_handoffs(task_id, status, handoff_id);\n\nCREATE TABLE task_goals (\n task_id TEXT PRIMARY KEY NOT NULL REFERENCES tasks(task_id),\n goal_id TEXT NOT NULL,\n objective TEXT NOT NULL,\n status TEXT NOT NULL CHECK (\n status IN ('active', 'blocked', 'budget_limited', 'complete')\n ),\n token_budget INTEGER,\n tokens_used INTEGER NOT NULL DEFAULT 0,\n time_used_seconds INTEGER NOT NULL DEFAULT 0,\n no_progress_active_count INTEGER NOT NULL DEFAULT 0,\n last_attempt_id INTEGER REFERENCES attempts(attempt_id),\n current_handoff_id INTEGER REFERENCES orchestrator_handoffs(handoff_id),\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n completed_at TEXT,\n CHECK (token_budget IS NULL OR token_budget > 0)\n);\n\nCREATE INDEX task_goals_status_idx ON task_goals(status);\n" },
{ name: "0016_task_base_branch.sql", sql: "-- Task-level effective base branch.\n--\n-- Existing tasks are backfilled from their repo default so later repo config\n-- changes do not alter already-enqueued work. New enqueue paths write the\n-- effective branch explicitly.\n\nALTER TABLE tasks ADD COLUMN base_branch TEXT;\n\nUPDATE tasks\n SET base_branch = (\n SELECT repos.base_branch\n FROM repos\n WHERE repos.repo_id = tasks.repo_id\n )\n WHERE base_branch IS NULL;\n" },
{ name: "0017_goal_completion_audit.sql", sql: "-- Goal completion audit gate.\n-- quay: foreign_keys_off\n--\n-- `completion_pending` is an internal status: the worker has made a terminal\n-- completion claim, but Quay has not yet accepted the claim and entered the\n-- PR lifecycle.\n\nALTER TABLE task_goals RENAME TO task_goals_old;\n\nCREATE TABLE task_goals (\n task_id TEXT PRIMARY KEY NOT NULL REFERENCES tasks(task_id),\n goal_id TEXT NOT NULL,\n objective TEXT NOT NULL,\n status TEXT NOT NULL CHECK (\n status IN (\n 'active',\n 'blocked',\n 'budget_limited',\n 'completion_pending',\n 'complete'\n )\n ),\n token_budget INTEGER,\n tokens_used INTEGER NOT NULL DEFAULT 0,\n time_used_seconds INTEGER NOT NULL DEFAULT 0,\n no_progress_active_count INTEGER NOT NULL DEFAULT 0,\n last_attempt_id INTEGER REFERENCES attempts(attempt_id),\n current_handoff_id INTEGER REFERENCES orchestrator_handoffs(handoff_id),\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n completed_at TEXT,\n CHECK (token_budget IS NULL OR token_budget > 0)\n);\n\nINSERT INTO task_goals (\n task_id, goal_id, objective, status, token_budget,\n tokens_used, time_used_seconds, no_progress_active_count,\n last_attempt_id, current_handoff_id, created_at, updated_at, completed_at\n)\nSELECT\n task_id, goal_id, objective, status, token_budget,\n tokens_used, time_used_seconds, no_progress_active_count,\n last_attempt_id, current_handoff_id, created_at, updated_at, completed_at\nFROM task_goals_old;\n\nDROP TABLE task_goals_old;\n\nCREATE INDEX task_goals_status_idx ON task_goals(status);\n" },
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/core/goals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export function loadGoalPromptContext(
WHERE task_id = ?
AND kind = 'task_objective'
AND attempt_id IS NULL
ORDER BY artifact_id ASC
ORDER BY artifact_id DESC
LIMIT 1`,
)
.get(taskId);
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/core/pr_review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1454,7 +1454,7 @@ function ensureTaskObjectiveArtifact(
WHERE task_id = ?
AND kind = 'task_objective'
AND attempt_id IS NULL
ORDER BY artifact_id ASC
ORDER BY artifact_id DESC
LIMIT 1`,
)
.get(taskId);
Expand Down Expand Up @@ -1641,7 +1641,7 @@ function loadReviewContextBrief(db: DB, taskId: string): string | null {
WHERE task_id = ?
AND kind = 'task_objective'
AND attempt_id IS NULL
ORDER BY artifact_id ASC
ORDER BY artifact_id DESC
LIMIT 1`,
)
.get(taskId);
Expand Down
Loading